-
diff --git a/docs/0.9.5/Add-a-wiki-on-your-website.html b/docs/0.9.5/Add-a-wiki-on-your-website.html
index 1aee04d570..50e8d021c1 100644
--- a/docs/0.9.5/Add-a-wiki-on-your-website.html
+++ b/docs/0.9.5/Add-a-wiki-on-your-website.html
@@ -427,7 +427,6 @@ necessary. If you’re interested in supporting this little project, you are mo
-
diff --git a/docs/0.9.5/Adding-Command-Tutorial.html b/docs/0.9.5/Adding-Command-Tutorial.html
index c1046d7064..497eeccc93 100644
--- a/docs/0.9.5/Adding-Command-Tutorial.html
+++ b/docs/0.9.5/Adding-Command-Tutorial.html
@@ -318,7 +318,6 @@ default character cmdset defaults to being defined as
-
diff --git a/docs/0.9.5/Apache-Config.html b/docs/0.9.5/Apache-Config.html
index fbc25cabe2..1a61778fa5 100644
--- a/docs/0.9.5/Apache-Config.html
+++ b/docs/0.9.5/Apache-Config.html
@@ -296,7 +296,6 @@ port but this should be applicable also to other types of proxies (like nginx).<
-
diff --git a/docs/0.9.5/Arxcode-installing-help.html b/docs/0.9.5/Arxcode-installing-help.html
index e91248a1e3..b965d6f634 100644
--- a/docs/0.9.5/Arxcode-installing-help.html
+++ b/docs/0.9.5/Arxcode-installing-help.html
@@ -337,7 +337,6 @@ on localhost at port 4000, and the webserver at http://localhost:4001/
-
diff --git a/docs/0.9.5/Attributes.html b/docs/0.9.5/Attributes.html
index 75abc3fc3d..0d9f242373 100644
--- a/docs/0.9.5/Attributes.html
+++ b/docs/0.9.5/Attributes.html
@@ -538,7 +538,6 @@ those will check for the 0.9.5 (v0.9.5 branch)
-
diff --git a/docs/0.9.5/Banning.html b/docs/0.9.5/Banning.html
index 62fb897e93..8773d63a6a 100644
--- a/docs/0.9.5/Banning.html
+++ b/docs/0.9.5/Banning.html
@@ -257,7 +257,6 @@ password of any account, including the superuser or admin accounts. This is a fe
-
diff --git a/docs/0.9.5/Batch-Code-Processor.html b/docs/0.9.5/Batch-Code-Processor.html
index e698ff1cf2..10288e5464 100644
--- a/docs/0.9.5/Batch-Code-Processor.html
+++ b/docs/0.9.5/Batch-Code-Processor.html
@@ -384,7 +384,6 @@ executed. When the code runs it has no knowledge of what file those strings wher
-
diff --git a/docs/0.9.5/Building-Permissions.html b/docs/0.9.5/Building-Permissions.html
index 186a9056a7..7c65ab44e5 100644
--- a/docs/0.9.5/Building-Permissions.html
+++ b/docs/0.9.5/Building-Permissions.html
@@ -177,7 +177,6 @@ levels. Note that you cannot escalate your permissions this way; If the Characte
-
diff --git a/docs/0.9.5/Building-a-mech-tutorial.html b/docs/0.9.5/Building-a-mech-tutorial.html
index f09c75a4af..828b55f7ab 100644
--- a/docs/0.9.5/Building-a-mech-tutorial.html
+++ b/docs/0.9.5/Building-a-mech-tutorial.html
@@ -402,7 +402,6 @@ shooting goodness would be made available to you only when you enter it.
-
diff --git a/docs/0.9.5/Choosing-An-SQL-Server.html b/docs/0.9.5/Choosing-An-SQL-Server.html
index b384c9bd84..09a9eed0b5 100644
--- a/docs/0.9.5/Choosing-An-SQL-Server.html
+++ b/docs/0.9.5/Choosing-An-SQL-Server.html
@@ -388,7 +388,6 @@ others. If you try other databases out, consider expanding this page with instru
-
diff --git a/docs/0.9.5/Client-Support-Grid.html b/docs/0.9.5/Client-Support-Grid.html
index 43c6db4489..3034d72e5a 100644
--- a/docs/0.9.5/Client-Support-Grid.html
+++ b/docs/0.9.5/Client-Support-Grid.html
@@ -250,7 +250,6 @@ parameter to disable it for that Evennia account permanently.
-
diff --git a/docs/0.9.5/Coding-Introduction.html b/docs/0.9.5/Coding-Introduction.html
index d25cb2bcac..2154fdc52d 100644
--- a/docs/0.9.5/Coding-Introduction.html
+++ b/docs/0.9.5/Coding-Introduction.html
@@ -207,7 +207,6 @@ can’t find the answer in the docs, don’t be shy to ask questions! The
-
diff --git a/docs/0.9.5/Command-Cooldown.html b/docs/0.9.5/Command-Cooldown.html
index ccc7bb87b3..cbd490671f 100644
--- a/docs/0.9.5/Command-Cooldown.html
+++ b/docs/0.9.5/Command-Cooldown.html
@@ -243,7 +243,6 @@ other types of attacks for a while before the warrior can recover.
-
diff --git a/docs/0.9.5/Command-Duration.html b/docs/0.9.5/Command-Duration.html
index 6e9ee5727d..aed8f695e3 100644
--- a/docs/0.9.5/Command-Duration.html
+++ b/docs/0.9.5/Command-Duration.html
@@ -730,7 +730,6 @@ callback when the server comes back up (it will resume the countdown and ignore
-
diff --git a/docs/0.9.5/Command-Prompt.html b/docs/0.9.5/Command-Prompt.html
index e5d23eff36..23eb07d501 100644
--- a/docs/0.9.5/Command-Prompt.html
+++ b/docs/0.9.5/Command-Prompt.html
@@ -288,7 +288,6 @@ directly the easiest way is to just wrap those with a multiple inheritance to yo
-
diff --git a/docs/0.9.5/Communications.html b/docs/0.9.5/Communications.html
index e7f5ca228a..f6969b6189 100644
--- a/docs/0.9.5/Communications.html
+++ b/docs/0.9.5/Communications.html
@@ -224,7 +224,6 @@ for channel communication (since the default ChannelCommand instead logs to a fi
-
diff --git a/docs/0.9.5/Connection-Screen.html b/docs/0.9.5/Connection-Screen.html
index 72e4d28979..b056065f12 100644
--- a/docs/0.9.5/Connection-Screen.html
+++ b/docs/0.9.5/Connection-Screen.html
@@ -139,7 +139,6 @@ tutorial section on how to add new commands to a default command set.
-
diff --git a/docs/0.9.5/Continuous-Integration.html b/docs/0.9.5/Continuous-Integration.html
index 6fac451285..bad7f44030 100644
--- a/docs/0.9.5/Continuous-Integration.html
+++ b/docs/0.9.5/Continuous-Integration.html
@@ -429,7 +429,6 @@ build steps could be added or removed at this point, adding some features like U
-
diff --git a/docs/0.9.5/Coordinates.html b/docs/0.9.5/Coordinates.html
index 37581c92f9..74cfad9bf0 100644
--- a/docs/0.9.5/Coordinates.html
+++ b/docs/0.9.5/Coordinates.html
@@ -590,7 +590,6 @@ square (E, G, M and O) are not in this circle. So we remove them.
-
diff --git a/docs/0.9.5/Directory-Overview.html b/docs/0.9.5/Directory-Overview.html
index 93d37ef13c..4a2ae332e1 100644
--- a/docs/0.9.5/Directory-Overview.html
+++ b/docs/0.9.5/Directory-Overview.html
@@ -184,7 +184,6 @@ having to import from their actual locations inside the source tree.
-
diff --git a/docs/0.9.5/Dynamic-In-Game-Map.html b/docs/0.9.5/Dynamic-In-Game-Map.html
index 7da23adab4..4c1b672654 100644
--- a/docs/0.9.5/Dynamic-In-Game-Map.html
+++ b/docs/0.9.5/Dynamic-In-Game-Map.html
@@ -823,7 +823,6 @@ also look into up/down directions and figure out how to display that in a good w
-
diff --git a/docs/0.9.5/EvEditor.html b/docs/0.9.5/EvEditor.html
index bd52ab18ba..f0d57056c3 100644
--- a/docs/0.9.5/EvEditor.html
+++ b/docs/0.9.5/EvEditor.html
@@ -347,7 +347,6 @@ editor can be useful if you want to test the code you have typed but add new lin
-
diff --git a/docs/0.9.5/Evennia-API.html b/docs/0.9.5/Evennia-API.html
index dc0a9e510b..1c8cdc572f 100644
--- a/docs/0.9.5/Evennia-API.html
+++ b/docs/0.9.5/Evennia-API.html
@@ -231,7 +231,6 @@ The flat API is defined in 0.9.5 (v0.9.5 branch)
-
diff --git a/docs/0.9.5/Evennia-Game-Index.html b/docs/0.9.5/Evennia-Game-Index.html
index db344c71c9..03924f6bdf 100644
--- a/docs/0.9.5/Evennia-Game-Index.html
+++ b/docs/0.9.5/Evennia-Game-Index.html
@@ -193,7 +193,6 @@ if you are not ready for players yet.
-
diff --git a/docs/0.9.5/Evennia-Introduction.html b/docs/0.9.5/Evennia-Introduction.html
index 77454b80a9..b8134875b9 100644
--- a/docs/0.9.5/Evennia-Introduction.html
+++ b/docs/0.9.5/Evennia-Introduction.html
@@ -282,7 +282,6 @@ your own game, you will end up with a small (very small) game that you can build
-
diff --git a/docs/0.9.5/Game-Planning.html b/docs/0.9.5/Game-Planning.html
index 0cfc999a0a..ae6c6beb7e 100644
--- a/docs/0.9.5/Game-Planning.html
+++ b/docs/0.9.5/Game-Planning.html
@@ -322,7 +322,6 @@ have made their dream game a reality!
-
diff --git a/docs/0.9.5/Gametime-Tutorial.html b/docs/0.9.5/Gametime-Tutorial.html
index ed14d217ff..cfda784f79 100644
--- a/docs/0.9.5/Gametime-Tutorial.html
+++ b/docs/0.9.5/Gametime-Tutorial.html
@@ -483,7 +483,6 @@ same way as described for the default one above.
-
diff --git a/docs/0.9.5/Guest-Logins.html b/docs/0.9.5/Guest-Logins.html
index 0b81f70944..8e4f134167 100644
--- a/docs/0.9.5/Guest-Logins.html
+++ b/docs/0.9.5/Guest-Logins.html
@@ -124,7 +124,6 @@ of nine names from
-
diff --git a/docs/0.9.5/HAProxy-Config.html b/docs/0.9.5/HAProxy-Config.html
index eb0dddf467..1f05b614fc 100644
--- a/docs/0.9.5/HAProxy-Config.html
+++ b/docs/0.9.5/HAProxy-Config.html
@@ -241,7 +241,6 @@ Linux mechanism for running things at specific times.
-
diff --git a/docs/0.9.5/IRC.html b/docs/0.9.5/IRC.html
index 686aeaefeb..86cea2829f 100644
--- a/docs/0.9.5/IRC.html
+++ b/docs/0.9.5/IRC.html
@@ -186,7 +186,6 @@ name of the IRC channel you used (#evennia here).
-
diff --git a/docs/0.9.5/Inputfuncs.html b/docs/0.9.5/Inputfuncs.html
index 66c9117c97..3901a72171 100644
--- a/docs/0.9.5/Inputfuncs.html
+++ b/docs/0.9.5/Inputfuncs.html
@@ -300,7 +300,6 @@ add more. By default the following fields/attributes can be monitored:
-
diff --git a/docs/0.9.5/Installing-on-Android.html b/docs/0.9.5/Installing-on-Android.html
index f74a7c20fb..3526efb7d6 100644
--- a/docs/0.9.5/Installing-on-Android.html
+++ b/docs/0.9.5/Installing-on-Android.html
@@ -222,7 +222,6 @@ killed if your phone is heavily taxed. Termux seems to keep a notification up to
-
diff --git a/docs/0.9.5/Internationalization.html b/docs/0.9.5/Internationalization.html
index b757a47dc7..524e405c59 100644
--- a/docs/0.9.5/Internationalization.html
+++ b/docs/0.9.5/Internationalization.html
@@ -191,7 +191,6 @@ your own repository clone) so we can integrate your translation into Evennia!0.9.5 (v0.9.5 branch)
-
diff --git a/docs/0.9.5/Learn-Python-for-Evennia-The-Hard-Way.html b/docs/0.9.5/Learn-Python-for-Evennia-The-Hard-Way.html
index e7ab8fadc7..364f156129 100644
--- a/docs/0.9.5/Learn-Python-for-Evennia-The-Hard-Way.html
+++ b/docs/0.9.5/Learn-Python-for-Evennia-The-Hard-Way.html
@@ -162,7 +162,6 @@ commands when obtaining a weapon.
-
diff --git a/docs/0.9.5/Licensing.html b/docs/0.9.5/Licensing.html
index f1ace577f3..3b7eecac9f 100644
--- a/docs/0.9.5/Licensing.html
+++ b/docs/0.9.5/Licensing.html
@@ -119,7 +119,6 @@ as Evennia itself, unless the individual contributor has specifically defined ot
-
diff --git a/docs/0.9.5/Manually-Configuring-Color.html b/docs/0.9.5/Manually-Configuring-Color.html
index b43a93a31b..6d0c7750b0 100644
--- a/docs/0.9.5/Manually-Configuring-Color.html
+++ b/docs/0.9.5/Manually-Configuring-Color.html
@@ -321,7 +321,6 @@ regardless of if Evennia thinks their client supports it or not.
-
diff --git a/docs/0.9.5/Messagepath.html b/docs/0.9.5/Messagepath.html
index 8de03676e2..2582647644 100644
--- a/docs/0.9.5/Messagepath.html
+++ b/docs/0.9.5/Messagepath.html
@@ -422,7 +422,6 @@ information needed between server and client.
-
diff --git a/docs/0.9.5/New-Models.html b/docs/0.9.5/New-Models.html
index 930ba277ce..9e4b1b0e7f 100644
--- a/docs/0.9.5/New-Models.html
+++ b/docs/0.9.5/New-Models.html
@@ -387,7 +387,6 @@ lot more information about querying the database.
-
diff --git a/docs/0.9.5/Objects.html b/docs/0.9.5/Objects.html
index 866375985a..73c6abb7a6 100644
--- a/docs/0.9.5/Objects.html
+++ b/docs/0.9.5/Objects.html
@@ -305,7 +305,6 @@ and display this as an error message. If this is not found, the Exit will instea
-
diff --git a/docs/0.9.5/Portal-And-Server.html b/docs/0.9.5/Portal-And-Server.html
index b42ec99513..a7dd173a25 100644
--- a/docs/0.9.5/Portal-And-Server.html
+++ b/docs/0.9.5/Portal-And-Server.html
@@ -107,7 +107,6 @@ This allows the two programs to communicate seamlessly.
-
diff --git a/docs/0.9.5/Profiling.html b/docs/0.9.5/Profiling.html
index f9cf29b525..6dfa008754 100644
--- a/docs/0.9.5/Profiling.html
+++ b/docs/0.9.5/Profiling.html
@@ -228,7 +228,6 @@ course hard to actually mimic human user behavior. For this, actual real-game te
-
diff --git a/docs/0.9.5/Python-basic-introduction.html b/docs/0.9.5/Python-basic-introduction.html
index 62012b1a8f..ebb585d570 100644
--- a/docs/0.9.5/Python-basic-introduction.html
+++ b/docs/0.9.5/Python-basic-introduction.html
@@ -354,7 +354,6 @@ about objects and to explore the Evennia library.
-
diff --git a/docs/0.9.5/Server-Conf.html b/docs/0.9.5/Server-Conf.html
index 02342f84f3..a65b5d907a 100644
--- a/docs/0.9.5/Server-Conf.html
+++ b/docs/0.9.5/Server-Conf.html
@@ -210,7 +210,6 @@ know about if you are an Evennia developer.
-
diff --git a/docs/0.9.5/Soft-Code.html b/docs/0.9.5/Soft-Code.html
index 7c71b37f2b..7aed45f7f6 100644
--- a/docs/0.9.5/Soft-Code.html
+++ b/docs/0.9.5/Soft-Code.html
@@ -177,7 +177,6 @@ pseudo-softcode plugin aimed at developers wanting to script their game from ins
-
diff --git a/docs/0.9.5/Start-Stop-Reload.html b/docs/0.9.5/Start-Stop-Reload.html
index 3f87e1138c..de81074647 100644
--- a/docs/0.9.5/Start-Stop-Reload.html
+++ b/docs/0.9.5/Start-Stop-Reload.html
@@ -304,7 +304,6 @@ In-game you should now get the message that the Server has successfully restarte
-
diff --git a/docs/0.9.5/Static-In-Game-Map.html b/docs/0.9.5/Static-In-Game-Map.html
index c7d798b9c2..a004bc836d 100644
--- a/docs/0.9.5/Static-In-Game-Map.html
+++ b/docs/0.9.5/Static-In-Game-Map.html
@@ -702,7 +702,6 @@ not add more features to your game by trying other tutorials: 0.9.5 (v0.9.5 branch)
-
diff --git a/docs/0.9.5/Tags.html b/docs/0.9.5/Tags.html
index a4d537cd39..25cda92543 100644
--- a/docs/0.9.5/Tags.html
+++ b/docs/0.9.5/Tags.html
@@ -304,7 +304,6 @@ is found in the 0.9.5 (v0.9.5 branch)
-
diff --git a/docs/0.9.5/Text-Encodings.html b/docs/0.9.5/Text-Encodings.html
index 8429fe6788..f77ee1e606 100644
--- a/docs/0.9.5/Text-Encodings.html
+++ b/docs/0.9.5/Text-Encodings.html
@@ -164,7 +164,6 @@ the Wikipedia article 0.9.5 (v0.9.5 branch)
-
diff --git a/docs/0.9.5/TextTags.html b/docs/0.9.5/TextTags.html
index 79e722623d..89c450bc04 100644
--- a/docs/0.9.5/TextTags.html
+++ b/docs/0.9.5/TextTags.html
@@ -477,7 +477,6 @@ value will be a float (so 0.9.5 (v0.9.5 branch)
-
diff --git a/docs/0.9.5/TickerHandler.html b/docs/0.9.5/TickerHandler.html
index f8c8c89845..9bf0419ab7 100644
--- a/docs/0.9.5/TickerHandler.html
+++ b/docs/0.9.5/TickerHandler.html
@@ -230,7 +230,6 @@ same time without input from something else.
-
diff --git a/docs/0.9.5/Tutorial-NPCs-listening.html b/docs/0.9.5/Tutorial-NPCs-listening.html
index 4179cf4dd4..9c53221941 100644
--- a/docs/0.9.5/Tutorial-NPCs-listening.html
+++ b/docs/0.9.5/Tutorial-NPCs-listening.html
@@ -252,7 +252,6 @@ Which way to go depends on the design requirements of your particular game.
-
diff --git a/docs/0.9.5/Tutorial-Searching-For-Objects.html b/docs/0.9.5/Tutorial-Searching-For-Objects.html
index 8f701e45ef..e0138f214a 100644
--- a/docs/0.9.5/Tutorial-Searching-For-Objects.html
+++ b/docs/0.9.5/Tutorial-Searching-For-Objects.html
@@ -583,7 +583,6 @@ in a format like the following:
-
diff --git a/docs/0.9.5/Tutorial-Vehicles.html b/docs/0.9.5/Tutorial-Vehicles.html
index 8d9a52a1b7..94c322e83d 100644
--- a/docs/0.9.5/Tutorial-Vehicles.html
+++ b/docs/0.9.5/Tutorial-Vehicles.html
@@ -652,7 +652,6 @@ direction to which room it goes.
-
diff --git a/docs/0.9.5/Understanding-Color-Tags.html b/docs/0.9.5/Understanding-Color-Tags.html
index 27c5f0e610..dba7e92343 100644
--- a/docs/0.9.5/Understanding-Color-Tags.html
+++ b/docs/0.9.5/Understanding-Color-Tags.html
@@ -266,7 +266,6 @@ push it over the limit, so to speak.
-
diff --git a/docs/0.9.5/Updating-Your-Game.html b/docs/0.9.5/Updating-Your-Game.html
index eb9be692a2..01a95baad4 100644
--- a/docs/0.9.5/Updating-Your-Game.html
+++ b/docs/0.9.5/Updating-Your-Game.html
@@ -235,7 +235,6 @@ you then just run e
-
diff --git a/docs/0.9.5/Using-Travis.html b/docs/0.9.5/Using-Travis.html
index 5bab3a991c..db545b3700 100644
--- a/docs/0.9.5/Using-Travis.html
+++ b/docs/0.9.5/Using-Travis.html
@@ -144,7 +144,6 @@ to that for making tests fitting your game.
-
diff --git a/docs/0.9.5/Weather-Tutorial.html b/docs/0.9.5/Weather-Tutorial.html
index 747905b60f..1e9d040290 100644
--- a/docs/0.9.5/Weather-Tutorial.html
+++ b/docs/0.9.5/Weather-Tutorial.html
@@ -163,7 +163,6 @@ weather came before it. Expanding it to be more realistic is a useful exercise.<
-
diff --git a/docs/0.9.5/Web-Character-Generation.html b/docs/0.9.5/Web-Character-Generation.html
index 5f544d5e9a..4df20b24d9 100644
--- a/docs/0.9.5/Web-Character-Generation.html
+++ b/docs/0.9.5/Web-Character-Generation.html
@@ -991,7 +991,6 @@ to see what happens. And do the same while checking the checkbox!
-
diff --git a/docs/0.9.5/Webclient-brainstorm.html b/docs/0.9.5/Webclient-brainstorm.html
index ddf25eee93..e0db573e5d 100644
--- a/docs/0.9.5/Webclient-brainstorm.html
+++ b/docs/0.9.5/Webclient-brainstorm.html
@@ -437,7 +437,6 @@ could stay in this mode, but they could also “lock” the gui layout at any ti
-importcopy
-importitertools
-importoperator
-fromfunctoolsimporttotal_ordering,wraps
-
-
-classcached_property:
- """
- Decorator that converts a method with a single self argument into a
- property cached on the instance.
-
- A cached property can be made out of an existing method:
- (e.g. ``url = cached_property(get_absolute_url)``).
- The optional ``name`` argument is obsolete as of Python 3.6 and will be
- deprecated in Django 4.0 (#30127).
- """
- name=None
-
- @staticmethod
- deffunc(instance):
- raiseTypeError(
- 'Cannot use cached_property instance without calling '
- '__set_name__() on it.'
- )
-
- def__init__(self,func,name=None):
- self.real_func=func
- self.__doc__=getattr(func,'__doc__')
-
- def__set_name__(self,owner,name):
- ifself.nameisNone:
- self.name=name
- self.func=self.real_func
- elifname!=self.name:
- raiseTypeError(
- "Cannot assign the same cached_property to two different names "
- "(%r and %r)."%(self.name,name)
- )
-
- def__get__(self,instance,cls=None):
- """
- Call the function and put the return value in instance.__dict__ so that
- subsequent attribute access on the instance returns the cached value
- instead of calling cached_property.__get__().
- """
- ifinstanceisNone:
- returnself
- res=instance.__dict__[self.name]=self.func(instance)
- returnres
-
-
-classclassproperty:
- """
- Decorator that converts a method with a single cls argument into a property
- that can be accessed directly from the class.
- """
- def__init__(self,method=None):
- self.fget=method
-
- def__get__(self,instance,cls=None):
- returnself.fget(cls)
-
- defgetter(self,method):
- self.fget=method
- returnself
-
-
-classPromise:
- """
- Base class for the proxy class created in the closure of the lazy function.
- It's used to recognize promises in code.
- """
- pass
-
-
-deflazy(func,*resultclasses):
- """
- Turn any callable into a lazy evaluated callable. result classes or types
- is required -- at least one is needed so that the automatic forcing of
- the lazy evaluation code is triggered. Results are not memoized; the
- function is evaluated on every access.
- """
-
- @total_ordering
- class__proxy__(Promise):
- """
- Encapsulate a function call and act as a proxy for methods that are
- called on the result of that function. The function is not evaluated
- until one of the methods on the result is called.
- """
- __prepared=False
-
- def__init__(self,args,kw):
- self.__args=args
- self.__kw=kw
- ifnotself.__prepared:
- self.__prepare_class__()
- self.__class__.__prepared=True
-
- def__reduce__(self):
- return(
- _lazy_proxy_unpickle,
- (func,self.__args,self.__kw)+resultclasses
- )
-
- def__repr__(self):
- returnrepr(self.__cast())
-
- @classmethod
- def__prepare_class__(cls):
- forresultclassinresultclasses:
- fortype_inresultclass.mro():
- formethod_nameintype_.__dict__:
- # All __promise__ return the same wrapper method, they
- # look up the correct implementation when called.
- ifhasattr(cls,method_name):
- continue
- meth=cls.__promise__(method_name)
- setattr(cls,method_name,meth)
- cls._delegate_bytes=bytesinresultclasses
- cls._delegate_text=strinresultclasses
- assertnot(cls._delegate_bytesandcls._delegate_text),(
- "Cannot call lazy() with both bytes and text return types.")
- ifcls._delegate_text:
- cls.__str__=cls.__text_cast
- elifcls._delegate_bytes:
- cls.__bytes__=cls.__bytes_cast
-
- @classmethod
- def__promise__(cls,method_name):
- # Builds a wrapper around some magic method
- def__wrapper__(self,*args,**kw):
- # Automatically triggers the evaluation of a lazy value and
- # applies the given magic method of the result type.
- res=func(*self.__args,**self.__kw)
- returngetattr(res,method_name)(*args,**kw)
- return__wrapper__
-
- def__text_cast(self):
- returnfunc(*self.__args,**self.__kw)
-
- def__bytes_cast(self):
- returnbytes(func(*self.__args,**self.__kw))
-
- def__bytes_cast_encoded(self):
- returnfunc(*self.__args,**self.__kw).encode()
-
- def__cast(self):
- ifself._delegate_bytes:
- returnself.__bytes_cast()
- elifself._delegate_text:
- returnself.__text_cast()
- else:
- returnfunc(*self.__args,**self.__kw)
-
- def__str__(self):
- # object defines __str__(), so __prepare_class__() won't overload
- # a __str__() method from the proxied class.
- returnstr(self.__cast())
-
- def__eq__(self,other):
- ifisinstance(other,Promise):
- other=other.__cast()
- returnself.__cast()==other
-
- def__lt__(self,other):
- ifisinstance(other,Promise):
- other=other.__cast()
- returnself.__cast()<other
-
- def__hash__(self):
- returnhash(self.__cast())
-
- def__mod__(self,rhs):
- ifself._delegate_text:
- returnstr(self)%rhs
- returnself.__cast()%rhs
-
- def__add__(self,other):
- returnself.__cast()+other
-
- def__radd__(self,other):
- returnother+self.__cast()
-
- def__deepcopy__(self,memo):
- # Instances of this class are effectively immutable. It's just a
- # collection of functions. So we don't need to do anything
- # complicated for copying.
- memo[id(self)]=self
- returnself
-
- @wraps(func)
- def__wrapper__(*args,**kw):
- # Creates the proxy object, instead of the actual value.
- return__proxy__(args,kw)
-
- return__wrapper__
-
-
-def_lazy_proxy_unpickle(func,args,kwargs,*resultclasses):
- returnlazy(func,*resultclasses)(*args,**kwargs)
-
-
-deflazystr(text):
- """
- Shortcut for the common case of a lazy callable that returns str.
- """
- returnlazy(str,str)(text)
-
-
-defkeep_lazy(*resultclasses):
- """
- A decorator that allows a function to be called with one or more lazy
- arguments. If none of the args are lazy, the function is evaluated
- immediately, otherwise a __proxy__ is returned that will evaluate the
- function when needed.
- """
- ifnotresultclasses:
- raiseTypeError("You must pass at least one argument to keep_lazy().")
-
- defdecorator(func):
- lazy_func=lazy(func,*resultclasses)
-
- @wraps(func)
- defwrapper(*args,**kwargs):
- ifany(isinstance(arg,Promise)forarginitertools.chain(args,kwargs.values())):
- returnlazy_func(*args,**kwargs)
- returnfunc(*args,**kwargs)
- returnwrapper
- returndecorator
-
-
-defkeep_lazy_text(func):
- """
- A decorator for functions that accept lazy arguments and return text.
- """
- returnkeep_lazy(str)(func)
-
-
-empty=object()
-
-
-defnew_method_proxy(func):
- definner(self,*args):
- ifself._wrappedisempty:
- self._setup()
- returnfunc(self._wrapped,*args)
- returninner
-
-
-classLazyObject:
- """
- A wrapper for another class that can be used to delay instantiation of the
- wrapped class.
-
- By subclassing, you have the opportunity to intercept and alter the
- instantiation. If you don't need to do that, use SimpleLazyObject.
- """
-
- # Avoid infinite recursion when tracing __init__ (#19456).
- _wrapped=None
-
- def__init__(self):
- # Note: if a subclass overrides __init__(), it will likely need to
- # override __copy__() and __deepcopy__() as well.
- self._wrapped=empty
-
- __getattr__=new_method_proxy(getattr)
-
- def__setattr__(self,name,value):
- ifname=="_wrapped":
- # Assign to __dict__ to avoid infinite __setattr__ loops.
- self.__dict__["_wrapped"]=value
- else:
- ifself._wrappedisempty:
- self._setup()
- setattr(self._wrapped,name,value)
-
- def__delattr__(self,name):
- ifname=="_wrapped":
- raiseTypeError("can't delete _wrapped.")
- ifself._wrappedisempty:
- self._setup()
- delattr(self._wrapped,name)
-
- def_setup(self):
- """
- Must be implemented by subclasses to initialize the wrapped object.
- """
- raiseNotImplementedError('subclasses of LazyObject must provide a _setup() method')
-
- # Because we have messed with __class__ below, we confuse pickle as to what
- # class we are pickling. We're going to have to initialize the wrapped
- # object to successfully pickle it, so we might as well just pickle the
- # wrapped object since they're supposed to act the same way.
- #
- # Unfortunately, if we try to simply act like the wrapped object, the ruse
- # will break down when pickle gets our id(). Thus we end up with pickle
- # thinking, in effect, that we are a distinct object from the wrapped
- # object, but with the same __dict__. This can cause problems (see #25389).
- #
- # So instead, we define our own __reduce__ method and custom unpickler. We
- # pickle the wrapped object as the unpickler's argument, so that pickle
- # will pickle it normally, and then the unpickler simply returns its
- # argument.
- def__reduce__(self):
- ifself._wrappedisempty:
- self._setup()
- return(unpickle_lazyobject,(self._wrapped,))
-
- def__copy__(self):
- ifself._wrappedisempty:
- # If uninitialized, copy the wrapper. Use type(self), not
- # self.__class__, because the latter is proxied.
- returntype(self)()
- else:
- # If initialized, return a copy of the wrapped object.
- returncopy.copy(self._wrapped)
-
- def__deepcopy__(self,memo):
- ifself._wrappedisempty:
- # We have to use type(self), not self.__class__, because the
- # latter is proxied.
- result=type(self)()
- memo[id(self)]=result
- returnresult
- returncopy.deepcopy(self._wrapped,memo)
-
- __bytes__=new_method_proxy(bytes)
- __str__=new_method_proxy(str)
- __bool__=new_method_proxy(bool)
-
- # Introspection support
- __dir__=new_method_proxy(dir)
-
- # Need to pretend to be the wrapped class, for the sake of objects that
- # care about this (especially in equality tests)
- __class__=property(new_method_proxy(operator.attrgetter("__class__")))
- __eq__=new_method_proxy(operator.eq)
- __lt__=new_method_proxy(operator.lt)
- __gt__=new_method_proxy(operator.gt)
- __ne__=new_method_proxy(operator.ne)
- __hash__=new_method_proxy(hash)
-
- # List/Tuple/Dictionary methods support
- __getitem__=new_method_proxy(operator.getitem)
- __setitem__=new_method_proxy(operator.setitem)
- __delitem__=new_method_proxy(operator.delitem)
- __iter__=new_method_proxy(iter)
- __len__=new_method_proxy(len)
- __contains__=new_method_proxy(operator.contains)
-
-
-defunpickle_lazyobject(wrapped):
- """
- Used to unpickle lazy objects. Just return its argument, which will be the
- wrapped object.
- """
- returnwrapped
-
-
-classSimpleLazyObject(LazyObject):
- """
- A lazy object initialized from any function.
-
- Designed for compound objects of unknown type. For builtins or objects of
- known type, use django.utils.functional.lazy.
- """
- def__init__(self,func):
- """
- Pass in a callable that returns the object to be wrapped.
-
- If copies are made of the resulting SimpleLazyObject, which can happen
- in various circumstances within Django, then you must ensure that the
- callable can be safely run more than once and will return the same
- value.
- """
- self.__dict__['_setupfunc']=func
- super().__init__()
-
- def_setup(self):
- self._wrapped=self._setupfunc()
-
- # Return a meaningful representation of the lazy object for debugging
- # without evaluating the wrapped object.
- def__repr__(self):
- ifself._wrappedisempty:
- repr_attr=self._setupfunc
- else:
- repr_attr=self._wrapped
- return'<%s: %r>'%(type(self).__name__,repr_attr)
-
- def__copy__(self):
- ifself._wrappedisempty:
- # If uninitialized, copy the wrapper. Use SimpleLazyObject, not
- # self.__class__, because the latter is proxied.
- returnSimpleLazyObject(self._setupfunc)
- else:
- # If initialized, return a copy of the wrapped object.
- returncopy.copy(self._wrapped)
-
- def__deepcopy__(self,memo):
- ifself._wrappedisempty:
- # We have to use SimpleLazyObject, not self.__class__, because the
- # latter is proxied.
- result=SimpleLazyObject(self._setupfunc)
- memo[id(self)]=result
- returnresult
- returncopy.deepcopy(self._wrapped,memo)
-
-
-defpartition(predicate,values):
- """
- Split the values into two sets, based on the return value of the function
- (True/False). e.g.:
-
- >>> partition(lambda x: x > 3, range(5))
- [0, 1, 2, 3], [4]
- """
- results=([],[])
- foriteminvalues:
- results[predicate(item)].append(item)
- returnresults
-
-
-
-
\ No newline at end of file
diff --git a/docs/0.9.5/_modules/evennia.html b/docs/0.9.5/_modules/evennia.html
index 63c895a17b..1dcdc09634 100644
--- a/docs/0.9.5/_modules/evennia.html
+++ b/docs/0.9.5/_modules/evennia.html
@@ -135,7 +135,6 @@
TASK_HANDLER=NoneTICKER_HANDLER=NoneMONITOR_HANDLER=None
-CHANNEL_HANDLER=None# ContainersGLOBAL_SCRIPTS=None
@@ -189,7 +188,7 @@
globalsignalsglobalsettings,lockfuncs,logger,utils,gametime,ansi,spawn,managersglobalcontrib,TICKER_HANDLER,MONITOR_HANDLER,SESSION_HANDLER
- globalCHANNEL_HANDLER,TASK_HANDLER
+ globalTASK_HANDLERglobalGLOBAL_SCRIPTS,OPTION_CLASSESglobalEvMenu,EvTable,EvForm,EvMore,EvEditorglobalANSIString
@@ -252,7 +251,6 @@
from.scripts.tickerhandlerimportTICKER_HANDLERfrom.scripts.taskhandlerimportTASK_HANDLERfrom.server.sessionhandlerimportSESSION_HANDLER
- from.comms.channelhandlerimportCHANNEL_HANDLERfrom.scripts.monitorhandlerimportMONITOR_HANDLER# containers
@@ -381,7 +379,6 @@
CMD_NOINPUT - no input was given on command line CMD_NOMATCH - no valid command key was found CMD_MULTIMATCH - multiple command matches were found
- CMD_CHANNEL - the command name is a channel name CMD_LOGINSTART - this command will be called as the very first command when an account connects to the server.
@@ -396,7 +393,6 @@
CMD_NOINPUT=cmdhandler.CMD_NOINPUTCMD_NOMATCH=cmdhandler.CMD_NOMATCHCMD_MULTIMATCH=cmdhandler.CMD_MULTIMATCH
- CMD_CHANNEL=cmdhandler.CMD_CHANNELCMD_LOGINSTART=cmdhandler.CMD_LOGINSTARTdelcmdhandler
@@ -514,7 +510,6 @@
[docs]defget_display_name(self,looker,**kwargs):
+ """
+ This is used by channels and other OOC communications methods to give a
+ custom display of this account's input.
+
+ Args:
+ looker (Account): The one that will see this name.
+ **kwargs: Unused by default, can be used to pass game-specific data.
+
+ Returns:
+ str: The name, possibly modified.
+
+ """
+ returnf"|c{self.key}|n"
+
# session-related methods
[docs]defdisconnect_session_from_account(self,session,reason=None):
@@ -316,11 +334,11 @@
raiseRuntimeError("Session not found")ifself.get_puppet(session)==obj:# already puppeting this object
- self.msg(_("You are already puppeting this object."))
+ self.msg("You are already puppeting this object.")returnifnotobj.access(self,"puppet"):# no access
- self.msg(_("You don't have permission to puppet '{key}'.").format(key=obj.key))
+ self.msg("You don't have permission to puppet '{obj.key}'.")returnifobj.account:# object already puppeted
@@ -336,8 +354,8 @@
else:txt1=f"Taking over |c{obj.name}|n from another of your sessions."txt2=f"|c{obj.name}|n|R is now acted from another of your sessions.|n"
- self.msg(_(txt1),session=session)
- self.msg(_(txt2),session=obj.sessions.all())
+ self.msg(txt1,session=session)
+ self.msg(txt2,session=obj.sessions.all())self.unpuppet_object(obj.sessions.get())elifobj.account.is_connected:# controlled by another account
@@ -359,8 +377,6 @@
obj.account=selfsession.puid=obj.idsession.puppet=obj
- # validate/start persistent scripts on object
- obj.scripts.validate()# re-cache locks to make sure superuser bypass is updatedobj.locks.cache_lock_bypass(obj)
@@ -569,7 +585,7 @@
# Update throttleifip:
- LOGIN_THROTTLE.update(ip,"Too many authentication failures.")
+ LOGIN_THROTTLE.update(ip,_("Too many authentication failures."))# Try to call post-failure hooksession=kwargs.get("session",None)
@@ -684,8 +700,9 @@
password (str): Password to set. Notes:
- This is called by Django also when logging in; it should not be mixed up with validation, since that
- would mean old passwords in the database (pre validation checks) could get invalidated.
+ This is called by Django also when logging in; it should not be mixed up with
+ validation, since that would mean old passwords in the database (pre validation checks)
+ could get invalidated. """super(DefaultAccount,self).set_password(password)
@@ -824,12 +841,10 @@
)logger.log_sec(f"Account Created: {account} (IP: {ip}).")
- exceptExceptionase:
+ exceptException:errors.append(
- _(
- "There was an error creating the Account. If this problem persists, contact an admin."
- )
- )
+ _("There was an error creating the Account. "
+ "If this problem persists, contact an admin."))logger.log_trace()returnNone,errors
@@ -845,7 +860,7 @@
# join the new account to the public channelpchannel=ChannelDB.objects.get_channel(settings.DEFAULT_CHANNELS[0]["key"])ifnotpchannelornotpchannel.connect(account):
- string=f"New account '{account.key}' could not connect to public channel!"
+ string="New account '{account.key}' could not connect to public channel!"errors.append(string)logger.log_err(string)
@@ -988,6 +1003,89 @@
self,raw_string,callertype="account",session=session,**kwargs)
+ # channel receive hooks
+
+
[docs]defat_pre_channel_msg(self,message,channel,senders=None,**kwargs):
+ """
+ Called by the Channel just before passing a message into `channel_msg`.
+ This allows for tweak messages per-user and also to abort the
+ receive on the receiver-level.
+
+ Args:
+ message (str): The message sent to the channel.
+ channel (Channel): The sending channel.
+ senders (list, optional): Accounts or Objects acting as senders.
+ For most normal messages, there is only a single sender. If
+ there are no senders, this may be a broadcasting message.
+ **kwargs: These are additional keywords passed into `channel_msg`.
+ If `no_prefix=True` or `emit=True` are passed, the channel
+ prefix will not be added (`[channelname]: ` by default)
+
+ Returns:
+ str or None: Allows for customizing the message for this recipient.
+ If returning `None` (or `False`) message-receiving is aborted.
+ The returning string will be passed into `self.channel_msg`.
+
+ Notes:
+ This support posing/emotes by starting channel-send with : or ;.
+
+ """
+ ifsenders:
+ sender_string=', '.join(sender.get_display_name(self)forsenderinsenders)
+ message_lstrip=message.lstrip()
+ ifmessage_lstrip.startswith((':',';')):
+ # this is a pose, should show as e.g. "User1 smiles to channel"
+ spacing=""ifmessage_lstrip[1:].startswith((':','\'',','))else" "
+ message=f"{sender_string}{spacing}{message_lstrip[1:]}"
+ else:
+ # normal message
+ message=f"{sender_string}: {message}"
+
+ ifnotkwargs.get("no_prefix")ornotkwargs.get("emit"):
+ message=channel.channel_prefix()+message
+
+ returnmessage
+
+
[docs]defchannel_msg(self,message,channel,senders=None,**kwargs):
+ """
+ This performs the actions of receiving a message to an un-muted
+ channel.
+
+ Args:
+ message (str): The message sent to the channel.
+ channel (Channel): The sending channel.
+ senders (list, optional): Accounts or Objects acting as senders.
+ For most normal messages, there is only a single sender. If
+ there are no senders, this may be a broadcasting message or
+ similar.
+ **kwargs: These are additional keywords originally passed into
+ `Channel.msg`.
+
+ Notes:
+ Before this, `Channel.at_pre_channel_msg` will fire, which offers a way
+ to customize the message for the receiver on the channel-level.
+
+ """
+ self.msg(text=(message,{"from_channel":channel.id}),
+ from_obj=senders,options={"from_channel":channel.id})
+
+
[docs]defat_post_channel_msg(self,message,channel,senders=None,**kwargs):
+ """
+ Called by `self.channel_msg` after message was received.
+
+ Args:
+ message (str): The message sent to the channel.
+ channel (Channel): The sending channel.
+ senders (list, optional): Accounts or Objects acting as senders.
+ For most normal messages, there is only a single sender. If
+ there are no senders, this may be a broadcasting message.
+ **kwargs: These are additional keywords passed into `channel_msg`.
+
+ """
+ pass
+
+ # search method
+
[docs]defsearch(self,searchdata,
@@ -1295,30 +1393,42 @@
def_send_to_connect_channel(self,message):"""
- Helper method for loading and sending to the comm channel
- dedicated to connection messages.
+ Helper method for loading and sending to the comm channel dedicated to
+ connection messages. This will also be sent to the mudinfo channel. Args: message (str): A message to send to the connect channel. """
- global_MUDINFO_CHANNEL
- ifnot_MUDINFO_CHANNEL:
- try:
- _MUDINFO_CHANNEL=ChannelDB.objects.filter(db_key=settings.CHANNEL_MUDINFO["key"])[
- 0
- ]
- exceptException:
- logger.log_trace()
+ global_MUDINFO_CHANNEL,_CONNECT_CHANNEL
+ if_MUDINFO_CHANNELisNone:
+ ifsettings.CHANNEL_MUDINFO:
+ try:
+ _MUDINFO_CHANNEL=ChannelDB.objects.get(
+ db_key=settings.CHANNEL_MUDINFO["key"])
+ exceptChannelDB.DoesNotExist:
+ logger.log_trace()
+ else:
+ _MUDINFO=False
+ if_CONNECT_CHANNELisNone:
+ ifsettings.CHANNEL_CONNECTINFO:
+ try:
+ _CONNECT_CHANNEL=ChannelDB.objects.get(
+ db_key=settings.CHANNEL_CONNECTINFO["key"])
+ exceptChannelDB.DoesNotExist:
+ logger.log_trace()
+ else:
+ _CONNECT_CHANNEL=False
+
ifsettings.USE_TZ:now=timezone.localtime()else:now=timezone.now()now="%02i-%02i-%02i(%02i:%02i)"%(now.year,now.month,now.day,now.hour,now.minute)if_MUDINFO_CHANNEL:
- _MUDINFO_CHANNEL.tempmsg(f"[{_MUDINFO_CHANNEL.key}, {now}]: {message}")
- else:
- logger.log_info(f"[{now}]: {message}")
+ _MUDINFO_CHANNEL.msg(f"[{now}]: {message}")
+ if_CONNECT_CHANNEL:
+ _CONNECT_CHANNEL.msg(f"[{now}]: {message}")
[docs]defat_post_login(self,session=None,**kwargs):"""
@@ -1505,7 +1615,7 @@
ifhasattr(target,"return_appearance"):returntarget.return_appearance(self)else:
- return_("{target} has no in-game appearance.").format(target=target)
+ returnf"{target} has no in-game appearance."else:# list of targets - make list to disconnect from dbcharacters=list(tarfortarintargetiftar)iftargetelse[]
@@ -1548,19 +1658,18 @@
ifis_suorlen(characters)<charmax:ifnotcharacters:result.append(
- _(
- "\n\n You don't have any characters yet. See |whelp @charcreate|n for creating one."
- )
+ "\n\n You don't have any characters yet. See |whelp charcreate|n for "
+ "creating one.")else:
- result.append("\n |w@charcreate <name> [=description]|n - create new character")
+ result.append("\n |wcharcreate <name> [=description]|n - create new character")result.append(
- "\n |w@chardelete <name>|n - delete a character (cannot be undone!)"
+ "\n |wchardelete <name>|n - delete a character (cannot be undone!)")ifcharacters:string_s_ending=len(characters)>1and"s"or""
- result.append("\n |w@ic <character>|n - enter the game (|w@ooc|n to get back here)")
+ result.append("\n |wic <character>|n - enter the game (|wooc|n to get back here)")ifis_su:result.append(f"\n\nAvailable character{string_s_ending} ({len(characters)}/unlimited):"
@@ -1582,11 +1691,12 @@
sid=sessinsessionsandsessions.index(sess)+1ifsessandsid:result.append(
- f"\n - |G{char.key}|n [{', '.join(char.permissions.all())}] (played by you in session {sid})"
- )
+ f"\n - |G{char.key}|n [{', '.join(char.permissions.all())}] "
+ f"(played by you in session {sid})")else:result.append(
- f"\n - |R{char.key}|n [{', '.join(char.permissions.all())}] (played by someone else)"
+ f"\n - |R{char.key}|n [{', '.join(char.permissions.all())}] "
+ "(played by someone else)")else:# character is "free to puppet"
@@ -1595,21 +1705,23 @@
returnlook_string
-classDefaultGuest(DefaultAccount):
+
[docs]classDefaultGuest(DefaultAccount):""" This class is used for guest logins. Unlike Accounts, Guests and their characters are deleted after disconnection.
+
"""
- @classmethod
+
[docs]@classmethoddefcreate(cls,**kwargs):""" Forwards request to cls.authenticate(); returns a DefaultGuest object if one is available for use.
- """
- returncls.authenticate(**kwargs)
- @classmethod
+ """
+ returncls.authenticate(**kwargs)
+
+
[docs]@classmethoddefauthenticate(cls,**kwargs):""" Gets or creates a Guest account object.
@@ -1673,7 +1785,7 @@
returnaccount,errors
- exceptExceptionase:
+ exceptException:# We are in the middle between logged in and -not, so we have# to handle tracebacks ourselves at this point. If we don't,# we won't see any errors at all.
@@ -1681,9 +1793,9 @@
logger.log_trace()returnNone,errors
- returnaccount,errors
+ returnaccount,errors
- defat_post_login(self,session=None,**kwargs):
+
[docs]defat_post_login(self,session=None,**kwargs):""" In theory, guests only have one character regardless of which MULTISESSION_MODE we're in. They don't get a choice.
@@ -1695,9 +1807,9 @@
"""self._send_to_connect_channel(_("|G{key} connected|n").format(key=self.key))
- self.puppet_object(session,self.db._last_puppet)
+ self.puppet_object(session,self.db._last_puppet)
- defat_server_shutdown(self):
+
[docs]defat_server_shutdown(self):""" We repeat the functionality of `at_disconnect()` here just to be on the safe side.
@@ -1706,9 +1818,9 @@
characters=self.db._playable_charactersforcharacterincharacters:ifcharacter:
- character.delete()
+ character.delete()
- defat_post_disconnect(self,**kwargs):
+
[docs]defat_post_disconnect(self,**kwargs):""" Once having disconnected, destroy the guest's characters and
@@ -1722,7 +1834,7 @@
forcharacterincharacters:ifcharacter:character.delete()
- self.delete()
+ self.delete()
[docs]classAccountDB(TypedObject,AbstractUser):""" This is a special model using Django's 'profile' functionality and extends the default Django User model. It is defined as such
@@ -135,7 +135,8 @@
"cmdset",max_length=255,null=True,
- help_text="optional python path to a cmdset class. If creating a Character, this will default to settings.CMDSET_CHARACTER.",
+ help_text="optional python path to a cmdset class. If creating a Character, this will "
+ "default to settings.CMDSET_CHARACTER.",)# marks if this is a "virtual" bot account objectdb_is_bot=models.BooleanField(
@@ -150,8 +151,8 @@
__applabel__="accounts"__settingsclasspath__=settings.BASE_SCRIPT_TYPECLASS
- # class Meta:
- # verbose_name = "Account"
+ classMeta:
+ verbose_name="Account"# cmdset_storage property# This seems very sensitive to caching, so leaving it be for now /Griatch
@@ -257,7 +258,6 @@
diff --git a/docs/0.9.5/_modules/evennia/commands/cmdhandler.html b/docs/0.9.5/_modules/evennia/commands/cmdhandler.html
index 43fba3d2dc..feb200b0b5 100644
--- a/docs/0.9.5/_modules/evennia/commands/cmdhandler.html
+++ b/docs/0.9.5/_modules/evennia/commands/cmdhandler.html
@@ -47,10 +47,6 @@
1. The calling object (caller) is analyzed based on its callertype.2. Cmdsets are gathered from different sources:
- - channels: all available channel names are auto-created into a cmdset, to allow
- for giving the channel name and have the following immediately
- sent to the channel. The sending is performed by the CMD_CHANNEL
- system command. - object cmdsets: all objects at caller's location are scanned for non-empty cmdsets. This includes cmdsets on exits. - caller: the caller is searched for its own currently active cmdset.
@@ -64,14 +60,12 @@
cmdset, or fallback to error message. Exit.7. If no match was found -> check for CMD_NOMATCH in current cmdset or fallback to error message. Exit.
-8. A single match was found. If this is a channel-command (i.e. the
- ommand name is that of a channel), --> check for CMD_CHANNEL in
- current cmdset or use channelhandler default. Exit.
-9. At this point we have found a normal command. We assign useful variables to it that
+8. At this point we have found a normal command. We assign useful variables to it that will be available to the command coder at run-time.
-12. We have a unique cmdobject, primed for use. Call all hooks:
+9. We have a unique cmdobject, primed for use. Call all hooks: `at_pre_cmd()`, `cmdobj.parse()`, `cmdobj.func()` and finally `at_post_cmd()`.
-13. Return deferred that will fire with the return from `cmdobj.func()` (unused by default).
+10. Return deferred that will fire with the return from `cmdobj.func()` (unused by default).
+
"""fromcollectionsimportdefaultdict
@@ -85,7 +79,6 @@
fromtwisted.internet.deferimportinlineCallbacks,returnValuefromdjango.confimportsettingsfromevennia.commands.commandimportInterruptCommand
-fromevennia.comms.channelhandlerimportCHANNELHANDLERfromevennia.utilsimportlogger,utilsfromevennia.utils.utilsimportstring_suggestions
@@ -116,12 +109,11 @@
CMD_NOMATCH="__nomatch_command"# command to call if multiple command matches were foundCMD_MULTIMATCH="__multimatch_command"
-# command to call if found command is the name of a channel
-CMD_CHANNEL="__send_to_channel_command"# command to call as the very first one when the user connects.# (is expected to display the login screen)CMD_LOGINSTART="__unloggedin_look_command"
+
# Function for handling multiple command matches._SEARCH_AT_RESULT=utils.variable_from_module(*settings.SEARCH_AT_RESULT.rsplit(".",1))
@@ -129,50 +121,50 @@
# is the normal "production message to echo to the account._ERROR_UNTRAPPED=(
- """
-An untrapped error occurred.
-""",
- """
-An untrapped error occurred. Please file a bug report detailing the steps to reproduce.
-""",
+ _("""
+An untrapped error occurred.
+"""),
+ _("""
+An untrapped error occurred. Please file a bug report detailing the steps to reproduce.
+"""),)_ERROR_CMDSETS=(
- """
-A cmdset merger-error occurred. This is often due to a syntax
-error in one of the cmdsets to merge.
-""",
- """
-A cmdset merger-error occurred. Please file a bug report detailing the
-steps to reproduce.
-""",
+ _("""
+A cmdset merger-error occurred. This is often due to a syntax
+error in one of the cmdsets to merge.
+"""),
+ _("""
+A cmdset merger-error occurred. Please file a bug report detailing the
+steps to reproduce.
+"""),)_ERROR_NOCMDSETS=(
- """
-No command sets found! This is a critical bug that can have
-multiple causes.
-""",
- """
-No command sets found! This is a sign of a critical bug. If
-disconnecting/reconnecting doesn't" solve the problem, try to contact
-the server admin through" some other means for assistance.
-""",
+ _("""
+No command sets found! This is a critical bug that can have
+multiple causes.
+"""),
+ _("""
+No command sets found! This is a sign of a critical bug. If
+disconnecting/reconnecting doesn't" solve the problem, try to contact
+the server admin through" some other means for assistance.
+"""),)_ERROR_CMDHANDLER=(
- """
-A command handler bug occurred. If this is not due to a local change,
-please file a bug report with the Evennia project, including the
-traceback and steps to reproduce.
-""",
- """
-A command handler bug occurred. Please notify staff - they should
-likely file a bug report with the Evennia project.
-""",
+ _("""
+A command handler bug occurred. If this is not due to a local change,
+please file a bug report with the Evennia project, including the
+traceback and steps to reproduce.
+"""),
+ _("""
+A command handler bug occurred. Please notify staff - they should
+likely file a bug report with the Evennia project.
+"""),)
-_ERROR_RECURSION_LIMIT=(
+_ERROR_RECURSION_LIMIT=_("Command recursion limit ({recursion_limit}) ""reached for '{raw_cmdname}' ({cmdclass}).")
@@ -195,7 +187,7 @@
production string (with a timestamp) to be shown to the user. """
- string="{traceback}\n{errmsg}\n(Traceback was logged {timestamp})."
+ string=_("{traceback}\n{errmsg}\n(Traceback was logged {timestamp}).")timestamp=logger.timeformat()tracestring=format_exc()logger.log_trace()
@@ -344,23 +336,11 @@
"""try:
- @inlineCallbacks
- def_get_channel_cmdset(account_or_obj):
- """
- Helper-method; Get channel-cmdsets
- """
- # Create cmdset for all account's available channels
- try:
- channel_cmdset=yieldCHANNELHANDLER.get_cmdset(account_or_obj)
- returnValue([channel_cmdset])
- exceptException:
- _msg_err(caller,_ERROR_CMDSETS)
- raiseErrorReported(raw_string)
-
@inlineCallbacksdef_get_local_obj_cmdsets(obj):""" Helper-method; Get Object-level cmdsets
+
"""# Gather cmdsets from location, objects in location or carriedtry:
@@ -414,6 +394,7 @@
""" Helper method; Get cmdset while making sure to trigger all hooks safely. Returns the stack and the valid options.
+
"""try:yieldobj.at_cmdset_get()
@@ -446,13 +427,6 @@
cmdsetforcmdsetinlocal_obj_cmdsetsifcmdset.key!="ExitCmdSet"]cmdsets+=local_obj_cmdsets
- ifnotcurrent.no_channels:
- # also objs may have channels
- channel_cmdsets=yield_get_channel_cmdset(obj)
- cmdsets+=channel_cmdsets
- ifnotcurrent.no_channels:
- channel_cmdsets=yield_get_channel_cmdset(account)
- cmdsets+=channel_cmdsetselifcallertype=="account":# we are calling the command from the account level
@@ -470,11 +444,6 @@
cmdsetforcmdsetinlocal_obj_cmdsetsifcmdset.key!="ExitCmdSet"]cmdsets+=local_obj_cmdsets
- ifnotcurrent.no_channels:
- # also objs may have channels
- cmdsets+=yield_get_channel_cmdset(obj)
- ifnotcurrent.no_channels:
- cmdsets+=yield_get_channel_cmdset(account)elifcallertype=="object":# we are calling the command from the object level
@@ -488,9 +457,6 @@
cmdsetforcmdsetinlocal_obj_cmdsetsifcmdset.key!="ExitCmdSet"]cmdsets+=yieldlocal_obj_cmdsets
- ifnotcurrent.no_channels:
- # also objs may have channels
- cmdsets+=yield_get_channel_cmdset(obj)else:raiseException("get_and_merge_cmdsets: callertype %s is not valid."%callertype)
@@ -649,11 +615,6 @@
# since this may be different for every command when# merging multiple cmdsets
- ifhasattr(cmd,"obj")andhasattr(cmd.obj,"scripts"):
- # cmd.obj is automatically made available by the cmdhandler.
- # we make sure to validate its scripts.
- yieldcmd.obj.scripts.validate()
-
if_testing:# only return the command instancereturnValue(cmd)
@@ -818,18 +779,6 @@
sysarg+=_(' Type "help" for help.')raiseExecSystemCommand(syscmd,sysarg)
- # Check if this is a Channel-cmd match.
- ifhasattr(cmd,"is_channel")andcmd.is_channel:
- # even if a user-defined syscmd is not defined, the
- # found cmd is already a system command in its own right.
- syscmd=yieldcmdset.get(CMD_CHANNEL)
- ifsyscmd:
- # replace system command with custom version
- cmd=syscmd
- cmd.session=session
- sysarg="%s:%s"%(cmdname,args)
- raiseExecSystemCommand(cmd,sysarg)
-
# A normal command.ret=yield_run_command(cmd,cmdname,args,raw_cmdname,cmdset,session,account)returnValue(ret)
@@ -903,7 +852,6 @@
[docs]deftry_num_differentiators(raw_string):""" Test if user tried to separate multi-matches with a number separator (default 1-name, 2-name etc). This is usually called last, if no other
@@ -167,7 +167,7 @@
# with a #num-command style syntax. We expect the regex to# contain the groups "number" and "name".mindex,new_raw_string=(num_ref_match.group("number"),num_ref_match.group("name"))
- returnmindex,new_raw_string
+ returnint(mindex),new_raw_stringelse:returnNone,None
@@ -211,19 +211,22 @@
ifnotraw_string:return[]
- # find mathces, first using the full name
+ # find matches, first using the full namematches=build_matches(raw_string,cmdset,include_prefixes=True)
- ifnotmatches:
- # try to match a number 1-cmdname, 2-cmdname etc
- mindex,new_raw_string=try_num_prefixes(raw_string)
- ifmindexisnotNone:
- returncmdparser(new_raw_string,cmdset,caller,match_index=int(mindex))
- if_CMD_IGNORE_PREFIXES:
- # still no match. Try to strip prefixes
- raw_string=(
- raw_string.lstrip(_CMD_IGNORE_PREFIXES)iflen(raw_string)>1elseraw_string
- )
- matches=build_matches(raw_string,cmdset,include_prefixes=False)
+
+ ifnotmatchesorlen(matches)>1:
+ # no single match, try parsing for optional numerical tags like 1-cmd
+ # or cmd-2, cmd.2 etc
+ match_index,new_raw_string=try_num_differentiators(raw_string)
+ ifmatch_indexisnotNone:
+ matches.extend(build_matches(new_raw_string,cmdset,include_prefixes=True))
+
+ ifnotmatchesand_CMD_IGNORE_PREFIXES:
+ # still no match. Try to strip prefixes
+ raw_string=(
+ raw_string.lstrip(_CMD_IGNORE_PREFIXES)iflen(raw_string)>1elseraw_string
+ )
+ matches=build_matches(raw_string,cmdset,include_prefixes=False)# only select command matches we are actually allowed to call.matches=[matchformatchinmatchesifmatch[2].access(caller,"cmd")]
@@ -299,7 +302,6 @@
-
diff --git a/docs/0.9.5/_modules/evennia/commands/cmdsethandler.html b/docs/0.9.5/_modules/evennia/commands/cmdsethandler.html
index a2137a6435..d2e520247e 100644
--- a/docs/0.9.5/_modules/evennia/commands/cmdsethandler.html
+++ b/docs/0.9.5/_modules/evennia/commands/cmdsethandler.html
@@ -103,6 +103,7 @@
can then implement separate sets for different situations. Forexample, you can have a 'On a boat' set, onto which you then tack onthe 'Fishing' set. Fishing from a boat? No problem!
+
"""importsysfromtracebackimportformat_exc
@@ -144,7 +145,7 @@
_ERROR_CMDSET_EXCEPTION=_("""{traceback}
-Compile/Run error when loading cmdset '{path}'.",
+Compile/Run error when loading cmdset '{path}'.(Traceback was logged {timestamp})""")
@@ -209,7 +210,7 @@
if"."inpath:modpath,classname=python_path.rsplit(".",1)else:
- raiseImportError("The path '%s' is not on the form modulepath.ClassName"%path)
+ raiseImportError(f"The path '{path}' is not on the form modulepath.ClassName")try:# first try to get from cache
@@ -353,6 +354,7 @@
def__str__(self):""" Display current commands
+
"""strings=["<CmdSetHandler> stack:"]
@@ -462,7 +464,8 @@
self.mergetype_stack.append(new_current.actual_mergetype)self.current=new_current
-
[docs]defadd(self,cmdset,emit_to_obj=None,persistent=True,default_cmdset=False,
+ **kwargs):""" Add a cmdset to the handler, on top of the old ones, unless it is set as the default one (it will then end up at the bottom of the stack)
@@ -471,7 +474,7 @@
cmdset (CmdSet or str): Can be a cmdset object or the python path to such an object. emit_to_obj (Object, optional): An object to receive error messages.
- permanent (bool, optional): This cmdset will remain across a server reboot.
+ persistent (bool, optional): Let cmdset remain across server reload. default_cmdset (Cmdset, optional): Insert this to replace the default cmdset position (there is only one such position, always at the bottom of the stack).
@@ -488,6 +491,11 @@
it's a 'quirk' that has to be documented. """
+ if"permanent"inkwargs:
+ logger.log_dep("obj.cmdset.add() kwarg 'permanent' has changed name to "
+ "'persistent' and now defaults to True.")
+ persistent=kwargs['permanent']ifpersistentisNoneelsepersistent
+
ifnot(isinstance(cmdset,str)orutils.inherits_from(cmdset,CmdSet)):string=_("Only CmdSets can be added to the cmdsethandler!")raiseException(string)
@@ -498,8 +506,8 @@
# this is (maybe) a python path. Try to import from cache.cmdset=self._import_cmdset(cmdset)ifcmdsetandcmdset.key!="_CMDSET_ERROR":
- cmdset.permanent=permanent
- ifpermanentandcmdset.key!="_CMDSET_ERROR":
+ cmdset.permanent=persistent# TODO change on cmdset too
+ ifpersistentandcmdset.key!="_CMDSET_ERROR":# store the path permanentlystorage=self.obj.cmdset_storageor[""]ifdefault_cmdset:
@@ -513,17 +521,21 @@
self.cmdset_stack.append(cmdset)self.update()
[docs]defadd_default(self,cmdset,emit_to_obj=None,persistent=True,**kwargs):""" Shortcut for adding a default cmdset. Args: cmdset (Cmdset): The Cmdset to add. emit_to_obj (Object, optional): Gets error messages
- permanent (bool, optional): The new Cmdset should survive a server reboot.
+ persistent (bool, optional): The new Cmdset should survive a server reboot. """
- self.add(cmdset,emit_to_obj=emit_to_obj,permanent=permanent,default_cmdset=True)
+ if"permanent"inkwargs:
+ logger.log_dep("obj.cmdset.add_default() kwarg 'permanent' has changed name to "
+ "'persistent'.")
+ persistent=kwargs['permanent']ifpersistentisNoneelsepersistent
+ self.add(cmdset,emit_to_obj=emit_to_obj,persistent=persistent,default_cmdset=True)
[docs]classCommand(metaclass=CommandMeta):"""
- Base command
+ ## Base command
+
+ (you may see this if a child command had no help text defined) Usage: command [args]
@@ -551,20 +569,6 @@
)[0]returnsettings.CLIENT_DEFAULT_WIDTH
-
[docs]defclient_height(self):
- """
- Get the client screenheight for the session using this command.
-
- Returns:
- client height (int): The height (in characters) of the client window.
-
- """
- ifself.session:
- returnself.session.protocol_flags.get(
- "SCREENHEIGHT",{0:settings.CLIENT_DEFAULT_HEIGHT}
- )[0]
- returnsettings.CLIENT_DEFAULT_HEIGHT
-
[docs]defstyled_table(self,*args,**kwargs):""" Create an EvTable styled by on user preferences.
@@ -609,6 +613,7 @@
border_left_char=border_left_char,border_right_char=border_right_char,border_top_char=border_top_char,
+ border_bottom_char=border_bottom_char,**kwargs,)returntable
-
diff --git a/docs/0.9.5/_modules/evennia/commands/default/account.html b/docs/0.9.5/_modules/evennia/commands/default/account.html
index e01b8a3494..bb032b4c50 100644
--- a/docs/0.9.5/_modules/evennia/commands/default/account.html
+++ b/docs/0.9.5/_modules/evennia/commands/default/account.html
@@ -861,7 +861,7 @@
testing which colors your client support Usage:
- color ansi||xterm256
+ color ansi | xterm256 Prints a color map along with in-mud color codes to use to produce them. It also tests what is supported in your client. Choices are
@@ -1130,7 +1130,6 @@
"""
-Comsystem command module.
+Communication commands:
-Comm commands are OOC commands and intended to be made available to
-the Account at all times (they go into the AccountCmdSet). So we
-make sure to homogenize self.caller to always be the account object
-for easy handling.
+- channel
+- page
+- irc/rss/grapevine linking"""
-importhashlib
-importtime
+
fromdjango.confimportsettings
-fromevennia.comms.modelsimportChannelDB,Msg
+fromevennia.comms.modelsimportMsgfromevennia.accounts.modelsimportAccountDBfromevennia.accountsimportbots
-fromevennia.comms.channelhandlerimportCHANNELHANDLERfromevennia.locks.lockhandlerimportLockException
-fromevennia.utilsimportcreate,logger,utils,evtable
-fromevennia.utils.utilsimportmake_iter,class_from_module
+fromevennia.comms.commsimportDefaultChannel
+fromevennia.utilsimportcreate,logger,utils
+fromevennia.utils.loggerimporttail_log_file
+fromevennia.utils.utilsimportclass_from_module
+fromevennia.utils.evmenuimportask_yes_noCOMMAND_DEFAULT_CLASS=class_from_module(settings.COMMAND_DEFAULT_CLASS)CHANNEL_DEFAULT_TYPECLASS=class_from_module(
@@ -66,18 +66,20 @@
# limit symbol import for API__all__=(
+ "CmdChannel",
+
"CmdAddCom","CmdDelCom","CmdAllCom",
- "CmdChannels","CmdCdestroy","CmdCBoot",
- "CmdCemit","CmdCWho","CmdChannelCreate","CmdClock","CmdCdesc",
+
"CmdPage",
+
"CmdIRC2Chan","CmdIRCStatus","CmdRSS2Chan",
@@ -85,36 +87,1212 @@
)_DEFAULT_WIDTH=settings.CLIENT_DEFAULT_WIDTH
+# helper functions to make it easier to override the main CmdChannel
+# command and to keep the legacy addcom etc commands around.
-deffind_channel(caller,channelname,silent=False,noaliases=False):
+
+
[docs]classCmdChannel(COMMAND_DEFAULT_CLASS):"""
- Helper function for searching for a single channel with
- some error handling.
+ Use and manage in-game channels.
+
+ Usage:
+ channel channelname <msg>
+ channel channel name = <msg>
+ channel (show all subscription)
+ channel/all (show available channels)
+ channel/alias channelname = alias[;alias...]
+ channel/unalias alias
+ channel/who channelname
+ channel/history channelname [= index]
+ channel/sub channelname [= alias[;alias...]]
+ channel/unsub channelname[,channelname, ...]
+ channel/mute channelname[,channelname,...]
+ channel/unmute channelname[,channelname,...]
+
+ channel/create channelname[;alias;alias[:typeclass]] [= description]
+ channel/destroy channelname [= reason]
+ channel/desc channelname = description
+ channel/lock channelname = lockstring
+ channel/unlock channelname = lockstring
+ channel/ban channelname (list bans)
+ channel/ban[/quiet] channelname[, channelname, ...] = subscribername [: reason]
+ channel/unban[/quiet] channelname[, channelname, ...] = subscribername
+ channel/boot[/quiet] channelname[,channelname,...] = subscribername [: reason]
+
+ # subtopics
+
+ ## sending
+
+ Usage: channel channelname msg
+ channel channel name = msg (with space in channel name)
+
+ This sends a message to the channel. Note that you will rarely use this
+ command like this; instead you can use the alias
+
+ channelname <msg>
+ channelalias <msg>
+
+ For example
+
+ public Hello World
+ pub Hello World
+
+ (this shortcut doesn't work for aliases containing spaces)
+
+ See channel/alias for help on setting channel aliases.
+
+ ## alias and unalias
+
+ Usage: channel/alias channel = alias[;alias[;alias...]]
+ channel/unalias alias
+ channel - this will list your subs and aliases to each channel
+
+ Set one or more personal aliases for referencing a channel. For example:
+
+ channel/alias warrior's guild = warrior;wguild;warchannel;warrior guild
+
+ You can now send to the channel using all of these:
+
+ warrior's guild Hello
+ warrior Hello
+ wguild Hello
+ warchannel Hello
+
+ Note that this will not work if the alias has a space in it. So the
+ 'warrior guild' alias must be used with the `channel` command:
+
+ channel warrior guild = Hello
+
+ Channel-aliases can be removed one at a time, using the '/unalias' switch.
+
+ ## who
+
+ Usage: channel/who channelname
+
+ List the channel's subscribers. Shows who are currently offline or are
+ muting the channel. Subscribers who are 'muting' will not see messages sent
+ to the channel (use channel/mute to mute a channel).
+
+ ## history
+
+ Usage: channel/history channel [= index]
+
+ This will display the last |c20|n lines of channel history. By supplying an
+ index number, you will step that many lines back before viewing those 20 lines.
+
+ For example:
+
+ channel/history public = 35
+
+ will go back 35 lines and show the previous 20 lines from that point (so
+ lines -35 to -55).
+
+ ## sub and unsub
+
+ Usage: channel/sub channel [=alias[;alias;...]]
+ channel/unsub channel
+
+ This subscribes you to a channel and optionally assigns personal shortcuts
+ for you to use to send to that channel (see aliases). When you unsub, all
+ your personal aliases will also be removed.
+
+ ## mute and unmute
+
+ Usage: channel/mute channelname
+ channel/unmute channelname
+
+ Muting silences all output from the channel without actually
+ un-subscribing. Other channel members will see that you are muted in the /who
+ list. Sending a message to the channel will automatically unmute you.
+
+ ## create and destroy
+
+ Usage: channel/create channelname[;alias;alias[:typeclass]] [= description]
+ channel/destroy channelname [= reason]
+
+ Creates a new channel (or destroys one you control). You will automatically
+ join the channel you create and everyone will be kicked and loose all aliases
+ to a destroyed channel.
+
+ ## lock and unlock
+
+ Usage: channel/lock channelname = lockstring
+ channel/unlock channelname = lockstring
+
+ Note: this is an admin command.
+
+ A lockstring is on the form locktype:lockfunc(). Channels understand three
+ locktypes:
+ listen - who may listen or join the channel.
+ send - who may send messages to the channel
+ control - who controls the channel. This is usually the one creating
+ the channel.
+
+ Common lockfuncs are all() and perm(). To make a channel everyone can
+ listen to but only builders can talk on, use this:
+
+ listen:all()
+ send: perm(Builders)
+
+ ## boot and ban
+
+ Usage:
+ channel/boot[/quiet] channelname[,channelname,...] = subscribername [: reason]
+ channel/ban channelname[, channelname, ...] = subscribername [: reason]
+ channel/unban channelname[, channelname, ...] = subscribername
+ channel/unban channelname
+ channel/ban channelname (list bans)
+
+ Booting will kick a named subscriber from channel(s) temporarily. The
+ 'reason' will be passed to the booted user. Unless the /quiet switch is
+ used, the channel will also be informed of the action. A booted user is
+ still able to re-connect, but they'll have to set up their aliases again.
+
+ Banning will blacklist a user from (re)joining the provided channels. It
+ will then proceed to boot them from those channels if they were connected.
+ The 'reason' and `/quiet` works the same as for booting.
+
+ Example:
+ boot mychannel1 = EvilUser : Kicking you to cool down a bit.
+ ban mychannel1,mychannel2= EvilUser : Was banned for spamming.
+
"""
- channels=CHANNEL_DEFAULT_TYPECLASS.objects.channel_search(channelname)
- ifnotchannels:
- ifnotnoaliases:
- channels=[
- chan
- forchaninCHANNEL_DEFAULT_TYPECLASS.objects.get_all_channels()
- ifchannelnameinchan.aliases.all()
- ]
- ifchannels:
+ key="channel"
+ aliases=["chan","channels"]
+ help_category="Comms"
+ # these cmd: lock controls access to the channel command itself
+ # the admin: lock controls access to /boot/ban/unban switches
+ # the manage: lock controls access to /create/destroy/desc/lock/unlock switches
+ locks="cmd:not pperm(channel_banned);admin:all();manage:all();changelocks:perm(Admin)"
+ switch_options=(
+ "list","all","history","sub","unsub","mute","unmute","alias","unalias",
+ "create","destroy","desc","lock","unlock","boot","ban","unban","who",)
+ # disable this in child command classes if wanting on-character channels
+ account_caller=True
+
+
[docs]defsearch_channel(self,channelname,exact=False,handle_errors=True):
+ """
+ Helper function for searching for a single channel with some error
+ handling.
+
+ Args:
+ channelname (str): Name, alias #dbref or partial name/alias to search
+ for.
+ exact (bool, optional): If an exact or fuzzy-match of the name should be done.
+ Note that even for a fuzzy match, an exactly given, unique channel name
+ will always be returned.
+ handle_errors (bool): If true, use `self.msg` to report errors if
+ there are non/multiple matches. If so, the return will always be
+ a single match or None.
+ Returns:
+ object, list or None: If `handle_errors` is `True`, this is either a found Channel
+ or `None`. Otherwise it's a list of zero, one or more channels found.
+ Notes:
+ The 'listen' and 'control' accesses are checked before returning.
+
+ """
+ caller=self.caller
+ # first see if this is a personal alias
+ channelname=caller.nicks.get(key=channelname,category="channel")orchannelname
+
+ # always try the exact match first.
+ channels=CHANNEL_DEFAULT_TYPECLASS.objects.channel_search(channelname,exact=True)
+
+ ifnotchannelsandnotexact:
+ # try fuzzy matching as well
+ channels=CHANNEL_DEFAULT_TYPECLASS.objects.channel_search(channelname,exact=exact)
+
+ # check permissions
+ channels=[channelforchannelinchannels
+ ifchannel.access(caller,'listen')orchannel.access(caller,'control')]
+
+ ifhandle_errors:
+ ifnotchannels:
+ self.msg(f"No channel found matching '{channelname}' "
+ "(could also be due to missing access).")
+ returnNone
+ eliflen(channels)>1:
+ self.msg("Multiple possible channel matches/alias for "
+ "'{channelname}':\n"+", ".join(chan.keyforchaninchannels))
+ returnNonereturnchannels[0]
- ifnotsilent:
- caller.msg("Channel '%s' not found."%channelname)
- returnNone
- eliflen(channels)>1:
- matches=", ".join(["%s(%s)"%(chan.key,chan.id)forchaninchannels])
- ifnotsilent:
- caller.msg("Multiple channels match (be more specific): \n%s"%matches)
- returnNone
- returnchannels[0]
+ else:
+ ifnotchannels:
+ return[]
+ eliflen(channels)>1:
+ returnlist(channels)
+ return[channels[0]]
+
+
[docs]defmsg_channel(self,channel,message,**kwargs):
+ """
+ Send a message to a given channel. This will check the 'send'
+ permission on the channel.
+
+ Args:
+ channel (Channel): The channel to send to.
+ message (str): The message to send.
+ **kwargs: Unused by default. These kwargs will be passed into
+ all channel messaging hooks for custom overriding.
+
+ """
+ ifnotchannel.access(self.caller,"send"):
+ caller.msg(f"You are not allowed to send messages to channel {channel}")
+ return
+
+ channel.msg(message,senders=self.caller,**kwargs)
+
+
[docs]defget_channel_history(self,channel,start_index=0):
+ """
+ View a channel's history.
+
+ Args:
+ channel (Channel): The channel to access.
+ message (str): The message to send.
+ **kwargs: Unused by default. These kwargs will be passed into
+ all channel messaging hooks for custom overriding.
+
+ """
+ caller=self.caller
+ log_file=channel.get_log_filename()
+
+ defsend_msg(lines):
+ returnself.msg(
+ "".join(line.split("[-]",1)[1]if"[-]"inlineelselineforlineinlines)
+ )
+ # asynchronously tail the log file
+ tail_log_file(log_file,start_index,20,callback=send_msg)
+
+
[docs]defsub_to_channel(self,channel):
+ """
+ Subscribe to a channel. Note that all permissions should
+ be checked before this step.
+
+ Args:
+ channel (Channel): The channel to access.
+
+ Returns:
+ bool, str: True, None if connection failed. If False,
+ the second part is an error string.
+
+ """
+ caller=self.caller
+
+ ifchannel.has_connection(caller):
+ returnFalse,f"Already listening to channel {channel.key}."
+
+ # this sets up aliases in post_join_channel by default
+ result=channel.connect(caller)
+
+ returnresult,""ifresultelsef"Were not allowed to subscribe to channel {channel.key}"
+
+
[docs]defunsub_from_channel(self,channel,**kwargs):
+ """
+ Un-Subscribe to a channel. Note that all permissions should
+ be checked before this step.
+
+ Args:
+ channel (Channel): The channel to unsub from.
+ **kwargs: Passed on to nick removal.
+
+ Returns:
+ bool, str: True, None if un-connection succeeded. If False,
+ the second part is an error string.
+
+ """
+ caller=self.caller
+
+ ifnotchannel.has_connection(caller):
+ returnFalse,f"Not listening to channel {channel.key}."
+
+ # this will also clean aliases
+ result=channel.disconnect(caller)
+
+ returnresult,""ifresultelsef"Could not unsubscribe from channel {channel.key}"
+
+
[docs]defadd_alias(self,channel,alias,**kwargs):
+ """
+ Add a new alias (nick) for the user to use with this channel.
+
+ Args:
+ channel (Channel): The channel to alias.
+ alias (str): The personal alias to use for this channel.
+ **kwargs: If given, passed into nicks.add.
+
+ Note:
+ We add two nicks - one is a plain `alias -> channel.key` that
+ we need to be able to reference this channel easily. The other
+ is a templated nick to easily be able to send messages to the
+ channel without needing to give the full `channel` command. The
+ structure of this nick is given by `self.channel_msg_pattern`
+ and `self.channel_msg_nick_replacement`. By default it maps
+ `alias <msg> -> channel <channelname> = <msg>`, so that you can
+ for example just write `pub Hello` to send a message.
+
+ The alias created is `alias $1 -> channel channel = $1`, to allow
+ for sending to channel using the main channel command.
+
+ """
+ channel.add_user_channel_alias(self.caller,alias,**kwargs)
+
+
[docs]defremove_alias(self,alias,**kwargs):
+ """
+ Remove an alias from a channel.
+
+ Args:
+ alias (str, optional): The alias to remove.
+ The channel will be reverse-determined from the
+ alias, if it exists.
+
+ Returns:
+ bool, str: True, None if removal succeeded. If False,
+ the second part is an error string.
+ **kwargs: If given, passed into nicks.get/add.
+
+ Note:
+ This will remove two nicks - the plain channel alias and the templated
+ nick used for easily sending messages to the channel.
+
+ """
+ ifself.caller.nicks.has(alias,category="channel",**kwargs):
+ DefaultChannel.remove_user_channel_alias(self.caller,alias)
+ returnTrue,""
+ returnFalse,"No such alias was defined."
+
+
[docs]defget_channel_aliases(self,channel):
+ """
+ Get a user's aliases for a given channel. The user is retrieved
+ through self.caller.
+
+ Args:
+ channel (Channel): The channel to act on.
+
+ Returns:
+ list: A list of zero, one or more alias-strings.
+
+ """
+ chan_key=channel.key.lower()
+ nicktuples=self.caller.nicks.get(category="channel",return_tuple=True,return_list=True)
+ ifnicktuples:
+ return[tup[2]fortupinnicktuplesiftup[3].lower()==chan_key]
+ return[]
+
+
[docs]defmute_channel(self,channel):
+ """
+ Temporarily mute a channel.
+
+ Args:
+ channel (Channel): The channel to alias.
+
+ Returns:
+ bool, str: True, None if muting successful. If False,
+ the second part is an error string.
+ """
+ ifchannel.mute(self.caller):
+ returnTrue,""
+ returnFalse,f"Channel {channel.key} was already muted."
+
+
[docs]defunmute_channel(self,channel):
+ """
+ Unmute a channel.
+
+ Args:
+ channel (Channel): The channel to alias.
+
+ Returns:
+ bool, str: True, None if unmuting successful. If False,
+ the second part is an error string.
+
+ """
+ ifchannel.unmute(self.caller):
+ returnTrue,""
+ returnFalse,f"Channel {channel.key} was already unmuted."
+
+
[docs]defcreate_channel(self,name,description,typeclass=None,aliases=None):
+ """
+ Create a new channel. Its name must not previously exist
+ (users can alias as needed). Will also connect to the
+ new channel.
+
+ Args:
+ name (str): The new channel name/key.
+ description (str): This is used in listings.
+ aliases (list): A list of strings - alternative aliases for the channel
+ (not to be confused with per-user aliases; these are available for
+ everyone).
+
+ Returns:
+ channel, str: new_channel, "" if creation successful. If False,
+ the second part is an error string.
+
+ """
+ caller=self.caller
+ iftypeclass:
+ typeclass=class_from_module(typeclass)
+ else:
+ typeclass=CHANNEL_DEFAULT_TYPECLASS
+
+ iftypeclass.objects.channel_search(name,exact=True):
+ returnFalse,f"Channel {name} already exists."
+
+ # set up the new channel
+ lockstring="send:all();listen:all();control:id(%s)"%caller.id
+
+ new_chan=create.create_channel(
+ name,aliases=aliases,desc=description,locks=lockstring,typeclass=typeclass)
+ self.sub_to_channel(new_chan)
+ returnnew_chan,""
+
+
[docs]defdestroy_channel(self,channel,message=None):
+ """
+ Destroy an existing channel. Access should be checked before
+ calling this function.
+
+ Args:
+ channel (Channel): The channel to alias.
+ message (str, optional): Final message to send onto the channel
+ before destroying it. If not given, a default message is
+ used. Set to the empty string for no message.
+
+ if typeclass:
+ pass
+
+ """
+ caller=self.caller
+
+ channel_key=channel.key
+ ifmessageisNone:
+ message=(f"|rChannel {channel_key} is being destroyed. "
+ "Make sure to clean any channel aliases.|n")
+ ifmessage:
+ channel.msg(message,senders=caller,bypass_mute=True)
+ channel.delete()
+ logger.log_sec(
+ "Channel {} was deleted by {}".format(channel_key,caller)
+ )
+
+
[docs]defset_lock(self,channel,lockstring):
+ """
+ Set a lockstring on a channel. Permissions must have been
+ checked before this call.
+
+ Args:
+ channel (Channel): The channel to operate on.
+ lockstring (str): A lockstring on the form 'type:lockfunc();...'
+
+ Returns:
+ bool, str: True, None if setting lock was successful. If False,
+ the second part is an error string.
+
+ """
+ try:
+ channel.locks.add(lockstring)
+ exceptLockExceptionaserr:
+ returnFalse,err
+ returnTrue,""
+
+
[docs]defunset_lock(self,channel,lockstring):
+ """
+ Remove locks in a lockstring on a channel. Permissions must have been
+ checked before this call.
+
+ Args:
+ channel (Channel): The channel to operate on.
+ lockstring (str): A lockstring on the form 'type:lockfunc();...'
+
+ Returns:
+ bool, str: True, None if setting lock was successful. If False,
+ the second part is an error string.
+
+ """
+ try:
+ channel.locks.remove(lockstring)
+ exceptLockExceptionaserr:
+ returnFalse,err
+ returnTrue,""
+
+
[docs]defset_desc(self,channel,description):
+ """
+ Set a channel description. This is shown in listings etc.
+
+ Args:
+ caller (Object or Account): The entity performing the action.
+ channel (Channel): The channel to operate on.
+ description (str): A short description of the channel.
+
+ Returns:
+ bool, str: True, None if setting lock was successful. If False,
+ the second part is an error string.
+
+ """
+ channel.db.desc=description
+
+
[docs]defboot_user(self,channel,target,quiet=False,reason=""):
+ """
+ Boot a user from a channel, with optional reason. This will
+ also remove all their aliases for this channel.
+
+ Args:
+ channel (Channel): The channel to operate on.
+ target (Object or Account): The entity to boot.
+ quiet (bool, optional): Whether or not to announce to channel.
+ reason (str, optional): A reason for the boot.
+
+ Returns:
+ bool, str: True, None if setting lock was successful. If False,
+ the second part is an error string.
+
+ """
+ ifnotchannel.subscriptions.has(target):
+ returnFalse,f"{target} is not connected to channel {channel.key}."
+ # find all of target's nicks linked to this channel and delete them
+ fornickin[
+ nick
+ fornickintarget.nicks.get(category="channel")or[]
+ ifnick.value[3].lower()==channel.key
+ ]:
+ nick.delete()
+ channel.disconnect(target)
+ reason=f" Reason: {reason}"ifreasonelse""
+ target.msg(f"You were booted from channel {channel.key} by {self.caller.key}.{reason}")
+ ifnotquiet:
+ channel.msg(f"{target.key} was booted from channel by {self.caller.key}.{reason}")
+
+ logger.log_sec(f"Channel Boot: {target} (Channel: {channel}, "
+ f"Reason: {reason.strip()}, Caller: {self.caller}")
+ returnTrue,""
+
+
[docs]defban_user(self,channel,target,quiet=False,reason=""):
+ """
+ Ban a user from a channel, by locking them out. This will also
+ boot them, if they are currently connected.
+
+ Args:
+ channel (Channel): The channel to operate on.
+ target (Object or Account): The entity to ban
+ quiet (bool, optional): Whether or not to announce to channel.
+ reason (str, optional): A reason for the ban
+
+ Returns:
+ bool, str: True, None if banning was successful. If False,
+ the second part is an error string.
+
+ """
+ self.boot_user(channel,target,quiet=quiet,reason=reason)
+ ifchannel.ban(target):
+ returnTrue,""
+ returnFalse,f"{target} is already banned from this channel."
+
+
[docs]defunban_user(self,channel,target):
+ """
+ Un-Ban a user from a channel. This will not reconnect them
+ to the channel, just allow them to connect again (assuming
+ they have the suitable 'listen' lock like everyone else).
+
+ Args:
+ channel (Channel): The channel to operate on.
+ target (Object or Account): The entity to unban
+
+ Returns:
+ bool, str: True, None if unbanning was successful. If False,
+ the second part is an error string.
+
+ """
+ ifchannel.unban(target):
+ returnTrue,""
+ returnFalse,f"{target} was not previously banned from this channel."
+
+
[docs]defchannel_list_bans(self,channel):
+ """
+ Show a channel's bans.
+
+ Args:
+ channel (Channel): The channel to operate on.
+
+ Returns:
+ list: A list of strings, each the name of a banned user.
+
+ """
+ return[banned.keyforbannedinchannel.banlist]
+
+
[docs]defchannel_list_who(self,channel):
+ """
+ Show a list of online people is subscribing to a channel. This will check
+ the 'control' permission of `caller` to determine if only online users
+ should be returned or everyone.
+
+ Args:
+ channel (Channel): The channel to operate on.
+
+ Returns:
+ list: A list of prepared strings, with name + markers for if they are
+ muted or offline.
+
+ """
+ caller=self.caller
+ mute_list=list(channel.mutelist)
+ online_list=channel.subscriptions.online()
+ ifchannel.access(caller,'control'):
+ # for those with channel control, show also offline users
+ all_subs=list(channel.subscriptions.all())
+ else:
+ # for others, only show online users
+ all_subs=online_list
+
+ who_list=[]
+ forsubscriberinall_subs:
+ name=subscriber.get_display_name(caller)
+ conditions=("muting"ifsubscriberinmute_listelse"",
+ "offline"ifsubscribernotinonline_listelse"")
+ conditions=[condforcondinconditionsifcond]
+ cond_text="("+", ".join(conditions)+")"ifconditionselse""
+ who_list.append(f"{name}{cond_text}")
+
+ returnwho_list
+
+
[docs]deflist_channels(self,channelcls=CHANNEL_DEFAULT_TYPECLASS):
+ """
+ Return a available channels.
+
+ Args:
+ channelcls (Channel, optional): The channel-class to query on. Defaults
+ to the default channel class from settings.
+
+ Returns:
+ tuple: A tuple `(subbed_chans, available_chans)` with the channels
+ currently subscribed to, and those we have 'listen' access to but
+ don't actually sub to yet.
+
+ """
+ caller=self.caller
+ subscribed_channels=list(channelcls.objects.get_subscriptions(caller))
+ unsubscribed_available_channels=[
+ chan
+ forchaninchannelcls.objects.get_all_channels()
+ ifchannotinsubscribed_channelsandchan.access(caller,"listen")
+ ]
+ returnsubscribed_channels,unsubscribed_available_channels
[docs]deffunc(self):
+ """
+ Main functionality of command.
+ """
+ # from evennia import set_trace;set_trace()
+
+ caller=self.caller
+ switches=self.switches
+ channel_names=[namefornameinself.lhslistifname]
+
+ #from evennia import set_trace;set_trace()
+
+ if'all'inswitches:
+ # show all available channels
+ subscribed,available=self.list_channels()
+ table=self.display_all_channels(subscribed,available)
+
+ self.msg(
+ "\n|wAvailable channels|n (use no argument to "
+ f"only show your subscriptions)\n{table}")
+ return
+
+ ifnotchannel_names:
+ # empty arg show only subscribed channels
+ subscribed,_=self.list_channels()
+ table=self.display_subbed_channels(subscribed)
+
+ self.msg("\n|wChannel subscriptions|n "
+ f"(use |w/all|n to see all available):\n{table}")
+ return
+
+ ifnotself.switchesandnotself.args:
+ self.msg("Usage[/switches]: channel [= message]")
+ return
+
+ if'create'inswitches:
+ # create a new channel
+
+ ifnotself.access(caller,"manage"):
+ self.msg("You don't have access to use channel/create.")
+ return
+
+ config=self.lhs
+ ifnotconfig:
+ self.msg("To create: channel/create name[;aliases][:typeclass] [= description]")
+ return
+ name,*typeclass=config.rsplit(":",1)
+ typeclass=typeclass[0]iftypeclasselseNone
+ name,*aliases=name.rsplit(";")
+ description=self.rhsor""
+ chan,err=self.create_channel(name,description,typeclass=typeclass,aliases=aliases)
+ ifchan:
+ self.msg(f"Created (and joined) new channel '{chan.key}'.")
+ else:
+ self.msg(err)
+ return
+
+ if'unalias'inswitches:
+ # remove a personal alias (no channel needed)
+ alias=self.args.strip()
+ ifnotalias:
+ self.msg("Specify the alias to remove as channel/unalias <alias>")
+ return
+ success,err=self.remove_alias(alias)
+ ifsuccess:
+ self.msg(f"Removed your channel alias '{alias}'.")
+ else:
+ self.msg(err)
+ return
+
+ possible_lhs_message=""
+ ifnotself.rhsandself.argsand" "inself.args:
+ # since we want to support messaging with `channel name text` (for
+ # channels without a space in their name), we need to check if the
+ # first 'channel name' is in fact 'channelname text'
+ no_rhs_channel_name=self.args.split(" ",1)[0]
+ possible_lhs_message=self.args[len(no_rhs_channel_name):]
+ ifpossible_lhs_message.strip()=='=':
+ possible_lhs_message=""
+ channel_names.append(no_rhs_channel_name)
-
[docs]classCmdAddCom(COMMAND_DEFAULT_CLASS):
+ channels=[]
+ errors=[]
+ forchannel_nameinchannel_names:
+ # find a channel by fuzzy-matching. This also checks
+ # 'listen/control' perms.
+ found_channels=self.search_channel(channel_name,exact=False,handle_errors=False)
+ ifnotfound_channels:
+ errors.append(f"No channel found matching '{channel_name}' "
+ "(could also be due to missing access).")
+ eliflen(found_channels)>1:
+ errors.append("Multiple possible channel matches/alias for "
+ "'{channel_name}':\n"+", ".join(chan.keyforchaninfound_channels))
+ else:
+ channels.append(found_channels[0])
+
+ ifnotchannels:
+ self.msg('\n'.join(errors))
+ return
+
+ # we have at least one channel at this point
+ channel=channels[0]
+
+ ifnotswitches:
+ ifself.rhs:
+ # send message to channel
+ self.msg_channel(channel,self.rhs.strip())
+ elifchannelandpossible_lhs_message:
+ # called on the form channelname message without =
+ self.msg_channel(channel,possible_lhs_message.strip())
+ else:
+ # inspect a given channel
+ subscribed,available=self.list_channels()
+ ifchannelinsubscribed:
+ table=self.display_subbed_channels([channel])
+ header=f"Channel |w{channel.key}|n"
+ self.msg(f"{header}\n(use |w{channel.key} <msg>|n (or a channel-alias) "
+ f"to chat and the 'channel' command "
+ f"to customize)\n{table}")
+ elifchannelinavailable:
+ table=self.display_all_channels([],[channel])
+ self.msg(
+ "\n|wNot subscribed to this channel|n (use /list to "
+ f"show all subscriptions)\n{table}")
+ return
+
+ if'history'inswitchesor'hist'inswitches:
+ # view channel history
+
+ index=self.rhsor0
+ try:
+ index=max(0,int(index))
+ exceptValueError:
+ self.msg("The history index (describing how many lines to go back) "
+ "must be an integer >= 0.")
+ return
+ self.get_channel_history(channel,start_index=index)
+ return
+
+ if'sub'inswitches:
+ # subscribe to a channel
+ aliases=[]
+ ifself.rhs:
+ aliases=set(alias.strip().lower()foraliasinself.rhs.split(";"))
+ success,err=self.sub_to_channel(channel)
+ ifsuccess:
+ foraliasinaliases:
+ self.add_alias(channel,alias)
+ alias_txt=', '.join(aliases)
+ alias_txt=f" using alias(es) {alias_txt}"ifaliaseselse''
+ self.msg("You are now subscribed "
+ f"to the channel {channel.key}{alias_txt}. Use /alias to "
+ "add additional aliases for referring to the channel.")
+ else:
+ self.msg(err)
+ return
+
+ if'unsub'inswitches:
+ # un-subscribe from a channel
+ success,err=self.unsub_from_channel(channel)
+ ifsuccess:
+ self.msg(f"You un-subscribed from channel {channel.key}. "
+ "All aliases were cleared.")
+ else:
+ self.msg(err)
+ return
+
+ if'alias'inswitches:
+ # create a new personal alias for a channel
+ alias=self.rhs
+ ifnotalias:
+ self.msg("Specify the alias as channel/alias channelname = alias")
+ return
+ self.add_alias(channel,alias)
+ self.msg(f"Added/updated your alias '{alias}' for channel {channel.key}.")
+ return
+
+ if'mute'inswitches:
+ # mute a given channel
+ success,err=self.mute_channel(channel)
+ ifsuccess:
+ self.msg(f"Muted channel {channel.key}.")
+ else:
+ self.msg(err)
+ return
+
+ if'unmute'inswitches:
+ # unmute a given channel
+ success,err=self.unmute_channel(channel)
+ ifsuccess:
+ self.msg(f"Un-muted channel {channel.key}.")
+ else:
+ self.msg(err)
+ return
+
+ if'destroy'inswitchesor'delete'inswitches:
+ # destroy a channel we control
+
+ ifnotself.access(caller,"manage"):
+ self.msg("You don't have access to use channel/destroy.")
+ return
+
+ ifnotchannel.access(caller,"control"):
+ self.msg("You can only delete channels you control.")
+ return
+
+ reason=self.rhsorNone
+
+ def_perform_delete(caller,*args,**kwargs):
+ self.destroy_channel(channel,message=reason)
+ self.msg(f"Channel {channel.key} was successfully deleted.")
+
+ ask_yes_no(
+ caller,
+ prompt=f"Are you sure you want to delete channel '{channel.key}' "
+ "(make sure name is correct!)?\nThis will disconnect and "
+ "remove all users' aliases. {options}?",
+ yes_action=_perform_delete,
+ no_action="Aborted.",
+ default="N"
+ )
+
+ if'desc'inswitches:
+ # set channel description
+
+ ifnotself.access(caller,"manage"):
+ self.msg("You don't have access to use channel/desc.")
+ return
+
+ ifnotchannel.access(caller,"control"):
+ self.msg("You can only change description of channels you control.")
+ return
+
+ desc=self.rhs.strip()
+
+ ifnotdesc:
+ self.msg("Usage: /desc channel = description")
+ return
+
+ self.set_desc(channel,desc)
+ self.msg("Updated channel description.")
+
+ if'lock'inswitches:
+ # add a lockstring to channel
+
+ ifnotself.access(caller,"changelocks"):
+ self.msg("You don't have access to use channel/lock.")
+ return
+
+ ifnotchannel.access(caller,"control"):
+ self.msg("You need 'control'-access to change locks on this channel.")
+ return
+
+ lockstring=self.rhs.strip()
+
+ ifnotlockstring:
+ self.msg("Usage: channel/lock channelname = lockstring")
+ return
+
+ success,err=self.set_lock(channel,self.rhs)
+ ifsuccess:
+ self.msg("Added/updated lock on channel.")
+ else:
+ self.msg(f"Could not add/update lock: {err}")
+ return
+
+ if'unlock'inswitches:
+ # remove/update lockstring from channel
+
+ ifnotself.access(caller,"changelocks"):
+ self.msg("You don't have access to use channel/unlock.")
+ return
+
+ ifnotchannel.access(caller,"control"):
+ self.msg("You need 'control'-access to change locks on this channel.")
+ return
+
+ lockstring=self.rhs.strip()
+
+ ifnotlockstring:
+ self.msg("Usage: channel/unlock channelname = lockstring")
+ return
+
+ success,err=self.unset_lock(channel,self.rhs)
+ ifsuccess:
+ self.msg("Removed lock from channel.")
+ else:
+ self.msg(f"Could not remove lock: {err}")
+ return
+
+ if'boot'inswitches:
+ # boot a user from channel(s)
+
+ ifnotself.access(caller,"admin"):
+ self.msg("You don't have access to use channel/boot.")
+ return
+
+ ifnotself.rhs:
+ self.msg("Usage: channel/boot channel[,channel,...] = username [:reason]")
+ return
+
+ target_str,*reason=self.rhs.rsplit(":",1)
+ reason=reason[0].strip()ifreasonelse""
+
+ forchaninchannels:
+
+ ifnotchan.access(caller,"control"):
+ self.msg(f"You need 'control'-access to boot a user from {chan.key}.")
+ return
+
+ # the target must be a member of all given channels
+ target=caller.search(target_str,candidates=chan.subscriptions.all())
+ ifnottarget:
+ self.msg(f"Cannot boot '{target_str}' - not in channel {chan.key}.")
+ return
+
+ def_boot_user(caller,*args,**kwargs):
+ forchaninchannels:
+ success,err=self.boot_user(chan,target,quiet=False,reason=reason)
+ ifsuccess:
+ self.msg(f"Booted {target.key} from channel {chan.key}.")
+ else:
+ self.msg(f"Cannot boot {target.key} from channel {chan.key}: {err}")
+
+ channames=", ".join(chan.keyforchaninchannels)
+ reasonwarn=(". Also note that your reason will be echoed to the channel"
+ ifreasonelse'')
+ ask_yes_no(
+ caller,
+ prompt=f"Are you sure you want to boot user {target.key} from "
+ f"channel(s) {channames} (make sure name/channels are correct{reasonwarn}). "
+ "{options}?",
+ yes_action=_boot_user,
+ no_action="Aborted.",
+ default="Y"
+ )
+ return
+
+ if'ban'inswitches:
+ # ban a user from channel(s)
+
+ ifnotself.access(caller,"admin"):
+ self.msg("You don't have access to use channel/ban.")
+ return
+
+ ifnotself.rhs:
+ # view bans for channels
+
+ ifnotchannel.access(caller,"control"):
+ self.msg(f"You need 'control'-access to view bans on channel {channel.key}")
+ return
+
+ bans=["Channel bans "
+ "(to ban, use channel/ban channel[,channel,...] = username [:reason]"]
+ bans.extend(self.channel_list_bans(channel))
+ self.msg("\n".join(bans))
+ return
+
+ target_str,*reason=self.rhs.rsplit(":",1)
+ reason=reason[0].strip()ifreasonelse""
+
+ forchaninchannels:
+ # the target must be a member of all given channels
+ ifnotchan.access(caller,"control"):
+ self.msg(f"You don't have access to ban users on channel {chan.key}")
+ return
+
+ target=caller.search(target_str,candidates=chan.subscriptions.all())
+
+ ifnottarget:
+ self.msg(f"Cannot ban '{target_str}' - not in channel {chan.key}.")
+ return
+
+ def_ban_user(caller,*args,**kwargs):
+ forchaninchannels:
+ success,err=self.ban_user(chan,target,quiet=False,reason=reason)
+ ifsuccess:
+ self.msg(f"Banned {target.key} from channel {chan.key}.")
+ else:
+ self.msg(f"Cannot boot {target.key} from channel {chan.key}: {err}")
+
+ channames=", ".join(chan.keyforchaninchannels)
+ reasonwarn=(". Also note that your reason will be echoed to the channel"
+ ifreasonelse'')
+ ask_yes_no(
+ caller,
+ f"Are you sure you want to ban user {target.key} from "
+ f"channel(s) {channames} (make sure name/channels are correct{reasonwarn}) "
+ "{options}?",
+ _ban_user,
+ "Aborted.",
+ )
+ return
+
+ if'unban'inswitches:
+ # unban a previously banned user from channel
+
+ ifnotself.access(caller,"admin"):
+ self.msg("You don't have access to use channel/unban.")
+ return
+
+ target_str=self.rhs.strip()
+
+ ifnottarget_str:
+ self.msg("Usage: channel[,channel,...] = user")
+ return
+
+ banlists=[]
+ forchaninchannels:
+ # the target must be a member of all given channels
+ ifnotchan.access(caller,"control"):
+ self.msg(f"You don't have access to unban users on channel {chan.key}")
+ return
+ banlists.extend(chan.banlist)
+
+ target=caller.search(target_str,candidates=banlists)
+ ifnottarget:
+ self.msg("Could not find a banned user '{target_str}' in given channel(s).")
+ return
+
+ forchaninchannels:
+ success,err=self.unban_user(channel,target)
+ ifsuccess:
+ self.msg(f"Un-banned {target_str} from channel {chan.key}")
+ else:
+ self.msg(err)
+ return
+
+ if"who"inswitches:
+ # view who's a member of a channel
+
+ who_list=[f"Subscribed to {channel.key}:"]
+ who_list.extend(self.channel_list_who(channel))
+ self.msg("\n".join(who_list))
+ return
+
+
+# a channel-command parent for use with Characters/Objects.
+classCmdObjectChannel(CmdChannel):
+ account_caller=False
+
+
+
[docs]classCmdAddCom(CmdChannel):"""
- add a channel alias and/or subscribe to a channel
+ Add a channel alias and/or subscribe to a channel Usage: addcom [alias=] <channel>
@@ -138,7 +1316,6 @@
caller=self.callerargs=self.args
- account=callerifnotargs:self.msg("Usage: addcom [alias =] channelname.")
@@ -152,42 +1329,36 @@
channelname=self.argsalias=None
- channel=find_channel(caller,channelname)
+ channel=self.search_channel(channelname)ifnotchannel:
- # we use the custom search method to handle errors.
- return
-
- # check permissions
- ifnotchannel.access(account,"listen"):
- self.msg("%s: You are not allowed to listen to this channel."%channel.key)returnstring=""
- ifnotchannel.has_connection(account):
+ ifnotchannel.has_connection(caller):# we want to connect as well.
- ifnotchannel.connect(account):
+ success,err=self.sub_to_channel(channel)
+ ifsuccess:# if this would have returned True, the account is connected
- self.msg("%s: You are not allowed to join this channel."%channel.key)
+ self.msg(f"You now listen to the channel {channel.key}")
+ else:
+ self.msg(f"{channel.key}: You are not allowed to join this channel.")return
- else:
- string+="You now listen to the channel %s. "%channel.key
+
+ ifchannel.unmute(caller):
+ self.msg(f"You unmute channel {channel.key}.")else:
- ifchannel.unmute(account):
- string+="You unmute channel %s."%channel.key
- else:
- string+="You are already connected to channel %s."%channel.key
+ self.msg(f"You are already connected to channel {channel.key}.")ifalias:# create a nick and add it to the caller.
- caller.nicks.add(alias,channel.key,category="channel")
- string+=" You can now refer to the channel %s with the alias '%s'."
- self.msg(string%(channel.key,alias))
+ self.add_alias(channel,alias)
+ self.msg(f" You can now refer to the channel {channel} with the alias '{alias}'.")else:string+=" No alias added."self.msg(string)
[docs]classCmdDelCom(CmdChannel):""" remove a channel alias and/or unsubscribe from channel
@@ -213,49 +1384,42 @@
"""Implementing the command. """caller=self.caller
- account=callerifnotself.args:self.msg("Usage: delcom <alias or channel>")return
- ostring=self.args.lower()
+ ostring=self.args.lower().strip()
- channel=find_channel(caller,ostring,silent=True,noaliases=True)
- ifchannel:
- # we have given a channel name - unsubscribe
- ifnotchannel.has_connection(account):
- self.msg("You are not listening to that channel.")
- return
- chkey=channel.key.lower()
+ channel=self.search_channel(ostring)
+ ifnotchannel:
+ return
+
+ ifnotchannel.has_connection(caller):
+ self.msg("You are not listening to that channel.")
+ return
+
+ ifostring==channel.key.lower():
+ # an exact channel name - unsubscribedelnicks="all"inself.switches# find all nicks linked to this channel and delete themifdelnicks:
- fornickin[
- nick
- fornickinmake_iter(caller.nicks.get(category="channel",return_obj=True))
- ifnickandnick.pkandnick.value[3].lower()==chkey
- ]:
- nick.delete()
- disconnect=channel.disconnect(account)
- ifdisconnect:
+ aliases=self.get_channel_aliases(channel)
+ foraliasinaliases:
+ self.remove_alias(alias)
+ success,err=self.unsub_from_channel(channel)
+ ifsuccess:wipednicks=" Eventual aliases were removed."ifdelnickselse""
- self.msg("You stop listening to channel '%s'.%s"%(channel.key,wipednicks))
+ self.msg(f"You stop listening to channel '{channel.key}'.{wipednicks}")
+ else:
+ self.msg(err)returnelse:# we are removing a channel nick
- channame=caller.nicks.get(key=ostring,category="channel")
- channel=find_channel(caller,channame,silent=True)
- ifnotchannel:
- self.msg("No channel with alias '%s' was found."%ostring)
- else:
- ifcaller.nicks.get(ostring,category="channel"):
- caller.nicks.remove(ostring,category="channel")
- self.msg("Your alias '%s' for channel %s was cleared."%(ostring,channel.key))
- else:
- self.msg("You had no such alias defined for this channel.")
+ self.remove_alias(ostring)
+ self.msg(f"Any alias '{ostring}' for channel {channel.key} was cleared.")
[docs]classCmdAllCom(CmdChannel):""" perform admin operations on all channels
@@ -270,6 +1434,7 @@
"""key="allcom"
+ aliases=[]# important to not inherit parent's aliaseslocks="cmd: not pperm(channel_banned)"help_category="Comms"
@@ -282,8 +1447,11 @@
caller=self.callerargs=self.argsifnotargs:
- self.execute_cmd("channels")
- self.msg("(Usage: allcom on | off | who | destroy)")
+ subscribed,available=self.list_channels()
+ table=self.display_all_channels(subscribed,available)
+ self.msg(
+ "\n|wAvailable channels:\n{table}")
+ returnreturnifargs=="on":
@@ -327,125 +1495,7 @@
# wrong inputself.msg("Usage: allcom on | off | who | clear")
-
-
[docs]classCmdChannels(COMMAND_DEFAULT_CLASS):
- """
- list all channels available to you
-
- Usage:
- channels
- clist
- comlist
-
- Lists all channels available to you, whether you listen to them or not.
- Use 'comlist' to only view your current channel subscriptions.
- Use addcom/delcom to join and leave channels
- """
-
- key="channels"
- aliases=["clist","comlist","chanlist","channellist","all channels"]
- help_category="Comms"
- locks="cmd: not pperm(channel_banned)"
-
- # this is used by the COMMAND_DEFAULT_CLASS parent
- account_caller=True
-
-
[docs]deffunc(self):
- """Implement function"""
-
- caller=self.caller
-
- # all channels we have available to listen to
- channels=[
- chan
- forchaninCHANNEL_DEFAULT_TYPECLASS.objects.get_all_channels()
- ifchan.access(caller,"listen")
- ]
- ifnotchannels:
- self.msg("No channels available.")
- return
- # all channel we are already subscribed to
- subs=CHANNEL_DEFAULT_TYPECLASS.objects.get_subscriptions(caller)
-
- ifself.cmdstring=="comlist":
- # just display the subscribed channels with no extra info
- comtable=self.styled_table(
- "|wchannel|n",
- "|wmy aliases|n",
- "|wdescription|n",
- align="l",
- maxwidth=_DEFAULT_WIDTH,
- )
- forchaninsubs:
- clower=chan.key.lower()
- nicks=caller.nicks.get(category="channel",return_obj=True)
- comtable.add_row(
- *[
- "%s%s"
- %(
- chan.key,
- chan.aliases.all()and"(%s)"%",".join(chan.aliases.all())or"",
- ),
- "%s"
- %",".join(
- nick.db_key
- fornickinmake_iter(nicks)
- ifnickandnick.value[3].lower()==clower
- ),
- chan.db.desc,
- ]
- )
- self.msg(
- "\n|wChannel subscriptions|n (use |wchannels|n to list all,"
- " |waddcom|n/|wdelcom|n to sub/unsub):|n\n%s"%comtable
- )
- else:
- # full listing (of channels caller is able to listen to)
- comtable=self.styled_table(
- "|wsub|n",
- "|wchannel|n",
- "|wmy aliases|n",
- "|wlocks|n",
- "|wdescription|n",
- maxwidth=_DEFAULT_WIDTH,
- )
- forchaninchannels:
- clower=chan.key.lower()
- nicks=caller.nicks.get(category="channel",return_obj=True)
- nicks=nicksor[]
- ifchannotinsubs:
- substatus="|rNo|n"
- elifcallerinchan.mutelist:
- substatus="|rMuted|n"
- else:
- substatus="|gYes|n"
- comtable.add_row(
- *[
- substatus,
- "%s%s"
- %(
- chan.key,
- chan.aliases.all()and"(%s)"%",".join(chan.aliases.all())or"",
- ),
- "%s"
- %",".join(
- nick.db_key
- fornickinmake_iter(nicks)
- ifnick.value[3].lower()==clower
- ),
- str(chan.locks),
- chan.db.desc,
- ]
- )
- comtable.reformat_column(0,width=9)
- comtable.reformat_column(3,width=14)
- self.msg(
- "\n|wAvailable channels|n (use |wcomlist|n,|waddcom|n and |wdelcom|n"
- " to manage subscriptions):\n%s"%comtable
- )
[docs]classCmdCdestroy(CmdChannel):""" destroy a channel you created
@@ -456,6 +1506,7 @@
"""key="cdestroy"
+ aliases=[]help_category="Comms"locks="cmd: not pperm(channel_banned)"
@@ -464,12 +1515,15 @@
[docs]deffunc(self):"""Destroy objects cleanly."""
+
caller=self.callerifnotself.args:self.msg("Usage: cdestroy <channelname>")return
- channel=find_channel(caller,self.args)
+
+ channel=self.search_channel(self.args)
+
ifnotchannel:self.msg("Could not find channel %s."%self.args)return
@@ -477,11 +1531,8 @@
self.msg("You are not allowed to do that.")returnchannel_key=channel.key
- message="%s is being destroyed. Make sure to change your aliases."%channel_key
- msgobj=create.create_message(caller,message,channel)
- channel.msg(msgobj)
- channel.delete()
- CHANNELHANDLER.update()
+ message=f"{channel.key} is being destroyed. Make sure to change your aliases."
+ self.destroy_channel(channel,message)self.msg("Channel '%s' was destroyed."%channel_key)logger.log_sec("Channel Deleted: %s (Caller: %s, IP: %s)."
@@ -489,7 +1540,7 @@
)
[docs]classCmdCemit(COMMAND_DEFAULT_CLASS):
- """
- send an admin message to a channel you control
-
- Usage:
- cemit[/switches] <channel> = <message>
-
- Switches:
- sendername - attach the sender's name before the message
- quiet - don't echo the message back to sender
-
- Allows the user to broadcast a message over a channel as long as
- they control it. It does not show the user's name unless they
- provide the /sendername switch.
-
- """
-
- key="cemit"
- aliases=["cmsg"]
- switch_options=("sendername","quiet")
- locks="cmd: not pperm(channel_banned) and pperm(Player)"
- help_category="Comms"
-
- # this is used by the COMMAND_DEFAULT_CLASS parent
- account_caller=True
-
-
[docs]classCmdClock(CmdChannel):""" change channel locks of a channel you control
@@ -707,8 +1698,8 @@
"""key="clock"
- locks="cmd:not pperm(channel_banned)"aliases=["clock"]
+ locks="cmd:not pperm(channel_banned) and perm(Admin)"help_category="Comms"# this is used by the COMMAND_DEFAULT_CLASS parent
@@ -722,14 +1713,13 @@
self.msg(string)return
- channel=find_channel(self.caller,self.lhs)
+ channel=self.search_channel(self.lhs)ifnotchannel:return
+
ifnotself.rhs:# no =, so just view the current locks
- string="Current locks on %s:"%channel.key
- string="%s\n%s"%(string,channel.locks)
- self.msg(string)
+ self.msg(f"Current locks on {channel.key}\n{channel.locks}")return# we want to add/change a lock.ifnotchannel.access(self.caller,"control"):
@@ -737,18 +1727,13 @@
self.msg(string)return# Try to add the lock
- try:
- channel.locks.add(self.rhs)
- exceptLockExceptionaserr:
- self.msg(err)
- return
- string="Lock(s) applied. "
- string+="Current locks on %s:"%channel.key
- string="%s\n%s"%(string,channel.locks)
- self.msg(string)
+ success,err=self.set_lock(channel,self.rhs)
+ ifsuccess:
+ self.msg(f"Lock(s) applied. Current locks on {channel.key}:\n{channel.locks}")
+ else:
+ self.msg(err)
-
-
[docs]classCmdCdesc(CmdChannel):""" describe a channel you control
@@ -757,9 +1742,11 @@
Changes the description of the channel as shown in channel lists.
+
"""key="cdesc"
+ aliases=[]locks="cmd:not pperm(channel_banned)"help_category="Comms"
@@ -774,18 +1761,15 @@
ifnotself.rhs:self.msg("Usage: cdesc <channel> = <description>")return
- channel=find_channel(caller,self.lhs)
+ channel=self.search_channel(self.lhs)ifnotchannel:
- self.msg("Channel '%s' not found."%self.lhs)return# check permissionsifnotchannel.access(caller,"control"):self.msg("You cannot admin this channel.")return
- # set the description
- channel.db.desc=self.rhs
- channel.save()
- self.msg("Description of channel '%s' set to '%s'."%(channel.key,self.rhs))
+ self.set_desc(channel,self.rhs)
+ self.msg(f"Description of channel '{channel.key}' set to '{self.rhs}'.")
[docs]classCmdPage(COMMAND_DEFAULT_CLASS):
@@ -793,16 +1777,19 @@
send a private message to another account Usage:
+ page <account> <message> page[/switches] [<account>,<account>,... = <message>] tell '' page <number>
- Switch:
+ Switches: last - shows who you last messaged list - show your last <number> of tells/pages (default)
- Send a message to target user (if online). If no
- argument is given, you will get a list of your latest messages.
+ Send a message to target user (if online). If no argument is given, you
+ will get a list of your latest messages. The equal sign is needed for
+ multiple targets or if sending to target with space in the name.
+
"""key="page"
@@ -821,9 +1808,10 @@
caller=self.caller# get the messages we've sent (not to channels)
- pages_we_sent=Msg.objects.get_messages_by_sender(caller,exclude_channel_messages=True)
+ pages_we_sent=Msg.objects.get_messages_by_sender(caller)# get last messages we've gotpages_we_got=Msg.objects.get_messages_by_receiver(caller)
+ targets,message,number=[],None,Noneif"last"inself.switches:ifpages_we_sent:
@@ -834,19 +1822,76 @@
self.msg("You haven't paged anyone yet.")return
- ifnotself.argsornotself.rhs:
- pages=pages_we_sent+pages_we_got
- pages=sorted(pages,key=lambdapage:page.date_created)
+ ifself.args:
+ ifself.rhs:
+ fortargetinself.lhslist:
+ target_obj=self.caller.search(target)
+ ifnottarget_obj:
+ return
+ targets.append(target_obj)
+ message=self.rhs.strip()
+ else:
+ target,*message=self.args.split(" ",4)
+ iftargetandtarget.isnumeric():
+ # a number to specify a historic page
+ number=int(target)
+ eliftarget:
+ target_obj=self.caller.search(target,quiet=True)
+ iftarget_obj:
+ # a proper target
+ targets=[target_obj[0]]
+ message=message[0].strip()
+ else:
+ # a message with a space in it - put it back together
+ message=target+" "+(message[0]ifmessageelse"")
+ else:
+ # a single-word message
+ message=message[0].strip()
- number=5
- ifself.args:
- try:
- number=int(self.args)
- exceptValueError:
- self.msg("Usage: tell [<account> = msg]")
+ pages=list(pages_we_sent)+list(pages_we_got)
+ pages=sorted(pages,key=lambdapage:page.date_created)
+
+ ifmessage:
+ # send a message
+ ifnottargets:
+ # no target given - send to last person we paged
+ ifpages_we_sent:
+ targets=pages_we_sent[-1].receivers
+ else:
+ self.msg("Who do you want page?")return
- iflen(pages)>number:
+ header="|wAccount|n |c%s|n |wpages:|n"%caller.key
+ ifmessage.startswith(":"):
+ message="%s%s"%(caller.key,message.strip(":").strip())
+
+ # create the persistent message object
+ create.create_message(caller,message,receivers=targets)
+
+ # tell the accounts they got a message.
+ received=[]
+ rstrings=[]
+ fortargetintargets:
+ ifnottarget.access(caller,"msg"):
+ rstrings.append(f"You are not allowed to page {target}.")
+ continue
+ target.msg(f"{header}{message}")
+ ifhasattr(target,"sessions")andnottarget.sessions.count():
+ received.append(f"|C{target.name}|n")
+ rstrings.append(
+ f"{received[-1]} is offline. They will see your message "
+ "if they list their pages later."
+ )
+ else:
+ received.append(f"|c{target.name}|n")
+ ifrstrings:
+ self.msg("\n".join(rstrings))
+ self.msg("You paged %s with: '%s'."%(", ".join(received),message))
+ return
+
+ else:
+ # no message to send
+ ifnumberisnotNoneandlen(pages)>number:lastpages=pages[-number:]else:lastpages=pages
@@ -890,6 +1935,7 @@
receiver=receiver,message=page.message,)
+
)lastpages="\n ".join(listing)
@@ -898,65 +1944,7 @@
else:string="You haven't paged anyone yet."self.msg(string)
- return
-
- # We are sending. Build a list of targets
-
- ifnotself.lhs:
- # If there are no targets, then set the targets
- # to the last person we paged.
- ifpages_we_sent:
- receivers=pages_we_sent[-1].receivers
- else:
- self.msg("Who do you want to page?")
- return
- else:
- receivers=self.lhslist
-
- recobjs=[]
- forreceiverinset(receivers):
- ifisinstance(receiver,str):
- pobj=caller.search(receiver)
- elifhasattr(receiver,"character"):
- pobj=receiver
- else:
- self.msg("Who do you want to page?")
- return
- ifpobj:
- recobjs.append(pobj)
- ifnotrecobjs:
- self.msg("Noone found to page.")
- return
-
- header="|wAccount|n |c%s|n |wpages:|n"%caller.key
- message=self.rhs
-
- # if message begins with a :, we assume it is a 'page-pose'
- ifmessage.startswith(":"):
- message="%s%s"%(caller.key,message.strip(":").strip())
-
- # create the persistent message object
- create.create_message(caller,message,receivers=recobjs)
-
- # tell the accounts they got a message.
- received=[]
- rstrings=[]
- forpobjinrecobjs:
- ifnotpobj.access(caller,"msg"):
- rstrings.append("You are not allowed to page %s."%pobj)
- continue
- pobj.msg("%s%s"%(header,message))
- ifhasattr(pobj,"sessions")andnotpobj.sessions.count():
- received.append("|C%s|n"%pobj.name)
- rstrings.append(
- "%s is offline. They will see your message if they list their pages later."
- %received[-1]
- )
- else:
- received.append("|c%s|n"%pobj.name)
- ifrstrings:
- self.msg("\n".join(rstrings))
- self.msg("You paged %s with: '%s'."%(", ".join(received),message))
[docs]classCmdIRC2Chan(COMMAND_DEFAULT_CLASS):""" Link an evennia channel to an external IRC channel
@@ -1120,7 +2107,7 @@
Check and reboot IRC bot. Usage:
- ircstatus [#dbref ping||nicklist||reconnect]
+ ircstatus [#dbref ping | nicklist | reconnect] If not given arguments, will return a list of all bots (like irc2chan/list). The 'ping' argument will ping the IRC network to
@@ -1433,7 +2420,6 @@
"""
-The help command. The basic idea is that help texts for commands
-are best written by those that write the commands - the admins. So
-command-help is all auto-loaded and searched from the current command
-set. The normal, database-tied help system is used for collaborative
-creation of other help topics such as RP help or game-world aides.
+The help command. The basic idea is that help texts for commands are best
+written by those that write the commands - the developers. So command-help is
+all auto-loaded and searched from the current command set. The normal,
+database-tied help system is used for collaborative creation of other help
+topics such as RP help or game-world aides. Help entries can also be created
+outside the game in modules given by ``settings.FILE_HELP_ENTRY_MODULES``.
+
"""
+fromdataclassesimportdataclassfromdjango.confimportsettingsfromcollectionsimportdefaultdict
-fromevennia.utils.utilsimportfill,dedent
-fromevennia.commands.commandimportCommand
+fromevennia.utils.utilsimportdedentfromevennia.help.modelsimportHelpEntryfromevennia.utilsimportcreate,evmore
+fromevennia.utils.ansiimportANSIString
+fromevennia.help.filehelpimportFILE_HELP_ENTRIESfromevennia.utils.eveditorimportEvEditor
-fromevennia.utils.utilsimportstring_suggestions,class_from_module
+fromevennia.utils.utilsimport(
+ class_from_module,
+ inherits_from,
+ format_grid,pad
+)
+fromevennia.help.utilsimporthelp_search_with_index,parse_entry_for_subcategoriesCOMMAND_DEFAULT_CLASS=class_from_module(settings.COMMAND_DEFAULT_CLASS)
-HELP_MORE=settings.HELP_MORE
-CMD_IGNORE_PREFIXES=settings.CMD_IGNORE_PREFIXES
+HELP_MORE_ENABLED=settings.HELP_MORE_ENABLED
+DEFAULT_HELP_CATEGORY=settings.DEFAULT_HELP_CATEGORY
+HELP_CLICKABLE_TOPICS=settings.HELP_CLICKABLE_TOPICS# limit symbol import for API__all__=("CmdHelp","CmdSetHelp")
-_DEFAULT_WIDTH=settings.CLIENT_DEFAULT_WIDTH
-_SEP="|C"+"-"*_DEFAULT_WIDTH+"|n"
+
+@dataclass
+classHelpCategory:
+ """
+ Mock 'help entry' to search categories with the same code.
+
+ """
+ key:str
+
+ @property
+ defsearch_index_entry(self):
+ return{
+ "key":self.key,
+ "aliases":"",
+ "category":self.key,
+ "tags":"",
+ "text":"",
+ }
+
+ def__hash__(self):
+ returnhash(id(self))
[docs]classCmdHelp(COMMAND_DEFAULT_CLASS):"""
- View help or a list of topics
+ Get help. Usage:
- help <topic or command>
- help list
- help all
+ help
+ help <topic, command or category>
+ help <topic>/<subtopic>
+ help <topic>/<subtopic>/<subsubtopic> ...
+
+ Use the 'help' command alone to see an index of all help topics, organized
+ by category.eSome big topics may offer additional sub-topics.
- This will search for help on commands and other
- topics related to the game. """key="help"
@@ -90,8 +121,13 @@
# Help messages are wrapped in an EvMore call (unless using the webclient# with separate help popups) If you want to avoid this, simply add
- # 'HELP_MORE = False' in your settings/conf/settings.py
- help_more=HELP_MORE
+ # 'HELP_MORE_ENABLED = False' in your settings/conf/settings.py
+ help_more=HELP_MORE_ENABLED
+
+ # colors for the help index
+ index_type_separator_clr="|w"
+ index_category_clr="|W"
+ index_topic_clr="|G"# suggestion cutoff, between 0 and 1 (1 => perfect match)suggestion_cutoff=0.6
@@ -99,6 +135,12 @@
# number of suggestions (set to 0 to remove suggestions from help)suggestion_maxnum=5
+ # separator between subtopics:
+ subtopic_separator_char=r"/"
+
+ # should topics disply their help entry when clicked
+ clickable_topics=HELP_CLICKABLE_TOPICS
+
[docs]defmsg_help(self,text):""" messages text to the caller, adding an extra oob argument to indicate
@@ -108,7 +150,7 @@
iftype(self).help_more:usemore=True
- ifself.sessionandself.session.protocol_keyin("websocket","ajax/comet"):
+ ifself.sessionandself.session.protocol_keyin("websocket","ajax/comet",):try:options=self.account.db._saved_webclient_optionsifoptionsandoptions["helppopup"]:
@@ -122,241 +164,541 @@
self.msg(text=(text,{"type":"help"}))
-
[docs]@staticmethod
- defformat_help_entry(title,help_text,aliases=None,suggested=None):
- """
- This visually formats the help entry.
+
[docs]defformat_help_entry(self,topic="",help_text="",aliases=None,suggested=None,
+ subtopics=None,click_topics=True):
+ """This visually formats the help entry. This method can be overriden to customize the way a help entry is displayed. Args:
- title (str): the title of the help entry.
- help_text (str): the text of the help entry.
- aliases (list of str or None): the list of aliases.
- suggested (list of str or None): suggested reading.
+ title (str, optional): The title of the help entry.
+ help_text (str, optional): Text of the help entry.
+ aliases (list, optional): List of help-aliases (displayed in header).
+ suggested (list, optional): Strings suggested reading (based on title).
+ subtopics (list, optional): A list of strings - the subcategories available
+ for this entry.
+ click_topics (bool, optional): Should help topics be clickable. Default is True.
- Returns the formatted string, ready to be sent.
+ Returns:
+ help_message (str): Help entry formated for console. """
- string=_SEP+"\n"
- iftitle:
- string+="|CHelp for |w%s|n"%title
+ separator="|C"+"-"*self.client_width()+"|n"
+ start=f"{separator}\n"
+
+ title=f"|CHelp for |w{topic}|n"iftopicelse"|rNo help found|n"
+
ifaliases:
- string+=" |C(aliases: %s|C)|n"%("|C,|n ".join("|w%s|n"%aliforaliinaliases))
- ifhelp_text:
- string+="\n%s"%dedent(help_text.rstrip())
+ aliases=(
+ " |C(aliases: {}|C)|n".format("|C,|n ".join(f"|w{ali}|n"foraliinaliases))
+ )
+ else:
+ aliases=''
+
+ help_text="\n"+dedent(help_text.strip('\n'))ifhelp_textelse""
+
+ ifsubtopics:
+ ifclick_topics:
+ subtopics=[
+ f"|lchelp {topic}/{subtop}|lt|w{topic}/{subtop}|n|le"
+ forsubtopinsubtopics
+ ]
+ else:
+ subtopics=[f"|w{topic}/{subtop}|n"forsubtopinsubtopics]
+ subtopics=(
+ "\n|CSubtopics:|n\n{}".format(
+ "\n ".join(format_grid(subtopics,width=self.client_width())))
+ )
+ else:
+ subtopics=''
+
ifsuggested:
- string+="\n\n|CSuggested:|n "
- string+="%s"%fill("|C,|n ".join("|w%s|n"%sugforsuginsuggested))
- string.strip()
- string+="\n"+_SEP
- returnstring
[docs]@staticmethod
- defformat_help_list(hdict_cmds,hdict_db):
- """
- Output a category-ordered list. The input are the
- pre-loaded help files for commands and database-helpfiles
- respectively. You can override this method to return a
- custom display of the list of commands and topics.
- """
- string=""
- ifhdict_cmdsandany(hdict_cmds.values()):
- string+="\n"+_SEP+"\n |CCommand help entries|n\n"+_SEP
- forcategoryinsorted(hdict_cmds.keys()):
- string+="\n |w%s|n:\n"%(str(category).title())
- string+="|G"+fill("|C, |G".join(sorted(hdict_cmds[category])))+"|n"
- ifhdict_dbandany(hdict_db.values()):
- string+="\n\n"+_SEP+"\n\r |COther help entries|n\n"+_SEP
- forcategoryinsorted(hdict_db.keys()):
- string+="\n\r |w%s|n:\n"%(str(category).title())
- string+=(
- "|G"
- +fill(", ".join(sorted([str(topic)fortopicinhdict_db[category]])))
- +"|n"
- )
- returnstring
+ end=start
-
[docs]defcheck_show_help(self,cmd,caller):
- """
- Helper method. If this return True, the given cmd
- auto-help will be viewable in the help listing.
- Override this to easily select what is shown to
- the account. Note that only commands available
- in the caller's merged cmdset are available.
+ partorder=(start,title+aliases,help_text,subtopics,suggested,end)
+
+ return"\n".join(part.rstrip()forpartinpartorderifpart)
+
+
[docs]defformat_help_index(self,cmd_help_dict=None,db_help_dict=None,title_lone_category=False,
+ click_topics=True):
+ """Output a category-ordered g for displaying the main help, grouped by
+ category. Args:
- cmd (Command): Command class from the merged cmdset
- caller (Character, Account or Session): The current caller
- executing the help command.
+ cmd_help_dict (dict): A dict `{"category": [topic, topic, ...]}` for
+ command-based help.
+ db_help_dict (dict): A dict `{"category": [topic, topic], ...]}` for
+ database-based help.
+ title_lone_category (bool, optional): If a lone category should
+ be titled with the category name or not. While pointless in a
+ general index, the title should probably show when explicitly
+ listing the category itself.
+ click_topics (bool, optional): If help-topics are clickable or not
+ (for webclient or telnet clients with MXP support).
+ Returns:
+ str: The help index organized into a grid.
+
+ Notes:
+ The input are the pre-loaded help files for commands and database-helpfiles
+ respectively. You can override this method to return a custom display of the list of
+ commands and topics. """
- # return only those with auto_help set and passing the cmd: lock
- returncmd.auto_helpandcmd.access(caller)
[docs]defshould_list_cmd(self,cmd,caller):
+ iflen(help_dict)==1andnottitle_lone_category:
+ # don't list categories if there is only one
+ forcategoryinhelp_dict:
+ # gather and sort the entries from the help dictionary
+ entries=sorted(set(help_dict.get(category,[])))
+
+ # make the help topics clickable
+ ifclick_topics:
+ entries=[
+ f'|lchelp {entry}|lt{entry}|le'forentryinentries
+ ]
+
+ # add the entries to the grid
+ grid.extend(entries)
+ else:
+ # list the categories
+ forcategoryinsorted(set(list(help_dict.keys()))):
+ category_str=f"-- {category.title()} "
+ grid.append(
+ ANSIString(
+ self.index_category_clr+category_str
+ +"-"*(width-len(category_str))
+ +self.index_topic_clr
+ )
+ )
+ verbatim_elements.append(len(grid)-1)
+
+ # gather and sort the entries from the help dictionary
+ entries=sorted(set(help_dict.get(category,[])))
+
+ # make the help topics clickable
+ ifclick_topics:
+ entries=[
+ f'|lchelp {entry}|lt{entry}|le'forentryinentries
+ ]
+
+ # add the entries to the grid
+ grid.extend(entries)
+
+ returngrid,verbatim_elements
+
+ help_index=""
+ width=self.client_width()
+ grid=[]
+ verbatim_elements=[]
+ cmd_grid,db_grid="",""
+
+ ifany(cmd_help_dict.values()):
+ # get the command-help entries by-category
+ sep1=(self.index_type_separator_clr
+ +pad("Commands",width=width,fillchar='-')
+ +self.index_topic_clr)
+ grid,verbatim_elements=_group_by_category(cmd_help_dict)
+ gridrows=format_grid(grid,width,sep=" ",verbatim_elements=verbatim_elements)
+ cmd_grid=ANSIString("\n").join(gridrows)ifgridrowselse""
+
+ ifany(db_help_dict.values()):
+ # get db-based help entries by-category
+ sep2=(self.index_type_separator_clr
+ +pad("Game & World",width=width,fillchar='-')
+ +self.index_topic_clr)
+ grid,verbatim_elements=_group_by_category(db_help_dict)
+ gridrows=format_grid(grid,width,sep=" ",verbatim_elements=verbatim_elements)
+ db_grid=ANSIString("\n").join(gridrows)ifgridrowselse""
+
+ # only show the main separators if there are actually both cmd and db-based help
+ ifcmd_gridanddb_grid:
+ help_index=f"{sep1}\n{cmd_grid}\n{sep2}\n{db_grid}"
+ else:
+ help_index=f"{cmd_grid}{db_grid}"
+
+ returnhelp_index
+
+
[docs]defcan_read_topic(self,cmd_or_topic,caller):
+ """
+ Helper method. If this return True, the given help topic
+ be viewable in the help listing. Note that even if this returns False,
+ the entry will still be visible in the help index unless `should_list_topic`
+ is also returning False.
+
+ Args:
+ cmd_or_topic (Command, HelpEntry or FileHelpEntry): The topic/command to test.
+ caller: the caller checking for access.
+
+ Returns:
+ bool: If command can be viewed or not.
+
+ Notes:
+ This uses the 'read' lock. If no 'read' lock is defined, the topic is assumed readable
+ by all.
+
+ """
+ ifinherits_from(cmd_or_topic,"evennia.commands.command.Command"):
+ returncmd_or_topic.auto_helpandcmd_or_topic.access(caller,'read',default=True)
+ else:
+ returncmd_or_topic.access(caller,'read',default=True)
+
+
[docs]defcan_list_topic(self,cmd_or_topic,caller):""" Should the specified command appear in the help table?
- This method only checks whether a specified command should
- appear in the table of topics/commands. The command can be
- used by the caller (see the 'check_show_help' method) and
- the command will still be available, for instance, if a
- character type 'help name of the command'. However, if
- you return False, the specified command will not appear in
- the table. This is sometimes useful to "hide" commands in
- the table, but still access them through the help system.
+ This method only checks whether a specified command should appear in the table of
+ topics/commands. The command can be used by the caller (see the 'should_show_help' method)
+ and the command will still be available, for instance, if a character type 'help name of the
+ command'. However, if you return False, the specified command will not appear in the table.
+ This is sometimes useful to "hide" commands in the table, but still access them through the
+ help system. Args:
- cmd: the command to be tested.
- caller: the caller of the help system.
+ cmd_or_topic (Command, HelpEntry or FileHelpEntry): The topic/command to test.
+ caller: the caller checking for access.
- Return:
- True: the command should appear in the table.
- False: the command shouldn't appear in the table.
+ Returns:
+ bool: If command should be listed or not.
+
+ Notes:
+ By default, the 'view' lock will be checked, and if no such lock is defined, the 'read'
+ lock will be used. If neither lock is defined, the help entry is assumed to be
+ accessible to all. """
- returncmd.access(caller,"view",default=True)
+ has_view=(
+ "view:"incmd_or_topic.locks
+ ifinherits_from(cmd_or_topic,"evennia.commands.command.Command")
+ elsecmd_or_topic.locks.get("view")
+ )
+
+ ifhas_view:
+ returncmd_or_topic.access(caller,'view',default=True)
+ else:
+ # no explicit 'view' lock - use the 'read' lock
+ returncmd_or_topic.access(caller,'read',default=True)
+
+
[docs]defcollect_topics(self,caller,mode='list'):
+ """
+ Collect help topics from all sources (cmd/db/file).
+
+ Args:
+ caller (Object or Account): The user of the Command.
+ mode (str): One of 'list' or 'query', where the first means we are collecting to view
+ the help index and the second because of wanting to search for a specific help
+ entry/cmd to read. This determines which access should be checked.
+
+ Returns:
+ tuple: A tuple of three dicts containing the different types of help entries
+ in the order cmd-help, db-help, file-help:
+ `({key: cmd,...}, {key: dbentry,...}, {key: fileentry,...}`
+
+ """
+ # start with cmd-help
+ cmdset=self.cmdset
+ # removing doublets in cmdset, caused by cmdhandler
+ # having to allow doublet commands to manage exits etc.
+ cmdset.make_unique(caller)
+ # retrieve all available commands and database / file-help topics.
+ # also check the 'cmd:' lock here
+ cmd_help_topics=[cmdforcmdincmdsetifcmdandcmd.access(caller,'cmd')]
+ # get all file-based help entries, checking perms
+ file_help_topics={
+ topic.key.lower().strip():topic
+ fortopicinFILE_HELP_ENTRIES.all()
+ }
+ # get db-based help entries, checking perms
+ db_help_topics={
+ topic.key.lower().strip():topic
+ fortopicinHelpEntry.objects.all()
+ }
+ ifmode=='list':
+ # check the view lock for all help entries/commands and determine key
+ cmd_help_topics={
+ cmd.auto_help_display_key
+ ifhasattr(cmd,"auto_help_display_key")elsecmd.key:cmd
+ forcmdincmd_help_topicsifself.can_list_topic(cmd,caller)}
+ db_help_topics={
+ key:entryforkey,entryindb_help_topics.items()
+ ifself.can_list_topic(entry,caller)
+ }
+ file_help_topics={
+ key:entryforkey,entryinfile_help_topics.items()
+ ifself.can_list_topic(entry,caller)}
+ else:
+ # query
+ cmd_help_topics={
+ cmd.auto_help_display_key
+ ifhasattr(cmd,"auto_help_display_key")elsecmd.key:cmd
+ forcmdincmd_help_topicsifself.can_read_topic(cmd,caller)}
+ db_help_topics={
+ key:entryforkey,entryindb_help_topics.items()
+ ifself.can_read_topic(entry,caller)
+ }
+ file_help_topics={
+ key:entryforkey,entryinfile_help_topics.items()
+ ifself.can_read_topic(entry,caller)}
+
+ returncmd_help_topics,db_help_topics,file_help_topics
+
+
[docs]defdo_search(self,query,entries,search_fields=None):
+ """
+ Perform a help-query search, default using Lunr search engine.
+
+ Args:
+ query (str): The help entry to search for.
+ entries (list): All possibilities. A mix of commands, HelpEntries and FileHelpEntries.
+ search_fields (list): A list of dicts defining how Lunr will find the
+ search data on the elements. If not given, will use a default.
+
+ Returns:
+ tuple: A tuple (match, suggestions).
+
+ """
+ ifnotsearch_fields:
+ # lunr search fields/boosts
+ search_fields=[
+ {"field_name":"key","boost":10},
+ {"field_name":"aliases","boost":9},
+ {"field_name":"category","boost":8},
+ {"field_name":"tags","boost":1},# tags are not used by default
+ ]
+ match,suggestions=None,None
+ formatch_queryin(query,f"{query}*"):
+ # We first do an exact word-match followed by a start-by query. The
+ # return of this will either be a HelpCategory, a Command or a
+ # HelpEntry/FileHelpEntry.
+ matches,suggestions=help_search_with_index(
+ match_query,entries,
+ suggestion_maxnum=self.suggestion_maxnum,
+ fields=search_fields
+ )
+ ifmatches:
+ match=matches[0]
+ break
+ returnmatch,suggestions
[docs]defparse(self):""" input is a string containing the command or topic to match.
+
+ The allowed syntax is
+ ::
+
+ help <topic>[/<subtopic>[/<subtopic>[/...]]]
+
+ The database/command query is always for `<topic>`, and any subtopics
+ is then parsed from there. If a `<topic>` has spaces in it, it is
+ always matched before assuming the space begins a subtopic.
+
"""
- self.original_args=self.args.strip()
- self.args=self.args.strip().lower()
[docs]deffunc(self):""" Run the dynamic help entry creator. """
- query,cmdset=self.args,self.cmdsetcaller=self.caller
-
- suggestion_cutoff=self.suggestion_cutoff
- suggestion_maxnum=self.suggestion_maxnum
+ query,subtopics,cmdset=self.topic,self.subtopics,self.cmdset
+ clickable_topics=self.clickable_topicsifnotquery:
- query="all"
+ # list all available help entries, grouped by category. We want to
+ # build dictionaries {category: [topic, topic, ...], ...}
- # removing doublets in cmdset, caused by cmdhandler
- # having to allow doublet commands to manage exits etc.
- cmdset.make_unique(caller)
+ cmd_help_topics,db_help_topics,file_help_topics= \
+ self.collect_topics(caller,mode='list')
- # retrieve all available commands and database topics
- all_cmds=[cmdforcmdincmdsetifself.check_show_help(cmd,caller)]
- all_topics=[
- topicfortopicinHelpEntry.objects.all()iftopic.access(caller,"view",default=True)
- ]
- all_categories=list(
- set(
- [cmd.help_category.lower()forcmdinall_cmds]
- +[topic.help_category.lower()fortopicinall_topics]
- )
- )
+ # db-topics override file-based ones
+ file_db_help_topics={**file_help_topics,**db_help_topics}
+
+ # group by category (cmds are listed separately)
+ cmd_help_by_category=defaultdict(list)
+ file_db_help_by_category=defaultdict(list)
+ forkey,cmdincmd_help_topics.items():
+ cmd_help_by_category[cmd.help_category].append(key)
+ forkey,entryinfile_db_help_topics.items():
+ file_db_help_by_category[entry.help_category].append(key)
+
+ # generate the index and display
+ output=self.format_help_index(cmd_help_by_category,
+ file_db_help_by_category,
+ click_topics=clickable_topics)
+ self.msg_help(output)
- ifqueryin("list","all"):
- # we want to list all available help entries, grouped by category
- hdict_cmd=defaultdict(list)
- hdict_topic=defaultdict(list)
- # create the dictionaries {category:[topic, topic ...]} required by format_help_list
- # Filter commands that should be reached by the help
- # system, but not be displayed in the table, or be displayed differently.
- forcmdinall_cmds:
- ifself.should_list_cmd(cmd,caller):
- key=(
- cmd.auto_help_display_key
- ifhasattr(cmd,"auto_help_display_key")
- elsecmd.key
- )
- hdict_cmd[cmd.help_category].append(key)
- [hdict_topic[topic.help_category].append(topic.key)fortopicinall_topics]
- # report back
- self.msg_help(self.format_help_list(hdict_cmd,hdict_topic))return
- # Try to access a particular command
+ # search for a specific entry. We need to check for 'read' access here before # building the
+ # set of possibilities.
+ cmd_help_topics,db_help_topics,file_help_topics= \
+ self.collect_topics(caller,mode='query')
- # build vocabulary of suggestions and rate them by string similarity.
- suggestions=None
- ifsuggestion_maxnum>0:
- vocabulary=(
- [cmd.keyforcmdinall_cmdsifcmd]
- +[topic.keyfortopicinall_topics]
- +all_categories
- )
- [vocabulary.extend(cmd.aliases)forcmdinall_cmds]
- suggestions=[
- sugg
- forsugginstring_suggestions(
- query,set(vocabulary),cutoff=suggestion_cutoff,maxnum=suggestion_maxnum
- )
- ifsugg!=query
- ]
- ifnotsuggestions:
- suggestions=[
- suggforsugginvocabularyifsugg!=queryandsugg.startswith(query)
- ]
+ # db-help topics takes priority over file-help
+ file_db_help_topics={**file_help_topics,**db_help_topics}
- # try an exact command auto-help match
- match=[cmdforcmdinall_cmdsifcmd==query]
+ # commands take priority over the other types
+ all_topics={**file_db_help_topics,**cmd_help_topics}
+
+ # get all categories
+ all_categories=list(set(
+ HelpCategory(topic.help_category)fortopicinall_topics.values()))
+
+ # all available help options - will be searched in order. We also check # the
+ # read-permission here.
+ entries=list(all_topics.values())+all_categories
+
+ # lunr search fields/boosts
+ match,suggestions=self.do_search(query,entries)ifnotmatch:
- # try an inexact match with prefixes stripped from query and cmds
- _query=query[1:]ifquery[0]inCMD_IGNORE_PREFIXESelsequery
+ # no topic matches found. Only give suggestions.
+ help_text=f"There is no help topic matching '{query}'."
- match=[
- cmd
- forcmdinall_cmds
- formincmd._matchset
- ifm==_queryorm[0]inCMD_IGNORE_PREFIXESandm[1:]==_query
- ]
+ ifnotsuggestions:
+ # we don't even have a good suggestion. Run a second search,
+ # doing a full-text search in the actual texts of the help
+ # entries
- iflen(match)==1:
- cmd=match[0]
- key=cmd.auto_help_display_keyifhasattr(cmd,"auto_help_display_key")elsecmd.key
- formatted=self.format_help_entry(
- key,cmd.get_help(caller,cmdset),aliases=cmd.aliases,suggested=suggestions,
- )
- self.msg_help(formatted)
- return
+ search_fields=[
+ {"field_name":"text","boost":1},
+ ]
- # try an exact database help entry match
- match=list(HelpEntry.objects.find_topicmatch(query,exact=True))
- iflen(match)==1:
- formatted=self.format_help_entry(
- match[0].key,
- match[0].entrytext,
- aliases=match[0].aliases.all(),
+ formatch_queryin[query,f"{query}*"]:
+ _,suggestions=help_search_with_index(
+ match_query,entries,
+ suggestion_maxnum=self.suggestion_maxnum,
+ fields=search_fields
+ )
+
+ ifsuggestions:
+ help_text+=(
+ "\n... But matches where found within the help "
+ "texts of the suggestions below.")
+ break
+
+ output=self.format_help_entry(
+ topic=None,# this will give a no-match style title
+ help_text=help_text,suggested=suggestions,
+ click_topics=clickable_topics)
- self.msg_help(formatted)
+
+ self.msg_help(output)return
- # try to see if a category name was entered
- ifqueryinall_categories:
- self.msg_help(
- self.format_help_list(
- {
- query:[
- cmd.auto_help_display_key
- ifhasattr(cmd,"auto_help_display_key")
- elsecmd.key
- forcmdinall_cmds
- ifcmd.help_category==query
- ]
- },
- {query:[topic.keyfortopicinall_topicsiftopic.help_category==query]},
- )
- )
+ ifisinstance(match,HelpCategory):
+ # no subtopics for categories - these are just lists of topics
+ category=match.key
+ category_lower=category.lower()
+ cmds_in_category=[keyforkey,cmdincmd_help_topics.items()
+ ifcategory_lower==cmd.help_category]
+ topics_in_category=[keyforkey,topicinfile_db_help_topics.items()
+ ifcategory_lower==topic.help_category]
+ output=self.format_help_index({category:cmds_in_category},
+ {category:topics_in_category},
+ title_lone_category=True,
+ click_topics=clickable_topics)
+ self.msg_help(output)return
- # no exact matches found. Just give suggestions.
- self.msg(
- self.format_help_entry(
- "",f"No help entry found for '{query}'",None,suggested=suggestions
- ),
- options={"type":"help"},
- )
+ ifinherits_from(match,"evennia.commands.command.Command"):
+ # a command match
+ topic=match.key
+ help_text=match.get_help(caller,cmdset)
+ aliases=match.aliases
+ suggested=suggestions[1:]
+ else:
+ # a database (or file-help) match
+ topic=match.key
+ help_text=match.entrytext
+ aliases=match.aliasesifisinstance(match.aliases,list)elsematch.aliases.all()
+ suggested=suggestions[1:]
+
+ # parse for subtopics. The subtopic_map is a dict with the current topic/subtopic
+ # text is stored under a `None` key and all other keys are subtopic titles pointing
+ # to nested dicts.
+
+ subtopic_map=parse_entry_for_subcategories(help_text)
+ help_text=subtopic_map[None]
+ subtopic_index=[subtopicforsubtopicinsubtopic_mapifsubtopicisnotNone]
+
+ ifsubtopics:
+ # if we asked for subtopics, parse the found topic_text to see if any match.
+ # the subtopics is a list describing the path through the subtopic_map.
+
+ forsubtopic_queryinsubtopics:
+
+ ifsubtopic_querynotinsubtopic_map:
+ # exact match failed. Try startswith-match
+ fuzzy_match=False
+ forkeyinsubtopic_map:
+ ifkeyandkey.startswith(subtopic_query):
+ subtopic_query=key
+ fuzzy_match=True
+ break
+
+ ifnotfuzzy_match:
+ # startswith failed - try an 'in' match
+ forkeyinsubtopic_map:
+ ifkeyandsubtopic_queryinkey:
+ subtopic_query=key
+ fuzzy_match=True
+ break
+
+ ifnotfuzzy_match:
+ # no match found - give up
+ checked_topic=topic+f"/{subtopic_query}"
+ output=self.format_help_entry(
+ topic=topic,
+ help_text=f"No help entry found for '{checked_topic}'",
+ subtopics=subtopic_index,
+ click_topics=clickable_topics
+ )
+ self.msg_help(output)
+ return
+
+ # if we get here we have an exact or fuzzy match
+
+ subtopic_map=subtopic_map.pop(subtopic_query)
+ subtopic_index=[subtopicforsubtopicinsubtopic_mapifsubtopicisnotNone]
+ # keep stepping down into the tree, append path to show position
+ topic=topic+f"/{subtopic_query}"
+
+ # we reached the bottom of the topic tree
+ help_text=subtopic_map[None]
+
+ output=self.format_help_entry(
+ topic=topic,
+ help_text=help_text,
+ aliases=aliasesifnotsubtopicselseNone,
+ subtopics=subtopic_index,
+ suggested=suggested,
+ click_topics=clickable_topics
+ )
+
+ self.msg_help(output)def_loadhelp(caller):
@@ -379,7 +721,7 @@
delcaller.db._editing_help
-
[docs]classCmdSetHelp(CmdHelp):""" Edit the help database.
@@ -394,22 +736,64 @@
delete - remove help topic. Examples:
- sethelp throw = This throws something at ...
+ sethelp lore = In the beginning was ... sethelp/append pickpocketing,Thievery = This steals ... sethelp/replace pickpocketing, ,attr(is_thief) = This steals ... sethelp/edit thievery
- This command manipulates the help database. A help entry can be created,
- appended/merged to and deleted. If you don't assign a category, the
- "General" category will be used. If no lockstring is specified, default
- is to let everyone read the help file.
+ If not assigning a category, the `settings.DEFAULT_HELP_CATEGORY` category
+ will be used. If no lockstring is specified, everyone will be able to read
+ the help entry. Sub-topics are embedded in the help text.
+
+ Note that this cannot modify command-help entries - these are modified
+ in-code, outside the game.
+
+ # SUBTOPICS
+
+ ## Adding subtopics
+
+ Subtopics helps to break up a long help entry into sub-sections. Users can
+ access subtopics with |whelp topic/subtopic/...|n Subtopics are created and
+ stored together with the main topic.
+
+ To start adding subtopics, add the text '# SUBTOPICS' on a new line at the
+ end of your help text. After this you can now add any number of subtopics,
+ each starting with '## <subtopic-name>' on a line, followed by the
+ help-text of that subtopic.
+ Use '### <subsub-name>' to add a sub-subtopic and so on. Max depth is 5. A
+ subtopic's title is case-insensitive and can consist of multiple words -
+ the user will be able to enter a partial match to access it.
+
+ For example:
+
+ | Main help text for <topic>
+ |
+ | # SUBTOPICS
+ |
+ | ## about
+ |
+ | Text for the '<topic>/about' subtopic'
+ |
+ | ### more about-info
+ |
+ | Text for the '<topic>/about/more about-info sub-subtopic
+ |
+ | ## extra
+ |
+ | Text for the '<topic>/extra' subtopic """key="sethelp"
+ aliases=[]switch_options=("edit","replace","append","extend","delete")locks="cmd:perm(Helper)"help_category="Building"
+ arg_regex=None
+
+
[docs]defparse(self):
+ """We want to use the default parser rather than the CmdHelp.parse"""
+ returnCOMMAND_DEFAULT_CLASS.parse(self)
[docs]deffunc(self):"""Implement the function"""
@@ -429,23 +813,71 @@
self.msg("You have to define a topic!")returntopicstrlist=topicstr.split(";")
- topicstr,aliases=(topicstrlist[0],topicstrlist[1:]iflen(topicstr)>1else[])
+ topicstr,aliases=(
+ topicstrlist[0],
+ topicstrlist[1:]iflen(topicstr)>1else[],
+ )aliastxt=("(aliases: %s)"%", ".join(aliases))ifaliaseselse""old_entry=None# check if we have an old entry with the same name
- try:
- forquerystrintopicstrlist:
- old_entry=HelpEntry.objects.find_topicmatch(querystr)# also search by alias
- ifold_entry:
- old_entry=list(old_entry)[0]
+
+ cmd_help_topics,db_help_topics,file_help_topics= \
+ self.collect_topics(self.caller,mode='query')
+ # db-help topics takes priority over file-help
+ file_db_help_topics={**file_help_topics,**db_help_topics}
+ # commands take priority over the other types
+ all_topics={**file_db_help_topics,**cmd_help_topics}
+ # get all categories
+ all_categories=list(set(
+ HelpCategory(topic.help_category)fortopicinall_topics.values()))
+ # all available help options - will be searched in order. We also check # the
+ # read-permission here.
+ entries=list(all_topics.values())+all_categories
+
+ # default setup
+ category=lhslist[1]ifnlist>1elseDEFAULT_HELP_CATEGORY
+ lockstring=",".join(lhslist[2:])ifnlist>2else"read:all()"
+
+ # search for existing entries of this or other types
+ old_entry=None
+ forquerystrintopicstrlist:
+ match,_=self.do_search(querystr,entries)
+ ifmatch:
+ warning=None
+ ifisinstance(match,HelpCategory):
+ warning=(f"'{querystr}' matches (or partially matches) the name of "
+ "help-category '{match.key}'. If you continue, your help entry will "
+ "take precedence and the category (or part of its name) *may* not "
+ "be usable for grouping help entries anymore.")
+ elifinherits_from(match,"evennia.commands.command.Command"):
+ warning=(f"'{querystr}' matches (or partially matches) the key/alias of "
+ "Command '{match.key}'. Command-help take precedence over other "
+ "help entries so your help *may* be impossible to reach for those "
+ "with access to that command.")
+ elifinherits_from(match,"evennia.help.filehelp.FileHelpEntry"):
+ warning=(f"'{querystr}' matches (or partially matches) the name/alias of the "
+ "file-based help file '{match.key}'. File-help entries cannot be "
+ "modified from in-game (they are files on-disk). If you continue, "
+ "your help entry *may* shadow the file-based one's name partly or "
+ "completely.")
+ ifwarning:
+ # show a warning for a clashing help-entry type. Even if user accepts this
+ # we don't break here since we may need to show warnings for other inputs.
+ # We don't count this as an old-entry hit because we can't edit these
+ # types of entries.
+ self.msg(f"|rWarning:\n|r{warning}|n")
+ repl=yield("|wDo you still want to continue? Y/[N]?|n")
+ ifrepl.lower()notin('y','yes'):
+ self.msg("Aborted.")
+ return
+ else:
+ # a db-based help entry - this is OK
+ old_entry=match
+ category=lhslist[1]ifnlist>1elseold_entry.help_category
+ lockstring=",".join(lhslist[2:])ifnlist>2elseold_entry.locks.get()break
- category=lhslist[1]ifnlist>1elseold_entry.help_category
- lockstring=",".join(lhslist[2:])ifnlist>2elseold_entry.locks.get()
- exceptException:
- old_entry=None
- category=lhslist[1]ifnlist>1else"General"
- lockstring=",".join(lhslist[2:])ifnlist>2else"view:all()"
+
category=category.lower()if"edit"inswitches:
@@ -458,7 +890,7 @@
helpentry=old_entryelse:helpentry=create.create_help_entry(
- topicstr,self.rhs,category=category,locks=lockstring,aliases=aliases
+ topicstr,self.rhs,category=category,locks=lockstring,aliases=aliases,)self.caller.db._editing_help=helpentry
@@ -487,6 +919,7 @@
old_entry.aliases.add(aliases)self.msg("Entry updated:\n%s%s"%(old_entry.entrytext,aliastxt))return
+
if"delete"inswitchesor"del"inswitches:# delete the help entryifnotold_entry:
@@ -513,8 +946,8 @@
self.msg("Overwrote the old topic '%s'%s."%(topicstr,aliastxt))else:self.msg(
- "Topic '%s'%s already exists. Use /replace to overwrite "
- "or /append or /merge to add text to it."%(topicstr,aliastxt)
+ f"Topic '{topicstr}'{aliastxt} already exists. Use /edit to open in editor, or "
+ "/replace, /append and /merge to modify it directly.")else:# no old entry. Create a new one.
@@ -522,7 +955,7 @@
topicstr,self.rhs,category=category,locks=lockstring,aliases=aliases)ifnew_entry:
- self.msg("Topic '%s'%s was successfully created."%(topicstr,aliastxt))
+ self.msg(f"Topic '{topicstr}'{aliastxt} was successfully created.")if"edit"inswitches:# open the line editor to edit the helptextself.caller.db._editing_help=new_entry
@@ -537,7 +970,7 @@
returnelse:self.msg(
- "Error when creating topic '%s'%s! Contact an admin."%(topicstr,aliastxt)
+ f"Error when creating topic '{topicstr}'{aliastxt}! Contact an admin.")
-
diff --git a/docs/0.9.5/_modules/evennia/commands/default/syscommands.html b/docs/0.9.5/_modules/evennia/commands/default/syscommands.html
index cc3167d62e..9126cbd6ca 100644
--- a/docs/0.9.5/_modules/evennia/commands/default/syscommands.html
+++ b/docs/0.9.5/_modules/evennia/commands/default/syscommands.html
@@ -68,7 +68,6 @@
fromevennia.commands.cmdhandlerimportCMD_NOINPUTfromevennia.commands.cmdhandlerimportCMD_NOMATCHfromevennia.commands.cmdhandlerimportCMD_MULTIMATCH
-fromevennia.commands.cmdhandlerimportCMD_CHANNELfromevennia.utilsimportutilsfromdjango.confimportsettings
@@ -145,53 +144,6 @@
matches=self.matches# at_search_result will itself msg the multimatch options to the caller.at_search_result([match[2]formatchinmatches],self.caller,query=matches[0][0])
-
-
-# Command called when the command given at the command line
-# was identified as a channel name, like there existing a
-# channel named 'ooc' and the user wrote
-# > ooc Hello!
-
-
-
[docs]classSystemSendToChannel(COMMAND_DEFAULT_CLASS):
- """
- This is a special command that the cmdhandler calls
- when it detects that the command given matches
- an existing Channel object key (or alias).
- """
-
- key=CMD_CHANNEL
- locks="cmd:all()"
-
-
[docs]classCmdScripts(COMMAND_DEFAULT_CLASS):"""
- list and manage all running scripts
+ List and manage all running scripts. Allows for creating new global
+ scripts. Usage:
- scripts[/switches] [#dbref, key, script.path or <obj>]
+ script[/switches] [#dbref, key, script.path or <obj>] Switches:
- start - start a script (must supply a script path)
- stop - stops an existing script
- kill - kills a script - without running its cleanup hooks
- validate - run a validation on the script(s)
+ create - create a new global script of given typeclass path. This will
+ auto-start the script's timer if it has one.
+ start - start/unpause an existing script's timer.
+ stop - stops an existing script's timer
+ pause - pause a script's timer
+ delete - deletes script. This will also stop the timer as needed If no switches are given, this command just views all active scripts. The argument can be either an object, at which point it
@@ -534,83 +541,100 @@
or #dbref. For using the /stop switch, a unique script #dbref is required since whole classes of scripts often have the same name.
- Use script for managing commands on objects.
+ Use the `script` build-level command for managing scripts attached to
+ objects.
+
"""key="scripts"
- aliases=["globalscript","listscripts"]
- switch_options=("start","stop","kill","validate")
+ aliases=["scripts"]
+ switch_options=("create","start","stop","pause","delete")locks="cmd:perm(listscripts) or perm(Admin)"help_category="System"excluded_typeclass_paths=["evennia.prototypes.prototypes.DbPrototype"]
+ switch_mapping={
+ "create":"|gCreated|n",
+ "start":"|gStarted|n",
+ "stop":"|RStopped|n",
+ "pause":"|Paused|n",
+ "delete":"|rDeleted|n"
+ }
+
+ def_search_script(self,args):
+ # test first if this is a script match
+ scripts=ScriptDB.objects.get_all_scripts(key=args)
+ ifscripts:
+ returnscripts
+ # try typeclass path
+ scripts=ScriptDB.objects.filter(db_typeclass_path__iendswith=args)
+ ifscripts:
+ returnscripts
+ # try to find an object instead.
+ objects=ObjectDB.objects.object_search(args)
+ ifobjects:
+ scripts=ScriptDB.objects.filter(db_obj__in=objects)
+ returnscripts
+
[docs]deffunc(self):"""implement method"""caller=self.callerargs=self.args
- ifargs:
- if"start"inself.switches:
- # global script-start mode
- new_script=create.create_script(args)
- ifnew_script:
- caller.msg("Global script %s was started successfully."%args)
- else:
- caller.msg("Global script %s could not start correctly. See logs."%args)
+ if"create"inself.switches:
+ # global script-start mode
+ verb=self.switch_mapping['create']
+ ifnotargs:
+ caller.msg("Usage script/create <key or typeclass>")return
-
- # test first if this is a script match
- scripts=ScriptDB.objects.get_all_scripts(key=args)
- ifnotscripts:
- # try to find an object instead.
- objects=ObjectDB.objects.object_search(args)
- ifobjects:
- scripts=[]
- forobjinobjects:
- # get all scripts on the object(s)
- scripts.extend(ScriptDB.objects.get_all_scripts_on_obj(obj))
- else:
- # we want all scripts.
- scripts=ScriptDB.objects.get_all_scripts()
- ifnotscripts:
- caller.msg("No scripts are running.")
- return
- # filter any found scripts by tag category.
- scripts=scripts.exclude(db_typeclass_path__in=self.excluded_typeclass_paths)
-
- ifnotscripts:
- string="No scripts found with a key '%s', or on an object named '%s'."%(args,args)
- caller.msg(string)
+ new_script=create.create_script(args)
+ ifnew_script:
+ caller.msg(f"Global Script {verb} - {new_script.key} ({new_script.typeclass_path})")
+ ScriptEvMore(caller,[new_script],session=self.session)
+ else:
+ caller.msg(f"Global Script |rNOT|n {verb} |r(see log)|n - arguments: {args}")return
- ifself.switchesandself.switches[0]in("stop","del","delete","kill"):
- # we want to delete something
- iflen(scripts)==1:
- # we have a unique match!
- if"kill"inself.switches:
- string="Killing script '%s'"%scripts[0].key
- scripts[0].stop(kill=True)
- else:
- string="Stopping script '%s'."%scripts[0].key
- scripts[0].stop()
- # import pdb # DEBUG
- # pdb.set_trace() # DEBUG
- ScriptDB.objects.validate()# just to be sure all is synced
- caller.msg(string)
- else:
- # multiple matches.
- ScriptEvMore(caller,scripts,session=self.session)
- caller.msg("Multiple script matches. Please refine your search")
- elifself.switchesandself.switches[0]in("validate","valid","val"):
- # run validation on all found scripts
- nr_started,nr_stopped=ScriptDB.objects.validate(scripts=scripts)
- string="Validated %s scripts. "%ScriptDB.objects.all().count()
- string+="Started %s and stopped %s scripts."%(nr_started,nr_stopped)
- caller.msg(string)
+ # all other switches require existing scripts
+ ifargs:
+ scripts=self._search_script(args)
+ ifnotscripts:
+ caller.msg(f"No scripts found matching '{args}'.")
+ returnelse:
- # No stopping or validation. We just want to view things.
+ scripts=ScriptDB.objects.all()
+ ifnotscripts:
+ caller.msg("No scripts found.")
+ return
+
+ ifargsandself.switches:
+ # global script-modifying mode
+ ifscripts.count()>1:
+ caller.msg("Multiple script matches. Please refine your search.")
+ return
+ script=scripts[0]
+ script_key=script.key
+ script_typeclass_path=script.typeclass_path
+ forswitchinself.switches:
+ verb=self.switch_mapping[switch]
+ msgs=[]
+ try:
+ getattr(script,switch)()
+ exceptException:
+ logger.log_trace()
+ msgs.append(f"Global Script |rNOT|n {verb} |r(see log)|n - "
+ f"{script_key} ({script_typeclass_path})|n")
+ else:
+ msgs.append(f"Global Script {verb} - "
+ f"{script_key} ({script_typeclass_path})")
+ caller.msg("\n".join(msgs))
+ if"delete"notinself.switches:
+ ScriptEvMore(caller,[script],session=self.session)
+ return
+ else:
+ # simply show the found scriptsScriptEvMore(caller,scripts.order_by("id"),session=self.session)
[docs]classCmdTasks(COMMAND_DEFAULT_CLASS):
+ """
+ Display or terminate active tasks (delays).
+
+ Usage:
+ tasks[/switch] [task_id or function_name]
+
+ Switches:
+ pause - Pause the callback of a task.
+ unpause - Process all callbacks made since pause() was called.
+ do_task - Execute the task (call its callback).
+ call - Call the callback of this task.
+ remove - Remove a task without executing it.
+ cancel - Stop a task from automatically executing.
+
+ Notes:
+ A task is a single use method of delaying the call of a function. Calls are created
+ in code, using `evennia.utils.delay`.
+ See |luhttps://www.evennia.com/docs/latest/Command-Duration.html|ltthe docs|le for help.
+
+ By default, tasks that are canceled and never called are cleaned up after one minute.
+
+ Examples:
+ - `tasks/cancel move_callback` - Cancels all movement delays from the slow_exit contrib.
+ In this example slow exits creates it's tasks with
+ `utils.delay(move_delay, move_callback)`
+ - `tasks/cancel 2` - Cancel task id 2.
+
+ """
+
+ key="tasks"
+ aliases=["delays","task"]
+ switch_options=("pause","unpause","do_task","call","remove","cancel")
+ locks="perm(Developer)"
+ help_category="System"
+
+
[docs]@staticmethod
+ defcoll_date_func(task):
+ """Replace regex characters in date string and collect deferred function name."""
+ t_comp_date=str(task[0]).replace('-','/')
+ t_func_name=str(task[1]).split(' ')
+ t_func_mem_ref=t_func_name[3]iflen(t_func_name)>=4elseNone
+ returnt_comp_date,t_func_mem_ref
+
+
[docs]defdo_task_action(self,*args,**kwargs):
+ """
+ Process the action of a tasks command.
+
+ This exists to gain support with yes or no function from EvMenu.
+ """
+ task_id=self.task_id
+
+ # get a reference of the global task handler
+ global_TASK_HANDLER
+ if_TASK_HANDLERisNone:
+ fromevennia.scripts.taskhandlerimportTASK_HANDLERas_TASK_HANDLER
+
+ # verify manipulating the correct task
+ task_args=_TASK_HANDLER.tasks.get(task_id,False)
+ ifnottask_args:# check if the task is still active
+ self.msg('Task completed while waiting for input.')
+ return
+ else:
+ # make certain a task with matching IDs has not been created
+ t_comp_date,t_func_mem_ref=self.coll_date_func(task_args)
+ ifself.t_comp_date!=t_comp_dateorself.t_func_mem_ref!=t_func_mem_ref:
+ self.msg('Task completed while waiting for input.')
+ return
+
+ # Do the action requested by command caller
+ action_return=self.task_action()
+ self.msg(f'{self.action_request} request completed.')
+ self.msg(f'The task function {self.action_request} returned: {action_return}')
+
+
[docs]deffunc(self):
+ # get a reference of the global task handler
+ global_TASK_HANDLER
+ if_TASK_HANDLERisNone:
+ fromevennia.scripts.taskhandlerimportTASK_HANDLERas_TASK_HANDLER
+ # handle no tasks active.
+ ifnot_TASK_HANDLER.tasks:
+ self.msg('There are no active tasks.')
+ ifself.switchesorself.args:
+ self.msg('Likely the task has completed and been removed.')
+ return
+
+ # handle caller's request to manipulate a task(s)
+ ifself.switchesandself.lhs:
+
+ # find if the argument is a task id or function name
+ action_request=self.switches[0]
+ try:
+ arg_is_id=int(self.lhslist[0])
+ exceptValueError:
+ arg_is_id=False
+
+ # if the argument is a task id, proccess the action on a single task
+ ifarg_is_id:
+
+ err_arg_msg='Switch and task ID are required when manipulating a task.'
+ task_comp_msg='Task completed while processing request.'
+
+ # handle missing arguments or switches
+ ifnotself.switchesandself.lhs:
+ self.msg(err_arg_msg)
+ return
+
+ # create a handle for the task
+ task_id=arg_is_id
+ task=TaskHandlerTask(task_id)
+
+ # handle task no longer existing
+ ifnottask.exists():
+ self.msg(f'Task {task_id} does not exist.')
+ return
+
+ # get a reference of the function caller requested
+ switch_action=getattr(task,action_request,False)
+ ifnotswitch_action:
+ self.msg(f'{self.switches[0]}, is not an acceptable task action or ' \
+ f'{task_comp_msg.lower()}')
+
+ # verify manipulating the correct task
+ iftask_idin_TASK_HANDLER.tasks:
+ task_args=_TASK_HANDLER.tasks.get(task_id,False)
+ ifnottask_args:# check if the task is still active
+ self.msg(task_comp_msg)
+ return
+ else:
+ t_comp_date,t_func_mem_ref=self.coll_date_func(task_args)
+ t_func_name=str(task_args[1]).split(' ')
+ t_func_name=t_func_name[1]iflen(t_func_name)>=2elseNone
+
+ iftask.exists():# make certain the task has not been called yet.
+ prompt=(f'{action_request.capitalize()} task {task_id} with completion date '
+ f'{t_comp_date} ({t_func_name}) {{options}}?')
+ no_msg=f'No {action_request} processed.'
+ # record variables for use in do_task_action method
+ self.task_id=task_id
+ self.t_comp_date=t_comp_date
+ self.t_func_mem_ref=t_func_mem_ref
+ self.task_action=switch_action
+ self.action_request=action_request
+ ask_yes_no(self.caller,
+ prompt=prompt,
+ yes_action=self.do_task_action,
+ no_action=no_msg,
+ default="Y",
+ allow_abort=True)
+ returnTrue
+ else:
+ self.msg(task_comp_msg)
+ return
+
+ # the argument is not a task id, process the action on all task deferring the function
+ # specified as an argument
+ else:
+
+ name_match_found=False
+ arg_func_name=self.lhslist[0].lower()
+
+ # repack tasks into a new dictionary
+ current_tasks={}
+ fortask_id,task_argsin_TASK_HANDLER.tasks.items():
+ current_tasks.update({task_id:task_args})
+
+ # call requested action on all tasks with the function name
+ fortask_id,task_argsincurrent_tasks.items():
+ t_func_name=str(task_args[1]).split(' ')
+ t_func_name=t_func_name[1]iflen(t_func_name)>=2elseNone
+ # skip this task if it is not for the function desired
+ ifarg_func_name!=t_func_name:
+ continue
+ name_match_found=True
+ task=TaskHandlerTask(task_id)
+ switch_action=getattr(task,action_request,False)
+ ifswitch_action:
+ action_return=switch_action()
+ self.msg(f'Task action {action_request} completed on task ID {task_id}.')
+ self.msg(f'The task function {action_request} returned: {action_return}')
+
+ # provide a message if not tasks of the function name was found
+ ifnotname_match_found:
+ self.msg(f'No tasks deferring function name {arg_func_name} found.')
+ return
+ returnTrue
+
+ # check if an maleformed request was created
+ elifself.switchesorself.lhs:
+ self.msg('Task command misformed.')
+ self.msg('Proper format tasks[/switch] [function name or task id]')
+ return
+
+ # No task manupilation requested, build a table of tasks and display it
+ # get the width of screen in characters
+ width=self.client_width()
+ # create table header and list to hold tasks data and actions
+ tasks_header=('Task ID','Completion Date','Function','Arguments','KWARGS',
+ 'persistent')
+ # empty list of lists, the size of the header
+ tasks_list=[list()foriinrange(len(tasks_header))]
+ fortask_id,taskin_TASK_HANDLER.tasks.items():
+ # collect data from the task
+ t_comp_date,t_func_mem_ref=self.coll_date_func(task)
+ t_func_name=str(task[1]).split(' ')
+ t_func_name=t_func_name[1]iflen(t_func_name)>=2elseNone
+ t_args=str(task[2])
+ t_kwargs=str(task[3])
+ t_pers=str(task[4])
+ # add task data to the tasks list
+ task_data=(task_id,t_comp_date,t_func_name,t_args,t_kwargs,t_pers)
+ foriinrange(len(tasks_header)):
+ tasks_list[i].append(task_data[i])
+ # create and display the table
+ tasks_table=EvTable(*tasks_header,table=tasks_list,maxwidth=width,border='cells',
+ align='center')
+ actions=(f'/{switch}'forswitchinself.switch_options)
+ helptxt=f"\nActions: {iter_to_str(actions)}"
+ self.msg(str(tasks_table)+helptxt)
-
diff --git a/docs/0.9.5/_modules/evennia/commands/default/tests.html b/docs/0.9.5/_modules/evennia/commands/default/tests.html
index 1be2d42e36..1f225bc793 100644
--- a/docs/0.9.5/_modules/evennia/commands/default/tests.html
+++ b/docs/0.9.5/_modules/evennia/commands/default/tests.html
@@ -57,14 +57,16 @@
importdatetimefromanythingimportAnything
+fromparameterizedimportparameterizedfromdjango.confimportsettings
+fromtwisted.internetimporttaskfromunittest.mockimportpatch,Mock,MagicMockfromevenniaimportDefaultRoom,DefaultExit,ObjectDBfromevennia.commands.default.cmdset_characterimportCharacterCmdSetfromevennia.utils.test_resourcesimportEvenniaTestfromevennia.commands.defaultimport(
- help,
+ helpashelp_module,general,system,admin,
@@ -75,7 +77,6 @@
unloggedin,syscommands,)
-fromevennia.commands.cmdparserimportbuild_matchesfromevennia.commands.default.muxcommandimportMuxCommandfromevennia.commands.commandimportCommand,InterruptCommandfromevennia.commandsimportcmdparser
@@ -89,24 +90,50 @@
# set up signal here since we are not starting the server
-_RE=re.compile(r"^\+|-+\+|\+-+|--+|\|(?:\s|$)",re.MULTILINE)
+_RE_STRIP_EVMENU=re.compile(r"^\+|-+\+|\+-+|--+|\|(?:\s|$)",re.MULTILINE)# ------------------------------------------------------------# Command testing# ------------------------------------------------------------
-
[docs]@patch("evennia.server.portal.portal.LoopingCall",new=MagicMock())classCommandTest(EvenniaTest):"""
- Tests a command
+ Tests a Command by running it and comparing what messages it sends with
+ expected values. This tests without actually spinning up the cmdhandler
+ for every test, which is more controlled.
+
+ Example:
+ ::
+
+ from commands.echo import CmdEcho
+
+ class MyCommandTest(CommandTest):
+
+ def test_echo(self):
+ '''
+ Test that the echo command really returns
+ what you pass into it.
+ '''
+ self.call(MyCommand(), "hello world!",
+ "You hear your echo: 'Hello world!'")
+
"""
+ # formatting for .call's error message
+ _ERROR_FORMAT="""
+=========================== Wanted message ===================================
+{expected_msg}
+=========================== Returned message =================================
+{returned_msg}
+==============================================================================
+""".rstrip()
+
[docs]defcall(self,cmdobj,
- args,
+ input_args,msg=None,cmdset=None,noansi=True,
@@ -118,54 +145,140 @@
raw_string=None,):"""
- Test a command by assigning all the needed
- properties to cmdobj and running
- cmdobj.at_pre_cmd()
- cmdobj.parse()
- cmdobj.func()
- cmdobj.at_post_cmd()
- The msgreturn value is compared to eventual
- output sent to caller.msg in the game
+ Test a command by assigning all the needed properties to a cmdobj and
+ running the sequence. The resulting `.msg` calls will be mocked and
+ the text= calls to them compared to a expected output.
+
+ Args:
+ cmdobj (Command): The command object to use.
+ input_args (str): This should be the full input the Command should
+ see, such as 'look here'. This will become `.args` for the Command
+ instance to parse.
+ msg (str or dict, optional): This is the expected return value(s)
+ returned through `caller.msg(text=...)` calls in the command. If a string, the
+ receiver is controlled with the `receiver` kwarg (defaults to `caller`).
+ If this is a `dict`, it is a mapping
+ `{receiver1: "expected1", receiver2: "expected2",...}` and `receiver` is
+ ignored. The message(s) are compared with the actual messages returned
+ to the receiver(s) as the Command runs. Each check uses `.startswith`,
+ so you can choose to only include the first part of the
+ returned message if that's enough to verify a correct result. EvMenu
+ decorations (like borders) are stripped and should not be included. This
+ should also not include color tags unless `noansi=False`.
+ If the command returns texts in multiple separate `.msg`-
+ calls to a receiver, separate these with `|` if `noansi=True`
+ (default) and `||` if `noansi=False`. If no `msg` is given (`None`),
+ then no automatic comparison will be done.
+ cmdset (str, optional): If given, make `.cmdset` available on the Command
+ instance as it runs. While `.cmdset` is normally available on the
+ Command instance by default, this is usually only used by
+ commands that explicitly operates/displays cmdsets, like
+ `examine`.
+ noansi (str, optional): By default the color tags of the `msg` is
+ ignored, this makes them significant. If unset, `msg` must contain
+ the same color tags as the actual return message.
+ caller (Object or Account, optional): By default `self.char1` is used as the
+ command-caller (the `.caller` property on the Command). This allows to
+ execute with another caller, most commonly an Account.
+ receiver (Object or Account, optional): This is the object to receive the
+ return messages we want to test. By default this is the same as `caller`
+ (which in turn defaults to is `self.char1`). Note that if `msg` is
+ a `dict`, this is ignored since the receiver is already specified there.
+ cmdstring (str, optional): Normally this is the Command's `key`.
+ This allows for tweaking the `.cmdname` property of the
+ Command`. This isb used for commands with multiple aliases,
+ where the command explicitly checs which alias was used to
+ determine its functionality.
+ obj (str, optional): This sets the `.obj` property of the Command - the
+ object on which the Command 'sits'. By default this is the same as `caller`.
+ This can be used for testing on-object Command interactions.
+ inputs (list, optional): A list of strings to pass to functions that pause to
+ take input from the user (normally using `@interactive` and
+ `ret = yield(question)` or `evmenu.get_input`). Each element of the
+ list will be passed into the command as if the user wrote that at the prompt.
+ raw_string (str, optional): Normally the `.raw_string` property is set as
+ a combination of your `key/cmdname` and `input_args`. This allows
+ direct control of what this is, for example for testing edge cases
+ or malformed inputs. Returns:
- msg (str): The received message that was sent to the caller.
+ str or dict: The message sent to `receiver`, or a dict of
+ `{receiver: "msg", ...}` if multiple are given. This is usually
+ only used with `msg=None` to do the validation externally.
+
+ Raises:
+ AssertionError: If the returns of `.msg` calls (tested with `.startswith`) does not
+ match `expected_input`.
+
+ Notes:
+ As part of the tests, all methods of the Command will be called in
+ the proper order:
+
+ - cmdobj.at_pre_cmd()
+ - cmdobj.parse()
+ - cmdobj.func()
+ - cmdobj.at_post_cmd() """
+ # The `self.char1` is created in the `EvenniaTest` base along with
+ # other helper objects like self.room and self.objcaller=callerifcallerelseself.char1
- receiver=receiverifreceiverelsecallercmdobj.caller=callercmdobj.cmdname=cmdstringifcmdstringelsecmdobj.keycmdobj.raw_cmdname=cmdobj.cmdnamecmdobj.cmdstring=cmdobj.cmdname# deprecated
- cmdobj.args=args
+ cmdobj.args=input_argscmdobj.cmdset=cmdsetcmdobj.session=SESSIONS.session_from_sessid(1)cmdobj.account=self.account
- cmdobj.raw_string=raw_stringifraw_stringisnotNoneelsecmdobj.key+" "+args
+ cmdobj.raw_string=raw_stringifraw_stringisnotNoneelsecmdobj.key+" "+input_argscmdobj.obj=objor(callerifcallerelseself.char1)
- # test
- old_msg=receiver.msginputs=inputsor[]
- try:
+ # set up receivers
+ receiver_mapping={}
+ ifisinstance(msg,dict):
+ # a mapping {receiver: msg, ...}
+ receiver_mapping={recv:str(msg).strip()ifmsgelseNone
+ forrecv,msginmsg.items()}
+ else:
+ # a single expected string and thus a single receiver (defaults to caller)
+ receiver=receiverifreceiverelsecaller
+ receiver_mapping[receiver]=str(msg).strip()ifmsgisnotNoneelseNone
+
+ unmocked_msg_methods={}
+ forreceiverinreceiver_mapping:
+ # save the old .msg method so we can get it back
+ # cleanly after the test
+ unmocked_msg_methods[receiver]=receiver.msg
+ # replace normal `.msg` with a mockreceiver.msg=Mock()
+
+ # Run the methods of the Command. This mimics what happens in the
+ # cmdhandler. This will have the mocked .msg be called as part of the
+ # execution. Mocks remembers what was sent to them so we will be able
+ # to retrieve what was sent later.
+ try:ifcmdobj.at_pre_cmd():returncmdobj.parse()ret=cmdobj.func()
- # handle func's with yield in them (generators)
+ # handle func's with yield in them (making them generators)ifisinstance(ret,types.GeneratorType):whileTrue:try:inp=inputs.pop()ifinputselseNoneifinp:try:
+ # this mimics a user's reply to a promptret.send(inp)exceptTypeError:next(ret)ret=ret.send(inp)else:
+ # non-input yield, like yield(10). We don't pause
+ # but fire it immediately.next(ret)exceptStopIteration:break
@@ -176,40 +289,62 @@
exceptInterruptCommand:pass
- # clean out evtable sugar. We only operate on text-type
- stored_msg=[
- args[0]ifargsandargs[0]elsekwargs.get("text",utils.to_str(kwargs))
- forname,args,kwargsinreceiver.msg.mock_calls
- ]
- # Get the first element of a tuple if msg received a tuple instead of a string
- stored_msg=[str(smsg[0])ifisinstance(smsg,tuple)elsestr(smsg)forsmsginstored_msg]
- ifmsgisnotNone:
- msg=str(msg)# to be safe, e.g. `py` command may return ints
- # set our separator for returned messages based on parsing ansi or not
- msg_sep="|"ifnoansielse"||"
- # Have to strip ansi for each returned message for the regex to handle it correctly
- returned_msg=msg_sep.join(
- _RE.sub("",ansi.parse_ansi(mess,strip_ansi=noansi))formessinstored_msg
- ).strip()
- msg=msg.strip()
- ifmsg==""andreturned_msgornotreturned_msg.startswith(msg):
- prt=""
- foric,charinenumerate(msg):
- importre
+ forinpininputs:
+ # if there are any inputs left, we may have a non-generator
+ # input to handle (get_input/ask_yes_no that uses a separate
+ # cmdset rather than a yield
+ caller.execute_cmd(inp)
- prt+=char
+ # At this point the mocked .msg methods on each receiver will have
+ # stored all calls made to them (that's a basic function of the Mock
+ # class). We will not extract them and compare to what we expected to
+ # go to each receiver.
- sep1="\n"+"="*30+"Wanted message"+"="*34+"\n"
- sep2="\n"+"="*30+"Returned message"+"="*32+"\n"
- sep3="\n"+"="*78
- retval=sep1+msg+sep2+returned_msg+sep3
- raiseAssertionError(retval)
- else:
- returned_msg="\n".join(str(msg)formsginstored_msg)
- returned_msg=ansi.parse_ansi(returned_msg,strip_ansi=noansi).strip()
- receiver.msg=old_msg
+ returned_msgs={}
+ forreceiver,expected_msginreceiver_mapping.items():
+ # get the stored messages from the Mock with Mock.mock_calls.
+ stored_msg=[
+ args[0]ifargsandargs[0]elsekwargs.get("text",utils.to_str(kwargs))
+ forname,args,kwargsinreceiver.msg.mock_calls
+ ]
+ # we can return this now, we are done using the mock
+ receiver.msg=unmocked_msg_methods[receiver]
- returnreturned_msg
+ # Get the first element of a tuple if msg received a tuple instead of a string
+ stored_msg=[str(smsg[0])
+ ifisinstance(smsg,tuple)elsestr(smsg)forsmsginstored_msg]
+ ifexpected_msgisNone:
+ # no expected_msg; just build the returned_msgs dict
+
+ returned_msg="\n".join(str(msg)formsginstored_msg)
+ returned_msgs[receiver]=ansi.parse_ansi(returned_msg,strip_ansi=noansi).strip()
+ else:
+ # compare messages to expected
+
+ # set our separator for returned messages based on parsing ansi or not
+ msg_sep="|"ifnoansielse"||"
+
+ # We remove Evmenu decorations since that just makes it harder
+ # to write the comparison string. We also strip ansi before this
+ # comparison since otherwise it would mess with the regex.
+ returned_msg=msg_sep.join(
+ _RE_STRIP_EVMENU.sub(
+ "",ansi.parse_ansi(mess,strip_ansi=noansi))
+ formessinstored_msg).strip()
+
+ # this is the actual test
+ ifexpected_msg==""andreturned_msgornotreturned_msg.startswith(expected_msg):
+ # failed the test
+ raiseAssertionError(
+ self._ERROR_FORMAT.format(
+ expected_msg=expected_msg,returned_msg=returned_msg)
+ )
+ # passed!
+ returned_msgs[receiver]=returned_msg
+
+ iflen(returned_msgs)==1:
+ returnlist(returned_msgs.values())[0]
+ returnreturned_msgs# ------------------------------------------------------------
@@ -314,16 +449,153 @@
[docs]defsetUp(self):
+ super().setUp()
+ # we need to set up a logger here since lunr takes over the logger otherwise
+ importlogging
+
+ logging.basicConfig(level=logging.ERROR)
[docs]deftest_set_help(self):self.call(
- help.CmdSetHelp(),
+ help_module.CmdSetHelp(),"testhelp, General = This is a test","Topic 'testhelp' was successfully created.",
+ cmdset=CharacterCmdSet())
- self.call(help.CmdHelp(),"testhelp","Help for testhelp",cmdset=CharacterCmdSet())
+ self.call(help_module.CmdHelp(),"testhelp","Help for testhelp",cmdset=CharacterCmdSet())
+
+ @parameterized.expand([
+ ("test",# main help entry
+ "Help for test\n\n"
+ "Main help text\n\n"
+ "Subtopics:\n"
+ " test/creating extra stuff"
+ " test/something else"
+ " test/more"
+ ),
+ ("test/creating extra stuff",# subtopic, full match
+ "Help for test/creating extra stuff\n\n"
+ "Help on creating extra stuff.\n\n"
+ "Subtopics:\n"
+ " test/creating extra stuff/subsubtopic\n"
+ ),
+ ("test/creating",# startswith-match
+ "Help for test/creating extra stuff\n\n"
+ "Help on creating extra stuff.\n\n"
+ "Subtopics:\n"
+ " test/creating extra stuff/subsubtopic\n"
+ ),
+ ("test/extra",# partial match
+ "Help for test/creating extra stuff\n\n"
+ "Help on creating extra stuff.\n\n"
+ "Subtopics:\n"
+ " test/creating extra stuff/subsubtopic\n"
+ ),
+ ("test/extra/subsubtopic",# partial subsub-match
+ "Help for test/creating extra stuff/subsubtopic\n\n"
+ "A subsubtopic text"
+ ),
+ ("test/creating extra/subsub",# partial subsub-match
+ "Help for test/creating extra stuff/subsubtopic\n\n"
+ "A subsubtopic text"
+ ),
+ ("test/Something else",# case
+ "Help for test/something else\n\n"
+ "Something else"
+ ),
+ ("test/More",# case
+ "Help for test/more\n\n"
+ "Another text\n\n"
+ "Subtopics:\n"
+ " test/more/second-more"
+ ),
+ ("test/More/Second-more",
+ "Help for test/more/second-more\n\n"
+ "The Second More text.\n\n"
+ "Subtopics:\n"
+ " test/more/second-more/more again"
+ " test/more/second-more/third more"
+ ),
+ ("test/More/-more",# partial match
+ "Help for test/more/second-more\n\n"
+ "The Second More text.\n\n"
+ "Subtopics:\n"
+ " test/more/second-more/more again"
+ " test/more/second-more/third more"
+ ),
+ ("test/more/second/more again",
+ "Help for test/more/second-more/more again\n\n"
+ "Even more text.\n"
+ ),
+ ("test/more/second/third",
+ "Help for test/more/second-more/third more\n\n"
+ "Third more text\n"
+ ),
+ ])
+ deftest_subtopic_fetch(self,helparg,expected):
+ """
+ Check retrieval of subtopics.
+
+ """
+ classTestCmd(Command):
+ """
+ Main help text
+
+ # SUBTOPICS
+
+ ## creating extra stuff
+
+ Help on creating extra stuff.
+
+ ### subsubtopic
+
+ A subsubtopic text
+
+ ## Something else
+
+ Something else
+
+ ## More
+
+ Another text
+
+ ### Second-More
+
+ The Second More text.
+
+ #### More again
+
+ Even more text.
+
+ #### Third more
+
+ Third more text
+
+ """
+ key="test"
+
+ classTestCmdSet(CmdSet):
+ defat_cmdset_creation(self):
+ self.add(TestCmd())
+ self.add(help_module.CmdHelp())
+
+ self.call(help_module.CmdHelp(),
+ helparg,
+ expected,
+ cmdset=TestCmdSet())
[docs]deftest_pause_unpause(self):
+ # test pause
+ args=f'/pause {self.task.get_id()}'
+ wanted_msg='Yes or No, pause task 1? With completion date'
+ cmd_result=self.call(system.CmdTasks(),args,wanted_msg)
+ self.assertRegex(cmd_result,'\. Deferring function func_test_cmd_tasks\.')
+ self.char1.execute_cmd('y')
+ self.assertTrue(self.task.paused)
+ self.task_handler.clock.advance(self.timedelay+1)
+ # test unpause
+ args=f'/unpause {self.task.get_id()}'
+ self.assertTrue(self.task.exists())
+ wanted_msg='Yes or No, unpause task 1? With completion date'
+ cmd_result=self.call(system.CmdTasks(),args,wanted_msg)
+ self.assertRegex(cmd_result,'\. Deferring function func_test_cmd_tasks\.')
+ self.char1.execute_cmd('y')
+ # verify task continues after unpause
+ self.task_handler.clock.advance(1)
+ self.assertFalse(self.task.exists())
+
+
[docs]deftest_do_task(self):
+ args=f'/do_task {self.task.get_id()}'
+ wanted_msg='Yes or No, do_task task 1? With completion date'
+ cmd_result=self.call(system.CmdTasks(),args,wanted_msg)
+ self.assertRegex(cmd_result,'\. Deferring function func_test_cmd_tasks\.')
+ self.char1.execute_cmd('y')
+ self.assertFalse(self.task.exists())
+
+
[docs]deftest_remove(self):
+ args=f'/remove {self.task.get_id()}'
+ wanted_msg='Yes or No, remove task 1? With completion date'
+ cmd_result=self.call(system.CmdTasks(),args,wanted_msg)
+ self.assertRegex(cmd_result,'\. Deferring function func_test_cmd_tasks\.')
+ self.char1.execute_cmd('y')
+ self.assertFalse(self.task.exists())
+
+
[docs]deftest_call(self):
+ args=f'/call {self.task.get_id()}'
+ wanted_msg='Yes or No, call task 1? With completion date'
+ cmd_result=self.call(system.CmdTasks(),args,wanted_msg)
+ self.assertRegex(cmd_result,'\. Deferring function func_test_cmd_tasks\.')
+ self.char1.execute_cmd('y')
+ # make certain the task is still active
+ self.assertTrue(self.task.active())
+ # go past delay time, the task should call do_task and remove itself after calling.
+ self.task_handler.clock.advance(self.timedelay+1)
+ self.assertFalse(self.task.exists())
+
+
[docs]deftest_cancel(self):
+ args=f'/cancel {self.task.get_id()}'
+ wanted_msg='Yes or No, cancel task 1? With completion date'
+ cmd_result=self.call(system.CmdTasks(),args,wanted_msg)
+ self.assertRegex(cmd_result,'\. Deferring function func_test_cmd_tasks\.')
+ self.char1.execute_cmd('y')
+ self.assertTrue(self.task.exists())
+ self.assertFalse(self.task.active())
+
+
[docs]deftest_func_name_manipulation(self):
+ self.task_handler.add(self.timedelay,func_test_cmd_tasks)# add an extra task
+ args=f'/remove func_test_cmd_tasks'
+ wanted_msg='Task action remove completed on task ID 1.|The task function remove returned: True|' \
+ 'Task action remove completed on task ID 2.|The task function remove returned: True'
+ self.call(system.CmdTasks(),args,wanted_msg)
+ self.assertFalse(self.task_handler.tasks)# no tasks should exist.
+
+
[docs]deftest_wrong_func_name(self):
+ args=f'/remove intentional_fail'
+ wanted_msg='No tasks deferring function name intentional_fail found.'
+ self.call(system.CmdTasks(),args,wanted_msg)
+ self.assertTrue(self.task.active())
+
+
[docs]deftest_no_input(self):
+ args=f'/cancel {self.task.get_id()}'
+ self.call(system.CmdTasks(),args)
+ # task should complete since no input was received
+ self.task_handler.clock.advance(self.timedelay+1)
+ self.assertFalse(self.task.exists())
[docs]deftest_task_complete_waiting_input(self):
+ """Test for task completing while waiting for input."""
+ self.call(system.CmdTasks(),f'/cancel {self.task.get_id()}')
+ self.task_handler.clock.advance(self.timedelay+1)
+ self.char1.msg=Mock()
+ self.char1.execute_cmd('y')
+ text=''
+ for_,_,kwargsinself.char1.msg.mock_calls:
+ text+=kwargs.get('text','')
+ self.assertEqual(text,'Task completed while waiting for input.')
+ self.assertFalse(self.task.exists())
+
+
[docs]deftest_new_task_waiting_input(self):
+ """
+ Test task completing than a new task with the same ID being made while waitinf for input.
+ """
+ self.assertTrue(self.task.get_id(),1)
+ self.call(system.CmdTasks(),f'/cancel {self.task.get_id()}')
+ self.task_handler.clock.advance(self.timedelay+1)
+ self.assertFalse(self.task.exists())
+ self.task=self.task_handler.add(self.timedelay,func_test_cmd_tasks)
+ self.assertTrue(self.task.get_id(),1)
+ self.char1.msg=Mock()
+ self.char1.execute_cmd('y')
+ text=''
+ for_,_,kwargsinself.char1.msg.mock_calls:
+ text+=kwargs.get('text','')
+ self.assertEqual(text,'Task completed while waiting for input.')
+
+
[docs]deftest_misformed_command(self):
+ wanted_msg='Task command misformed.|Proper format tasks[/switch] ' \
+ '[function name or task id]'
+ self.call(system.CmdTasks(),f'/cancel',wanted_msg)
[docs]deftest_typeclass(self):
@@ -1194,8 +1627,8 @@
self.call(building.CmdScript(),"Obj ","dbref ")self.call(
- building.CmdScript(),"/start Obj","0 scripts started on Obj"
- )# because it's already started
+ building.CmdScript(),"/start Obj","1 scripts started on Obj"
+ )# we allow running start again; this should still happenself.call(building.CmdScript(),"/stop Obj","Stopping script")self.call(
@@ -1464,21 +1897,13 @@
self.call(comms.CmdAddCom(),"tc = testchan",
- "You are already connected to channel testchan. You can now",
+ "You are already connected to channel testchan.| You can now",receiver=self.account,)self.call(comms.CmdDelCom(),"tc",
- "Your alias 'tc' for channel testchan was cleared.",
- receiver=self.account,
- )
-
-
[docs]deftest_channels(self):
- self.call(
- comms.CmdChannels(),
- "",
- "Available channels (use comlist,addcom and delcom to manage",
+ "Any alias 'tc' for channel testchan was cleared.",receiver=self.account,)
@@ -1486,7 +1911,7 @@
self.call(comms.CmdAllCom(),"",
- "Available channels (use comlist,addcom and delcom to manage",
+ "Available channels:",receiver=self.account,)
@@ -1506,14 +1931,6 @@
receiver=self.account,)
-
[docs]deftest_cemit(self):
- self.call(
- comms.CmdCemit(),
- "testchan = Test Message",
- "[testchan] Test Message|Sent to channel testchan: Test Message",
- receiver=self.account,
- )
[docs]@patch("evennia.contrib.tutorial_examples.red_button.repeat")
+ @patch("evennia.contrib.tutorial_examples.red_button.delay")
+ deftest_batch_commands(self,mock_delay,mock_repeat):# cannot test batchcode here, it must run inside the server processself.call(batchprocess.CmdBatchCommands(),
@@ -1562,7 +2203,8 @@
confirm=building.CmdDestroy.confirmbuilding.CmdDestroy.confirm=Falseself.call(building.CmdDestroy(),"button","button was destroyed.")
- building.CmdDestroy.confirm=confirm
-
diff --git a/docs/0.9.5/_modules/evennia/commands/default/unloggedin.html b/docs/0.9.5/_modules/evennia/commands/default/unloggedin.html
index 9a71d40229..46fe8ac28a 100644
--- a/docs/0.9.5/_modules/evennia/commands/default/unloggedin.html
+++ b/docs/0.9.5/_modules/evennia/commands/default/unloggedin.html
@@ -335,6 +335,7 @@
|wquit|n - abort the connectionFirst create an account e.g. with |wcreate Anna c67jHL8p|n
+(If you have spaces in your name, use double quotes: |wcreate "Anna the Barbarian" c67jHL8p|nNext you can connect to the game: |wconnect Anna c67jHL8p|nYou can use the |wlook|n command if you want to see the connect screen again.
@@ -572,7 +573,6 @@
-"""
-This defines how Comm models are displayed in the web admin interface.
-
-"""
-
-fromdjango.contribimportadmin
-fromevennia.comms.modelsimportChannelDB
-fromevennia.typeclasses.adminimportAttributeInline,TagInline
-fromdjango.confimportsettings
-
-
-
[docs]defsubscriptions(self,obj):
- """
- Helper method to get subs from a channel.
-
- Args:
- obj (Channel): The channel to get subs from.
-
- """
- return", ".join([str(sub)forsubinobj.subscriptions.all()])
-
-
[docs]defsave_model(self,request,obj,form,change):
- """
- Model-save hook.
-
- Args:
- request (Request): Incoming request.
- obj (Object): Database object.
- form (Form): Form instance.
- change (bool): If this is a change or a new object.
-
- """
- obj.save()
- ifnotchange:
- # adding a new object
- # have to call init with typeclass passed to it
- obj.set_class_from_typeclass(typeclass_path=settings.BASE_CHANNEL_TYPECLASS)
- obj.at_init()
-"""
-The channel handler, accessed from this module as CHANNEL_HANDLER is a
-singleton that handles the stored set of channels and how they are
-represented against the cmdhandler.
-
-If there is a channel named 'newbie', we want to be able to just write
-
- newbie Hello!
-
-For this to work, 'newbie', the name of the channel, must be
-identified by the cmdhandler as a command name. The channelhandler
-stores all channels as custom 'commands' that the cmdhandler can
-import and look through.
-
-> Warning - channel names take precedence over command names, so make
-sure to not pick clashing channel names.
-
-Unless deleting a channel you normally don't need to bother about the
-channelhandler at all - the create_channel method handles the update.
-
-To delete a channel cleanly, delete the channel object, then call
-update() on the channelhandler. Or use Channel.objects.delete() which
-does this for you.
-
-"""
-fromdjango.confimportsettings
-fromevennia.commandsimportcmdset
-fromevennia.utils.loggerimporttail_log_file
-fromevennia.utils.utilsimportclass_from_module
-fromdjango.utils.translationimportgettextas_
-
-# we must late-import these since any overloads are likely to
-# themselves be using these classes leading to a circular import.
-
-_CHANNEL_HANDLER_CLASS=None
-_CHANNEL_COMMAND_CLASS=None
-_CHANNELDB=None
-_COMMAND_DEFAULT_CLASS=class_from_module(settings.COMMAND_DEFAULT_CLASS)
-
-
[docs]classChannelCommand(_COMMAND_DEFAULT_CLASS):
- """
- {channelkey} channel
-
- {channeldesc}
-
- Usage:
- {lower_channelkey} <message>
- {lower_channelkey}/history [start]
- {lower_channelkey} off - mutes the channel
- {lower_channelkey} on - unmutes the channel
-
- Switch:
- history: View 20 previous messages, either from the end or
- from <start> number of messages from the end.
-
- Example:
- {lower_channelkey} Hello World!
- {lower_channelkey}/history
- {lower_channelkey}/history 30
-
- """
-
- # ^note that channeldesc and lower_channelkey will be filled
- # automatically by ChannelHandler
-
- # this flag is what identifies this cmd as a channel cmd
- # and branches off to the system send-to-channel command
- # (which is customizable by admin)
- is_channel=True
- key="general"
- help_category="Channel Names"
- obj=None
- arg_regex=r"\s.*?|/history.*?"
-
-
[docs]defparse(self):
- """
- Simple parser
- """
- # cmdhandler sends channame:msg here.
- channelname,msg=self.args.split(":",1)
- self.history_start=None
- ifmsg.startswith("/history"):
- arg=msg[8:]
- try:
- self.history_start=int(arg)ifargelse0
- exceptValueError:
- # if no valid number was given, ignore it
- pass
- self.args=(channelname.strip(),msg.strip())
-
-
[docs]deffunc(self):
- """
- Create a new message and send it to channel, using
- the already formatted input.
- """
- global_CHANNELDB
- ifnot_CHANNELDB:
- fromevennia.comms.modelsimportChannelDBas_CHANNELDB
-
- channelkey,msg=self.args
- caller=self.caller
- ifnotmsg:
- self.msg(_("Say what?"))
- return
- channel=_CHANNELDB.objects.get_channel(channelkey)
-
- ifnotchannel:
- self.msg(_("Channel '%s' not found.")%channelkey)
- return
- ifnotchannel.has_connection(caller):
- string=_("You are not connected to channel '%s'.")
- self.msg(string%channelkey)
- return
- ifnotchannel.access(caller,"send"):
- string=_("You are not permitted to send to channel '%s'.")
- self.msg(string%channelkey)
- return
- ifmsg=="on":
- caller=callerifnothasattr(caller,"account")elsecaller.account
- unmuted=channel.unmute(caller)
- ifunmuted:
- self.msg(_("You start listening to %s.")%channel)
- return
- self.msg(_("You were already listening to %s.")%channel)
- return
- ifmsg=="off":
- caller=callerifnothasattr(caller,"account")elsecaller.account
- muted=channel.mute(caller)
- ifmuted:
- self.msg(_("You stop listening to %s.")%channel)
- return
- self.msg(_("You were already not listening to %s.")%channel)
- return
- ifself.history_startisnotNone:
- # Try to view history
- log_file=channel.attributes.get("log_file",default="channel_%s.log"%channel.key)
-
- defsend_msg(lines):
- returnself.msg(
- "".join(line.split("[-]",1)[1]if"[-]"inlineelselineforlineinlines)
- )
-
- tail_log_file(log_file,self.history_start,20,callback=send_msg)
- else:
- caller=callerifnothasattr(caller,"account")elsecaller.account
- ifcallerinchannel.mutelist:
- self.msg(_("You currently have %s muted.")%channel)
- return
- channel.msg(msg,senders=self.caller,online=True)
-
-
[docs]defget_extra_info(self,caller,**kwargs):
- """
- Let users know that this command is for communicating on a channel.
-
- Args:
- caller (TypedObject): A Character or Account who has entered an ambiguous command.
-
- Returns:
- A string with identifying information to disambiguate the object, conventionally with a preceding space.
- """
- return_(" (channel)")
-
-
-
[docs]classChannelHandler(object):
- """
- The ChannelHandler manages all active in-game channels and
- dynamically creates channel commands for users so that they can
- just give the channel's key or alias to write to it. Whenever a
- new channel is created in the database, the update() method on
- this handler must be called to sync it with the database (this is
- done automatically if creating the channel with
- evennia.create_channel())
-
- """
-
-
[docs]defadd(self,channel):
- """
- Add an individual channel to the handler. This is called
- whenever a new channel is created.
-
- Args:
- channel (Channel): The channel to add.
-
- Notes:
- To remove a channel, simply delete the channel object and
- run self.update on the handler. This should usually be
- handled automatically by one of the deletion methos of
- the Channel itself.
-
- """
- global_CHANNEL_COMMAND_CLASS
- ifnot_CHANNEL_COMMAND_CLASS:
- _CHANNEL_COMMAND_CLASS=class_from_module(settings.CHANNEL_COMMAND_CLASS)
-
- # map the channel to a searchable command
- cmd=_CHANNEL_COMMAND_CLASS(
- key=channel.key.strip().lower(),
- aliases=channel.aliases.all(),
- locks="cmd:all();%s"%channel.locks,
- help_category="Channel names",
- obj=channel,
- is_channel=True,
- )
- # format the help entry
- key=channel.key
- cmd.__doc__=cmd.__doc__.format(
- channelkey=key,
- lower_channelkey=key.strip().lower(),
- channeldesc=channel.attributes.get("desc",default="").strip(),
- )
- self._cached_channel_cmds[channel]=cmd
- self._cached_channels[key]=channel
- self._cached_cmdsets={}
-
- add_channel=add# legacy alias
-
-
[docs]defremove(self,channel):
- """
- Remove channel from channelhandler. This will also delete it.
-
- Args:
- channel (Channel): Channel to remove/delete.
-
- """
- ifchannel.pk:
- channel.delete()
- self.update()
-
-
[docs]defupdate(self):
- """
- Updates the handler completely, including removing old removed
- Channel objects. This must be called after deleting a Channel.
-
- """
- global_CHANNELDB
- ifnot_CHANNELDB:
- fromevennia.comms.modelsimportChannelDBas_CHANNELDB
- self._cached_channel_cmds={}
- self._cached_cmdsets={}
- self._cached_channels={}
- forchannelin_CHANNELDB.objects.get_all_channels():
- self.add(channel)
-
-
[docs]defget(self,channelname=None):
- """
- Get a channel from the handler, or all channels
-
- Args:
- channelame (str, optional): Channel key, case insensitive.
- Returns
- channels (list): The matching channels in a list, or all
- channels in the handler.
-
- """
- ifchannelname:
- channel=self._cached_channels.get(channelname.lower(),None)
- return[channel]ifchannelelse[]
- returnlist(self._cached_channels.values())
-
-
[docs]defget_cmdset(self,source_object):
- """
- Retrieve cmdset for channels this source_object has
- access to send to.
-
- Args:
- source_object (Object): An object subscribing to one
- or more channels.
-
- Returns:
- cmdsets (list): The Channel-Cmdsets `source_object` has
- access to.
-
- """
- ifsource_objectinself._cached_cmdsets:
- returnself._cached_cmdsets[source_object]
- else:
- # create a new cmdset holding all viable channels
- chan_cmdset=None
- chan_cmds=[
- channelcmd
- forchannel,channelcmdinself._cached_channel_cmds.items()
- ifchannel.subscriptions.has(source_object)
- andchannelcmd.access(source_object,"send")
- ]
- ifchan_cmds:
- chan_cmdset=cmdset.CmdSet()
- chan_cmdset.key="ChannelCmdSet"
- chan_cmdset.priority=101
- chan_cmdset.duplicates=True
- forcmdinchan_cmds:
- chan_cmdset.add(cmd)
- self._cached_cmdsets[source_object]=chan_cmdset
- returnchan_cmdset
-
-
-# set up the singleton
-CHANNEL_HANDLER=class_from_module(settings.CHANNEL_HANDLER_CLASS)()
-CHANNELHANDLER=CHANNEL_HANDLER# legacy
-
-
-
-
\ No newline at end of file
diff --git a/docs/0.9.5/_modules/evennia/comms/comms.html b/docs/0.9.5/_modules/evennia/comms/comms.html
index 878b2e8826..5229476fb6 100644
--- a/docs/0.9.5/_modules/evennia/comms/comms.html
+++ b/docs/0.9.5/_modules/evennia/comms/comms.html
@@ -48,23 +48,48 @@
fromdjango.utils.textimportslugifyfromevennia.typeclasses.modelsimportTypeclassBase
-fromevennia.comms.modelsimportTempMsg,ChannelDB
+fromevennia.comms.modelsimportChannelDBfromevennia.comms.managersimportChannelManagerfromevennia.utilsimportcreate,loggerfromevennia.utils.utilsimportmake_iter
-_CHANNEL_HANDLER=None
-
[docs]classDefaultChannel(ChannelDB,metaclass=TypeclassBase):""" This is the base class for all Channel Comms. Inherit from this to create different types of communication channels.
+ Class-level variables:
+ - `send_to_online_only` (bool, default True) - if set, will only try to
+ send to subscribers that are actually active. This is a useful optimization.
+ - `log_file` (str, default `"channel_{channelname}.log"`). This is the
+ log file to which the channel history will be saved. The `{channelname}` tag
+ will be replaced by the key of the Channel. If an Attribute 'log_file'
+ is set, this will be used instead. If this is None and no Attribute is found,
+ no history will be saved.
+ - `channel_prefix_string` (str, default `"[{channelname} ]"`) - this is used
+ as a simple template to get the channel prefix with `.channel_prefix()`.
+
"""objects=ChannelManager()
+ # channel configuration
+
+ # only send to characters/accounts who has an active session (this is a
+ # good optimization since people can still recover history separately).
+ send_to_online_only=True
+ # store log in log file. `channel_key tag will be replace with key of channel.
+ # Will use log_file Attribute first, if given
+ log_file="channel_{channelname}.log"
+ # which prefix to use when showing were a message is coming from. Set to
+ # None to disable and set this later.
+ channel_prefix_string="[{channelname}] "
+
+ # default nick-alias replacements (default using the 'channel' command)
+ channel_msg_nick_pattern=r"{alias}\s*?|{alias}\s+?(?P<arg1>.+?)"
+ channel_msg_nick_replacement="channel {channelname} = $1"
+
[docs]defat_first_save(self):""" Called by the typeclass system the very first time the channel
@@ -74,7 +99,6 @@
"""self.basetype_setup()self.at_channel_creation()
- self.attributes.add("log_file","channel_%s.log"%self.key)ifhasattr(self,"_createdict"):# this is only set if the channel was created# with the utils.create.create_channel function.
@@ -96,14 +120,11 @@
self.tags.batch_add(*cdict["tags"])
[docs]defbasetype_setup(self):
- # delayed import of the channelhandler
- global_CHANNEL_HANDLER
- ifnot_CHANNEL_HANDLER:
- fromevennia.comms.channelhandlerimportCHANNEL_HANDLERas_CHANNEL_HANDLER
- # register ourselves with the channelhandler.
- _CHANNEL_HANDLER.add(self)
+ self.locks.add("send:all();listen:all();control:perm(Admin)")
- self.locks.add("send:all();listen:all();control:perm(Admin)")
+ # make sure we don't have access to a same-named old channel's history.
+ log_file=self.get_log_filename()
+ logger.rotate_log_file(log_file,num_lines_to_append=0)
[docs]defget_log_filename(self):
+ """
+ File name to use for channel log.
+
+ Returns:
+ str: The filename to use (this is always assumed to be inside
+ settings.LOG_DIR)
+
+ """
+ ifnotself._log_file:
+ self._log_file=self.attributes.get(
+ "log_file",self.log_file.format(channelname=self.key.lower()))
+ returnself._log_file
+
+
[docs]defset_log_filename(self,filename):
+ """
+ Set a custom log filename.
+
+ Args:
+ filename (str): The filename to set. This is a path starting from
+ inside the settings.LOG_DIR location.
+
+ """
+ self.attributes.add("log_file",filename)
+
[docs]defhas_connection(self,subscriber):""" Checks so this account is actually listening
@@ -142,6 +190,10 @@
defmutelist(self):returnself.db.mute_listor[]
+ @property
+ defbanlist(self):
+ returnself.db.ban_listor[]
+
@propertydefwholist(self):subs=self.subscriptions.all()
@@ -170,6 +222,10 @@
**kwargs (dict): Arbitrary, optional arguments for users overriding the call (unused by default).
+ Returns:
+ bool: True if muting was successful, False if we were already
+ muted.
+
"""mutelist=self.mutelistifsubscribernotinmutelist:
@@ -180,19 +236,67 @@
[docs]defunmute(self,subscriber,**kwargs):"""
- Removes an entity to the list of muted subscribers. A muted subscriber will no longer see channel messages,
- but may use channel commands.
+ Removes an entity from the list of muted subscribers. A muted subscriber
+ will no longer see channel messages, but may use channel commands. Args: subscriber (Object or Account): The subscriber to unmute. **kwargs (dict): Arbitrary, optional arguments for users overriding the call (unused by default).
+ Returns:
+ bool: True if unmuting was successful, False if we were already
+ unmuted.
+
"""mutelist=self.mutelistifsubscriberinmutelist:mutelist.remove(subscriber)
- self.db.mute_list=mutelist
+ returnTrue
+ returnFalse
+
+
[docs]defban(self,target,**kwargs):
+ """
+ Ban a given user from connecting to the channel. This will not stop
+ users already connected, so the user must be booted for this to take
+ effect.
+
+ Args:
+ target (Object or Account): The entity to unmute. This need not
+ be a subscriber.
+ **kwargs (dict): Arbitrary, optional arguments for users
+ overriding the call (unused by default).
+
+ Returns:
+ bool: True if banning was successful, False if target was already
+ banned.
+ """
+ banlist=self.banlist
+ iftargetnotinbanlist:
+ banlist.append(target)
+ self.db.ban_list=banlist
+ returnTrue
+ returnFalse
+
+
[docs]defunban(self,target,**kwargs):
+ """
+ Un-Ban a given user. This will not reconnect them - they will still
+ have to reconnect and set up aliases anew.
+
+ Args:
+ target (Object or Account): The entity to unmute. This need not
+ be a subscriber.
+ **kwargs (dict): Arbitrary, optional arguments for users
+ overriding the call (unused by default).
+
+ Returns:
+ bool: True if unbanning was successful, False if target was not
+ previously banned.
+ """
+ banlist=list(self.banlist)
+ iftargetinbanlist:
+ banlist=[bannedforbannedinbanlistifbanned!=target]
+ self.db.ban_list=banlistreturnTruereturnFalse
[docs]@classmethod
- defcreate(cls,key,account=None,*args,**kwargs):
+ defcreate(cls,key,creator=None,*args,**kwargs):""" Creates a basic Channel with default parameters, unless otherwise specified or extended.
@@ -294,7 +398,8 @@
Args: key (str): This must be unique.
- account (Account): Account to attribute this object to.
+ creator (Account or Object): Entity to associate with this channel
+ (used for tracking) Keyword Args: aliases (list of str): List of alternative (likely shorter) keynames.
@@ -322,8 +427,8 @@
# Record creator id and creation IPifip:obj.db.creator_ip=ip
- ifaccount:
- obj.db.creator_id=account.id
+ ifcreator:
+ obj.db.creator_id=creator.idexceptExceptionasexc:errors.append("An error occurred while creating this '%s' object."%key)
@@ -333,284 +438,189 @@
[docs]defdelete(self):"""
- Deletes channel while also cleaning up channelhandler.
+ Deletes channel. """self.attributes.clear()self.aliases.clear()
- super().delete()
- fromevennia.comms.channelhandlerimportCHANNELHANDLER
+ forsubscriberinself.subscriptions.all():
+ self.disconnect(subscriber)
+ super().delete()
- CHANNELHANDLER.update()
-
-
[docs]defmessage_transform(
- self,msgobj,emit=False,prefix=True,sender_strings=None,external=False,**kwargs
- ):
- """
- Generates the formatted string sent to listeners on a channel.
-
- Args:
- msgobj (Msg): Message object to send.
- emit (bool, optional): In emit mode the message is not associated
- with a specific sender name.
- prefix (bool, optional): Prefix `msg` with a text given by `self.channel_prefix`.
- sender_strings (list, optional): Used by bots etc, one string per external sender.
- external (bool, optional): If this is an external sender or not.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- """
- ifsender_stringsorexternal:
- body=self.format_external(msgobj,sender_strings,emit=emit)
- else:
- body=self.format_message(msgobj,emit=emit)
- ifprefix:
- body="%s%s"%(self.channel_prefix(msgobj,emit=emit),body)
- msgobj.message=body
- returnmsgobj
-
-
[docs]defdistribute_message(self,msgobj,online=False,**kwargs):
- """
- Method for grabbing all listeners that a message should be
- sent to on this channel, and sending them a message.
-
- Args:
- msgobj (Msg or TempMsg): Message to distribute.
- online (bool): Only send to receivers who are actually online
- (not currently used):
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- Notes:
- This is also where logging happens, if enabled.
-
- """
- # get all accounts or objects connected to this channel and send to them
- ifonline:
- subs=self.subscriptions.online()
- else:
- subs=self.subscriptions.all()
- forentityinsubs:
- # if the entity is muted, we don't send them a message
- ifentityinself.mutelist:
- continue
- try:
- # note our addition of the from_channel keyword here. This could be checked
- # by a custom account.msg() to treat channel-receives differently.
- entity.msg(
- msgobj.message,from_obj=msgobj.senders,options={"from_channel":self.id}
- )
- exceptAttributeErrorase:
- logger.log_trace("%s\nCannot send msg to '%s'."%(e,entity))
-
- ifmsgobj.keep_log:
- # log to file
- logger.log_file(
- msgobj.message,self.attributes.get("log_file")or"channel_%s.log"%self.key
- )
-
-
[docs]defmsg(
- self,
- msgobj,
- header=None,
- senders=None,
- sender_strings=None,
- keep_log=None,
- online=False,
- emit=False,
- external=False,
- ):
- """
- Send the given message to all accounts connected to channel. Note that
- no permission-checking is done here; it is assumed to have been
- done before calling this method. The optional keywords are not used if
- persistent is False.
-
- Args:
- msgobj (Msg, TempMsg or str): If a Msg/TempMsg, the remaining
- keywords will be ignored (since the Msg/TempMsg object already
- has all the data). If a string, this will either be sent as-is
- (if persistent=False) or it will be used together with `header`
- and `senders` keywords to create a Msg instance on the fly.
- header (str, optional): A header for building the message.
- senders (Object, Account or list, optional): Optional if persistent=False, used
- to build senders for the message.
- sender_strings (list, optional): Name strings of senders. Used for external
- connections where the sender is not an account or object.
- When this is defined, external will be assumed. The list will be
- filtered so each sender-string only occurs once.
- keep_log (bool or None, optional): This allows to temporarily change the logging status of
- this channel message. If `None`, the Channel's `keep_log` Attribute will
- be used. If `True` or `False`, that logging status will be used for this
- message only (note that for unlogged channels, a `True` value here will
- create a new log file only for this message).
- online (bool, optional) - If this is set true, only messages people who are
- online. Otherwise, messages all accounts connected. This can
- make things faster, but may not trigger listeners on accounts
- that are offline.
- emit (bool, optional) - Signals to the message formatter that this message is
- not to be directly associated with a name.
- external (bool, optional): Treat this message as being
- agnostic of its sender.
-
- Returns:
- success (bool): Returns `True` if message sending was
- successful, `False` otherwise.
-
- """
- senders=make_iter(senders)ifsenderselse[]
- ifisinstance(msgobj,str):
- # given msgobj is a string - convert to msgobject (always TempMsg)
- msgobj=TempMsg(senders=senders,header=header,message=msgobj,channels=[self])
- # we store the logging setting for use in distribute_message()
- msgobj.keep_log=keep_logifkeep_logisnotNoneelseself.db.keep_log
-
- # start the sending
- msgobj=self.pre_send_message(msgobj)
- ifnotmsgobj:
- returnFalse
- ifsender_strings:
- sender_strings=list(set(make_iter(sender_strings)))
- msgobj=self.message_transform(
- msgobj,emit=emit,sender_strings=sender_strings,external=external
- )
- self.distribute_message(msgobj,online=online)
- self.post_send_message(msgobj)
- returnTrue
-
-
[docs]deftempmsg(self,message,header=None,senders=None):
- """
- A wrapper for sending non-persistent messages.
-
- Args:
- message (str): Message to send.
- header (str, optional): Header of message to send.
- senders (Object or list, optional): Senders of message to send.
-
- """
- self.msg(message,senders=senders,header=header,keep_log=False)
[docs]defchannel_prefix(self):""" Hook method. How the channel should prefix itself for users.
- Args:
- msg (str, optional): Prefix text
- emit (bool, optional): Switches to emit mode, which usually
- means to not prefix the channel's info.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
Returns:
- prefix (str): The created channel prefix.
+ str: The channel prefix. """
- return""ifemitelse"[%s] "%self.key
[docs]defadd_user_channel_alias(self,user,alias,**kwargs):"""
- Hook method. Function used to format a list of sender names.
+ Add a personal user-alias for this channel to a given subscriber. Args:
- senders (list): Sender object names.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
+ user (Object or Account): The one to alias this channel.
+ alias (str): The desired alias.
- Returns:
- formatted_list (str): The list of names formatted appropriately.
+ Note:
+ This is tightly coupled to the default `channel` command. If you
+ change that, you need to change this as well.
+
+ We add two nicks - one is a plain `alias -> channel.key` that
+ users need to be able to reference this channel easily. The other
+ is a templated nick to easily be able to send messages to the
+ channel without needing to give the full `channel` command. The
+ structure of this nick is given by `self.channel_msg_nick_pattern`
+ and `self.channel_msg_nick_replacement`. By default it maps
+ `alias <msg> -> channel <channelname> = <msg>`, so that you can
+ for example just write `pub Hello` to send a message.
+
+ The alias created is `alias $1 -> channel channel = $1`, to allow
+ for sending to channel using the main channel command.
+
+ """
+ chan_key=self.key.lower()
+
+ # the message-pattern allows us to type the channel on its own without
+ # needing to use the `channel` command explicitly.
+ msg_nick_pattern=self.channel_msg_nick_pattern.format(alias=alias)
+ msg_nick_replacement=self.channel_msg_nick_replacement.format(channelname=chan_key)
+ user.nicks.add(msg_nick_pattern,msg_nick_replacement,category="inputline",
+ pattern_is_regex=True,**kwargs)
+
+ ifchan_key!=alias:
+ # this allows for using the alias for general channel lookups
+ user.nicks.add(alias,chan_key,category="channel",**kwargs)
+
+
[docs]@classmethod
+ defremove_user_channel_alias(cls,user,alias,**kwargs):
+ """
+ Remove a personal channel alias from a user.
+
+ Args:
+ user (Object or Account): The user to remove an alias from.
+ alias (str): The alias to remove.
+ **kwargs: Unused by default. Can be used to pass extra variables
+ into a custom implementation. Notes:
- This function exists separately so that external sources
- can use it to format source names in the same manner as
- normal object/account names.
+ The channel-alias actually consists of two aliases - one
+ channel-based one for searching channels with the alias and one
+ inputline one for doing the 'channelalias msg' - call.
+
+ This is a classmethod because it doesn't actually operate on the
+ channel instance.
+
+ It sits on the channel because the nick structure for this is
+ pretty complex and needs to be located in a central place (rather
+ on, say, the channel command). """
- ifnotsenders:
- return""
- return", ".join(senders)
[docs]defat_pre_msg(self,message,**kwargs):"""
- Hook method. Detects if the sender is posing, and modifies the
- message accordingly.
+ Called before the starting of sending the message to a receiver. This
+ is called before any hooks on the receiver itself. If this returns
+ None/False, the sending will be aborted. Args:
- msgobj (Msg or TempMsg): The message to analyze for a pose.
- sender_string (str): The name of the sender/poser.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
+ message (str): The message to send.
+ **kwargs (any): Keywords passed on from `.msg`. This includes
+ `senders`. Returns:
- string (str): A message that combines the `sender_string`
- component with `msg` in different ways depending on if a
- pose was performed or not (this must be analyzed by the
- hook).
+ str, False or None: Any custom changes made to the message. If
+ falsy, no message will be sent. """
- pose=False
- message=msgobj.message
- message_start=message.lstrip()
- ifmessage_start.startswith((":",";")):
- pose=True
- message=message[1:]
- ifnotmessage.startswith((":","'",",")):
- ifnotmessage.startswith(" "):
- message=" "+message
- ifpose:
- return"%s%s"%(sender_string,message)
+ returnmessage
+
+
[docs]defmsg(self,message,senders=None,bypass_mute=False,**kwargs):
+ """
+ Send message to channel, causing it to be distributed to all non-muted
+ subscribed users of that channel.
+
+ Args:
+ message (str): The message to send.
+ senders (Object, Account or list, optional): If not given, there is
+ no way to associate one or more senders with the message (like
+ a broadcast message or similar).
+ bypass_mute (bool, optional): If set, always send, regardless of
+ individual mute-state of subscriber. This can be used for
+ global announcements or warnings/alerts.
+ **kwargs (any): This will be passed on to all hooks. Use `no_prefix`
+ to exclude the channel prefix.
+
+ Notes:
+ The call hook calling sequence is:
+
+ - `msg = channel.at_pre_msg(message, **kwargs)` (aborts for all if return None)
+ - `msg = receiver.at_pre_channel_msg(msg, channel, **kwargs)` (aborts for receiver if return None)
+ - `receiver.at_channel_msg(msg, channel, **kwargs)`
+ - `receiver.at_post_channel_msg(msg, channel, **kwargs)``
+ Called after all receivers are processed:
+ - `channel.at_post_all_msg(message, **kwargs)`
+
+ (where the senders/bypass_mute are embedded into **kwargs for
+ later access in hooks)
+
+ """
+ senders=make_iter(senders)ifsenderselse[]
+ ifself.send_to_online_only:
+ receivers=self.subscriptions.online()else:
- return"%s: %s"%(sender_string,message)
[docs]defat_post_msg(self,message,**kwargs):"""
- Hook method. Used for formatting external messages. This is
- needed as a separate operation because the senders of external
- messages may not be in-game objects/accounts, and so cannot
- have things like custom user preferences.
+ This is called after sending to *all* valid recipients. It is normally
+ used for logging/channel history. Args:
- msgobj (Msg or TempMsg): The message to send.
- senders (list): Strings, one per sender.
- emit (bool, optional): A sender-agnostic message or not.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- Returns:
- transformed (str): A formatted string.
+ message (str): The message sent.
+ **kwargs (any): Keywords passed on from `msg`, including `senders`. """
- ifemitornotsenders:
- returnmsgobj.message
- senders=", ".join(senders)
- returnself.pose_transform(msgobj,senders)
-
-
[docs]defformat_message(self,msgobj,emit=False,**kwargs):
- """
- Hook method. Formats a message body for display.
-
- Args:
- msgobj (Msg or TempMsg): The message object to send.
- emit (bool, optional): The message is agnostic of senders.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- Returns:
- transformed (str): The formatted message.
-
- """
- # We don't want to count things like external sources as senders for
- # the purpose of constructing the message string.
- senders=[senderforsenderinmsgobj.sendersifhasattr(sender,"key")]
- ifnotsenders:
- emit=True
- ifemit:
- returnmsgobj.message
- else:
- senders=[sender.keyforsenderinmsgobj.senders]
- senders=", ".join(senders)
- returnself.pose_transform(msgobj,senders)
+ # save channel history to log file
+ log_file=self.get_log_filename()
+ iflog_file:
+ senders=",".join(sender.keyforsenderinkwargs.get("senders",[]))
+ senders=f"{senders}: "ifsenderselse""
+ message=f"{senders}{message}"
+ logger.log_file(message,log_file)
[docs]defpre_join_channel(self,joiner,**kwargs):"""
@@ -637,8 +647,13 @@
**kwargs (dict): Arbitrary, optional arguments for users overriding the call (unused by default).
+ Notes:
+ By default this adds the needed channel nicks to the joiner.
+
"""
- pass
[docs]defpre_leave_channel(self,leaver,**kwargs):"""
@@ -666,36 +681,12 @@
overriding the call (unused by default). """
- pass
-
-
[docs]defpre_send_message(self,msg,**kwargs):
- """
- Hook method. Runs before a message is sent to the channel and
- should return the message object, after any transformations.
- If the message is to be discarded, return a false value.
-
- Args:
- msg (Msg or TempMsg): Message to send.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- Returns:
- result (Msg, TempMsg or bool): If False, abort send.
-
- """
- returnmsg
-
-
[docs]defpost_send_message(self,msg,**kwargs):
- """
- Hook method. Run after a message is sent to the channel.
-
- Args:
- msg (Msg or TempMsg): Message sent.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- """
- pass
[docs]defweb_get_detail_url(self):
@@ -770,8 +761,10 @@
a named view of 'channel-detail' would be referenced by this method. ex.
- url(r'channels/(?P<slug>[\w\d\-]+)/$',
- ChannelDetailView.as_view(), name='channel-detail')
+ ::
+
+ url(r'channels/(?P<slug>[\w\d\-]+)/$',
+ ChannelDetailView.as_view(), name='channel-detail') If no View has been created and defined in urls.py, returns an HTML anchor.
@@ -789,7 +782,7 @@
"%s-detail"%slugify(self._meta.verbose_name),kwargs={"slug":slugify(self.db_key)},)
- except:
+ exceptException:return"#"
[docs]defweb_get_update_url(self):
@@ -804,8 +797,10 @@
a named view of 'channel-update' would be referenced by this method. ex.
- url(r'channels/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/change/$',
- ChannelUpdateView.as_view(), name='channel-update')
+ ::
+
+ url(r'channels/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/change/$',
+ ChannelUpdateView.as_view(), name='channel-update') If no View has been created and defined in urls.py, returns an HTML anchor.
@@ -823,7 +818,7 @@
"%s-update"%slugify(self._meta.verbose_name),kwargs={"slug":slugify(self.db_key)},)
- except:
+ exceptException:return"#"
# Used by Django Sites/Admin
- get_absolute_url=web_get_detail_url
+ get_absolute_url=web_get_detail_url
+
+ # TODO Evennia 1.0+ removed hooks. Remove in 1.1.
+
[docs]defmessage_transform(self,*args,**kwargs):
+ raiseRuntimeError("Channel.message_transform is no longer used in 1.0+. "
+ "Use Account/Object.at_pre_channel_msg instead.")
+
+
[docs]defdistribute_message(self,msgobj,online=False,**kwargs):
+ raiseRuntimeError("Channel.distribute_message is no longer used in 1.0+.")
+
+
[docs]defformat_senders(self,senders=None,**kwargs):
+ raiseRuntimeError("Channel.format_senders is no longer used in 1.0+. "
+ "Use Account/Object.at_pre_channel_msg instead.")
+
+
[docs]defpose_transform(self,msgobj,sender_string,**kwargs):
+ raiseRuntimeError("Channel.pose_transform is no longer used in 1.0+. "
+ "Use Account/Object.at_pre_channel_msg instead.")
+
+
[docs]defformat_external(self,msgobj,senders,emit=False,**kwargs):
+ raiseRuntimeError("Channel.format_external is no longer used in 1.0+. "
+ "Use Account/Object.at_pre_channel_msg instead.")
+
+
[docs]defformat_message(self,msgobj,emit=False,**kwargs):
+ raiseRuntimeError("Channel.format_message is no longer used in 1.0+. "
+ "Use Account/Object.at_pre_channel_msg instead.")
+
+
[docs]defpre_send_message(self,msg,**kwargs):
+ raiseRuntimeError("Channel.pre_send_message was renamed to Channel.at_pre_msg.")
+
+
[docs]defpost_send_message(self,msg,**kwargs):
+ raiseRuntimeError("Channel.post_send_message was renamed to Channel.at_post_msg.")
[docs]defget_messages_by_sender(self,sender):""" Get all messages sent by one entity - this could be either a account or an object Args: sender (Account or Object): The sender of the message.
- exclude_channel_messages (bool, optional): Only return messages
- not aimed at a channel (that is, private tells for example) Returns:
- messages (list): List of matching messages
+ QuerySet: Matching messages. Raises: CommError: For incorrect sender types. """obj,typ=identify_object(sender)
- ifexclude_channel_messages:
- # explicitly exclude channel recipients
- iftyp=="account":
- returnlist(
- self.filter(db_sender_accounts=obj,db_receivers_channels__isnull=True).exclude(
- db_hide_from_accounts=obj
- )
- )
- eliftyp=="object":
- returnlist(
- self.filter(db_sender_objects=obj,db_receivers_channels__isnull=True).exclude(
- db_hide_from_objects=obj
- )
- )
- else:
- raiseCommError
+ iftyp=="account":
+ returnself.filter(db_sender_accounts=obj).exclude(db_hide_from_accounts=obj)
+ eliftyp=="object":
+ returnself.filter(db_sender_objects=obj).exclude(db_hide_from_objects=obj)
+ eliftyp=="script":
+ returnself.filter(db_sender_scripts=obj)else:
- # get everything, channel or not
- iftyp=="account":
- returnlist(self.filter(db_sender_accounts=obj).exclude(db_hide_from_accounts=obj))
- eliftyp=="object":
- returnlist(self.filter(db_sender_objects=obj).exclude(db_hide_from_objects=obj))
- else:
- raiseCommError
+ raiseCommError
[docs]defget_messages_by_receiver(self,recipient):"""
@@ -250,7 +243,7 @@
recipient (Object, Account or Channel): The recipient of the messages to search for. Returns:
- messages (list): Matching messages.
+ Queryset: Matching messages. Raises: CommError: If the `recipient` is not of a valid type.
@@ -258,34 +251,21 @@
"""obj,typ=identify_object(recipient)iftyp=="account":
- returnlist(self.filter(db_receivers_accounts=obj).exclude(db_hide_from_accounts=obj))
+ returnself.filter(db_receivers_accounts=obj).exclude(db_hide_from_accounts=obj)eliftyp=="object":
- returnlist(self.filter(db_receivers_objects=obj).exclude(db_hide_from_objects=obj))
- eliftyp=="channel":
- returnlist(self.filter(db_receivers_channels=obj).exclude(db_hide_from_channels=obj))
+ returnself.filter(db_receivers_objects=obj).exclude(db_hide_from_objects=obj)
+ eliftyp=='script':
+ returnself.filter(db_receivers_scripts=obj)else:raiseCommError
-
[docs]defget_messages_by_channel(self,channel):
- """
- Get all persistent messages sent to one channel.
-
- Args:
- channel (Channel): The channel to find messages for.
-
- Returns:
- messages (list): Persistent Msg objects saved for this channel.
-
- """
- returnself.filter(db_receivers_channels=channel).exclude(db_hide_from_channels=channel)
-
[docs]defsearch_message(self,sender=None,receiver=None,freetext=None,dbref=None):""" Search the message database for particular messages. At least one of the arguments must be given to do a search. Args:
- sender (Object or Account, optional): Get messages sent by a particular account or object
+ sender (Object, Account or Script, optional): Get messages sent by a particular sender. receiver (Object, Account or Channel, optional): Get messages received by a certain account,object or channel freetext (str): Search for a text string in a message. NOTE:
@@ -296,40 +276,45 @@
always gives only one match. Returns:
- messages (list or Msg): A list of message matches or a single match if `dbref` was given.
+ Queryset: Message matches. """# unique msg idifdbref:
- msg=self.objects.filter(id=dbref)
- ifmsg:
- returnmsg[0]
+ returnself.objects.filter(id=dbref)# We use Q objects to gradually build up the query - this way we only# need to do one database lookup at the end rather than gradually# refining with multiple filter:s. Django Note: Q objects can be# combined with & and | (=AND,OR). ~ negates the queryset
- # filter by sender
+ # filter by sender (we need __pk to avoid an error with empty Q() objects)sender,styp=identify_object(sender)
+ ifsender:
+ spk=sender.pkifstyp=="account":
- sender_restrict=Q(db_sender_accounts=sender)&~Q(db_hide_from_accounts=sender)
+ sender_restrict=Q(db_sender_accounts__pk=spk)&~Q(db_hide_from_accounts__pk=spk)elifstyp=="object":
- sender_restrict=Q(db_sender_objects=sender)&~Q(db_hide_from_objects=sender)
+ sender_restrict=Q(db_sender_objects__pk=spk)&~Q(db_hide_from_objects__pk=spk)
+ elifstyp=='script':
+ sender_restrict=Q(db_sender_scripts__pk=spk)else:sender_restrict=Q()# filter by receiverreceiver,rtyp=identify_object(receiver)
+ ifreceiver:
+ rpk=receiver.pkifrtyp=="account":
- receiver_restrict=Q(db_receivers_accounts=receiver)&~Q(
- db_hide_from_accounts=receiver
- )
+ receiver_restrict=(
+ Q(db_receivers_accounts__pk=rpk)&~Q(db_hide_from_accounts__pk=rpk))elifrtyp=="object":
- receiver_restrict=Q(db_receivers_objects=receiver)&~Q(db_hide_from_objects=receiver)
+ receiver_restrict=Q(db_receivers_objects__pk=rpk)&~Q(db_hide_from_objects__pk=rpk)
+ elifrtyp=='script':
+ receiver_restrict=Q(db_receivers_scripts__pk=rpk)elifrtyp=="channel":
- receiver_restrict=Q(db_receivers_channels=receiver)&~Q(
- db_hide_from_channels=receiver
- )
+ raiseDeprecationWarning(
+ "Msg.objects.search don't accept channel recipients since "
+ "Channels no longer accepts Msg objects.")else:receiver_restrict=Q()# filter by full text
@@ -338,7 +323,7 @@
else:fulltext_restrict=Q()# execute the query
- returnlist(self.filter(sender_restrict&receiver_restrict&fulltext_restrict))
-
diff --git a/docs/0.9.5/_modules/evennia/comms/models.html b/docs/0.9.5/_modules/evennia/comms/models.html
index 9fc772f293..dcf1e67da1 100644
--- a/docs/0.9.5/_modules/evennia/comms/models.html
+++ b/docs/0.9.5/_modules/evennia/comms/models.html
@@ -57,6 +57,7 @@
Channels are central objects that act as targets for Msgs. Accounts canconnect to channels by use of a ChannelConnect object (this object isnecessary to easily be able to delete connections on the fly).
+
"""fromdjango.confimportsettingsfromdjango.utilsimporttimezone
@@ -75,8 +76,6 @@
_SA=object.__setattr___DA=object.__delattr__
-_CHANNELHANDLER=None
-
# ------------------------------------------------------------#
@@ -85,7 +84,7 @@
# ------------------------------------------------------------
-
[docs]classMsg(SharedMemoryModel):""" A single message. This model describes all ooc messages sent in-game, both to channels and between accounts.
@@ -96,17 +95,16 @@
- db_sender_accounts: Account senders - db_sender_objects: Object senders - db_sender_scripts: Script senders
- - db_sender_external: External senders (defined as string names)
+ - db_sender_external: External sender (defined as string name) - db_receivers_accounts: Receiving accounts - db_receivers_objects: Receiving objects - db_receivers_scripts: Receiveing scripts
- - db_receivers_channels: Receiving channels
+ - db_receiver_external: External sender (defined as string name) - db_header: Header text - db_message: The actual message text - db_date_created: time message was created / sent - db_hide_from_sender: bool if message should be hidden from sender - db_hide_from_receivers: list of receiver objects to hide message from
- - db_hide_from_channels: list of channels objects to hide message from - db_lock_storage: Internal storage of lock strings. """
@@ -118,14 +116,11 @@
# These databse fields are all set using their corresponding properties,# named same as the field, but withtout the db_* prefix.
- # Sender is either an account, an object or an external sender, like
- # an IRC channel; normally there is only one, but if co-modification of
- # a message is allowed, there may be more than one "author"db_sender_accounts=models.ManyToManyField("accounts.AccountDB",related_name="sender_account_set",blank=True,
- verbose_name="sender(account)",
+ verbose_name="Senders (Accounts)",db_index=True,)
@@ -133,14 +128,14 @@
"objects.ObjectDB",related_name="sender_object_set",blank=True,
- verbose_name="sender(object)",
+ verbose_name="Senders (Objects)",db_index=True,)db_sender_scripts=models.ManyToManyField("scripts.ScriptDB",related_name="sender_script_set",blank=True,
- verbose_name="sender(script)",
+ verbose_name="Senders (Scripts)",db_index=True,)db_sender_external=models.CharField(
@@ -149,16 +144,15 @@
null=True,blank=True,db_index=True,
- help_text="identifier for external sender, for example a sender over an "
- "IRC connection (i.e. someone who doesn't have an exixtence in-game).",
+ help_text="Identifier for single external sender, for use with senders "
+ "not represented by a regular database model.")
- # The destination objects of this message. Stored as a
- # comma-separated string of object dbrefs. Can be defined along
- # with channels below.
+
db_receivers_accounts=models.ManyToManyField("accounts.AccountDB",related_name="receiver_account_set",blank=True,
+ verbose_name="Receivers (Accounts)",help_text="account receivers",)
@@ -166,16 +160,25 @@
"objects.ObjectDB",related_name="receiver_object_set",blank=True,
+ verbose_name="Receivers (Objects)",help_text="object receivers",)db_receivers_scripts=models.ManyToManyField("scripts.ScriptDB",related_name="receiver_script_set",blank=True,
+ verbose_name="Receivers (Scripts)",help_text="script_receivers",)
- db_receivers_channels=models.ManyToManyField(
- "ChannelDB",related_name="channel_set",blank=True,help_text="channel recievers"
+
+ db_receiver_external=models.CharField(
+ "external receiver",
+ max_length=1024,
+ null=True,
+ blank=True,
+ db_index=True,
+ help_text="Identifier for single external receiver, for use with recievers "
+ "not represented by a regular database model.")# header could be used for meta-info about the message if your system needs
@@ -192,7 +195,7 @@
"locks",blank=True,help_text="access locks on this message.")
- # these can be used to filter/hide a given message from supplied objects/accounts/channels
+ # these can be used to filter/hide a given message from supplied objects/accountsdb_hide_from_accounts=models.ManyToManyField("accounts.AccountDB",related_name="hide_from_accounts_set",blank=True)
@@ -200,33 +203,27 @@
db_hide_from_objects=models.ManyToManyField("objects.ObjectDB",related_name="hide_from_objects_set",blank=True)
- db_hide_from_channels=models.ManyToManyField(
- "ChannelDB",related_name="hide_from_channels_set",blank=True
- )db_tags=models.ManyToManyField(Tag,blank=True,
- help_text="tags on this message. Tags are simple string markers to identify, group and alias messages.",
+ help_text="tags on this message. Tags are simple string markers to "
+ "identify, group and alias messages.",)# Database managerobjects=managers.MsgManager()_is_deleted=False
-
[docs]defremove_sender(self,senders):""" Remove a single sender or a list of senders. Args: senders (Account, Object, str or list): Senders to remove.
+ If a string, removes the external sender. """
+ ifisinstance(senders,str):
+ self.db_sender_external=""
+ self.save(update_fields=["db_sender_external"])
+ return
+
forsenderinmake_iter(senders):ifnotsender:continue
- ifisinstance(sender,str):
- self.db_sender_external=""
- self.save(update_fields=["db_sender_external"])ifnothasattr(sender,"__dbclass__"):raiseValueError("This is a not a typeclassed object!")clsname=sender.__dbclass__.__name__
@@ -306,26 +303,33 @@
elifclsname=="ScriptDB":self.db_sender_accounts.remove(sender)
- # receivers property
- # @property
- def__receivers_get(self):
+ @property
+ defreceivers(self):""" Getter. Allows for value = self.receivers.
- Returns four lists of receivers: accounts, objects, scripts and channels.
+ Returns four lists of receivers: accounts, objects, scripts and
+ external_receivers.
+
"""return(list(self.db_receivers_accounts.all())+list(self.db_receivers_objects.all())+list(self.db_receivers_scripts.all())
- +list(self.db_receivers_channels.all())
+ +([self.db_receiver_external]ifself.db_receiver_externalelse[]))
- # @receivers.setter
- def__receivers_set(self,receivers):
+ @receivers.setter
+ defreceivers(self,receivers):"""
- Setter. Allows for self.receivers = value.
- This appends a new receiver to the message.
+ Setter. Allows for self.receivers = value. This appends a new receiver
+ to the message. If a string, replaces an external receiver.
+
"""
+ ifisinstance(receivers,str):
+ self.db_receiver_external=receivers
+ self.save(update_fields=['db_receiver_external'])
+ return
+
forreceiverinmake_iter(receivers):ifnotreceiver:continue
@@ -338,32 +342,34 @@
self.db_receivers_accounts.add(receiver)elifclsname=="ScriptDB":self.db_receivers_scripts.add(receiver)
- elifclsname=="ChannelDB":
- self.db_receivers_channels.add(receiver)
- # @receivers.deleter
- def__receivers_del(self):
+ @receivers.deleter
+ defreceivers(self):"Deleter. Clears all receivers"self.db_receivers_accounts.clear()self.db_receivers_objects.clear()self.db_receivers_scripts.clear()
- self.db_receivers_channels.clear()
+ self.db_receiver_external=""self.save()
- receivers=property(__receivers_get,__receivers_set,__receivers_del)
-
-
[docs]defremove_receiver(self,receivers):"""
- Remove a single receiver or a list of receivers.
+ Remove a single receiver, a list of receivers, or a single extral receiver. Args:
- receivers (Account, Object, Script, Channel or list): Receiver to remove.
+ receivers (Account, Object, Script, list or str): Receiver
+ to remove. A string removes the external receiver. """
+ ifisinstance(receivers,str):
+ self.db_receiver_external=""
+ self.save(update_fields="db_receiver_external")
+ return
+
forreceiverinmake_iter(receivers):ifnotreceiver:continue
- ifnothasattr(receiver,"__dbclass__"):
+ elifnothasattr(receiver,"__dbclass__"):raiseValueError("This is a not a typeclassed object!")clsname=receiver.__dbclass__.__name__ifclsname=="ObjectDB":
@@ -371,47 +377,26 @@
elifclsname=="AccountDB":self.db_receivers_accounts.remove(receiver)elifclsname=="ScriptDB":
- self.db_receivers_scripts.remove(receiver)
- elifclsname=="ChannelDB":
- self.db_receivers_channels.remove(receiver)
+ self.db_receivers_scripts.remove(receiver)
- # channels property
- # @property
- def__channels_get(self):
- "Getter. Allows for value = self.channels. Returns a list of channels."
- returnself.db_receivers_channels.all()
-
- # @channels.setter
- def__channels_set(self,value):
- """
- Setter. Allows for self.channels = value.
- Requires a channel to be added.
- """
- forvalin(vforvinmake_iter(value)ifv):
- self.db_receivers_channels.add(val)
-
- # @channels.deleter
- def__channels_del(self):
- "Deleter. Allows for del self.channels"
- self.db_receivers_channels.clear()
- self.save()
-
- channels=property(__channels_get,__channels_set,__channels_del)
-
- def__hide_from_get(self):
+ @property
+ defhide_from(self):""" Getter. Allows for value = self.hide_from.
- Returns 3 lists of accounts, objects and channels
+ Returns two lists of accounts and objects.
+
"""return(self.db_hide_from_accounts.all(),self.db_hide_from_objects.all(),
- self.db_hide_from_channels.all(),)
- # @hide_from_sender.setter
- def__hide_from_set(self,hiders):
- "Setter. Allows for self.hide_from = value. Will append to hiders"
+ @hide_from.setter
+ defhide_from(self,hiders):
+ """
+ Setter. Allows for self.hide_from = value. Will append to hiders.
+
+ """forhiderinmake_iter(hiders):ifnothider:continue
@@ -422,34 +407,31 @@
self.db_hide_from_accounts.add(hider.__dbclass__)elifclsname=="ObjectDB":self.db_hide_from_objects.add(hider.__dbclass__)
- elifclsname=="ChannelDB":
- self.db_hide_from_channels.add(hider.__dbclass__)
- # @hide_from_sender.deleter
- def__hide_from_del(self):
- "Deleter. Allows for del self.hide_from_senders"
+ @hide_from.deleter
+ defhide_from(self):
+ """
+ Deleter. Allows for del self.hide_from_senders
+
+ """self.db_hide_from_accounts.clear()self.db_hide_from_objects.clear()
- self.db_hide_from_channels.clear()self.save()
- hide_from=property(__hide_from_get,__hide_from_set,__hide_from_del)
-
## Msg class methods#def__str__(self):
- "This handles what is shown when e.g. printing the message"
- senders=",".join(getattr(obj,"key",str(obj))forobjinself.senders)
+ """
+ This handles what is shown when e.g. printing the message.
- receivers=",".join(
- ["[%s]"%getattr(obj,"key",str(obj))forobjinself.channels]
- +[getattr(obj,"key",str(obj))forobjinself.receivers]
- )
+ """
+ senders=",".join(getattr(obj,"key",str(obj))forobjinself.senders)
+ receivers=",".join(getattr(obj,"key",str(obj))forobjinself.receivers)return"%s->%s: %s"%(senders,receivers,crop(self.message,width=40))
-
[docs]classTempMsg(object):"""
- This is a non-persistent object for sending temporary messages
- that will not be stored. It mimics the "real" Msg object, but
- doesn't require sender to be given.
+ This is a non-persistent object for sending temporary messages that will not be stored. It
+ mimics the "real" Msg object, but doesn't require sender to be given. """
-
[docs]def__init__(self,senders=None,receivers=None,
- channels=None,message="",header="",type="",
@@ -496,18 +476,16 @@
Args: senders (any or list, optional): Senders of the message.
- receivers (Account, Object, Channel or list, optional): Receivers of this message.
- channels (Channel or list, optional): Channels to send to.
+ receivers (Account, Object, Script or list, optional): Receivers of this message. message (str, optional): Message to send. header (str, optional): Header of message. type (str, optional): Message class, if any. lockstring (str, optional): Lock for the message.
- hide_from (Account, Object, Channel or list, optional): Entities to hide this message from.
+ hide_from (Account, Object, or list, optional): Entities to hide this message from. """self.senders=sendersandmake_iter(senders)or[]self.receivers=receiversandmake_iter(receivers)or[]
- self.channels=channelsandmake_iter(channels)or[]self.type=typeself.header=headerself.message=message
@@ -515,21 +493,20 @@
self.hide_from=hide_fromandmake_iter(hide_from)or[]self.date_created=timezone.now()
def__str__(self):""" This handles what is shown when e.g. printing the message.
+
"""senders=",".join(obj.keyforobjinself.senders)
- receivers=",".join(
- ["[%s]"%obj.keyforobjinself.channels]+[obj.keyforobjinself.receivers]
- )
+ receivers=",".join(obj.keyforobjinself.receivers)return"%s->%s: %s"%(senders,receivers,crop(self.message,width=40))
-
[docs]defremove_receiver(self,receiver):""" Remove a receiver or a list of receivers Args:
- receiver (Object, Account, Channel, str or list): Receivers to remove.
+ receiver (Object, Account, Script, str or list): Receivers to remove.
+
"""foroinmake_iter(receiver):
@@ -557,7 +535,7 @@
exceptValueError:pass# nothing to remove
[docs]defaccess(self,accessing_obj,access_type="read",default=False):""" Checks lock access.
@@ -585,6 +563,7 @@
This handler manages subscriptions to the channel and hides away which type of entity is subscribing (Account or Object)
+
"""def__init__(self,obj):
@@ -644,9 +623,6 @@
no hooks will be called. """
- global_CHANNELHANDLER
- ifnot_CHANNELHANDLER:
- fromevennia.comms.channelhandlerimportCHANNEL_HANDLERas_CHANNELHANDLERforsubscriberinmake_iter(entity):ifsubscriber:clsname=subscriber.__dbclass__.__name__
@@ -655,7 +631,6 @@
self.obj.db_object_subscriptions.add(subscriber)elifclsname=="AccountDB":self.obj.db_account_subscriptions.add(subscriber)
- _CHANNELHANDLER._cached_cmdsets.pop(subscriber,None)self._recache()defremove(self,entity):
@@ -667,9 +642,6 @@
entities to un-subscribe from the channel. """
- global_CHANNELHANDLER
- ifnot_CHANNELHANDLER:
- fromevennia.comms.channelhandlerimportCHANNEL_HANDLERas_CHANNELHANDLERforsubscriberinmake_iter(entity):ifsubscriber:clsname=subscriber.__dbclass__.__name__
@@ -678,7 +650,6 @@
self.obj.db_account_subscriptions.remove(entity)elifclsname=="ObjectDB":self.obj.db_object_subscriptions.remove(entity)
- _CHANNELHANDLER._cached_cmdsets.pop(subscriber,None)self._recache()defall(self):
@@ -714,7 +685,8 @@
ifnotobj.is_connected:continueexceptObjectDoesNotExist:
- # a subscribed object has already been deleted. Mark that we need a recache and ignore it
+ # a subscribed object has already been deleted. Mark that we need a recache and
+ # ignore itrecache_needed=Truecontinuesubs.append(obj)
@@ -732,7 +704,7 @@
self._cache=None
-
[docs]classChannelDB(TypedObject):""" This is the basis of a comm channel, only implementing the very basics of distributing messages.
@@ -768,7 +740,7 @@
__defaultclasspath__="evennia.comms.comms.DefaultChannel"__applabel__="comms"
- classMeta(object):
+ classMeta:"Define Django meta options"verbose_name="Channel"verbose_name_plural="Channels"
@@ -777,7 +749,7 @@
"Echoes the text representation of the channel."return"Channel '%s' (%s)"%(self.key,self.db.desc)
-
[docs]defat_server_start(self):"""Set up the event system when starting. Note that this hook is called every time the server restarts
@@ -744,7 +745,6 @@
-
diff --git a/docs/0.9.5/_modules/evennia/contrib/rplanguage.html b/docs/0.9.5/_modules/evennia/contrib/rplanguage.html
index 49b532011a..3f97eb48da 100644
--- a/docs/0.9.5/_modules/evennia/contrib/rplanguage.html
+++ b/docs/0.9.5/_modules/evennia/contrib/rplanguage.html
@@ -101,21 +101,46 @@
Below is an example of "elvish", using "rounder" vowels and sounds: ```python
- phonemes = "oi oh ee ae aa eh ah ao aw ay er ey ow ia ih iy " \
- "oy ua uh uw y p b t d f v t dh s z sh zh ch jh k " \
- "ng g m n l r w",
+ # vowel/consonant grammar possibilities
+ grammar = ("v vv vvc vcc vvcc cvvc vccv vvccv vcvccv vcvcvcc vvccvvcc "
+ "vcvvccvvc cvcvvcvvcc vcvcvvccvcvv")
+
+ # all not in this group is considered a consonant vowels = "eaoiuy"
- grammar = "v vv vvc vcc vvcc cvvc vccv vvccv vcvccv vcvcvcc vvccvvcc " \
- "vcvvccvvc cvcvvcvvcc vcvcvvccvcvv",
+
+ # you need a representative of all of the minimal grammars here, so if a
+ # grammar v exists, there must be atleast one phoneme available with only
+ # one vowel in it
+ phonemes = ("oi oh ee ae aa eh ah ao aw ay er ey ow ia ih iy "
+ "oy ua uh uw y p b t d f v t dh s z sh zh ch jh k "
+ "ng g m n l r w")
+
+ # how much the translation varies in length compared to the original. 0 is
+ # smallest, higher values give ever bigger randomness (including removing
+ # short words entirely) word_length_variance = 1
+
+ # if a proper noun (word starting with capitalized letter) should be
+ # translated or not. If not (default) it means e.g. names will remain
+ # unchanged across languages.
+ noun_translate = False
+
+ # all proper nouns (words starting with a capital letter not at the beginning
+ # of a sentence) can have either a postfix or -prefix added at all times noun_postfix = "'la"
+
+ # words in dict will always be translated this way. The 'auto_translations'
+ # is instead a list or filename to file with words to use to help build a
+ # bigger dictionary by creating random translations of each word in the
+ # list *once* and saving the result for subsequent use. manual_translations = {"the":"y'e", "we":"uyi", "she":"semi", "he":"emi", "you": "do", 'me':'mi','i':'me', 'be':"hy'e", 'and':'y'} rplanguage.add_language(key="elvish", phonemes=phonemes, grammar=grammar, word_length_variance=word_length_variance,
+ noun_translate=noun_translate, noun_postfix=noun_postfix, vowels=vowels,
- manual_translations=manual_translations
+ manual_translations=manual_translations, auto_translations="my_word_file.txt") ```
@@ -158,7 +183,8 @@
_RE_FLAGS=re.MULTILINE+re.IGNORECASE+re.DOTALL+re.UNICODE_RE_GRAMMAR=re.compile(r"vv|cc|v|c",_RE_FLAGS)_RE_WORD=re.compile(r"\w+",_RE_FLAGS)
-_RE_EXTRA_CHARS=re.compile(r"\s+(?=\W)|[,.?;](?=[,.?;]|\s+[,.?;])",_RE_FLAGS)
+# superfluous chars, except ` ... `
+_RE_EXTRA_CHARS=re.compile(r"\s+(?!... )(?=\W)|[,.?;](?!.. )(?=[,?;]|\s+[,.?;])",_RE_FLAGS)
[docs]classLanguageError(RuntimeError):
@@ -239,9 +265,13 @@
0 means a minimal variance, higher variance may mean words have wildly varying length; this strongly affects how the language "looks".
- noun_translate (bool, optional): If a proper noun, identified as a
- capitalized word, should be translated or not. By default they
- will not, allowing for e.g. the names of characters to be understandable.
+ noun_translate (bool, optional): If a proper noun should be translated or
+ not. By default they will not, allowing for e.g. the names of characters
+ to be understandable. A 'noun' is identified as a capitalized word
+ *not at the start of a sentence*. This simple metric means that names
+ starting a sentence always will be translated (- but hey, maybe
+ the fantasy language just never uses a noun at the beginning of
+ sentences, who knows?) noun_prefix (str, optional): A prefix to go before every noun in this language (if any). noun_postfix (str, optuonal): A postfix to go after every noun
@@ -286,7 +316,7 @@
# {"vv": ["ea", "oh", ...], ...}grammar2phonemes=defaultdict(list)forphonemeinphonemes.split():
- ifre.search("\W",phoneme):
+ ifre.search(r"\W",phoneme):raiseLanguageError("The phoneme '%s' contains an invalid character"%phoneme)gram="".join(["v"ifcharinvowelselse"c"forcharinphoneme])grammar2phonemes[gram].append(phoneme)
@@ -294,7 +324,7 @@
# allowed grammar are grouped by lengthgramdict=defaultdict(list)forgramingrammar.split():
- ifre.search("\W|(!=[cv])",gram):
+ ifre.search(r"\W|(!=[cv])",gram):raiseLanguageError("The grammar '%s' is invalid (only 'c' and 'v' are allowed)"%gram)
@@ -321,7 +351,13 @@
# use the corresponding lengthstructure=choice(grammar[wlen])formatchin_RE_GRAMMAR.finditer(structure):
- new_word+=choice(grammar2phonemes[match.group()])
+ try:
+ new_word+=choice(grammar2phonemes[match.group()])
+ exceptIndexError:
+ raiseIndexError(
+ "Could not find a matching phoneme for the grammar "
+ f"'{match.group()}'. Make there is at least one phoneme matching this "
+ "combination of consonants and vowels.")translation[word.lower()]=new_word.lower()ifmanual_translations:
@@ -360,6 +396,11 @@
word=match.group()lword=len(word)
+ # find out what preceeded this word
+ wpos=match.start()
+ preceeding=match.string[:wpos].strip()
+ start_sentence=preceeding.endswith((".","!","?"))ornotpreceeding
+
iflen(word)<=self.level:# below level. Don't translatenew_word=word
@@ -369,11 +410,6 @@
ifnotnew_word:# no dictionary translation. Generate one
- # find out what preceeded this word
- wpos=match.start()
- preceeding=match.string[:wpos].strip()
- start_sentence=preceeding.endswith((".","!","?"))ornotpreceeding
-
# make up translation on the fly. Length can# vary from un-translated word.wlen=max(
@@ -408,24 +444,30 @@
breakifword.istitle():
- title_word=""
- ifnotstart_sentenceandnotself.language.get("noun_translate",False):
- # don't translate what we identify as proper nouns (names)
- title_word=word
- elifnew_word:
- title_word=new_word
+ ifnotstart_sentence:
+ # this is a noun. We miss nouns at the start of
+ # sentences this way, but it's as good as we can get
+ # with this simple analysis. Maybe the fantasy language
+ # just don't consider nouns at the beginning of
+ # sentences, who knows?
+ ifnotself.language.get("noun_translate",False):
+ # don't translate what we identify as proper nouns (names)
+ new_word=word
- iftitle_word:
- # Regardless of if we translate or not, we will add the custom prefix/postfixes
- new_word="%s%s%s"%(
- self.language["noun_prefix"],
- title_word.capitalize(),
- self.language["noun_postfix"],
+ # add noun prefix and/or postfix
+ new_word="{prefix}{word}{postfix}".format(
+ prefix=self.language["noun_prefix"],
+ word=new_word.capitalize(),
+ postfix=self.language["noun_postfix"],)iflen(word)>1andword.isupper():# keep LOUD words loud also when translatednew_word=new_word.upper()
+
+ ifstart_sentence:
+ new_word=new_word.capitalize()
+
returnnew_word
-# ------------------------------------------------------------
+# -----------------------------------------------------------------------------## Whisper obscuration#
-# This obsucration table is designed by obscuring certain
-# vowels first, following by consonants that tend to be
-# more audible over long distances, like s. Finally it
-# does non-auditory replacements, like exclamation marks
-# and capitalized letters (assumed to be spoken louder) that may still
-# give a user some idea of the sentence structure. Then the word
-# lengths are also obfuscated and finally the whisper # length itself.
+# This obsucration table is designed by obscuring certain vowels first,
+# following by consonants that tend to be more audible over long distances,
+# like s. Finally it does non-auditory replacements, like exclamation marks and
+# capitalized letters (assumed to be spoken louder) that may still give a user
+# some idea of the sentence structure. Then the word lengths are also
+# obfuscated and finally the whisper length itself.#
-# ------------------------------------------------------------
+# ------------------------------------------------------------------------------_RE_WHISPER_OBSCURE=[
@@ -621,7 +662,6 @@
diff --git a/docs/0.9.5/_modules/evennia/contrib/rpsystem.html b/docs/0.9.5/_modules/evennia/contrib/rpsystem.html
index 6c6cf18278..fbe24c2cf3 100644
--- a/docs/0.9.5/_modules/evennia/contrib/rpsystem.html
+++ b/docs/0.9.5/_modules/evennia/contrib/rpsystem.html
@@ -324,7 +324,8 @@
the markers and a tuple (langname, saytext), where langname can be None. Raises:
- rplanguage.LanguageError: If an invalid language was specified.
+ evennia.contrib.rpsystem.LanguageError: If an invalid language was
+ specified. Notes: Note that no errors are raised if the wrong language identifier
@@ -1702,7 +1703,6 @@
[docs]defat_defeat(defeated):""" Announces the defeat of a fighter in combat.
-
+
Args: defeated (obj): Fighter that's been defeated.
-
+
Notes: All this does is announce a defeat message by default, but if you want anything else to happen to defeated fighters (like putting them
@@ -523,6 +523,7 @@
ifdisengage_check:# All characters have disengagedself.obj.msg_contents("All fighters have disengaged! Combat is over!")self.stop()# Stop this script and end combat.
+ self.delete()return# Check to see if only one character is left standing. If so, end combat.
@@ -538,6 +539,7 @@
LastStanding=fighter# Pick the one fighter left with HP remainingself.obj.msg_contents("Only %s remains! Combat is over!"%LastStanding)self.stop()# Stop this script and end combat.
+ self.delete()return# Cycle to the next turn.
@@ -856,7 +858,6 @@
-
diff --git a/docs/0.9.5/_modules/evennia/contrib/turnbattle/tb_range.html b/docs/0.9.5/_modules/evennia/contrib/turnbattle/tb_range.html
index 7f8e8f8e7b..a19b1b81f9 100644
--- a/docs/0.9.5/_modules/evennia/contrib/turnbattle/tb_range.html
+++ b/docs/0.9.5/_modules/evennia/contrib/turnbattle/tb_range.html
@@ -683,7 +683,6 @@
Args: to_init (object): Object to initialize range field for.
-
Keyword Args: anchor_obj (object): Object to copy range values from, or None for a random object. add_distance (int): Distance to put between to_init object and anchor object.
@@ -1512,7 +1511,6 @@
Source code for evennia.contrib.tutorial_examples.cmdset_red_button
-"""
-This defines the cmdset for the red_button. Here we have defined
-the commands and the cmdset in the same module, but if you
-have many different commands to merge it is often better
-to define the cmdset separately, picking and choosing from
-among the available commands as to what should be included in the
-cmdset - this way you can often re-use the commands too.
-"""
-
-importrandom
-fromevenniaimportCommand,CmdSet
-
-# Some simple commands for the red button
-
-# ------------------------------------------------------------
-# Commands defined on the red button
-# ------------------------------------------------------------
-
-
-
[docs]classCmdNudge(Command):
- """
- Try to nudge the button's lid
-
- Usage:
- nudge lid
-
- This command will have you try to
- push the lid of the button away.
- """
-
- key="nudge lid"# two-word command name!
- aliases=["nudge"]
- locks="cmd:all()"
-
-
[docs]deffunc(self):
- """
- nudge the lid. Random chance of success to open it.
- """
- rand=random.random()
- ifrand<0.5:
- self.caller.msg("You nudge at the lid. It seems stuck.")
- elifrand<0.7:
- self.caller.msg("You move the lid back and forth. It won't budge.")
- else:
- self.caller.msg("You manage to get a nail under the lid.")
- self.caller.execute_cmd("open lid")
[docs]deffunc(self):
- """
- Note that we choose to implement this with checking for
- if the lid is open/closed. This is because this command
- is likely to be tried regardless of the state of the lid.
-
- An alternative would be to make two versions of this command
- and tuck them into the cmdset linked to the Open and Closed
- lid-state respectively.
-
- """
-
- ifself.obj.db.lid_open:
- string="You reach out to press the big red button ..."
- string+="\n\nA BOOM! A bright light blinds you!"
- string+="\nThe world goes dark ..."
- self.caller.msg(string)
- self.caller.location.msg_contents(
- "%s presses the button. BOOM! %s is blinded by a flash!"
- %(self.caller.name,self.caller.name),
- exclude=self.caller,
- )
- # the button's method will handle all setup of scripts etc.
- self.obj.press_button(self.caller)
- else:
- string="You cannot push the button - there is a glass lid covering it."
- self.caller.msg(string)
-
-
-
[docs]classCmdSmashGlass(Command):
- """
- smash glass
-
- Usage:
- smash glass
-
- Try to smash the glass of the button.
- """
-
- key="smash glass"
- aliases=["smash lid","break lid","smash"]
- locks="cmd:all()"
-
-
[docs]deffunc(self):
- """
- The lid won't open, but there is a small chance
- of causing the lamp to break.
- """
- rand=random.random()
-
- ifrand<0.2:
- string="You smash your hand against the glass"
- string+=" with all your might. The lid won't budge"
- string+=" but you cause quite the tremor through the button's mount."
- string+="\nIt looks like the button's lamp stopped working for the time being."
- self.obj.lamp_works=False
- elifrand<0.6:
- string="You hit the lid hard. It doesn't move an inch."
- else:
- string="You place a well-aimed fist against the glass of the lid."
- string+=" Unfortunately all you get is a pain in your hand. Maybe"
- string+=" you should just try to open the lid instead?"
- self.caller.msg(string)
- self.caller.location.msg_contents(
- "%s tries to smash the glass of the button."%(self.caller.name),exclude=self.caller
- )
-
-
-
[docs]classCmdOpenLid(Command):
- """
- open lid
-
- Usage:
- open lid
-
- """
-
- key="open lid"
- aliases=["open button","open"]
- locks="cmd:all()"
-
-
[docs]deffunc(self):
- "simply call the right function."
-
- ifself.obj.db.lid_locked:
- self.caller.msg("This lid seems locked in place for the moment.")
- return
-
- string="\nA ticking sound is heard, like a winding mechanism. Seems "
- string+="the lid will soon close again."
- self.caller.msg(string)
- self.caller.location.msg_contents(
- "%s opens the lid of the button."%(self.caller.name),exclude=self.caller
- )
- # add the relevant cmdsets to button
- self.obj.cmdset.add(LidClosedCmdSet)
- # call object method
- self.obj.open_lid()
-
-
-
[docs]classCmdCloseLid(Command):
- """
- close the lid
-
- Usage:
- close lid
-
- Closes the lid of the red button.
- """
-
- key="close lid"
- aliases=["close"]
- locks="cmd:all()"
-
-
[docs]deffunc(self):
- "Close the lid"
-
- self.obj.close_lid()
-
- # this will clean out scripts dependent on lid being open.
- self.caller.msg("You close the button's lid. It clicks back into place.")
- self.caller.location.msg_contents(
- "%s closes the button's lid."%(self.caller.name),exclude=self.caller
- )
-
-
-
[docs]classCmdBlindLook(Command):
- """
- Looking around in darkness
-
- Usage:
- look <obj>
-
- ... not that there's much to see in the dark.
-
- """
-
- key="look"
- aliases=["l","get","examine","ex","feel","listen"]
- locks="cmd:all()"
-
-
[docs]deffunc(self):
- "This replaces all the senses when blinded."
-
- # we decide what to reply based on which command was
- # actually tried
-
- ifself.cmdstring=="get":
- string="You fumble around blindly without finding anything."
- elifself.cmdstring=="examine":
- string="You try to examine your surroundings, but can't see a thing."
- elifself.cmdstring=="listen":
- string="You are deafened by the boom."
- elifself.cmdstring=="feel":
- string="You fumble around, hands outstretched. You bump your knee."
- else:
- # trying to look
- string="You are temporarily blinded by the flash. "
- string+="Until it wears off, all you can do is feel around blindly."
- self.caller.msg(string)
- self.caller.location.msg_contents(
- "%s stumbles around, blinded."%(self.caller.name),exclude=self.caller
- )
-
-
-
[docs]classCmdBlindHelp(Command):
- """
- Help function while in the blinded state
-
- Usage:
- help
-
- """
-
- key="help"
- aliases="h"
- locks="cmd:all()"
-
-
[docs]deffunc(self):
- "Give a message."
- self.caller.msg("You are beyond help ... until you can see again.")
-
-
-# ---------------------------------------------------------------
-# Command sets for the red button
-# ---------------------------------------------------------------
-
-# We next tuck these commands into their respective command sets.
-# (note that we are overdoing the cdmset separation a bit here
-# to show how it works).
-
-
-
[docs]classDefaultCmdSet(CmdSet):
- """
- The default cmdset always sits
- on the button object and whereas other
- command sets may be added/merge onto it
- and hide it, removing them will always
- bring it back. It's added to the object
- using obj.cmdset.add_default().
- """
-
- key="RedButtonDefault"
- mergetype="Union"# this is default, we don't really need to put it here.
-
-
[docs]defat_cmdset_creation(self):
- "Init the cmdset"
- self.add(CmdPush())
-
-
-
[docs]classLidClosedCmdSet(CmdSet):
- """
- A simple cmdset tied to the redbutton object.
-
- It contains the commands that launches the other
- command sets, making the red button a self-contained
- item (i.e. you don't have to manually add any
- scripts etc to it when creating it).
- """
-
- key="LidClosedCmdSet"
- # default Union is used *except* if we are adding to a
- # cmdset named LidOpenCmdSet - this one we replace
- # completely.
- key_mergetype={"LidOpenCmdSet":"Replace"}
-
-
[docs]defat_cmdset_creation(self):
- "Populates the cmdset when it is instantiated."
- self.add(CmdNudge())
- self.add(CmdSmashGlass())
- self.add(CmdOpenLid())
-
-
-
[docs]classLidOpenCmdSet(CmdSet):
- """
- This is the opposite of the Closed cmdset.
- """
-
- key="LidOpenCmdSet"
- # default Union is used *except* if we are adding to a
- # cmdset named LidClosedCmdSet - this one we replace
- # completely.
- key_mergetype={"LidClosedCmdSet":"Replace"}
-
-
[docs]defat_cmdset_creation(self):
- "setup the cmdset (just one command)"
- self.add(CmdCloseLid())
-
-
-
[docs]classBlindCmdSet(CmdSet):
- """
- This is the cmdset added to the *account* when
- the button is pushed.
- """
-
- key="BlindCmdSet"
- # we want it to completely replace all normal commands
- # until the timed script removes it again.
- mergetype="Replace"
- # we want to stop the account from walking around
- # in this blinded state, so we hide all exits too.
- # (channel commands will still work).
- no_exits=True# keep account in the same room
- no_objs=True# don't allow object commands
-
-
-
-
-
\ No newline at end of file
diff --git a/docs/0.9.5/_modules/evennia/contrib/tutorial_examples/red_button.html b/docs/0.9.5/_modules/evennia/contrib/tutorial_examples/red_button.html
index 65f6d86cfa..a9b4a3e63e 100644
--- a/docs/0.9.5/_modules/evennia/contrib/tutorial_examples/red_button.html
+++ b/docs/0.9.5/_modules/evennia/contrib/tutorial_examples/red_button.html
@@ -47,14 +47,381 @@
Create this button with
- @create/drop examples.red_button.RedButton
+ create/drop red_button.RedButtonNote that you must drop the button before you can see its messages!
+
+## Technical
+
+The button's functionality is controlled by CmdSets that gets added and removed
+depending on the 'state' the button is in.
+
+- Lid-closed state: In this state the button is covered by a glass cover and trying
+ to 'push' it will fail. You can 'nudge', 'smash' or 'open' the lid.
+- Lid-open state: In this state the lid is open but will close again after a certain
+ time. Using 'push' now will press the button and trigger the Blind-state.
+- Blind-state: In this mode you are blinded by a bright flash. This will affect your
+ normal commands like 'look' and help until the blindness wears off after a certain
+ time.
+
+Timers are handled by persistent delays on the button. These are examples of
+`evennia.utils.utils.delay` calls that wait a certain time before calling a method -
+such as when closing the lid and un-blinding a character.
+
"""importrandomfromevenniaimportDefaultObject
-fromevennia.contrib.tutorial_examplesimportred_button_scriptsasscriptexamples
-fromevennia.contrib.tutorial_examplesimportcmdset_red_buttonascmdsetexamples
+fromevenniaimportCommand,CmdSet
+fromevennia.utils.utilsimportdelay,repeat,interactive
+
+
+# Commands on the button (not all awailable at the same time)
+
+
+# Commands for the state when the lid covering the button is closed.
+
+
[docs]deffunc(self):
+ """
+ This is the version of push used when the lid is closed.
+
+ An alternative would be to make a 'push' command in a default cmdset
+ that is always available on the button and then use if-statements to
+ check if the lid is open or closed.
+
+ """
+ self.caller.msg("You cannot push the button = there is a glass lid covering it.")
+
+
+
[docs]classCmdNudge(Command):
+ """
+ Try to nudge the button's lid.
+
+ Usage:
+ nudge lid
+
+ This command will have you try to push the lid of the button away.
+
+ """
+
+ key="nudge lid"# two-word command name!
+ aliases=["nudge"]
+ locks="cmd:all()"
+
+
[docs]deffunc(self):
+ """
+ Nudge the lid. Random chance of success to open it.
+
+ """
+ rand=random.random()
+ ifrand<0.5:
+ self.caller.msg("You nudge at the lid. It seems stuck.")
+ elifrand<0.7:
+ self.caller.msg("You move the lid back and forth. It won't budge.")
+ else:
+ self.caller.msg("You manage to get a nail under the lid.")
+ # self.obj is the button object
+ self.obj.to_open_state()
+
+
+
[docs]classCmdSmashGlass(Command):
+ """
+ Smash the protective glass.
+
+ Usage:
+ smash glass
+
+ Try to smash the glass of the button.
+
+ """
+
+ key="smash glass"
+ aliases=["smash lid","break lid","smash"]
+ locks="cmd:all()"
+
+
[docs]deffunc(self):
+ """
+ The lid won't open, but there is a small chance of causing the lamp to
+ break.
+
+ """
+ rand=random.random()
+ self.caller.location.msg_contents(
+ f"{self.caller.name} tries to smash the glass of the button.",
+ exclude=self.caller)
+
+ ifrand<0.2:
+ string=("You smash your hand against the glass"
+ " with all your might. The lid won't budge"
+ " but you cause quite the tremor through the button's mount."
+ "\nIt looks like the button's lamp stopped working for the time being, "
+ "but the lid is still as closed as ever.")
+ # self.obj is the button itself
+ self.obj.break_lamp()
+ elifrand<0.6:
+ string="You hit the lid hard. It doesn't move an inch."
+ else:
+ string=("You place a well-aimed fist against the glass of the lid."
+ " Unfortunately all you get is a pain in your hand. Maybe"
+ " you should just try to just ... open the lid instead?")
+ self.caller.msg(string)
+
+
+
[docs]classCmdOpenLid(Command):
+ """
+ open lid
+
+ Usage:
+ open lid
+
+ """
+
+ key="open lid"
+ aliases=["open button"]
+ locks="cmd:all()"
+
+
[docs]deffunc(self):
+ "simply call the right function."
+
+ ifself.obj.db.lid_locked:
+ self.caller.msg("This lid seems locked in place for the moment.")
+ return
+
+ string="\nA ticking sound is heard, like a winding mechanism. Seems "
+ string+="the lid will soon close again."
+ self.caller.msg(string)
+ self.caller.location.msg_contents(
+ f"{self.caller.name} opens the lid of the button.",
+ exclude=self.caller)
+ self.obj.to_open_state()
+
+
+
[docs]classLidClosedCmdSet(CmdSet):
+ """
+ A simple cmdset tied to the redbutton object.
+
+ It contains the commands that launches the other
+ command sets, making the red button a self-contained
+ item (i.e. you don't have to manually add any
+ scripts etc to it when creating it).
+
+ Note that this is given with a `key_mergetype` set. This
+ is set up so that the cmdset with merge with Union merge type
+ *except* if the other cmdset to merge with is LidOpenCmdSet,
+ in which case it will Replace that. So these two cmdsets will
+ be mutually exclusive.
+
+ """
+
+ key="LidClosedCmdSet"
+
+
[docs]defat_cmdset_creation(self):
+ "Populates the cmdset when it is instantiated."
+ self.add(CmdPushLidClosed())
+ self.add(CmdNudge())
+ self.add(CmdSmashGlass())
+ self.add(CmdOpenLid())
+
+
+# Commands for the state when the button's protective cover is open - now the
+# push command will work. You can also close the lid again.
+
+
[docs]classCmdPushLidOpen(Command):
+ """
+ Push the red button
+
+ Usage:
+ push button
+
+ """
+
+ key="push button"
+ aliases=["push","press button","press"]
+ locks="cmd:all()"
+
+ @interactive
+ deffunc(self):
+ """
+ This version of push will immediately trigger the next button state.
+
+ The use of the @interactive decorator allows for using `yield` to add
+ simple pauses in how quickly a message is returned to the user. This
+ kind of pause will not survive a server reload.
+
+ """
+ # pause a little between each message.
+ self.caller.msg("You reach out to press the big red button ...")
+ yield(2)# pause 2s before next message
+ self.caller.msg("\n\n|wBOOOOM! A bright light blinds you!|n")
+ yield(1)# pause 1s before next message
+ self.caller.msg("\n\n|xThe world goes dark ...|n")
+
+ name=self.caller.name
+ self.caller.location.msg_contents(
+ f"{name} presses the button. BOOM! {name} is blinded by a flash!",
+ exclude=self.caller)
+ self.obj.blind_target(self.caller)
+
+
+
[docs]classCmdCloseLid(Command):
+ """
+ Close the lid
+
+ Usage:
+ close lid
+
+ Closes the lid of the red button.
+ """
+
+ key="close lid"
+ aliases=["close"]
+ locks="cmd:all()"
+
+
[docs]deffunc(self):
+ "Close the lid"
+
+ self.obj.to_closed_state()
+
+ # this will clean out scripts dependent on lid being open.
+ self.caller.msg("You close the button's lid. It clicks back into place.")
+ self.caller.location.msg_contents(
+ f"{self.caller.name} closes the button's lid.",
+ exclude=self.caller)
+
+
+
[docs]classLidOpenCmdSet(CmdSet):
+ """
+ This is the opposite of the Closed cmdset.
+
+ Note that this is given with a `key_mergetype` set. This
+ is set up so that the cmdset with merge with Union merge type
+ *except* if the other cmdset to merge with is LidClosedCmdSet,
+ in which case it will Replace that. So these two cmdsets will
+ be mutually exclusive.
+
+ """
+
+ key="LidOpenCmdSet"
+
+
[docs]defat_cmdset_creation(self):
+ """Setup the cmdset"""
+ self.add(CmdPushLidOpen())
+ self.add(CmdCloseLid())
+
+
+# Commands for when the button has been pushed and the player is blinded. This
+# replaces commands on the player making them 'blind' for a while.
+
+
[docs]classCmdBlindLook(Command):
+ """
+ Looking around in darkness
+
+ Usage:
+ look <obj>
+
+ ... not that there's much to see in the dark.
+
+ """
+
+ key="look"
+ aliases=["l","get","examine","ex","feel","listen"]
+ locks="cmd:all()"
+
+
[docs]deffunc(self):
+ "This replaces all the senses when blinded."
+
+ # we decide what to reply based on which command was
+ # actually tried
+
+ ifself.cmdstring=="get":
+ string="You fumble around blindly without finding anything."
+ elifself.cmdstring=="examine":
+ string="You try to examine your surroundings, but can't see a thing."
+ elifself.cmdstring=="listen":
+ string="You are deafened by the boom."
+ elifself.cmdstring=="feel":
+ string="You fumble around, hands outstretched. You bump your knee."
+ else:
+ # trying to look
+ string=("You are temporarily blinded by the flash. "
+ "Until it wears off, all you can do is feel around blindly.")
+ self.caller.msg(string)
+ self.caller.location.msg_contents(
+ f"{self.caller.name} stumbles around, blinded.",
+ exclude=self.caller)
+
+
+
[docs]classCmdBlindHelp(Command):
+ """
+ Help function while in the blinded state
+
+ Usage:
+ help
+
+ """
+
+ key="help"
+ aliases="h"
+ locks="cmd:all()"
+
+
[docs]deffunc(self):
+ """
+ Just give a message while blinded. We could have added this to the
+ CmdBlindLook command too if we wanted to keep things more compact.
+
+ """
+ self.caller.msg("You are beyond help ... until you can see again.")
+
+
+
[docs]classBlindCmdSet(CmdSet):
+ """
+ This is the cmdset added to the *account* when
+ the button is pushed.
+
+ Since this has mergetype Replace it will completely remove the commands of
+ all other cmdsets while active. To allow some limited interaction
+ (pose/say) we import those default commands and add them too.
+
+ We also disable all exit-commands generated by exits and
+ object-interactions while blinded by setting `no_exits` and `no_objs` flags
+ on the cmdset. This is to avoid the player walking off or interfering with
+ other objects while blinded. Account-level commands however (channel messaging
+ etc) will not be affected by the blinding.
+
+ """
+
+ key="BlindCmdSet"
+ # we want it to completely replace all normal commands
+ # until the timed script removes it again.
+ mergetype="Replace"
+ # we want to stop the player from walking around
+ # in this blinded state, so we hide all exits too.
+ # (channel commands will still work).
+ no_exits=True# keep player in the same room
+ no_objs=True# don't allow object commands
+
+
+
## Definition of the object itself
@@ -63,149 +430,192 @@
[docs]classRedButton(DefaultObject):"""
- This class describes an evil red button. It will use the script
- definition in contrib/examples/red_button_scripts to blink at regular
- intervals. It also uses a series of script and commands to handle
- pushing the button and causing effects when doing so.
+ This class describes an evil red button. It will blink invitingly and
+ temporarily blind whomever presses it.
- The following attributes can be set on the button:
- desc_lid_open - description when lid is open
- desc_lid_closed - description when lid is closed
- desc_lamp_broken - description when lamp is broken
+ The button can take a few optional attributes controlling how things will
+ be displayed in its various states. This is a useful way to give builders
+ the option to customize a complex object from in-game. Actual return messages
+ to event-actions are (in this example) left with each command, but one could
+ also imagine having those handled via Attributes as well, if one wanted a
+ completely in-game customizable button without needing to tweak command
+ classes.
+
+ Attributes:
+ - `desc_closed_lid`: This is the description to show of the button
+ when the lid is closed.
+ - `desc_open_lid`": Shown when the lid is open
+ - `auto_close_msg`: Message to show when lid auto-closes
+ - `desc_add_lamp_broken`: Extra desc-line added after normal desc when lamp
+ is broken.
+ - blink_msg: A list of strings to randomly choose from when the lamp
+ blinks.
+
+ Notes:
+ The button starts with lid closed. To set the initial description,
+ you can either set desc after creating it or pass a `desc` attribute
+ when creating it, such as
+ `button = create_object(RedButton, ..., attributes=[('desc', 'my desc')])`. """
+ # these are the pre-set descriptions. Setting attributes will override
+ # these on the fly.
+
+ desc_closed_lid=("This is a large red button, inviting yet evil-looking. "
+ "A closed glass lid protects it.")
+ desc_open_lid=("This is a large red button, inviting yet evil-looking. "
+ "Its glass cover is open and the button exposed.")
+ auto_close_msg="The button's glass lid silently slides back in place."
+ lamp_breaks_msg="The lamp flickers, the button going dark."
+ desc_add_lamp_broken="\nThe big red button has stopped blinking for the time being."
+ # note that this is a list. A random message will display each time
+ blink_msgs=["The red button flashes briefly.",
+ "The red button blinks invitingly.",
+ "The red button flashes. You know you wanna push it!"]
[docs]defat_object_creation(self):"""
- This function is called when object is created. Use this
- instead of e.g. __init__.
- """
- # store desc (default, you can change this at creation time)
- desc="This is a large red button, inviting yet evil-looking. "
- desc+="A closed glass lid protects it."
- self.db.desc=desc
+ This function is called (once) when object is created.
- # We have to define all the variables the scripts
- # are checking/using *before* adding the scripts or
- # they might be deactivated before even starting!
- self.db.lid_open=False
+ """self.db.lamp_works=True
- self.db.lid_locked=False
- self.cmdset.add_default(cmdsetexamples.DefaultCmdSet,permanent=True)
+ # start closed
+ self.to_closed_state()
- # since the cmdsets relevant to the button are added 'on the fly',
- # we need to setup custom scripts to do this for us (also, these scripts
- # check so they are valid (i.e. the lid is actually still closed)).
- # The AddClosedCmdSet script makes sure to add the Closed-cmdset.
- self.scripts.add(scriptexamples.ClosedLidState)
- # the script EventBlinkButton makes the button blink regularly.
- self.scripts.add(scriptexamples.BlinkButtonEvent)
+ # start blinking every 35s.
+ repeat(35,self._do_blink,persistent=True)
- # state-changing methods
-
-
[docs]defopen_lid(self):
+ def_do_blink(self):"""
- Opens the glass lid and start the timer so it will soon close
- again.
+ Have the button blink invitingly unless it's broken. """
+ ifself.locationandself.db.lamp_works:
+ possible_messages=self.db.blink_msgsorself.blink_msgs
+ self.location.msg_contents(random.choice(possible_messages))
- ifself.db.lid_open:
- return
- desc=self.db.desc_lid_open
- ifnotdesc:
- desc="This is a large red button, inviting yet evil-looking. "
- desc+="Its glass cover is open and the button exposed."
- self.db.desc=desc
- self.db.lid_open=True
-
- # with the lid open, we validate scripts; this will clean out
- # scripts that depend on the lid to be closed.
- self.scripts.validate()
- # now add new scripts that define the open-lid state
- self.scripts.add(scriptexamples.OpenLidState)
- # we also add a scripted event that will close the lid after a while.
- # (this one cleans itself after being called once)
- self.scripts.add(scriptexamples.CloseLidEvent)
-
-
[docs]defclose_lid(self):
+ def_set_desc(self,attrname=None):"""
- Close the glass lid. This validates all scripts on the button,
- which means that scripts only being valid when the lid is open
- will go away automatically.
-
- """
-
- ifnotself.db.lid_open:
- return
- desc=self.db.desc_lid_closed
- ifnotdesc:
- desc="This is a large red button, inviting yet evil-looking. "
- desc+="Its glass cover is closed, protecting it."
- self.db.desc=desc
- self.db.lid_open=False
-
- # clean out scripts depending on lid to be open
- self.scripts.validate()
- # add scripts related to the closed state
- self.scripts.add(scriptexamples.ClosedLidState)
-
-
[docs]defbreak_lamp(self,feedback=True):
- """
- Breaks the lamp in the button, stopping it from blinking.
+ Set a description, based on the attrname given, taking the lamp-status
+ into account. Args:
- feedback (bool): Show a message about breaking the lamp.
+ attrname (str, optional): This will first check for an Attribute with this name,
+ secondly for a property on the class. So if `attrname="auto_close_msg"`,
+ we will first look for an attribute `.db.auto_close_msg` and if that's
+ not found we'll use `.auto_close_msg` instead. If unset (`None`), the
+ currently set desc will not be changed (only lamp will be checked).
+
+ Notes:
+ If `self.db.lamp_works` is `False`, we'll append
+ `desc_add_lamp_broken` text.
+
+ """
+ ifattrname:
+ # change desc
+ desc=self.attributes.get(attrname)orgetattr(self,attrname)
+ else:
+ # use existing desc
+ desc=self.db.desc
+
+ ifnotself.db.lamp_works:
+ # lamp not working. Add extra to button's desc
+ desc+=self.db.desc_add_lamp_brokenorself.desc_add_lamp_broken
+
+ self.db.desc=desc
+
+ # state-changing methods and actions
+
+
[docs]defto_closed_state(self,msg=None):
+ """
+ Switches the button to having its lid closed.
+
+ Args:
+ msg (str, optional): If given, display a message to the room
+ when lid closes.
+
+ This will first try to get the Attribute (self.db.desc_closed_lid) in
+ case it was set by a builder and if that was None, it will fall back to
+ self.desc_closed_lid, the default description (note that lack of .db).
+ """
+ self._set_desc("desc_closed_lid")
+ # remove lidopen-state, if it exists
+ self.cmdset.remove(LidOpenCmdSet)
+ # add lid-closed cmdset
+ self.cmdset.add(LidClosedCmdSet)
+
+ ifmsgandself.location:
+ self.location.msg_contents(msg)
+
+
[docs]defto_open_state(self):
+ """
+ Switches the button to having its lid open. This also starts a timer
+ that will eventually close it again.
+
+ """
+ self._set_desc("desc_open_lid")
+ # remove lidopen-state, if it exists
+ self.cmdset.remove(LidClosedCmdSet)
+ # add lid-open cmdset
+ self.cmdset.add(LidOpenCmdSet)
+
+ # wait 20s then call self.to_closed_state with a message as argument
+ delay(35,self.to_closed_state,
+ self.db.auto_close_msgorself.auto_close_msg,
+ persistent=True)
+
+ def_unblind_target(self,caller):
+ """
+ This is called to un-blind after a certain time.
+
+ """
+ caller.cmdset.remove(BlindCmdSet)
+ caller.msg("You blink feverishly as your eyesight slowly returns.")
+ self.location.msg_contents(
+ f"{caller.name} seems to be recovering their eyesight, blinking feverishly.",
+ exclude=caller)
+
+
[docs]defblind_target(self,caller):
+ """
+ Someone was foolish enough to press the button! Blind them
+ temporarily.
+
+ Args:
+ caller (Object): The one to be blinded.
+
+ """
+
+ # we don't need to remove other cmdsets, this will replace all,
+ # then restore whatever was there when it goes away.
+ caller.cmdset.add(BlindCmdSet)
+
+ # wait 20s then call self._unblind to remove blindness effect. The
+ # persistent=True means the delay should survive a server reload.
+ delay(20,self._unblind_target,caller,
+ persistent=True)
+
+ def_unbreak_lamp(self):
+ """
+ This is called to un-break the lamp after a certain time.
+
+ """
+ # we do this quietly, the user will just notice it starting blinking again
+ self.db.lamp_works=True
+ self._set_desc()
+
+
[docs]defbreak_lamp(self):
+ """
+ Breaks the lamp in the button, stopping it from blinking for a while """self.db.lamp_works=False
- desc=self.db.desc_lamp_broken
- ifnotdesc:
- self.db.desc+="\nThe big red button has stopped blinking for the time being."
- else:
- self.db.desc=desc
+ # this will update the desc with the info about the broken lamp
+ self._set_desc()
+ self.location.msg_contents(self.db.lamp_breaks_msgorself.lamp_breaks_msg)
- iffeedbackandself.location:
- self.location.msg_contents("The lamp flickers, the button going dark.")
- self.scripts.validate()
-
-
[docs]defpress_button(self,pobject):
- """
- Someone was foolish enough to press the button!
-
- Args:
- pobject (Object): The person pressing the button
-
- """
- # deactivate the button so it won't flash/close lid etc.
- self.scripts.add(scriptexamples.DeactivateButtonEvent)
- # blind the person pressing the button. Note that this
- # script is set on the *character* pressing the button!
- pobject.scripts.add(scriptexamples.BlindedState)
-
- # script-related methods
-
-
[docs]defblink(self):
- """
- The script system will regularly call this
- function to make the button blink. Now and then
- it won't blink at all though, to add some randomness
- to how often the message is echoed.
- """
- loc=self.location
- ifloc:
- rand=random.random()
- ifrand<0.2:
- string="The red button flashes briefly."
- elifrand<0.4:
- string="The red button blinks invitingly."
- elifrand<0.6:
- string="The red button flashes. You know you wanna push it!"
- else:
- # no blink
- return
- loc.msg_contents(string)
+ # wait 21s before unbreaking the lamp again
+ delay(21,self._unbreak_lamp)
@@ -243,7 +653,6 @@
Source code for evennia.contrib.tutorial_examples.red_button_scripts
-"""
-Example of scripts.
-
-These are scripts intended for a particular object - the
-red_button object type in contrib/examples. A few variations
-on uses of scripts are included.
-
-"""
-fromevenniaimportDefaultScript
-fromevennia.contrib.tutorial_examplesimportcmdset_red_buttonascmdsetexamples
-
-#
-# Scripts as state-managers
-#
-# Scripts have many uses, one of which is to statically
-# make changes when a particular state of an object changes.
-# There is no "timer" involved in this case (although there could be),
-# whenever the script determines it is "invalid", it simply shuts down
-# along with all the things it controls.
-#
-# To show as many features as possible of the script and cmdset systems,
-# we will use three scripts controlling one state each of the red_button,
-# each with its own set of commands, handled by cmdsets - one for when
-# the button has its lid open, and one for when it is closed and a
-# last one for when the player pushed the button and gets blinded by
-# a bright light. The last one also has a timer component that allows it
-# to remove itself after a while (and the player recovers their eyesight).
-
-
-
[docs]classClosedLidState(DefaultScript):
- """
- This manages the cmdset for the "closed" button state. What this
- means is that while this script is valid, we add the RedButtonClosed
- cmdset to it (with commands like open, nudge lid etc)
- """
-
-
[docs]defat_script_creation(self):
- "Called when script first created."
- self.key="closed_lid_script"
- self.desc="Script that manages the closed-state cmdsets for red button."
- self.persistent=True
-
-
[docs]defat_start(self):
- """
- This is called once every server restart, so we want to add the
- (memory-resident) cmdset to the object here. is_valid is automatically
- checked so we don't need to worry about adding the script to an
- open lid.
- """
- # All we do is add the cmdset for the closed state.
- self.obj.cmdset.add(cmdsetexamples.LidClosedCmdSet)
-
-
[docs]defis_valid(self):
- """
- The script is only valid while the lid is closed.
- self.obj is the red_button on which this script is defined.
- """
- returnnotself.obj.db.lid_open
-
-
[docs]defat_stop(self):
- """
- When the script stops we must make sure to clean up after us.
-
- """
- self.obj.cmdset.delete(cmdsetexamples.LidClosedCmdSet)
-
-
-
[docs]classOpenLidState(DefaultScript):
- """
- This manages the cmdset for the "open" button state. This will add
- the RedButtonOpen
- """
-
-
[docs]defat_script_creation(self):
- "Called when script first created."
- self.key="open_lid_script"
- self.desc="Script that manages the opened-state cmdsets for red button."
- self.persistent=True
-
-
[docs]defat_start(self):
- """
- This is called once every server restart, so we want to add the
- (memory-resident) cmdset to the object here. is_valid is
- automatically checked, so we don't need to worry about
- adding the cmdset to a closed lid-button.
- """
- self.obj.cmdset.add(cmdsetexamples.LidOpenCmdSet)
-
-
[docs]defis_valid(self):
- """
- The script is only valid while the lid is open.
- self.obj is the red_button on which this script is defined.
- """
- returnself.obj.db.lid_open
-
-
[docs]defat_stop(self):
- """
- When the script stops (like if the lid is closed again)
- we must make sure to clean up after us.
- """
- self.obj.cmdset.delete(cmdsetexamples.LidOpenCmdSet)
-
-
-
[docs]classBlindedState(DefaultScript):
- """
- This is a timed state.
-
- This adds a (very limited) cmdset TO THE ACCOUNT, during a certain time,
- after which the script will close and all functions are
- restored. It's up to the function starting the script to actually
- set it on the right account object.
- """
-
-
[docs]defat_script_creation(self):
- """
- We set up the script here.
- """
- self.key="temporary_blinder"
- self.desc="Temporarily blinds the account for a little while."
- self.interval=20# seconds
- self.start_delay=True# we don't want it to stop until after 20s.
- self.repeats=1# this will go away after interval seconds.
- self.persistent=False# we will ditch this if server goes down
-
-
[docs]defat_start(self):
- """
- We want to add the cmdset to the linked object.
-
- Note that the RedButtonBlind cmdset is defined to completly
- replace the other cmdsets on the stack while it is active
- (this means that while blinded, only operations in this cmdset
- will be possible for the account to perform). It is however
- not persistent, so should there be a bug in it, we just need
- to restart the server to clear out of it during development.
- """
- self.obj.cmdset.add(cmdsetexamples.BlindCmdSet)
-
-
[docs]defat_stop(self):
- """
- It's important that we clear out that blinded cmdset
- when we are done!
- """
- self.obj.msg("You blink feverishly as your eyesight slowly returns.")
- self.obj.location.msg_contents(
- "%s seems to be recovering their eyesight."%self.obj.name,exclude=self.obj
- )
- self.obj.cmdset.delete()# this will clear the latest added cmdset,
- # (which is the blinded one).
-
-
-#
-# Timer/Event-like Scripts
-#
-# Scripts can also work like timers, or "events". Below we
-# define three such timed events that makes the button a little
-# more "alive" - one that makes the button blink menacingly, another
-# that makes the lid covering the button slide back after a while.
-#
-
-
-
[docs]classCloseLidEvent(DefaultScript):
- """
- This event closes the glass lid over the button
- some time after it was opened. It's a one-off
- script that should be started/created when the
- lid is opened.
- """
-
-
[docs]defat_script_creation(self):
- """
- Called when script object is first created. Sets things up.
- We want to have a lid on the button that the user can pull
- aside in order to make the button 'pressable'. But after a set
- time that lid should auto-close again, making the button safe
- from pressing (and deleting this command).
- """
- self.key="lid_closer"
- self.desc="Closes lid on a red buttons"
- self.interval=20# seconds
- self.start_delay=True# we want to pospone the launch.
- self.repeats=1# we only close the lid once
- self.persistent=True# even if the server crashes in those 20 seconds,
- # the lid will still close once the game restarts.
-
-
[docs]defis_valid(self):
- """
- This script can only operate if the lid is open; if it
- is already closed, the script is clearly invalid.
-
- Note that we are here relying on an self.obj being
- defined (and being a RedButton object) - this we should be able to
- expect since this type of script is always tied to one individual
- red button object and not having it would be an error.
- """
- returnself.obj.db.lid_open
-
-
[docs]defat_repeat(self):
- """
- Called after self.interval seconds. It closes the lid. Before this method is
- called, self.is_valid() is automatically checked, so there is no need to
- check this manually.
- """
- self.obj.close_lid()
-
-
-
[docs]classBlinkButtonEvent(DefaultScript):
- """
- This timed script lets the button flash at regular intervals.
- """
-
-
[docs]defat_script_creation(self):
- """
- Sets things up. We want the button's lamp to blink at
- regular intervals, unless it's broken (can happen
- if you try to smash the glass, say).
- """
- self.key="blink_button"
- self.desc="Blinks red buttons"
- self.interval=35# seconds
- self.start_delay=False# blink right away
- self.persistent=True# keep blinking also after server reboot
-
-
[docs]defis_valid(self):
- """
- Button will keep blinking unless it is broken.
- """
- returnself.obj.db.lamp_works
-
-
[docs]defat_repeat(self):
- """
- Called every self.interval seconds. Makes the lamp in
- the button blink.
- """
- self.obj.blink()
-
-
-
[docs]classDeactivateButtonEvent(DefaultScript):
- """
- This deactivates the button for a short while (it won't blink, won't
- close its lid etc). It is meant to be called when the button is pushed
- and run as long as the blinded effect lasts. We cannot put these methods
- in the AddBlindedCmdSet script since that script is defined on the *account*
- whereas this one must be defined on the *button*.
- """
-
-
[docs]defat_script_creation(self):
- """
- Sets things up.
- """
- self.key="deactivate_button"
- self.desc="Deactivate red button temporarily"
- self.interval=21# seconds
- self.start_delay=True# wait with the first repeat for self.interval seconds.
- self.persistent=True
- self.repeats=1# only do this once
-
-
[docs]defat_start(self):
- """
- Deactivate the button. Observe that this method is always
- called directly, regardless of the value of self.start_delay
- (that just controls when at_repeat() is called)
- """
- # closing the lid will also add the ClosedState script
- self.obj.close_lid()
- # lock the lid so other accounts can't access it until the
- # first one's effect has worn off.
- self.obj.db.lid_locked=True
- # breaking the lamp also sets a correct desc
- self.obj.break_lamp(feedback=False)
-
-
[docs]defat_repeat(self):
- """
- When this is called, reset the functionality of the button.
- """
- # restore button's desc.
-
- self.obj.db.lamp_works=True
- desc="This is a large red button, inviting yet evil-looking. "
- desc+="Its glass cover is closed, protecting it."
- self.db.desc=desc
- # re-activate the blink button event.
- self.obj.scripts.add(BlinkButtonEvent)
- # unlock the lid
- self.obj.db.lid_locked=False
- self.obj.scripts.validate()
-
-
-
\ No newline at end of file
diff --git a/docs/0.9.5/_modules/evennia/contrib/tutorial_examples/tests.html b/docs/0.9.5/_modules/evennia/contrib/tutorial_examples/tests.html
index 92fd98cdf1..add0bf97c6 100644
--- a/docs/0.9.5/_modules/evennia/contrib/tutorial_examples/tests.html
+++ b/docs/0.9.5/_modules/evennia/contrib/tutorial_examples/tests.html
@@ -147,7 +147,6 @@
-
diff --git a/docs/0.9.5/_modules/evennia/contrib/tutorial_world/intro_menu.html b/docs/0.9.5/_modules/evennia/contrib/tutorial_world/intro_menu.html
index 791970ed09..04e990a98a 100644
--- a/docs/0.9.5/_modules/evennia/contrib/tutorial_world/intro_menu.html
+++ b/docs/0.9.5/_modules/evennia/contrib/tutorial_world/intro_menu.html
@@ -473,8 +473,9 @@
and the main window.- Use |y<Return>|n (or click the arrow on the right) to send your input.
-- Use |yShift + <up/down-arrow>|n to step back and forth in your command-history.
-- Use |yShift + <Return>|n to add a new line to your input without sending.
+- Use |yCtrl + <up/down-arrow>|n to step back and forth in your command-history.
+- Use |yCtrl + <Return>|n to add a new line to your input without sending.
+(Cmd instead of Ctrl-key on Macs)There is also some |wextra|n info to learn about customizing the webclient.
@@ -735,7 +736,7 @@
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://www.evennia.com/docs/latest
+ |yhttps://www.evennia.com/docs/latest|n- You can start by trying to build some stuff by following the |wBuilder quick-start|n:
@@ -857,7 +858,6 @@
-
diff --git a/docs/0.9.5/_modules/evennia/contrib/tutorial_world/mob.html b/docs/0.9.5/_modules/evennia/contrib/tutorial_world/mob.html
index df7851cb77..a56e5de2b8 100644
--- a/docs/0.9.5/_modules/evennia/contrib/tutorial_world/mob.html
+++ b/docs/0.9.5/_modules/evennia/contrib/tutorial_world/mob.html
@@ -110,7 +110,7 @@
stationary (idling) until attacked. aggressive: if set, will attack Characters in the same room using whatever Weapon it
- carries (see tutorial_world.objects.Weapon).
+ carries (see tutorial_world.objects.TutorialWeapon). if unset, the mob will never engage in combat no matter what. hunting: if set, the mob will pursue enemies trying
@@ -209,9 +209,9 @@
be "ticked". Args:
- interval (int): The number of seconds
+ interval (int or None): The number of seconds between ticks
- hook_key (str): The name of the method
+ hook_key (str or None): The name of the method (on this mob) to call every interval seconds. stop (bool, optional): Just stop the
@@ -413,16 +413,11 @@
return# we use the same attack commands as defined in
- # tutorial_world.objects.Weapon, assuming that
+ # tutorial_world.objects.TutorialWeapon, assuming that# the mob is given a Weapon to attack with.attack_cmd=random.choice(("thrust","pierce","stab","slash","chop"))self.execute_cmd("%s%s"%(attack_cmd,target))
- iftarget.db.healthisNone:
- # This is not an attackable target
- logger.log_err(f"{self.key} found {target} had an `health` attribute of `None`.")
- return
-
# analyze the current stateiftarget.db.health<=0:# we reduced the target to <= 0 health. Move them to the
@@ -517,7 +512,6 @@
-
diff --git a/docs/0.9.5/_modules/evennia/contrib/tutorial_world/objects.html b/docs/0.9.5/_modules/evennia/contrib/tutorial_world/objects.html
index 67dbabe55b..9574e1aa7e 100644
--- a/docs/0.9.5/_modules/evennia/contrib/tutorial_world/objects.html
+++ b/docs/0.9.5/_modules/evennia/contrib/tutorial_world/objects.html
@@ -55,8 +55,8 @@
ObeliskLightSourceCrumblingWall
-Weapon
-WeaponRack
+TutorialWeapon
+TutorialWeaponRack"""
@@ -832,7 +832,7 @@
# -------------------------------------------------------------#
-# Weapon - object type
+# TutorialWeapon - object type## A weapon is necessary in order to fight in the tutorial# world. A weapon (which here is assumed to be a bladed
@@ -972,7 +972,7 @@
self.add(CmdAttack())
-
[docs]defat_object_creation(self):"""Called at first creation of the object"""super().at_object_creation()self.db.hit=0.4# hit chance
@@ -993,7 +993,7 @@
self.db.magic=Falseself.cmdset.add_default(CmdSetWeapon,permanent=True)
[docs]defreset(self):""" When reset, the weapon is simply deleted, unless it has a place to return to.
@@ -1023,7 +1023,7 @@
WEAPON_PROTOTYPES={"weapon":{
- "typeclass":"evennia.contrib.tutorial_world.objects.Weapon",
+ "typeclass":"evennia.contrib.tutorial_world.objects.TutorialWeapon","key":"Weapon","hit":0.2,"parry":0.2,
@@ -1168,7 +1168,7 @@
self.add(CmdGetWeapon())
[docs]classTutorialWeaponRack(TutorialObject):""" This object represents a weapon store. When people use the "get weapon" command on this rack, it will produce one
@@ -1185,7 +1185,7 @@
"""
-
[docs]defat_object_creation(self):""" called at creation """
@@ -1199,13 +1199,12 @@
|wstab/thrust/pierce <target>|n - poke at the enemy. More damage but harder to hit. |wslash/chop/bash <target>|n - swipe at the enemy. Less damage but easier to hit. |wdefend/parry|n - protect yourself and make yourself harder to hit.)
- """
- ).strip()
+ """).strip()self.db.no_more_weapons_msg="you find nothing else of use."self.db.available_weapons=["knife","dagger","sword","club"]
[docs]defproduce_weapon(self,caller):""" This will produce a new weapon from the rack, assuming the caller hasn't already gotten one. When
@@ -1261,7 +1260,6 @@
diff --git a/docs/0.9.5/_modules/evennia/contrib/tutorial_world/rooms.html b/docs/0.9.5/_modules/evennia/contrib/tutorial_world/rooms.html
index 0fc3e2d28d..2d5d3f7640 100644
--- a/docs/0.9.5/_modules/evennia/contrib/tutorial_world/rooms.html
+++ b/docs/0.9.5/_modules/evennia/contrib/tutorial_world/rooms.html
@@ -119,7 +119,6 @@
helptext+="\n\n (Write 'give up' if you want to abandon your quest.)"caller.msg(helptext)
-
# for the @detail command we inherit from MuxCommand, since# we want to make use of MuxCommand's pre-parsing of '=' in the# argument.
@@ -244,26 +243,22 @@
looking_at_obj.at_desc(looker=caller)return
-
[docs]classCmdTutorialGiveUp(default_cmds.MuxCommand):""" Give up the tutorial-world quest and return to Limbo, the start room of the server. """
-
key="give up"
- aliases=["abort"]
+ aliases=['abort']
[docs]deffunc(self):outro_room=OutroRoom.objects.all()ifoutro_room:outro_room=outro_room[0]else:
- self.caller.msg(
- "That didn't work (seems like a bug). "
- "Try to use the |wteleport|n command instead."
- )
+ self.caller.msg("That didn't work (seems like a bug). "
+ "Try to use the |wteleport|n command instead.")returnself.caller.move_to(outro_room)
[docs]deffunc(self):from.intro_menuimportinit_menu
-
# quell also superusersifself.caller.account:self.caller.account.execute_cmd("quell")
@@ -501,7 +493,6 @@
character.account.execute_cmd("quell")character.msg("(Auto-quelling while in tutorial-world)")
[docs]classHelpEntry(SharedMemoryModel):""" A generic help entry.
@@ -114,11 +114,11 @@
db_tags=models.ManyToManyField(Tag,blank=True,
- help_text="tags on this object. Tags are simple string markers to identify, group and alias objects.",
+ help_text="tags on this object. Tags are simple string markers to "
+ "identify, group and alias objects.",)
- # (deprecated, only here to allow MUX helpfile load (don't use otherwise)).
- # TODO: remove this when not needed anymore.
- db_staff_only=models.BooleanField(default=False)
+ # Creation date. This is not changed once the object is created.
+ db_date_created=models.DateTimeField("creation date",editable=False,auto_now=True)# Database managerobjects=HelpEntryManager()
@@ -126,19 +126,19 @@
# lazy-loaded handlers
-
[docs]defaccess(self,accessing_obj,access_type="read",default=True):"""
- Determines if another object has permission to access.
- accessing_obj - object trying to access this one
- access_type - type of access sought
- default - what to return if no lock of access_type was found
+ Determines if another object has permission to access this help entry.
+
+ Accesses used by default:
+ 'read' - read the help entry itself.
+ 'view' - see help entry in help index.
+
+ Args:
+ accessing_obj (Object or Account): Entity trying to access this one.
+ access_type (str): type of access sought.
+ default (bool): What to return if no lock of `access_type` was found.
+
"""returnself.locks.check(accessing_obj,access_type=access_type,default=default)
+ @property
+ defsearch_index_entry(self):
+ """
+ Property for easily retaining a search index entry for this object.
+ """
+ return{
+ "key":self.db_key,
+ "aliases":" ".join(self.aliases.all()),
+ "category":self.db_help_category,
+ "text":self.db_entrytext,
+ "tags":" ".join(str(tag)fortaginself.tags.all()),
+ }
+
## Web/Django methods#
-
[docs]defweb_get_admin_url(self):""" Returns the URI path for the Django Admin page for this object.
@@ -183,7 +202,7 @@
"admin:%s_%s_change"%(content_type.app_label,content_type.model),args=(self.id,))
[docs]@classmethoddefweb_get_create_url(cls):""" Returns the URI path for a View that allows users to create new
@@ -196,7 +215,9 @@
a named view of 'character-create' would be referenced by this method. ex.
- url(r'characters/create/', ChargenView.as_view(), name='character-create')
+ ::
+
+ url(r'characters/create/', ChargenView.as_view(), name='character-create') If no View has been created and defined in urls.py, returns an HTML anchor.
@@ -211,10 +232,10 @@
"""try:returnreverse("%s-create"%slugify(cls._meta.verbose_name))
- except:
+ exceptException:return"#"
[docs]defweb_get_detail_url(self):""" Returns the URI path for a View that allows users to view details for this object.
@@ -226,8 +247,9 @@
a named view of 'character-detail' would be referenced by this method. ex.
- url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/$',
- CharDetailView.as_view(), name='character-detail')
+ ::
+ url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/$',
+ CharDetailView.as_view(), name='character-detail') If no View has been created and defined in urls.py, returns an HTML anchor.
@@ -245,11 +267,10 @@
"%s-detail"%slugify(self._meta.verbose_name),kwargs={"category":slugify(self.db_help_category),"topic":slugify(self.db_key)},)
- exceptExceptionase:
- print(e)
+ exceptException:return"#"
[docs]defweb_get_update_url(self):""" Returns the URI path for a View that allows users to update this object.
@@ -261,8 +282,10 @@
a named view of 'character-update' would be referenced by this method. ex.
- url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/change/$',
- CharUpdateView.as_view(), name='character-update')
+ ::
+
+ url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/change/$',
+ CharUpdateView.as_view(), name='character-update') If no View has been created and defined in urls.py, returns an HTML anchor.
@@ -280,10 +303,10 @@
"%s-update"%slugify(self._meta.verbose_name),kwargs={"category":slugify(self.db_help_category),"topic":slugify(self.db_key)},)
- except:
+ exceptException:return"#"
[docs]defweb_get_delete_url(self):""" Returns the URI path for a View that allows users to delete this object.
@@ -294,8 +317,10 @@
a named view of 'character-detail' would be referenced by this method. ex.
- url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/delete/$',
- CharDeleteView.as_view(), name='character-delete')
+ ::
+
+ url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/delete/$',
+ CharDeleteView.as_view(), name='character-delete') If no View has been created and defined in urls.py, returns an HTML anchor.
@@ -313,7 +338,7 @@
"%s-delete"%slugify(self._meta.verbose_name),kwargs={"category":slugify(self.db_help_category),"topic":slugify(self.db_key)},)
- except:
+ exceptException:return"#"
diff --git a/docs/0.9.5/_modules/evennia/locks/lockfuncs.html b/docs/0.9.5/_modules/evennia/locks/lockfuncs.html
index a535b34acb..af555513a7 100644
--- a/docs/0.9.5/_modules/evennia/locks/lockfuncs.html
+++ b/docs/0.9.5/_modules/evennia/locks/lockfuncs.html
@@ -52,81 +52,6 @@
with a lock variable/field, so be careful to not expecta certain object type.
-
-**Appendix: MUX locks**
-
-Below is a list nicked from the MUX help file on the locks available
-in standard MUX. Most of these are not relevant to core Evennia since
-locks in Evennia are considerably more flexible and can be implemented
-on an individual command/typeclass basis rather than as globally
-available like the MUX ones. So many of these are not available in
-basic Evennia, but could all be implemented easily if needed for the
-individual game.
-
-```
-MUX Name: Affects: Effect:
-----------------------------------------------------------------------
-DefaultLock: Exits: controls who may traverse the exit to
- its destination.
- Evennia: "traverse:<lockfunc()>"
- Rooms: controls whether the account sees the
- SUCC or FAIL message for the room
- following the room description when
- looking at the room.
- Evennia: Custom typeclass
- Accounts/Things: controls who may GET the object.
- Evennia: "get:<lockfunc()"
- EnterLock: Accounts/Things: controls who may ENTER the object
- Evennia:
- GetFromLock: All but Exits: controls who may gets things from a
- given location.
- Evennia:
- GiveLock: Accounts/Things: controls who may give the object.
- Evennia:
- LeaveLock: Accounts/Things: controls who may LEAVE the object.
- Evennia:
- LinkLock: All but Exits: controls who may link to the location
- if the location is LINK_OK (for linking
- exits or setting drop-tos) or ABODE (for
- setting homes)
- Evennia:
- MailLock: Accounts: controls who may @mail the account.
- Evennia:
- OpenLock: All but Exits: controls who may open an exit.
- Evennia:
- PageLock: Accounts: controls who may page the account.
- Evennia: "send:<lockfunc()>"
- ParentLock: All: controls who may make @parent links to
- the object.
- Evennia: Typeclasses and
- "puppet:<lockstring()>"
- ReceiveLock: Accounts/Things: controls who may give things to the
- object.
- Evennia:
- SpeechLock: All but Exits: controls who may speak in that location
- Evennia:
- TeloutLock: All but Exits: controls who may teleport out of the
- location.
- Evennia:
- TportLock: Rooms/Things: controls who may teleport there
- Evennia:
- UseLock: All but Exits: controls who may USE the object, GIVE
- the object money and have the PAY
- attributes run, have their messages
- heard and possibly acted on by LISTEN
- and AxHEAR, and invoke $-commands
- stored on the object.
- Evennia: Commands and Cmdsets.
- DropLock: All but rooms: controls who may drop that object.
- Evennia:
- VisibleLock: All: Controls object visibility when the
- object is not dark and the looker
- passes the lock. In DARK locations, the
- object must also be set LIGHT and the
- viewer must pass the VisibleLock.
- Evennia: Room typeclass with
- Dark/light script
-```"""
@@ -153,16 +78,21 @@
[docs]defself(accessing_obj,accessed_obj,*args,**kwargs):""" Check if accessing_obj is the same as accessed_obj
@@ -580,8 +514,6 @@
Only true if accessed_obj has the specified tag and optional category. """
- ifhasattr(accessed_obj,"obj"):
- accessed_obj=accessed_obj.objtagkey=args[0]ifargselseNonecategory=args[1]iflen(args)>1elseNonereturnbool(accessed_obj.tags.get(tagkey,category=category))
@@ -613,9 +545,6 @@
in your inventory will also pass the lock). """
- ifhasattr(accessed_obj,"obj"):
- accessed_obj=accessed_obj.obj
-
def_recursive_inside(obj,accessed_obj,lvl=1):ifobj.location:ifobj.location==accessed_obj:
@@ -690,17 +619,6 @@
returnFalse
-
[docs]defsuperuser(*args,**kwargs):
- """
- Only accepts an accesing_obj that is superuser (e.g. user #1)
-
- Since a superuser would not ever reach this check (superusers
- bypass the lock entirely), any user who gets this far cannot be a
- superuser, hence we just return False. :)
- """
- returnFalse
-
-
[docs]defhas_account(accessing_obj,accessed_obj,*args,**kwargs):""" Only returns true if accessing_obj has_account is true, that is,
@@ -779,7 +697,6 @@
[docs]classLockHandler:""" This handler should be attached to all objects implementing permission checks, under the property 'lockhandler'.
@@ -274,7 +276,8 @@
funcname,rest=(part.strip().strip(")")forpartinfuncstring.split("(",1))func=_LOCKFUNCS.get(funcname,None)ifnotcallable(func):
- elist.append(_("Lock: lock-function '%s' is not available.")%funcstring)
+ elist.append(_("Lock: lock-function '{lockfunc}' is not available.").format(
+ lockfunc=funcstring))continueargs=list(arg.strip()forarginrest.split(",")ifargand"="notinarg)kwargs=dict(
@@ -301,16 +304,13 @@
continueifaccess_typeinlocks:duplicates+=1
- wlist.append(
- _(
- "LockHandler on %(obj)s: access type '%(access_type)s' changed from '%(source)s' to '%(goal)s' "
- %{
- "obj":self.obj,
- "access_type":access_type,
- "source":locks[access_type][2],
- "goal":raw_lockstring,
- }
- )
+ wlist.append(_(
+ "LockHandler on {obj}: access type '{access_type}' "
+ "changed from '{source}' to '{goal}' ".format(
+ obj=self.obj,
+ access_type=access_type,
+ source=locks[access_type][2],
+ goal=raw_lockstring)))locks[access_type]=(evalstring,tuple(lock_funcs),raw_lockstring)ifwlistandWARNING_LOG:
@@ -325,12 +325,14 @@
def_cache_locks(self,storage_lockstring):""" Store data
+
"""self.locks=self._parse_lockstring(storage_lockstring)def_save_locks(self):""" Store locks to obj
+
"""self.obj.lock_storage=";".join([tup[2]fortupinself.locks.values()])
@@ -734,6 +736,28 @@
access_type=access_type,)
+defcheck_perm(obj,permission,no_superuser_bypass=False):
+ """
+ Shortcut for checking if an object has the given `permission`. If the
+ permission is in `settings.PERMISSION_HIERARCHY`, the check passes
+ if the object has this permission or higher.
+
+ This is equivalent to calling the perm() lockfunc, but without needing
+ an accessed object.
+
+ Args:
+ obj (Object, Account): The object to check access. If this has a linked
+ Account, the account is checked instead (same rules as per perm()).
+ permission (str): The permission string to check.
+ no_superuser_bypass (bool, optional): If unset, the superuser
+ will always pass this check.
+
+ """
+ fromevennia.locks.lockfuncsimportperm
+ ifnotno_superuser_bypassandobj.is_superuser:
+ returnTrue
+ returnperm(obj,None,permission)
+
defvalidate_lockstring(lockstring):"""
@@ -833,7 +857,6 @@
-#
-# This sets up how models are displayed
-# in the web admin interface.
-#
-fromdjangoimportforms
-fromdjango.confimportsettings
-fromdjango.contribimportadmin
-fromevennia.typeclasses.adminimportAttributeInline,TagInline
-fromevennia.objects.modelsimportObjectDB
-fromdjango.contrib.admin.utilsimportflatten_fieldsets
-fromdjango.utils.translationimportgettextas_
-
-
-
-
- db_key=forms.CharField(
- label="Name/Key",
- widget=forms.TextInput(attrs={"size":"78"}),
- help_text="Main identifier, like 'apple', 'strong guy', 'Elizabeth' etc. "
- "If creating a Character, check so the name is unique among characters!",
- )
- db_typeclass_path=forms.CharField(
- label="Typeclass",
- initial=settings.BASE_OBJECT_TYPECLASS,
- widget=forms.TextInput(attrs={"size":"78"}),
- help_text="This defines what 'type' of entity this is. This variable holds a "
- "Python path to a module with a valid Evennia Typeclass. If you are "
- "creating a Character you should use the typeclass defined by "
- "settings.BASE_CHARACTER_TYPECLASS or one derived from that.",
- )
- db_cmdset_storage=forms.CharField(
- label="CmdSet",
- initial="",
- required=False,
- widget=forms.TextInput(attrs={"size":"78"}),
- help_text="Most non-character objects don't need a cmdset"
- " and can leave this field blank.",
- )
- raw_id_fields=("db_destination","db_location","db_home")
-
-
-
[docs]classObjectEditForm(ObjectCreateForm):
- """
- Form used for editing. Extends the create one with more fields
-
- """
-
-
-
- db_lock_storage=forms.CharField(
- label="Locks",
- required=False,
- widget=forms.Textarea(attrs={"cols":"100","rows":"2"}),
- help_text="In-game lock definition string. If not given, defaults will be used. "
- "This string should be on the form "
- "<i>type:lockfunction(args);type2:lockfunction2(args);...",
- )
[docs]defget_form(self,request,obj=None,**kwargs):
- """
- Use special form during creation.
-
- Args:
- request (Request): Incoming request.
- obj (Object, optional): Database object.
-
- """
- defaults={}
- ifobjisNone:
- defaults.update(
- {"form":self.add_form,"fields":flatten_fieldsets(self.add_fieldsets)}
- )
- defaults.update(kwargs)
- returnsuper().get_form(request,obj,**defaults)
-
-
[docs]defsave_model(self,request,obj,form,change):
- """
- Model-save hook.
-
- Args:
- request (Request): Incoming request.
- obj (Object): Database object.
- form (Form): Form instance.
- change (bool): If this is a change or a new object.
-
- """
- obj.save()
- ifnotchange:
- # adding a new object
- # have to call init with typeclass passed to it
- obj.set_class_from_typeclass(typeclass_path=obj.db_typeclass_path)
- obj.basetype_setup()
- obj.basetype_posthook_setup()
- obj.at_object_creation()
- obj.at_init()
-
-
-
\ No newline at end of file
diff --git a/docs/0.9.5/_modules/evennia/objects/manager.html b/docs/0.9.5/_modules/evennia/objects/manager.html
index 873baddf8f..f40d3bb5c3 100644
--- a/docs/0.9.5/_modules/evennia/objects/manager.html
+++ b/docs/0.9.5/_modules/evennia/objects/manager.html
@@ -43,7 +43,6 @@
Custom manager for Objects."""importre
-fromitertoolsimportchainfromdjango.db.modelsimportQfromdjango.confimportsettingsfromdjango.db.models.fieldsimportexceptions
@@ -195,7 +194,8 @@
Args: attribute_name (str): Attribute key to search for.
- attribute_value (any): Attribute value to search for. This can also be database objects.
+ attribute_value (any): Attribute value to search for. This can also be database
+ objects. candidates (list, optional): Candidate objects to limit search to. typeclasses (list, optional): Python pats to restrict matches with.
@@ -632,6 +632,7 @@
""" Clear the db_sessid field of all objects having also the db_account field set.
+
"""self.filter(db_sessid__isnull=False).update(db_sessid=None)
@@ -675,7 +676,6 @@
diff --git a/docs/0.9.5/_modules/evennia/objects/models.html b/docs/0.9.5/_modules/evennia/objects/models.html
index 3c624913c7..931679cee2 100644
--- a/docs/0.9.5/_modules/evennia/objects/models.html
+++ b/docs/0.9.5/_modules/evennia/objects/models.html
@@ -54,6 +54,7 @@
the database object. Like everything else, they can be accessedtransparently through the decorating TypeClass."""
+fromcollectionsimportdefaultdictfromdjango.confimportsettingsfromdjango.dbimportmodelsfromdjango.core.exceptionsimportObjectDoesNotExist
@@ -65,7 +66,7 @@
fromevennia.utils.utilsimportmake_iter,dbref,lazy_property
-
[docs]classContentsHandler:""" Handles and caches the contents of an object to avoid excessive lookups (this is done very often due to cmdhandler needing to look
@@ -73,7 +74,7 @@
of the ObjectDB. """
-
[docs]defload(self):
+ """
+ Retrieves all objects from database. Used for initializing.
+
+ Returns:
+ Objects (list of ObjectDB)
+ """
+ returnlist(self.obj.locations_set.all())
[docs]defget(self,exclude=None,content_type=None):""" Return the contents of the cache. Args: exclude (Object or list of Object): object(s) to ignore
+ content_type (str or None): Filter list by a content-type. If None, don't filter. Returns: objects (list): the Objects inside this location """
- ifexclude:
- pks=[pkforpkinself._pkcacheifpknotin[excl.pkforexclinmake_iter(exclude)]]
+ ifcontent_typeisnotNone:
+ pks=self._typecache[content_type]else:pks=self._pkcache
+ ifexclude:
+ pks=pks-{excl.pkforexclinmake_iter(exclude)}try:return[self._idcache[pk]forpkinpks]exceptKeyError:
@@ -120,12 +136,11 @@
try:return[self._idcache[pk]forpkinpks]exceptKeyError:
- # this means the central instance_cache was totally flushed.
- # Re-fetching from database will rebuild the necessary parts of the cache
- # for next fetch.
- returnlist(ObjectDB.objects.filter(db_location=self.obj))
+ # this means an actual failure of caching. Return real database match.
+ logger.log_err("contents cache failed for %s."%self.obj.key)
+ returnself.load()
[docs]classObjectDB(TypedObject):""" All objects in the game use the ObjectDB model to store data in the database. This is handled transparently through
@@ -280,7 +301,7 @@
__defaultclasspath__="evennia.objects.objects.DefaultObject"__applabel__="objects"
-
[docs]defat_db_location_postsave(self,new):""" This is called automatically after the location field was saved, no matter how. It checks for a variable
@@ -398,7 +419,7 @@
)[o.contents_cache.init()foroinself.__dbclass__.get_all_cached_instances()]
[docs]classObjectSessionHandler:"""
- Handles the get/setting of the sessid
- comma-separated integer field
+ Handles the get/setting of the sessid comma-separated integer field
+
"""
[docs]def__init__(self,obj):
@@ -148,7 +149,7 @@
]ifNoneinsessions:# this happens only if our cache has gone out of sync with the SessionHandler.
- self._recache()
+
returnself.get(sessid=sessid)returnsessions
@@ -247,6 +248,9 @@
"""
+ # Used for sorting / filtering in inventories / room contents.
+ _content_types=("object",)
+
# lockstring of newly created objects, for easy overloading.# Will be formatted with the appropriate attributes.lockstring="control:id({account_id}) or perm(Admin);delete:id({account_id}) or perm(Admin)"
@@ -265,7 +269,7 @@
[docs]defcontents_get(self,exclude=None,content_type=None):""" Returns the contents of this object, i.e. all objects that has this object set as its location.
@@ -309,17 +313,18 @@
Args: exclude (Object): Object to exclude from returned contents list
+ content_type (str): A content_type to filter by. None for no
+ filtering. Returns: contents (list): List of contents of this Object. Notes:
- Also available as the `contents` property.
+ Also available as the `contents` property, minus exclusion
+ and filtering. """
- con=self.contents_cache.get(exclude=exclude)
- # print "contents_get:", self, con, id(self), calledby() # DEBUG
- returncon
[docs]defcontents_set(self,*args):"You cannot replace this property"
@@ -335,6 +340,7 @@
""" Returns all exits from this object, i.e. all objects at this location having the property destination != `None`.
+
"""return[exiforexiinself.contentsifexi.destination]
@@ -381,6 +387,7 @@
Returns: singular (str): The singular form to display. plural (str): The determined plural form of the key, including the count.
+
"""plural_category="plural_key"key=kwargs.get("key",self.key)
@@ -415,6 +422,7 @@
nofound_string=None,multimatch_string=None,use_dbref=None,
+ stacked=0,):""" Returns an Object matching a search string/condition
@@ -443,7 +451,9 @@
to search. Note that this is used to query the *contents* of a location and will not match for the location itself - if you want that, don't set this or use `candidates` to specify
- exactly which objects should be searched.
+ exactly which objects should be searched. If this nor candidates are
+ given, candidates will include caller's inventory, current location and
+ all objects in the current location. attribute_name (str): Define which property to search. If set, no key+alias search will be performed. This can be used to search database fields (db_ will be automatically
@@ -471,10 +481,19 @@
will be treated like a normal string. If `None` (default), the ability to query by #dbref is turned on if `self` has the permission 'Builder' and is turned off otherwise.
+ stacked (int, optional): If > 0, multimatches will be analyzed to determine if they
+ only contains identical objects; these are then assumed 'stacked' and no multi-match
+ error will be generated, instead `stacked` number of matches will be returned. If
+ `stacked` is larger than number of matches, returns that number of matches. If
+ the found stack is a mix of objects, return None and handle the multi-match
+ error depending on the value of `quiet`. Returns:
- match (Object, None or list): will return an Object/None if `quiet=False`,
- otherwise it will return a list of 0, 1 or more matches.
+ Object: If finding a match an `quiet=False`
+ None: If not finding a unique match and `quiet=False`.
+ list: With 0, 1 or more matching objects if `quiet=True`
+ list: With 2 or more matching objects if `stacked` is a positive integer and
+ the matched stack has only object-copies. Notes: To find Accounts, use eg. `evennia.account_search`. If
@@ -542,8 +561,29 @@
use_dbref=use_dbref,)
+ nresults=len(results)
+ ifstacked>0andnresults>1:
+ # handle stacks, disable multimatch errors
+ nstack=nresults
+ ifnotexact:
+ # we re-run exact match agains one of the matches to
+ # make sure we were not catching partial matches not belonging
+ # to the stack
+ nstack=len(ObjectDB.objects.get_objs_with_key_or_alias(
+ results[0].key,
+ exact=True,
+ candidates=list(results),
+ typeclasses=[typeclass]iftypeclasselseNone
+ ))
+ ifnstack==nresults:
+ # a valid stack, return multiple results
+ returnlist(results)[:stacked]
+
ifquiet:
+ # don't auto-handle error messagingreturnlist(results)
+
+ # handle error messagesreturn_AT_SEARCH_RESULT(results,self,
@@ -703,6 +743,7 @@
Keyword Args: Keyword arguments will be passed to the function for all objects.
+
"""contents=self.contentsifexclude:
@@ -719,64 +760,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)
[docs]defmove_to(self,
@@ -838,7 +909,7 @@
self.msg("%s%s"%(string,""iferrisNoneelse" (%s)"%err))return
- errtxt=_("Couldn't perform move ('%s'). Contact an admin.")
+ errtxt=_("Couldn't perform move ({err}). Contact an admin.")ifnotemit_to_obj:emit_to_obj=self
@@ -857,10 +928,10 @@
# Before the move, call eventual pre-commands.ifmove_hooks:try:
- ifnotself.at_before_move(destination):
+ ifnotself.at_before_move(destination,**kwargs):returnFalseexceptExceptionaserr:
- logerr(errtxt%"at_before_move()",err)
+ logerr(errtxt.format(err="at_before_move()"),err)returnFalse# Save the old location
@@ -869,9 +940,9 @@
# Call hook on source locationifmove_hooksandsource_location:try:
- source_location.at_object_leave(self,destination)
+ source_location.at_object_leave(self,destination,**kwargs)exceptExceptionaserr:
- logerr(errtxt%"at_object_leave()",err)
+ logerr(errtxt.format(err="at_object_leave()"),err)returnFalseifnotquiet:
@@ -879,14 +950,14 @@
try:self.announce_move_from(destination,**kwargs)exceptExceptionaserr:
- logerr(errtxt%"at_announce_move()",err)
+ logerr(errtxt.format(err="at_announce_move()"),err)returnFalse# Perform movetry:self.location=destinationexceptExceptionaserr:
- logerr(errtxt%"location change",err)
+ logerr(errtxt.format(err="location change"),err)returnFalseifnotquiet:
@@ -894,25 +965,25 @@
try:self.announce_move_to(source_location,**kwargs)exceptExceptionaserr:
- logerr(errtxt%"announce_move_to()",err)
+ logerr(errtxt.format(err="announce_move_to()"),err)returnFalseifmove_hooks:# Perform eventual extra commands on the receiving location# (the object has already arrived at this point)try:
- destination.at_object_receive(self,source_location)
+ destination.at_object_receive(self,source_location,**kwargs)exceptExceptionaserr:
- logerr(errtxt%"at_object_receive()",err)
+ logerr(errtxt.format(err="at_object_receive()"),err)returnFalse# Execute eventual extra commands on this object after moving it# (usually calling 'look')ifmove_hooks:try:
- self.at_after_move(source_location)
+ self.at_after_move(source_location,**kwargs)exceptExceptionaserr:
- logerr(errtxt%"at_after_move",err)
+ logerr(errtxt.format(err="at_after_move"),err)returnFalsereturnTrue
@@ -920,6 +991,7 @@
""" Destroys all of the exits and any exits pointing to this object as a destination.
+
"""forout_exitin[exiforexiinObjectDB.objects.get_contents(self)ifexi.db_destination]:out_exit.delete()
@@ -930,6 +1002,7 @@
""" Moves all objects (accounts/things) to their home location or to default home.
+
"""# Gather up everything that thinks this is its location.default_home_id=int(settings.DEFAULT_HOME.lstrip("#"))
@@ -939,8 +1012,8 @@
# we are deleting default home!default_home=NoneexceptException:
- string=_("Could not find default home '(#%d)'.")
- logger.log_err(string%default_home_id)
+ string=_("Could not find default home '(#{dbid})'.")
+ logger.log_err(string.format(dbid=default_home_id))default_home=Noneforobjinself.contents:
@@ -952,18 +1025,17 @@
# If for some reason it's still None...ifnothome:
- string="Missing default home, '%s(#%d)' "
- string+="now has a null location."obj.location=Noneobj.msg(_("Something went wrong! You are dumped into nowhere. Contact an admin."))
- logger.log_err(string%(obj.name,obj.dbid))
+ logger.log_err("Missing default home - '{name}(#{dbid})' now "
+ "has a null location.".format(name=obj.name,dbid=obj.dbid))returnifobj.has_account:ifhome:string="Your current location has ceased to exist,"
- string+=" moving you to %s(#%d)."
- obj.msg(_(string)%(home.name,home.dbid))
+ string+=" moving you to (#{dbid})."
+ obj.msg(_(string).format(dbid=home.dbid))else:# Famous last words: The account should never see this.string="This place should not exist ... contact an admin."
@@ -1070,8 +1142,8 @@
[docs]defat_object_post_copy(self,new_obj,**kwargs):"""
- Called by DefaultObject.copy(). Meant to be overloaded. In case there's extra data not covered by
- .copy(), this can be used to deal with it.
+ Called by DefaultObject.copy(). Meant to be overloaded. In case there's extra data not
+ covered by .copy(), this can be used to deal with it. Args: new_obj (Object): The new Copy of this object.
@@ -1119,7 +1191,7 @@
self.account=Noneforscriptin_ScriptDB.objects.get_all_scripts_on_obj(self):
- script.stop()
+ script.delete()# Destroy any exits to and from this room, if anyself.clear_exits()
@@ -1512,7 +1584,8 @@
ifnotsource_locationandself.location.has_account:# This was created from nowhere and added to an account's# inventory; it's probably the result of a create command.
- string="You now have %s in your possession."%self.get_display_name(self.location)
+ string=_("You now have {name} in your possession.").format(
+ name=self.get_display_name(self.location))self.location.msg(string)return
@@ -1520,9 +1593,9 @@
ifmsg:string=msgelse:
- string="{object} arrives to {destination} from {origin}."
+ string=_("{object} arrives to {destination} from {origin}.")else:
- string="{object} arrives to {destination}."
+ string=_("{object} arrives to {destination}.")origin=source_locationdestination=self.location
@@ -1705,20 +1778,26 @@
**kwargs (dict): Arbitrary, optional arguments for users overriding the call (unused by default). """
+
+ deffilter_visible(obj_list):
+ # Helper method to determine if objects are visible to the looker.
+ return[objforobjinobj_listifobj!=lookerandobj.access(looker,"view")]
+
ifnotlooker:return""
+
# get and identify all objects
- visible=(conforconinself.contentsifcon!=lookerandcon.access(looker,"view"))
- exits,users,things=[],[],defaultdict(list)
- forconinvisible:
- key=con.get_display_name(looker)
- ifcon.destination:
- exits.append(key)
- elifcon.has_account:
- users.append("|c%s|n"%key)
- else:
- # things can be pluralized
- things[key].append(con)
+ exits_list=filter_visible(self.contents_get(content_type="exit"))
+ users_list=filter_visible(self.contents_get(content_type="character"))
+ things_list=filter_visible(self.contents_get(content_type="object"))
+
+ things=defaultdict(list)
+
+ forthinginthings_list:
+ things[thing.key].append(thing)
+ users=[f"|c{user.key}|n"foruserinusers_list]
+ exits=[ex.keyforexinexits_list]
+
# get description, build stringstring="|c%s|n\n"%self.get_display_name(looker)desc=self.db.desc
@@ -1923,7 +2002,8 @@
a say. This is sent by the whisper command by default. Other verbal commands could use this hook in similar ways.
- receivers (Object or iterable): If set, this is the target or targets for the say/whisper.
+ receivers (Object or iterable): If set, this is the target or targets for the
+ say/whisper. Returns: message (str): The (possibly modified) text to be spoken.
@@ -1954,8 +2034,8 @@
msg_self (bool or str, optional): If boolean True, echo `message` to self. If a string, return that message. If False or unset, don't echo to self. msg_location (str, optional): The message to echo to self's location.
- receivers (Object or iterable, optional): An eventual receiver or receivers of the message
- (by default only used by whispers).
+ receivers (Object or iterable, optional): An eventual receiver or receivers of the
+ message (by default only used by whispers). msg_receivers(str): Specific message to pass to the receiver(s). This will parsed with the {receiver} placeholder replaced with the given receiver. Keyword Args:
@@ -1990,8 +2070,7 @@
msg_type="whisper"msg_self=('{self} whisper to {all_receivers}, "|n{speech}|n"'
- ifmsg_selfisTrue
- elsemsg_self
+ ifmsg_selfisTrueelsemsg_self)msg_receivers=msg_receiversor'{object} whispers: "|n{speech}|n"'msg_location=None
@@ -2078,6 +2157,9 @@
"""
+ # Tuple of types used for indexing inventory contents. Characters generally wouldn't be in
+ # anyone's inventory, but this also governs displays in room contents.
+ _content_types=("character",)# lockstring of newly created rooms, for easy overloading.# Will be formatted with the appropriate attributes.lockstring=(
@@ -2117,6 +2199,13 @@
# If no typeclass supplied, use this classkwargs["typeclass"]=kwargs.pop("typeclass",cls)
+ # Normalize to latin characters and validate, if necessary, the supplied key
+ key=cls.normalize_name(key)
+
+ ifnotcls.validate_name(key):
+ errors.append(_("Invalid character name."))
+ returnobj,errors
+
# Set the supplied key as the name of the intended objectkwargs["key"]=key
@@ -2133,7 +2222,7 @@
# Check to make sure account does not have too many charsifaccount:iflen(account.characters)>=settings.MAX_NR_CHARACTERS:
- errors.append("There are too many characters associated with this account.")
+ errors.append(_("There are too many characters associated with this account."))returnobj,errors# Create the Character
@@ -2149,7 +2238,8 @@
# Add locksifnotlocksandaccount:
- # Allow only the character itself and the creator account to puppet this character (and Developers).
+ # Allow only the character itself and the creator account to puppet this character
+ # (and Developers).locks=cls.lockstring.format(**{"character_id":obj.id,"account_id":account.id})elifnotlocksandnotaccount:locks=cls.lockstring.format(**{"character_id":obj.id,"account_id":-1})
@@ -2158,14 +2248,47 @@
# If no description is set, set a default descriptionifdescriptionornotobj.db.desc:
- obj.db.desc=descriptionifdescriptionelse"This is a character."
+ obj.db.desc=descriptionifdescriptionelse_("This is a character.")exceptExceptionase:
- errors.append("An error occurred while creating this '%s' object."%key)
+ errors.append(f"An error occurred while creating object '{key} object.")logger.log_err(e)returnobj,errors
+
[docs]@classmethod
+ defnormalize_name(cls,name):
+ """
+ Normalize the character name prior to creating. Note that this should be refactored to
+ support i18n for non-latin scripts, but as we (currently) have no bug reports requesting
+ better support of non-latin character sets, requiring character names to be latinified is an
+ acceptable option.
+
+ Args:
+ name (str) : The name of the character
+
+ Returns:
+ latin_name (str) : A valid name.
+ """
+
+ fromevennia.utils.utilsimportlatinify
+
+ latin_name=latinify(name,default="X")
+ returnlatin_name
+
+
[docs]@classmethod
+ defvalidate_name(cls,name):
+ """ Validate the character name prior to creating. Overload this function to add custom validators
+
+ Args:
+ name (str) : The name of the character
+ Returns:
+ valid (bool) : True if character creation should continue; False if it should fail
+
+ """
+
+ returnTrue# Default validator does not perform any operations
+
[docs]defbasetype_setup(self):""" Setup character-specific security.
@@ -2197,6 +2320,7 @@
Args: account (Account): This is the connecting account. session (Session): Session controlling the connection.
+
"""if(self.locationisNone
@@ -2210,7 +2334,8 @@
self.db.prelogout_location=self.location# save location again to be sure.else:account.msg(
- "|r%s has no location and no home is set.|n"%self,session=session
+ _("|r{obj} has no location and no home is set.|n").format(obj=self),
+ session=session)# Note to set home.
[docs]defat_post_puppet(self,**kwargs):
@@ -2228,11 +2353,12 @@
puppeting this Object. """
- self.msg("\nYou become |c%s|n.\n"%self.name)
+ self.msg(_("\nYou become |c{name}|n.\n").format(name=self.key))self.msg((self.at_look(self.location),{"type":"look"}),options=None)defmessage(obj,from_obj):
- obj.msg("%s has entered the game."%self.get_display_name(obj),from_obj=from_obj)
+ obj.msg(_("{name} has entered the game.").format(name=self.get_display_name(obj)),
+ from_obj=from_obj)self.location.for_contents(message,exclude=[self],from_obj=self)
@@ -2255,7 +2381,8 @@
ifself.location:defmessage(obj,from_obj):
- obj.msg("%s has left the game."%self.get_display_name(obj),from_obj=from_obj)
+ obj.msg(_("{name} has left the game.").format(name=self.get_display_name(obj)),
+ from_obj=from_obj)self.location.for_contents(message,exclude=[self],from_obj=self)self.db.prelogout_location=self.location
@@ -2266,6 +2393,7 @@
""" Returns the idle time of the least idle session in seconds. If no sessions are connected it returns nothing.
+
"""idle=[session.cmd_last_visibleforsessioninself.sessions.all()]ifidle:
@@ -2277,6 +2405,7 @@
""" Returns the maximum connection time of all connected sessions in seconds. Returns nothing if there are no sessions.
+
"""conn=[session.conn_timeforsessioninself.sessions.all()]ifconn:
@@ -2294,6 +2423,10 @@
location is always `None`. """
+ # A tuple of strings used for indexing this object inside an inventory.
+ # Generally, a room isn't expected to HAVE a location, but maybe in some games?
+ _content_types=("room",)
+
# lockstring of newly created rooms, for easy overloading.# Will be formatted with the {id} of the creating object.lockstring=(
@@ -2366,7 +2499,7 @@
# If no description is set, set a default descriptionifdescriptionornotobj.db.desc:
- obj.db.desc=descriptionifdescriptionelse"This is a room."
+ obj.db.desc=descriptionifdescriptionelse_("This is a room.")exceptExceptionase:errors.append("An error occurred while creating this '%s' object."%key)
@@ -2428,7 +2561,9 @@
overriding the call (unused by default). Returns:
- A string with identifying information to disambiguate the command, conventionally with a preceding space.
+ A string with identifying information to disambiguate the command, conventionally with a
+ preceding space.
+
"""ifself.obj.destination:return" (exit to %s)"%self.obj.destination.get_display_name(caller)
@@ -2452,6 +2587,7 @@
"""
+ _content_types=("exit",)exit_command=ExitCommandpriority=101
@@ -2569,7 +2705,7 @@
# If no description is set, set a default descriptionifdescriptionornotobj.db.desc:
- obj.db.desc=descriptionifdescriptionelse"This is an exit."
+ obj.db.desc=descriptionifdescriptionelse_("This is an exit.")exceptExceptionase:errors.append("An error occurred while creating this '%s' object."%key)
@@ -2666,7 +2802,7 @@
read for an error string instead. """
- traversing_object.msg("You cannot go there.")
+ traversing_object.msg(_("You cannot go there."))
"""
-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
@@ -76,315 +71,33 @@
"""
-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,
+}
@@ -120,7 +123,6 @@
""" Homogenize the more free-form prototype supported pre Evennia 0.7 into the stricter form.
-
Args: prototype (dict): Prototype. custom_keys (list, optional): Custom keys which should not be interpreted as attrs, beyond
@@ -195,11 +197,30 @@
# to remove a default prototype, override it with an empty dict.# internally we store as (key, desc, locks, tags, prototype_dict)prots=[]
- forvariable_name,protinall_from_module(mod).items():
- ifisinstance(prot,dict):
- if"prototype_key"notinprot:
- prot["prototype_key"]=variable_name.lower()
+
+ prototype_list=variable_from_module(mod,"PROTOTYPE_LIST")
+ ifprototype_list:
+ # found mod.PROTOTYPE_LIST - this should be a list of valid
+ # prototype dicts that must have 'prototype_key' set.
+ forprotinprototype_list:
+ ifnotisinstance(prot,dict):
+ logger.log_err(f"Prototype read from {mod}.PROTOTYPE_LIST "
+ f"is not a dict (skipping): {prot}")
+ continue
+ elif"prototype_key"notinprot:
+ logger.log_err(f"Prototype read from {mod}.PROTOTYPE_LIST "
+ f"is missing the 'prototype_key' (skipping): {prot}")
+ continueprots.append((prot["prototype_key"],homogenize_prototype(prot)))
+ else:
+ # load all global dicts in module as prototypes. If the prototype_key
+ # is not given, the variable name will be used.
+ forvariable_name,protinall_from_module(mod).items():
+ ifisinstance(prot,dict):
+ if"prototype_key"notinprot:
+ prot["prototype_key"]=variable_name.lower()
+ prots.append((prot["prototype_key"],homogenize_prototype(prot)))
+
# assign module path to each prototype_key for easy reference_MODULE_PROTOTYPE_MODULES.update({prototype_key.lower():modforprototype_key,_inprots})# make sure the prototype contains all meta info
@@ -228,6 +249,7 @@
[docs]classDbPrototype(DefaultScript):""" This stores a single prototype, in an Attribute `prototype`.
+
"""
[docs]defat_script_creation(self):
@@ -279,7 +301,7 @@
prototype_key=in_prototype.get("prototype_key")ifnotprototype_key:
- raiseValidationError("Prototype requires a prototype_key")
+ raiseValidationError(_("Prototype requires a prototype_key"))prototype_key=str(prototype_key).lower()
@@ -287,7 +309,8 @@
ifprototype_keyin_MODULE_PROTOTYPES:mod=_MODULE_PROTOTYPE_MODULES.get(prototype_key,"N/A")raisePermissionError(
- "{} is a read-only prototype ""(defined as code in {}).".format(prototype_key,mod)
+ _("{protkey} is a read-only prototype ""(defined as code in {module}).").format(
+ protkey=prototype_key,module=mod))# make sure meta properties are included with defaults
@@ -354,20 +377,23 @@
ifprototype_keyin_MODULE_PROTOTYPES:mod=_MODULE_PROTOTYPE_MODULES.get(prototype_key.lower(),"N/A")raisePermissionError(
- "{} is a read-only prototype ""(defined as code in {}).".format(prototype_key,mod)
+ _("{protkey} is a read-only prototype ""(defined as code in {module}).").format(
+ protkey=prototype_key,module=mod))stored_prototype=DbPrototype.objects.filter(db_key__iexact=prototype_key)ifnotstored_prototype:
- raisePermissionError("Prototype {} was not found.".format(prototype_key))
+ raisePermissionError(_("Prototype {prototype_key} was not found.").format(
+ prototype_key=prototype_key))stored_prototype=stored_prototype[0]ifcaller:ifnotstored_prototype.access(caller,"edit"):raisePermissionError(
- "{} needs explicit 'edit' permissions to "
- "delete prototype {}.".format(caller,prototype_key)
+ _("{caller} needs explicit 'edit' permissions to "
+ "delete prototype {prototype_key}.").format(
+ caller=caller,prototype_key=prototype_key))stored_prototype.delete()returnTrue
@@ -466,7 +492,11 @@
nmodules=len(module_prototypes)ndbprots=db_matches.count()ifnmodules+ndbprots!=1:
- raiseKeyError(f"Found {nmodules+ndbprots} matching prototypes {module_prototypes}.")
+ raiseKeyError(_(
+ "Found {num} matching prototypes {module_prototypes}.").format(
+ num=nmodules+ndbprots,
+ module_prototypes=module_prototypes)
+ )ifreturn_iterators:# trying to get the entire set of prototypes - we must paginate
@@ -496,10 +526,14 @@
Listing 1000+ prototypes can be very slow. So we customize EvMore to display an EvTable per paginated page rather than to try creating an EvTable for the entire dataset and then paginate it.
+
"""
[docs]def__init__(self,caller,*args,session=None,**kwargs):
- """Store some extra properties on the EvMore class"""
+ """
+ Store some extra properties on the EvMore class
+
+ """self.show_non_use=kwargs.pop("show_non_use",False)self.show_non_edit=kwargs.pop("show_non_edit",False)super().__init__(caller,*args,session=session,**kwargs)
@@ -510,6 +544,7 @@
and we must handle these separately since they cannot be paginated in the same way. We will build the prototypes so that the db-prototypes come first (they are likely the most volatile), followed by the mod-prototypes.
+
"""dbprot_query,modprot_list=inp# set the number of entries per page to half the reported height of the screen
@@ -531,6 +566,7 @@
""" The listing is separated in db/mod prototypes, so we need to figure out which one to pick based on the page number. Also, pageno starts from 0.
+
"""dbprot_pages,modprot_list=self._data
@@ -539,15 +575,16 @@
else:# get the correct slice, adjusted for the db-prototypespageno=max(0,pageno-self._npages_db)
- returnmodprot_list[pageno*self.height:pageno*self.height+self.height]
[docs]defpage_formatter(self,page):
- """Input is a queryset page from django.Paginator"""
+ """
+ Input is a queryset page from django.Paginator
+
+ """caller=self._caller# get use-permissions of readonly attributes (edit is always False)
- display_tuples=[]
-
table=EvTable("|wKey|n","|wSpawn/Edit|n",
@@ -616,7 +653,7 @@
dbprot_query,modprot_list=search_prototype(key,tags,return_iterators=True)ifnotdbprot_queryandnotmodprot_list:
- caller.msg("No prototypes found.",session=session)
+ caller.msg(_("No prototypes found."),session=session)returnNone# get specific prototype (one value or exception)
@@ -667,7 +704,7 @@
protkey=protkeyandprotkey.lower()orprototype.get("prototype_key",None)ifstrictandnotbool(protkey):
- _flags["errors"].append("Prototype lacks a `prototype_key`.")
+ _flags["errors"].append(_("Prototype lacks a 'prototype_key'."))protkey="[UNSET]"typeclass=prototype.get("typeclass")
@@ -676,12 +713,13 @@
ifstrictandnot(typeclassorprototype_parent):ifis_prototype_base:_flags["errors"].append(
- "Prototype {} requires `typeclass` ""or 'prototype_parent'.".format(protkey)
+ _("Prototype {protkey} requires `typeclass` ""or 'prototype_parent'.").format(
+ protkey=protkey))else:_flags["warnings"].append(
- "Prototype {} can only be used as a mixin since it lacks "
- "a typeclass or a prototype_parent.".format(protkey)
+ _("Prototype {protkey} can only be used as a mixin since it lacks "
+ "'typeclass' or 'prototype_parent' keys.").format(protkey=protkey))ifstrictandtypeclass:
@@ -689,9 +727,9 @@
class_from_module(typeclass)exceptImportErroraserr:_flags["errors"].append(
- "{}: Prototype {} is based on typeclass {}, which could not be imported!".format(
- err,protkey,typeclass
- )
+ _("{err}: Prototype {protkey} is based on typeclass {typeclass}, "
+ "which could not be imported!").format(
+ err=err,protkey=protkey,typeclass=typeclass))# recursively traverse prototype_parent chain
@@ -699,19 +737,22 @@
forprotstringinmake_iter(prototype_parent):protstring=protstring.lower()ifprotkeyisnotNoneandprotstring==protkey:
- _flags["errors"].append("Prototype {} tries to parent itself.".format(protkey))
+ _flags["errors"].append(_("Prototype {protkey} tries to parent itself.").format(
+ protkey=protkey))protparent=protparents.get(protstring)ifnotprotparent:_flags["errors"].append(
- "Prototype {}'s prototype_parent '{}' was not found.".format(protkey,protstring)
+ _("Prototype {protkey}'s prototype_parent '{parent}' was not found.").format(
+ protkey=protkey,parent=protstring))ifid(prototype)in_flags["visited"]:_flags["errors"].append(
- "{} has infinite nesting of prototypes.".format(protkeyorprototype)
+ _("{protkey} has infinite nesting of prototypes.").format(
+ protkey=protkeyorprototype))if_flags["errors"]:
- raiseRuntimeError("Error: "+"\nError: ".join(_flags["errors"]))
+ raiseRuntimeError(f"{_ERRSTR}: "+f"\n{_ERRSTR}: ".join(_flags["errors"]))_flags["visited"].append(id(prototype))_flags["depth"]+=1validate_prototype(
@@ -726,16 +767,16 @@
# if we get back to the current level without a typeclass it's an error.ifstrictandis_prototype_baseand_flags["depth"]<=0andnot_flags["typeclass"]:_flags["errors"].append(
- "Prototype {} has no `typeclass` defined anywhere in its parent\n "
- "chain. Add `typeclass`, or a `prototype_parent` pointing to a "
- "prototype with a typeclass.".format(protkey)
+ _("Prototype {protkey} has no `typeclass` defined anywhere in its parent\n "
+ "chain. Add `typeclass`, or a `prototype_parent` pointing to a "
+ "prototype with a typeclass.").format(protkey=protkey))if_flags["depth"]<=0:if_flags["errors"]:
- raiseRuntimeError("Error: "+"\nError: ".join(_flags["errors"]))
+ raiseRuntimeError(f"{_ERRSTR}:_"+f"\n{_ERRSTR}: ".join(_flags["errors"]))if_flags["warnings"]:
- raiseRuntimeWarning("Warning: "+"\nWarning: ".join(_flags["warnings"]))
+ raiseRuntimeWarning(f"{_WARNSTR}: "+f"\n{_WARNSTR}: ".join(_flags["warnings"]))# make sure prototype_locks are set to defaultsprototype_locks=[
@@ -752,18 +793,7 @@
prototype["prototype_locks"]=prototype_locks
[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.
@@ -775,45 +805,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
[docs]definit_spawn_value(value,validator=None,caller=None):""" Analyze the prototype value and produce a value useful at the point of spawning.
@@ -964,6 +976,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)
@@ -978,7 +992,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
diff --git a/docs/0.9.5/_modules/evennia/prototypes/spawner.html b/docs/0.9.5/_modules/evennia/prototypes/spawner.html
index 7a5cc8d0da..d1409cbac8 100644
--- a/docs/0.9.5/_modules/evennia/prototypes/spawner.html
+++ b/docs/0.9.5/_modules/evennia/prototypes/spawner.html
@@ -81,8 +81,8 @@
supported are 'edit' and 'use'. prototype_tags(list, optional): List of tags or tuples (tag, category) used to group prototype in listings
- prototype_parent (str, tuple or callable, optional): name (prototype_key) of eventual parent prototype, or
- a list of parents, for multiple left-to-right inheritance.
+ prototype_parent (str, tuple or callable, optional): name (prototype_key) of eventual parent
+ prototype, or a list of parents, for multiple left-to-right inheritance. prototype: Deprecated. Same meaning as 'parent'. typeclass (str or callable, optional): if not set, will use typeclass of parent prototype or use
@@ -179,6 +179,7 @@
importtimefromdjango.confimportsettings
+fromdjango.utils.translationimportgettextas_importevenniafromevennia.objects.modelsimportObjectDB
@@ -396,8 +397,8 @@
This is most useful for displaying. implicit_keep (bool, optional): If set, the resulting diff will assume KEEP unless the new prototype explicitly change them. That is, if a key exists in `prototype1` and
- not in `prototype2`, it will not be REMOVEd but set to KEEP instead. This is particularly
- useful for auto-generated prototypes when updating objects.
+ not in `prototype2`, it will not be REMOVEd but set to KEEP instead. This is
+ particularly useful for auto-generated prototypes when updating objects. Returns: diff (dict): A structure detailing how to convert prototype1 to prototype2. All
@@ -510,8 +511,8 @@
out.extend(_get_all_nested_diff_instructions(val))else:raiseRuntimeError(
- "Diff contains non-dicts that are not on the "
- "form (old, new, inst): {}".format(diffpart)
+ _("Diff contains non-dicts that are not on the "
+ "form (old, new, action_to_take): {diffpart}").format(diffpart))returnout
@@ -648,7 +649,8 @@
return"\n ".join(lineforlineintextsifline)
-
[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.
@@ -665,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.
@@ -716,33 +719,35 @@
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))
@@ -752,8 +757,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,)
@@ -764,7 +769,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":
@@ -877,7 +882,7 @@
# Spawner mechanism
-
[docs]defspawn(*prototypes,caller=None,**kwargs):""" Spawn a number of prototyped objects.
@@ -886,6 +891,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
@@ -951,32 +957,41 @@
"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",settings.DEFAULT_HOME)
- create_kwargs["db_home"]=init_spawn_value(val,value_to_obj)
+ val=prot.pop("home",None)
+ ifval:
+ 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,caller=caller)
+ exceptObjectDB.DoesNotExist:
+ # settings.DEFAULT_HOME not existing is common for unittests
+ passval=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))
+ for(tag,category,*data)inval:
+ tags.append((init_spawn_value(tag,str,caller=caller),category,data[0]
+ ifdataelseNone))prototype_key=prototype.get("prototype_key",None)ifprototype_key:
@@ -984,11 +999,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_"))
@@ -996,8 +1011,9 @@
# the rest are attribute tuples (attrname, value, category, locks)val=make_iter(prot.pop("attrs",[]))attributes=[]
- for(attrname,value,category,locks)inval:
- attributes.append((attrname,init_spawn_value(value),category,locks))
+ for(attrname,value,*rest)inval:
+ attributes.append((attrname,init_spawn_value(value,caller=caller),
+ rest[0]ifrestelseNone,rest[1]iflen(rest)>1elseNone))simple_attributes=[]forkey,valuein(
@@ -1008,7 +1024,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
@@ -1068,7 +1084,6 @@
-#
-# This sets up how models are displayed
-# in the web admin interface.
-#
-fromdjango.confimportsettings
-
-fromevennia.typeclasses.adminimportAttributeInline,TagInline
-
-fromevennia.scripts.modelsimportScriptDB
-fromdjango.contribimportadmin
-
-
-
[docs]defsave_model(self,request,obj,form,change):
- """
- Model-save hook.
-
- Args:
- request (Request): Incoming request.
- obj (Object): Database object.
- form (Form): Form instance.
- change (bool): If this is a change or a new object.
-
- """
- obj.save()
- ifnotchange:
- # adding a new object
- # have to call init with typeclass passed to it
- obj.set_class_from_typeclass(typeclass_path=obj.db_typeclass_path)
-
-
-
\ No newline at end of file
diff --git a/docs/0.9.5/_modules/evennia/scripts/manager.html b/docs/0.9.5/_modules/evennia/scripts/manager.html
index ee6fe1d221..a525c6a5bf 100644
--- a/docs/0.9.5/_modules/evennia/scripts/manager.html
+++ b/docs/0.9.5/_modules/evennia/scripts/manager.html
@@ -145,112 +145,22 @@
scripts=self.get_id(dbref)forscriptinmake_iter(scripts):script.stop()
-
- defremove_non_persistent(self,obj=None):
- """
- This cleans up the script database of all non-persistent
- scripts. It is called every time the server restarts.
-
- Args:
- obj (Object, optional): Only remove non-persistent scripts
- assigned to this object.
-
- """
- ifobj:
- to_stop=self.filter(db_obj=obj,db_persistent=False,db_is_active=True)
- to_delete=self.filter(db_obj=obj,db_persistent=False,db_is_active=False)
- else:
- to_stop=self.filter(db_persistent=False,db_is_active=True)
- to_delete=self.filter(db_persistent=False,db_is_active=False)
- nr_deleted=to_stop.count()+to_delete.count()
- forscriptinto_stop:
- script.stop()
- forscriptinto_delete:script.delete()
- returnnr_deleted
- defvalidate(self,scripts=None,obj=None,key=None,dbref=None,init_mode=None):
+ defupdate_scripts_after_server_start(self):"""
- This will step through the script database and make sure
- all objects run scripts that are still valid in the context
- they are in. This is called by the game engine at regular
- intervals but can also be initiated by player scripts.
-
- Only one of the arguments are supposed to be supplied
- at a time, since they are exclusive to each other.
-
- Args:
- scripts (list, optional): A list of script objects to
- validate.
- obj (Object, optional): Validate only scripts defined on
- this object.
- key (str): Validate only scripts with this key.
- dbref (int): Validate only the single script with this
- particular id.
- init_mode (str, optional): This is used during server
- upstart and can have three values:
- - `None` (no init mode). Called during run.
- - `"reset"` - server reboot. Kill non-persistent scripts
- - `"reload"` - server reload. Keep non-persistent scripts.
- Returns:
- nr_started, nr_stopped (tuple): Statistics on how many objects
- where started and stopped.
-
- Notes:
- This method also makes sure start any scripts it validates
- which should be harmless, since already-active scripts have
- the property 'is_running' set and will be skipped.
+ Update/sync/restart/delete scripts after server shutdown/restart. """
+ forscriptinself.filter(db_is_active=True,db_persistent=False):
+ script._stop_task()
- # we store a variable that tracks if we are calling a
- # validation from within another validation (avoids
- # loops).
+ forscriptinself.filter(db_is_active=True):
+ script._unpause_task(auto_unpause=True)
+ script.at_server_start()
- globalVALIDATE_ITERATION
- ifVALIDATE_ITERATION>0:
- # we are in a nested validation. Exit.
- VALIDATE_ITERATION-=1
- returnNone,None
- VALIDATE_ITERATION+=1
-
- # not in a validation - loop. Validate as normal.
-
- nr_started=0
- nr_stopped=0
-
- ifinit_mode:
- ifinit_mode=="reset":
- # special mode when server starts or object logs in.
- # This deletes all non-persistent scripts from database
- nr_stopped+=self.remove_non_persistent(obj=obj)
- # turn off the activity flag for all remaining scripts
- scripts=self.get_all_scripts()
- forscriptinscripts:
- script.is_active=False
-
- elifnotscripts:
- # normal operation
- ifdbrefandself.dbref(dbref,reqhash=False):
- scripts=self.get_id(dbref)
- elifobj:
- scripts=self.get_all_scripts_on_obj(obj,key=key)
- else:
- scripts=self.get_all_scripts(key=key)
-
- ifnotscripts:
- # no scripts available to validate
- VALIDATE_ITERATION-=1
- returnNone,None
-
- forscriptinscripts:
- ifscript.is_valid():
- nr_started+=script.start(force_restart=init_mode)
- else:
- script.stop()
- nr_stopped+=1
- VALIDATE_ITERATION-=1
- returnnr_started,nr_stopped
+ forscriptinself.filter(db_is_active=False):
+ script.at_server_start()defsearch_script(self,ostring,obj=None,only_timed=False,typeclass=None):"""
@@ -362,7 +272,6 @@
[docs]classScriptDB(TypedObject):""" The Script database representation.
@@ -142,7 +142,7 @@
# how often to run Script (secs). -1 means there is no timerdb_interval=models.IntegerField(
- "interval",default=-1,help_text="how often to repeat script, in seconds. -1 means off."
+ "interval",default=-1,help_text="how often to repeat script, in seconds. <= 0 means off.")# start script right away or wait interval seconds firstdb_start_delay=models.BooleanField(
@@ -151,7 +151,7 @@
# how many times this script is to be repeated, if interval!=0.db_repeats=models.IntegerField("number of repeats",default=0,help_text="0 means off.")# defines if this script should survive a reboot or not
- db_persistent=models.BooleanField("survive server reboot",default=False)
+ db_persistent=models.BooleanField("survive server reboot",default=True)# defines if this script has already been started in this sessiondb_is_active=models.BooleanField("script active",default=False)
@@ -257,7 +257,6 @@
diff --git a/docs/0.9.5/_modules/evennia/scripts/monitorhandler.html b/docs/0.9.5/_modules/evennia/scripts/monitorhandler.html
index d6534ca340..4400da98c8 100644
--- a/docs/0.9.5/_modules/evennia/scripts/monitorhandler.html
+++ b/docs/0.9.5/_modules/evennia/scripts/monitorhandler.html
@@ -68,11 +68,13 @@
""" This is a resource singleton that allows for registering callbacks for when a field or Attribute is updated (saved).
+
"""
[docs]def__init__(self):""" Initialize the handler.
+
"""self.savekey="_monitorhandler_save"self.monitors=defaultdict(lambda:defaultdict(dict))
# alias to delete
@@ -195,22 +193,7 @@
Get all scripts stored in this handler. """
- returnScriptDB.objects.get_all_scripts_on_obj(self.obj)
-
-
[docs]defvalidate(self,init_mode=False):
- """
- Runs a validation on this object's scripts only. This should
- be called regularly to crank the wheels.
-
- Args:
- init_mode (str, optional): - This is used during server
- upstart and can have three values:
- - `False` (no init mode). Called during run.
- - `"reset"` - server reboot. Kill non-persistent scripts
- - `"reload"` - server reload. Keep non-persistent scripts.
-
- """
- ScriptDB.objects.validate(obj=self.obj,init_mode=init_mode)
-
diff --git a/docs/0.9.5/_modules/evennia/scripts/scripts.html b/docs/0.9.5/_modules/evennia/scripts/scripts.html
index 9dfd4ad981..4b4a11ac23 100644
--- a/docs/0.9.5/_modules/evennia/scripts/scripts.html
+++ b/docs/0.9.5/_modules/evennia/scripts/scripts.html
@@ -48,7 +48,6 @@
fromtwisted.internet.deferimportDeferred,maybeDeferredfromtwisted.internet.taskimportLoopingCall
-fromdjango.core.exceptionsimportObjectDoesNotExistfromdjango.utils.translationimportgettextas_fromevennia.typeclasses.modelsimportTypeclassBasefromevennia.scripts.modelsimportScriptDB
@@ -58,21 +57,11 @@
__all__=["DefaultScript","DoNothing","Store"]
-FLUSHING_INSTANCES=False# whether we're in the process of flushing scripts from the cache
-SCRIPT_FLUSH_TIMERS={}# stores timers for scripts that are currently being flushed
-
-
-defrestart_scripts_after_flush():
- """After instances are flushed, validate scripts so they're not dead for a long period of time"""
- globalFLUSHING_INSTANCES
- ScriptDB.objects.validate()
- FLUSHING_INSTANCES=False
-
-
classExtendedLoopingCall(LoopingCall):"""
- LoopingCall that can start at a delay different
- than `self.interval`.
+ Custom child of LoopingCall that can start at a delay different than
+ `self.interval` and self.count=0. This allows it to support pausing
+ by resuming at a later period. """
@@ -90,10 +79,10 @@
interval (int): Repeat interval in seconds. now (bool, optional): Whether to start immediately or after `start_delay` seconds.
- start_delay (int): The number of seconds before starting.
- If None, wait interval seconds. Only valid if `now` is `False`.
- It is used as a way to start with a variable start time
- after a pause.
+ start_delay (int, optional): This only applies is `now=False`. It gives
+ number of seconds to wait before starting. If `None`, use
+ `interval` as this value instead. Internally, this is used as a
+ way to start with a variable start time after a pause. count_start (int): Number of repeats to start at. The count goes up every time the system repeats. This is used to implement something repeating `N` number of times etc.
@@ -172,7 +161,7 @@
of start_delay into account. Returns:
- next (int or None): The time in seconds until the next call. This
+ int or None: The time in seconds until the next call. This takes `start_delay` into account. Returns `None` if the task is not running.
@@ -180,7 +169,7 @@
ifself.runningandself.interval>0:total_runtime=self.clock.seconds()-self.starttimeinterval=self.start_delayorself.interval
- returninterval-(total_runtime%self.interval)
+ returnmax(0,interval-(total_runtime%self.interval))classScriptBase(ScriptDB,metaclass=TypeclassBase):
@@ -188,6 +177,8 @@
Base class for scripts. Don't inherit from this, inherit from the class `DefaultScript` below instead.
+ This handles the timer-component of the Script.
+
"""objects=ScriptManager()
@@ -198,36 +189,176 @@
def__repr__(self):returnstr(self)
- def_start_task(self):
+ defat_idmapper_flush(self):"""
- Start task runner.
+ If we're flushing this object, make sure the LoopingCall is gone too.
+ """
+ ret=super().at_idmapper_flush()
+ ifretandself.ndb._task:
+ self.ndb._pause_task(auto_pause=True)
+ # TODO - restart anew ?
+ returnret
+
+ def_start_task(self,interval=None,start_delay=None,repeats=None,force_restart=False,
+ auto_unpause=False,**kwargs):
+ """
+ Start/Unpause task runner, optionally with new values. If given, this will
+ update the Script's fields.
+
+ Keyword Args:
+ interval (int): How often to tick the task, in seconds. If this is <= 0,
+ no task will start and properties will not be updated on the Script.
+ start_delay (int): If the start should be delayed.
+ repeats (int): How many repeats. 0 for infinite repeats.
+ force_restart (bool): If set, always create a new task running even if an
+ old one already was running. Otherwise this will only happen if
+ new script properties were passed.
+ auto_unpause (bool): This is an automatic unpaused (used e.g by Evennia after
+ a reload) and should not un-pause manually paused Script timers.
+ Note:
+ If setting the `start-delay` of a *paused* Script, the Script will
+ restart exactly after that new start-delay, ignoring the time it
+ was paused at. If only changing the `interval`, the Script will
+ come out of pause comparing the time it spent in the *old* interval
+ with the *new* interval in order to determine when next to fire.
+
+ Examples:
+ - Script previously had an interval of 10s and was paused 5s into that interval.
+ Script is now restarted with a 20s interval. It will next fire after 15s.
+ - Same Script is restarted with a 3s interval. It will fire immediately. """
+ ifself.pkisNone:
+ # script object already deleted from db - don't start a new timer
+ raiseScriptDB.DoesNotExist
+
+ # handle setting/updating fields
+ update_fields=[]
+ old_interval=self.db_interval
+ ifintervalisnotNone:
+ self.db_interval=interval
+ update_fields.append("db_interval")
+ ifstart_delayisnotNone:
+ self.db_start_delay=start_delay
+ update_fields.append("db_start_delay")
+ ifrepeatsisnotNone:
+ self.db_repeats=repeats
+ update_fields.append("db_repeats")
+
+ # validate interval
+ ifself.db_intervalandself.db_interval>0:
+ ifnotself.is_active:
+ self.db_is_active=True
+ update_fields.append("db_is_active")
+ else:
+ # no point in starting a task with no interval.
+ return
+
+ restart=bool(update_fields)orforce_restart
+ self.save(update_fields=update_fields)
+
+ ifself.ndb._taskandself.ndb._task.running:
+ ifrestart:
+ # a change needed/forced; stop/remove old task
+ self._stop_task()
+ else:
+ # task alreaady running and no changes needed
+ return
+
ifnotself.ndb._task:
+ # we should have a fresh task after this pointself.ndb._task=ExtendedLoopingCall(self._step_task)
- ifself.db._paused_time:
- # the script was paused; restarting
- callcount=self.db._paused_callcountor0
- self.ndb._task.start(
- self.db_interval,now=False,start_delay=self.db._paused_time,count_start=callcount
- )
- delself.db._paused_time
- delself.db._paused_repeats
+ self._unpause_task(interval=interval,start_delay=start_delay,
+ auto_unpause=auto_unpause,
+ old_interval=old_interval)
- elifnotself.ndb._task.running:
- # starting script anew
+ ifnotself.ndb._task.running:
+ # if not unpausing started it, start script anew with the new valuesself.ndb._task.start(self.db_interval,now=notself.db_start_delay)
- def_stop_task(self):
+ self.at_start(**kwargs)
+
+ def_pause_task(self,auto_pause=False,**kwargs):"""
- Stop task runner
+ Pause task where it is, saving the current status.
+
+ Args:
+ auto_pause (str):
+
+ """
+ ifnotself.db._paused_time:
+ # only allow pause if not already paused
+ task=self.ndb._task
+ iftask:
+ self.db._paused_time=task.next_call_time()
+ self.db._paused_callcount=task.callcount
+ self.db._manually_paused=notauto_pause
+ iftask.running:
+ task.stop()
+ self.ndb._task=None
+
+ self.at_pause(auto_pause=auto_pause,**kwargs)
+
+ def_unpause_task(self,interval=None,start_delay=None,auto_unpause=False,
+ old_interval=0,**kwargs):
+ """
+ Unpause task from paused status. This is used for auto-paused tasks, such
+ as tasks paused on a server reload.
+
+ Args:
+ interval (int): How often to tick the task, in seconds.
+ start_delay (int): If the start should be delayed.
+ auto_unpause (bool): If set, this will only unpause scripts that were unpaused
+ automatically (useful during a system reload/shutdown).
+ old_interval (int): The old Script interval (or current one if nothing changed). Used
+ to recalculate the unpause startup interval.
+
+ """
+ paused_time=self.db._paused_time
+ ifpaused_time:
+ ifauto_unpauseandself.db._manually_paused:
+ # this was manually paused.
+ return
+
+ # task was paused. This will use the new values as needed.
+ callcount=self.db._paused_callcountor0
+ ifstart_delayisNoneandintervalisnotNone:
+ # adjust start-delay based on how far we were into previous interval
+ start_delay=max(0,interval-(old_interval-paused_time))
+ else:
+ start_delay=paused_time
+
+ ifnotself.ndb._task:
+ self.ndb._task=ExtendedLoopingCall(self._step_task)
+
+ self.ndb._task.start(
+ self.db_interval,now=False,start_delay=start_delay,count_start=callcount
+ )
+ delself.db._paused_time
+ delself.db._paused_callcount
+ delself.db._manually_paused
+
+ self.at_start(**kwargs)
+
+ def_stop_task(self,**kwargs):
+ """
+ Stop task runner and delete the task. """task=self.ndb._taskiftaskandtask.running:task.stop()self.ndb._task=None
+ self.db_is_active=False
+
+ # make sure this is not confused as a paused script
+ delself.db._paused_time
+ delself.db._paused_callcount
+ delself.db._manually_paused
+
+ self.save(update_fields=["db_is_active"])
+ self.at_stop(**kwargs)def_step_errback(self,e):"""
@@ -236,8 +367,8 @@
"""cname=self.__class__.__name__estring=_(
- "Script %(key)s(#%(dbid)s) of type '%(cname)s': at_repeat() error '%(err)s'."
- )%{"key":self.key,"dbid":self.dbid,"cname":cname,"err":e.getErrorMessage()}
+ "Script {key}(#{dbid}) of type '{name}': at_repeat() error '{err}'.".format(
+ key=self.key,dbid=self.dbid,name=cname,err=e.getErrorMessage()))try:self.db_obj.msg(estring)exceptException:
@@ -280,12 +411,7 @@
logger.log_trace()returnNone
- defat_script_creation(self):
- """
- Should be overridden in child.
-
- """
- pass
+ # Access methods / hooksdefat_first_save(self,**kwargs):"""
@@ -347,12 +473,185 @@
forkey,valueincdict["nattributes"]:self.nattributes.add(key,value)
- ifnotcdict.get("autostart"):
- # don't auto-start the script
- return
+ ifcdict.get("autostart"):
+ # autostart the script
+ self._start_task(force_restart=True)
- # auto-start script (default)
- self.start()
+ defdelete(self):
+ """
+ Delete the Script. Makes sure to stop any timer tasks first.
+
+ """
+ self._stop_task()
+ self.at_script_delete()
+ super().delete()
+
+ defat_script_creation(self):
+ """
+ Should be overridden in child.
+
+ """
+ pass
+
+ defat_script_delete(self):
+ """
+ Called when script is deleted, after at_stop.
+
+ """
+ pass
+
+ defis_valid(self):
+ """
+ If returning False, `at_repeat` will not be called and timer will stop
+ updating.
+ """
+ returnTrue
+
+ defat_repeat(self,**kwargs):
+ """
+ Called repeatedly every `interval` seconds, once `.start()` has
+ been called on the Script at least once.
+
+ Args:
+ **kwargs (dict): Arbitrary, optional arguments for users
+ overriding the call (unused by default).
+
+ """
+ pass
+
+ defat_start(self,**kwargs):
+ pass
+
+ defat_pause(self,**kwargs):
+ pass
+
+ defat_stop(self,**kwargs):
+ pass
+
+
+ defstart(self,interval=None,start_delay=None,repeats=None,**kwargs):
+ """
+ Start/Unpause timer component, optionally with new values. If given,
+ this will update the Script's fields. This will start `at_repeat` being
+ called every `interval` seconds.
+
+ Keyword Args:
+ interval (int): How often to fire `at_repeat` in seconds.
+ start_delay (int): If the start of ticking should be delayed.
+ repeats (int): How many repeats. 0 for infinite repeats.
+ **kwargs: Optional (default unused) kwargs passed on into the `at_start` hook.
+
+ Notes:
+ If setting the `start-delay` of a *paused* Script, the Script will
+ restart exactly after that new start-delay, ignoring the time it
+ was paused at. If only changing the `interval`, the Script will
+ come out of pause comparing the time it spent in the *old* interval
+ with the *new* interval in order to determine when next to fire.
+
+ Examples:
+ - Script previously had an interval of 10s and was paused 5s into that interval.
+ Script is now restarted with a 20s interval. It will next fire after 15s.
+ - Same Script is restarted with a 3s interval. It will fire immediately.
+
+ """
+ self._start_task(interval=interval,start_delay=start_delay,repeats=repeats,**kwargs)
+
+ # legacy alias
+ update=start
+
+ defstop(self,**kwargs):
+ """
+ Stop the Script's timer component. This will not delete the Sctipt,
+ just stop the regular firing of `at_repeat`. Running `.start()` will
+ start the timer anew, optionally with new settings..
+
+ Args:
+ **kwargs: Optional (default unused) kwargs passed on into the `at_stop` hook.
+
+ """
+ self._stop_task(**kwargs)
+
+ defpause(self,**kwargs):
+ """
+ Manually the Script's timer component manually.
+
+ Args:
+ **kwargs: Optional (default unused) kwargs passed on into the `at_pause` hook.
+
+ """
+ self._pause_task(manual_pause=True,**kwargs)
+
+ defunpause(self,**kwargs):
+ """
+ Manually unpause a Paused Script.
+
+ Args:
+ **kwargs: Optional (default unused) kwargs passed on into the `at_start` hook.
+
+ """
+ self._unpause_task(**kwargs)
+
+ deftime_until_next_repeat(self):
+ """
+ Get time until the script fires it `at_repeat` hook again.
+
+ Returns:
+ int or None: Time in seconds until the script runs again.
+ If not a timed script, return `None`.
+
+ Notes:
+ This hook is not used in any way by the script's stepping
+ system; it's only here for the user to be able to check in
+ on their scripts and when they will next be run.
+
+ """
+ task=self.ndb._task
+ iftask:
+ try:
+ returnint(round(task.next_call_time()))
+ exceptTypeError:
+ pass
+ returnNone
+
+ defremaining_repeats(self):
+ """
+ Get the number of returning repeats for limited Scripts.
+
+ Returns:
+ int or None: The number of repeats remaining until the Script
+ stops. Returns `None` if it has unlimited repeats.
+
+ """
+ task=self.ndb._task
+ iftask:
+ returnmax(0,self.db_repeats-task.callcount)
+ returnNone
+
+ defreset_callcount(self,value=0):
+ """
+ Reset the count of the number of calls done.
+
+ Args:
+ value (int, optional): The repeat value to reset to. Default
+ is to set it all the way back to 0.
+
+ Notes:
+ This is only useful if repeats != 0.
+
+ """
+ task=self.ndb._task
+ iftask:
+ task.callcount=max(0,int(value))
+
+ defforce_repeat(self):
+ """
+ Fire a premature triggering of the script callback. This
+ will reset the timer and count down repeats as if the script
+ had fired normally.
+ """
+ task=self.ndb._task
+ iftask:
+ task.force_repeat()
[docs]deftime_until_next_repeat(self):
- """
- Get time until the script fires it `at_repeat` hook again.
-
- Returns:
- next (int): Time in seconds until the script runs again.
- If not a timed script, return `None`.
-
- Notes:
- This hook is not used in any way by the script's stepping
- system; it's only here for the user to be able to check in
- on their scripts and when they will next be run.
-
- """
- task=self.ndb._task
- iftask:
- try:
- returnint(round(task.next_call_time()))
- exceptTypeError:
- pass
- returnNone
-
-
[docs]defremaining_repeats(self):
- """
- Get the number of returning repeats for limited Scripts.
-
- Returns:
- remaining (int or `None`): The number of repeats
- remaining until the Script stops. Returns `None`
- if it has unlimited repeats.
-
- """
- task=self.ndb._task
- iftask:
- returnmax(0,self.db_repeats-task.callcount)
- returnNone
-
-
[docs]defat_idmapper_flush(self):
- """If we're flushing this object, make sure the LoopingCall is gone too"""
- ret=super(DefaultScript,self).at_idmapper_flush()
- ifretandself.ndb._task:
- try:
- fromtwisted.internetimportreactor
-
- globalFLUSHING_INSTANCES
- # store the current timers for the _task and stop it to avoid duplicates after cache flush
- paused_time=self.ndb._task.next_call_time()
- callcount=self.ndb._task.callcount
- self._stop_task()
- SCRIPT_FLUSH_TIMERS[self.id]=(paused_time,callcount)
- # here we ensure that the restart call only happens once, not once per script
- ifnotFLUSHING_INSTANCES:
- FLUSHING_INSTANCES=True
- reactor.callLater(2,restart_scripts_after_flush)
- exceptException:
- importtraceback
-
- traceback.print_exc()
- returnret
-
-
[docs]defstart(self,force_restart=False):
- """
- Called every time the script is started (for persistent
- scripts, this is usually once every server start)
-
- Args:
- force_restart (bool, optional): Normally an already
- started script will not be started again. if
- `force_restart=True`, the script will always restart
- the script, regardless of if it has started before.
-
- Returns:
- result (int): 0 or 1 depending on if the script successfully
- started or not. Used in counting.
-
- """
- ifself.is_activeandnotforce_restart:
- # The script is already running, but make sure we have a _task if
- # this is after a cache flush
- ifnotself.ndb._taskandself.db_interval>0:
- self.ndb._task=ExtendedLoopingCall(self._step_task)
- try:
- start_delay,callcount=SCRIPT_FLUSH_TIMERS[self.id]
- delSCRIPT_FLUSH_TIMERS[self.id]
- now=False
- except(KeyError,ValueError,TypeError):
- now=notself.db_start_delay
- start_delay=None
- callcount=0
- self.ndb._task.start(
- self.db_interval,now=now,start_delay=start_delay,count_start=callcount
- )
- return0
-
- obj=self.obj
- ifobj:
- # check so the scripted object is valid and initalized
- try:
- obj.cmdset
- exceptAttributeError:
- # this means the object is not initialized.
- logger.log_trace()
- self.is_active=False
- return0
-
- # try to restart a paused script
- try:
- ifself.unpause(manual_unpause=False):
- return1
- exceptRuntimeError:
- # manually paused.
- return0
-
- # start the script from scratch
- self.is_active=True
- try:
- self.at_start()
- exceptException:
- logger.log_trace()
-
- ifself.db_interval>0:
- self._start_task()
- return1
-
-
[docs]defstop(self,kill=False):
- """
- Called to stop the script from running. This also deletes the
- script.
-
- Args:
- kill (bool, optional): - Stop the script without
- calling any relevant script hooks.
-
- Returns:
- result (int): 0 if the script failed to stop, 1 otherwise.
- Used in counting.
-
- """
- ifnotkill:
- try:
- self.at_stop()
- exceptException:
- logger.log_trace()
- self._stop_task()
- try:
- self.delete()
- exceptAssertionError:
- logger.log_trace()
- return0
- exceptObjectDoesNotExist:
- return0
- return1
-
-
[docs]defpause(self,manual_pause=True):
- """
- This stops a running script and stores its active state.
- It WILL NOT call the `at_stop()` hook.
-
- """
- self.db._manual_pause=manual_pause
- ifnotself.db._paused_time:
- # only allow pause if not already paused
- task=self.ndb._task
- iftask:
- self.db._paused_time=task.next_call_time()
- self.db._paused_callcount=task.callcount
- self._stop_task()
- self.is_active=False
-
-
[docs]defunpause(self,manual_unpause=True):
- """
- Restart a paused script. This WILL call the `at_start()` hook.
-
- Args:
- manual_unpause (bool, optional): This is False if unpause is
- called by the server reload/reset mechanism.
- Returns:
- result (bool): True if unpause was triggered, False otherwise.
-
- Raises:
- RuntimeError: If trying to automatically resart this script
- (usually after a reset/reload), but it was manually paused,
- and so should not the auto-unpaused.
-
- """
- ifnotmanual_unpauseandself.db._manual_pause:
- # if this script was paused manually (by a direct call of pause),
- # it cannot be automatically unpaused (e.g. by a @reload)
- raiseRuntimeError
-
- # Ensure that the script is fully unpaused, so that future calls
- # to unpause do not raise a RuntimeError
- self.db._manual_pause=False
-
- ifself.db._paused_time:
- # only unpause if previously paused
- self.is_active=True
-
- try:
- self.at_start()
- exceptException:
- logger.log_trace()
-
- self._start_task()
- returnTrue
-
-
[docs]defrestart(self,interval=None,repeats=None,start_delay=None):
- """
- Restarts an already existing/running Script from the
- beginning, optionally using different settings. This will
- first call the stop hooks, and then the start hooks again.
- Args:
- interval (int, optional): Allows for changing the interval
- of the Script. Given in seconds. if `None`, will use the already stored interval.
- repeats (int, optional): The number of repeats. If unset, will
- use the previous setting.
- start_delay (bool, optional): If we should wait `interval` seconds
- before starting or not. If `None`, re-use the previous setting.
-
- """
- try:
- self.at_stop()
- exceptException:
- logger.log_trace()
- self._stop_task()
- self.is_active=False
- # remove all pause flags
- delself.db._paused_time
- delself.db._manual_pause
- delself.db._paused_callcount
- # set new flags and start over
- ifintervalisnotNone:
- interval=max(0,interval)
- self.interval=interval
- ifrepeatsisnotNone:
- self.repeats=repeats
- ifstart_delayisnotNone:
- self.start_delay=start_delay
- self.start()
-
-
[docs]defreset_callcount(self,value=0):
- """
- Reset the count of the number of calls done.
-
- Args:
- value (int, optional): The repeat value to reset to. Default
- is to set it all the way back to 0.
-
- Notes:
- This is only useful if repeats != 0.
-
- """
- task=self.ndb._task
- iftask:
- task.callcount=max(0,int(value))
-
-
[docs]defforce_repeat(self):
- """
- Fire a premature triggering of the script callback. This
- will reset the timer and count down repeats as if the script
- had fired normally.
- """
- task=self.ndb._task
- iftask:
- task.force_repeat()
-
[docs]defis_valid(self):"""
- Is called to check if the script is valid to run at this time.
- Should return a boolean. The method is assumed to collect all
- needed information from its related self.obj.
+ Is called to check if the script's timer is valid to run at this time.
+ Should return a boolean. If False, the timer will be stopped. """
- returnnotself._is_deleted
+ returnTrue
[docs]defat_start(self,**kwargs):"""
- Called whenever the script is started, which for persistent
- scripts is at least once every server start. It will also be
- called when starting again after a pause (such as after a
- server reload)
+ Called whenever the script timer is started, which for persistent
+ timed scripts is at least once every server start. It will also be
+ called when starting again after a pause (including after a
+ server reload). Args: **kwargs (dict): Arbitrary, optional arguments for users
@@ -696,18 +728,38 @@
**kwargs (dict): Arbitrary, optional arguments for users overriding the call (unused by default).
+ """
+ pass
+
+
[docs]defat_pause(self,manual_pause=True,**kwargs):
+ """
+ Called when this script's timer pauses.
+
+ Args:
+ manual_pause (bool): If set, pausing was done by a direct call. The
+ non-manual pause indicates the script was paused as part of
+ the server reload.
+
"""pass
[docs]defat_stop(self,**kwargs):"""
- Called whenever when it's time for this script to stop (either
- because is_valid returned False or it runs out of iterations)
+ Called whenever when it's time for this script's timer to stop (either
+ because is_valid returned False, it ran out of iterations or it was manuallys
+ stopped.
- Args
+ Args: **kwargs (dict): Arbitrary, optional arguments for users overriding the call (unused by default).
+ """
+ pass
+
+
[docs]defat_script_delete(self):
+ """
+ Called when the Script is deleted, after at_stop().
+
"""pass
@@ -724,6 +776,15 @@
""" This hook is called whenever the server is shutting down fully (i.e. not for a restart).
+ """
+ pass
+
+
[docs]defat_server_start(self):
+ """
+ This hook is called after the server has started. It can be used to add
+ post-startup setup for Scripts without a timer component (for which at_start
+ could be used).
+
"""pass
[docs]defpause(self):
- """Pause the callback of a task.
- To resume use TaskHandlerTask.unpause
+ """
+ Pause the callback of a task.
+ To resume use `TaskHandlerTask.unpause`.
+
"""d=self.deferredifd:d.pause()
[docs]defunpause(self):
- """Unpause a task, run the task if it has passed delay time."""
+ """
+ Unpause a task, run the task if it has passed delay time.
+
+ """d=self.deferredifd:d.unpause()
@propertydefpaused(self):
- """A task attribute to check if the deferred instance of a task has been paused.
+ """
+ A task attribute to check if the deferred instance of a task has been paused. This exists to mock usage of a twisted deferred object.
@@ -134,7 +140,8 @@
returnNone
[docs]defdo_task(self):
- """Execute the task (call its callback).
+ """
+ Execute the task (call its callback). If calling before timedelay, cancel the deferred instance affliated to this task. Remove the task from the dictionary of current tasks on a successful callback.
@@ -147,7 +154,8 @@
returnTASK_HANDLER.do_task(self.task_id)
[docs]defcall(self):
- """Call the callback of a task.
+ """
+ Call the callback of a task. Leave the task unaffected otherwise. This does not use the task's deferred instance. The only requirement is that the task exist in task handler.
@@ -214,7 +222,8 @@
returnNone
[docs]defexists(self):
- """Check if a task exists.
+ """
+ Check if a task exists. Most task handler methods check for existence for you. Returns:
@@ -224,7 +233,8 @@
returnTASK_HANDLER.exists(self.task_id)
[docs]defget_id(self):
- """ Returns the global id for this task. For use with
+ """
+ Returns the global id for this task. For use with `evennia.scripts.taskhandler.TASK_HANDLER`. Returns:
@@ -256,7 +266,7 @@
self.clock=reactor# number of seconds before an uncalled canceled task is removed from TaskHandlerself.stale_timeout=60
- self._now=False# used in unit testing to manually set now time
+ self._now=False# used in unit testing to manually set now time
[docs]defload(self):"""Load from the ServerConfig.
@@ -312,7 +322,10 @@
returnTrue
[docs]defsave(self):
- """Save the tasks in ServerConfig."""
+ """
+ Save the tasks in ServerConfig.
+
+ """fortask_id,(date,callback,args,kwargs,persistent,_)inself.tasks.items():iftask_idinself.to_save:
@@ -320,21 +333,30 @@
ifnotpersistent:continue
+ safe_callback=callbackifgetattr(callback,"__self__",None):# `callback` is an instance methodobj=callback.__self__name=callback.__name__
- callback=(obj,name)
+ safe_callback=(obj,name)# Check if callback can be pickled. args and kwargs have been checked
- safe_callback=None
+ try:
+ dbserialize(safe_callback)
+ except(TypeError,AttributeError,PickleError)aserr:
+ raiseValueError(
+ "the specified callback {callback} cannot be pickled. "
+ "It must be a top-level function in a module or an "
+ "instance method ({err}).".format(callback=callback,err=err)
+ )
+ self.to_save[task_id]=dbserialize((date,safe_callback,args,kwargs))
- self.to_save[task_id]=dbserialize((date,callback,args,kwargs))ServerConfig.objects.conf("delayed_tasks",self.to_save)
[docs]defadd(self,timedelay,callback,*args,**kwargs):
- """Add a new task.
+ """
+ Add a new task. If the persistent kwarg is truthy: The callback, args and values for kwarg will be serialized. Type
@@ -378,17 +400,6 @@
safe_args=[]safe_kwargs={}
- # an unsaveable callback should immediately abort
- try:
- dbserialize(callback)
- except(TypeError,AttributeError,PickleError):
- raiseValueError(
- "the specified callback {} cannot be pickled. "
- "It must be a top-level function in a module or an "
- "instance method.".format(callback)
- )
- return
-
# Check that args and kwargs contain picklable informationforarginargs:try:
@@ -440,7 +451,8 @@
returnTaskHandlerTask(task_id)
[docs]defexists(self,task_id):
- """Check if a task exists.
+ """
+ Check if a task exists. Most task handler methods check for existence for you. Args:
@@ -456,7 +468,8 @@
returnFalse
[docs]defactive(self,task_id):
- """Check if a task is active (has not been called yet).
+ """
+ Check if a task is active (has not been called yet). Args: task_id (int): an existing task ID.
@@ -474,7 +487,8 @@
returnFalse
[docs]defcancel(self,task_id):
- """Stop a task from automatically executing.
+ """
+ Stop a task from automatically executing. This will not remove the task. Args:
@@ -500,7 +514,8 @@
returnFalse
[docs]defremove(self,task_id):
- """Remove a task without executing it.
+ """
+ Remove a task without executing it. Deletes the instance of the task's deferred. Args:
@@ -526,8 +541,8 @@
returnTrue
[docs]defclear(self,save=True,cancel=True):
- """clear all tasks.
- By default tasks are canceled and removed from the database also.
+ """
+ Clear all tasks. By default tasks are canceled and removed from the database as well. Args: save=True (bool): Should changes to persistent tasks be saved to database.
@@ -549,7 +564,8 @@
returnTrue
[docs]defcall_task(self,task_id):
- """Call the callback of a task.
+ """
+ Call the callback of a task. Leave the task unaffected otherwise. This does not use the task's deferred instance. The only requirement is that the task exist in task handler.
@@ -569,7 +585,8 @@
returncallback(*args,**kwargs)
[docs]defdo_task(self,task_id):
- """Execute the task (call its callback).
+ """
+ Execute the task (call its callback). If calling before timedelay cancel the deferred instance affliated to this task. Remove the task from the dictionary of current tasks on a successful callback.
@@ -614,7 +631,8 @@
returnNone
[docs]defcreate_delays(self):
- """Create the delayed tasks for the persistent tasks.
+ """
+ Create the delayed tasks for the persistent tasks. This method should be automatically called when Evennia starts. """
@@ -668,7 +686,6 @@
[docs]classAMPClientFactory(protocol.ReconnectingClientFactory):
@@ -74,7 +76,7 @@
"""self.server=server
- self.protocol=AMPServerClientProtocol
+ self.protocol=class_from_module(settings.AMP_CLIENT_PROTOCOL_CLASS)self.maxDelay=10# not really used unless connecting to multiple servers, but# avoids having to check for its existence on the protocol
@@ -326,7 +328,6 @@
-
diff --git a/docs/0.9.5/_modules/evennia/server/deprecations.html b/docs/0.9.5/_modules/evennia/server/deprecations.html
index 02ce935419..3e972a9a05 100644
--- a/docs/0.9.5/_modules/evennia/server/deprecations.html
+++ b/docs/0.9.5/_modules/evennia/server/deprecations.html
@@ -45,7 +45,7 @@
These all print to the terminal."""
-
+importos
[docs]defcheck_errors(settings):"""
@@ -100,6 +100,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_ENABLED"))
+ 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,"
@@ -142,6 +157,26 @@
"settings.CYCLE_LOGFILES is unused and should be removed. ""Use PORTAL/SERVER_LOG_DAY_ROTATION and PORTAL/SERVER_LOG_MAX_SIZE ""to control log cycling."
+ )
+ ifhasattr(settings,"CHANNEL_COMMAND_CLASS")orhasattr(settings,"CHANNEL_HANDLER_CLASS"):
+ raiseDeprecationWarning(
+ "settings.CHANNEL_HANDLER_CLASS and CHANNEL COMMAND_CLASS are "
+ "unused and should be removed. The ChannelHandler is no more; "
+ "channels are now handled by aliasing the default 'channel' command.")
+
+ template_overrides_dir=os.path.join(settings.GAME_DIR,"web","template_overrides")
+ static_overrides_dir=os.path.join(settings.GAME_DIR,"web","static_overrides")
+ ifos.path.exists(template_overrides_dir):
+ raiseDeprecationWarning(
+ f"The template_overrides directory ({template_overrides_dir}) has changed name.\n"
+ " - Rename your existing `template_overrides` folder to `templates` instead."
+ )
+ ifos.path.exists(static_overrides_dir):
+ raiseDeprecationWarning(
+ f"The static_overrides directory ({static_overrides_dir}) has changed name.\n"
+ " 1. Delete any existing `web/static` folder and all its contents (this "
+ "was auto-generated)\n"
+ " 2. Rename your existing `static_overrides` folder to `static` instead.")
-
diff --git a/docs/0.9.5/_modules/evennia/server/evennia_launcher.html b/docs/0.9.5/_modules/evennia/server/evennia_launcher.html
index d2e031c5ca..c25733b858 100644
--- a/docs/0.9.5/_modules/evennia/server/evennia_launcher.html
+++ b/docs/0.9.5/_modules/evennia/server/evennia_launcher.html
@@ -133,9 +133,9 @@
# requirementsPYTHON_MIN="3.7"
-TWISTED_MIN="18.0.0"
-DJANGO_MIN="2.2.5"
-DJANGO_LT="3.0"
+TWISTED_MIN="20.3.0"
+DJANGO_MIN="3.2.0"
+DJANGO_LT="4.0"try:sys.path[1]=EVENNIA_ROOT
@@ -1464,7 +1464,18 @@
"\nCreate a superuser below. The superuser is Account #1, the 'owner' ""account of the server. Email is optional and can be empty.\n")
- django.core.management.call_command("createsuperuser",interactive=True)
+ fromosimportenviron
+
+ username=environ.get("EVENNIA_SUPERUSER_USERNAME")
+ email=environ.get("EVENNIA_SUPERUSER_EMAIL")
+ password=environ.get("EVENNIA_SUPERUSER_PASSWORD")
+
+ if(usernameisnotNone)and(passwordisnotNone)andlen(password)>0:
+ fromevennia.accounts.modelsimportAccountDB
+ superuser=AccountDB.objects.create_superuser(username,email,password)
+ superuser.save()
+ else:
+ django.core.management.call_command("createsuperuser",interactive=True)
[docs]defcheck_database(always_return=False):
@@ -2300,7 +2311,7 @@
ifoptionin("makemessages","compilemessages"):# some commands don't require the presence of a game directory to workneed_gamedir=False
- ifoptionin("shell","check","makemigrations","createsuperuser"):
+ ifoptionin("shell","check","makemigrations","createsuperuser","shell_plus"):# some django commands requires the database to exist,# or evennia._init to have run before they work right.check_db=True
@@ -2370,7 +2381,6 @@
-
diff --git a/docs/0.9.5/_modules/evennia/server/initial_setup.html b/docs/0.9.5/_modules/evennia/server/initial_setup.html
index 86dd556b1d..0d2e6f2144 100644
--- a/docs/0.9.5/_modules/evennia/server/initial_setup.html
+++ b/docs/0.9.5/_modules/evennia/server/initial_setup.html
@@ -66,13 +66,11 @@
"""
-LIMBO_DESC=_(
- """
-Welcome to your new |wEvennia|n-based game! Visit http://www.evennia.com if you need
-help, want to contribute, report issues or just join the community.
-As Account #1 you can create a demo/tutorial area with '|wbatchcommand tutorial_world.build|n'.
- """
-)
+LIMBO_DESC=_("""
+Welcome to your new |wEvennia|n-based game! Visit https://www.evennia.com if you need
+help, want to contribute, report issues or just join the community.
+As Account #1 you can create a demo/tutorial area with '|wbatchcommand tutorial_world.build|n'.
+""")WARNING_POSTGRESQL_FIX="""
@@ -81,7 +79,7 @@
but the superuser was not yet connected to them. Please use in game commands to connect Account #1 to those channels when first logging in.
- """
+"""
[docs]defdataReceived(self,data):""" Handle non-AMP messages, such as HTTP communication.
+
"""# print("dataReceived: {}".format(data))ifdata[:1]==NUL:
@@ -454,6 +461,7 @@
that is irrelevant. If a true connection error happens, the portal will continuously try to reconnect, showing the problem that way.
+
"""# print("ConnectionLost: {}: {}".format(self, reason))try:
@@ -463,20 +471,20 @@
# Error handling
-
+"""
+
+MCCP - Mud Client Compression Protocol
+
+This implements the MCCP v2 telnet protocol as per
+http://tintin.sourceforge.net/mccp/. MCCP allows for the server to
+compress data when sending to supporting clients, reducing bandwidth
+by 70-90%.. The compression is done using Python's builtin zlib
+library. If the client doesn't support MCCP, server sends uncompressed
+as normal. Note: On modern hardware you are not likely to notice the
+effect of MCCP unless you have extremely heavy traffic or sits on a
+terribly slow connection.
+
+This protocol is implemented by the telnet protocol importing
+mccp_compress and calling it from its write methods.
+"""
+importzlib
+
+# negotiations for v1 and v2 of the protocol
+MCCP=bytes([86])# b"\x56"
+FLUSH=zlib.Z_SYNC_FLUSH
+
+
+
[docs]defmccp_compress(protocol,data):
+ """
+ Handles zlib compression, if applicable.
+
+ Args:
+ data (str): Incoming data to compress.
+
+ Returns:
+ stream (binary): Zlib-compressed data.
+
+ """
+ ifhasattr(protocol,"zlib"):
+ returnprotocol.zlib.compress(data)+protocol.zlib.flush(FLUSH)
+ returndata
+
+
+
[docs]classMccp:
+ """
+ Implements the MCCP protocol. Add this to a
+ variable on the telnet protocol to set it up.
+
+ """
+
+
[docs]def__init__(self,protocol):
+ """
+ initialize MCCP by storing protocol on
+ ourselves and calling the client to see if
+ it supports MCCP. Sets callbacks to
+ start zlib compression in that case.
+
+ Args:
+ protocol (Protocol): The active protocol instance.
+
+ """
+
+ self.protocol=protocol
+ self.protocol.protocol_flags["MCCP"]=False
+ # ask if client will mccp, connect callbacks to handle answer
+ self.protocol.will(MCCP).addCallbacks(self.do_mccp,self.no_mccp)
+
+
[docs]defno_mccp(self,option):
+ """
+ Called if client doesn't support mccp or chooses to turn it off.
+
+ Args:
+ option (Option): Option dict (not used).
+
+ """
+ ifhasattr(self.protocol,"zlib"):
+ delself.protocol.zlib
+ self.protocol.protocol_flags["MCCP"]=False
+ self.protocol.handshake_done()
+
+
[docs]defdo_mccp(self,option):
+ """
+ The client supports MCCP. Set things up by
+ creating a zlib compression stream.
+
+ Args:
+ option (Option): Option dict (not used).
+
+ """
+ self.protocol.protocol_flags["MCCP"]=True
+ self.protocol.requestNegotiation(MCCP,b"")
+ self.protocol.zlib=zlib.compressobj(9)
+ self.protocol.handshake_done()
+"""
+
+MSSP - Mud Server Status Protocol
+
+This implements the MSSP telnet protocol as per
+http://tintin.sourceforge.net/mssp/. MSSP allows web portals and
+listings to have their crawlers find the mud and automatically
+extract relevant information about it, such as genre, how many
+active players and so on.
+
+
+"""
+fromdjango.confimportsettings
+fromevennia.utilsimportutils
+
+MSSP=bytes([70])# b"\x46"
+MSSP_VAR=bytes([1])# b"\x01"
+MSSP_VAL=bytes([2])# b"\x02"
+
+# try to get the customized mssp info, if it exists.
+MSSPTable_CUSTOM=utils.variable_from_module(settings.MSSP_META_MODULE,"MSSPTable",default={})
+
+
+
[docs]classMssp:
+ """
+ Implements the MSSP protocol. Add this to a variable on the telnet
+ protocol to set it up.
+
+ """
+
+
[docs]def__init__(self,protocol):
+ """
+ initialize MSSP by storing protocol on ourselves and calling
+ the client to see if it supports MSSP.
+
+ Args:
+ protocol (Protocol): The active protocol instance.
+
+ """
+ self.protocol=protocol
+ self.protocol.will(MSSP).addCallbacks(self.do_mssp,self.no_mssp)
+
+
[docs]defget_player_count(self):
+ """
+ Get number of logged-in players.
+
+ Returns:
+ count (int): The number of players in the MUD.
+
+ """
+ returnstr(self.protocol.sessionhandler.count_loggedin())
+
+
[docs]defget_uptime(self):
+ """
+ Get how long the portal has been online (reloads are not counted).
+
+ Returns:
+ uptime (int): Number of seconds of uptime.
+
+ """
+ returnstr(self.protocol.sessionhandler.uptime)
+
+
[docs]defno_mssp(self,option):
+ """
+ Called when mssp is not requested. This is the normal
+ operation.
+
+ Args:
+ option (Option): Not used.
+
+ """
+ self.protocol.handshake_done()
+
+
[docs]defdo_mssp(self,option):
+ """
+ Negotiate all the information.
+
+ Args:
+ option (Option): Not used.
+
+ """
+
+ self.mssp_table={
+ # Required fields
+ "NAME":settings.SERVERNAME,
+ "PLAYERS":self.get_player_count,
+ "UPTIME":self.get_uptime,
+ "PORT":list(
+ str(port)forportinreversed(settings.TELNET_PORTS)
+ ),# most important port should be last in list
+ # Evennia auto-filled
+ "CRAWL DELAY":"-1",
+ "CODEBASE":utils.get_evennia_version(mode="pretty"),
+ "FAMILY":"Custom",
+ "ANSI":"1",
+ "GMCP":"1"ifsettings.TELNET_OOB_ENABLEDelse"0",
+ "ATCP":"0",
+ "MCCP":"1",
+ "MCP":"0",
+ "MSDP":"1"ifsettings.TELNET_OOB_ENABLEDelse"0",
+ "MSP":"0",
+ "MXP":"1",
+ "PUEBLO":"0",
+ "SSL":"1"ifsettings.SSL_ENABLEDelse"0",
+ "UTF-8":"1",
+ "ZMP":"0",
+ "VT100":"1",
+ "XTERM 256 COLORS":"1",
+ }
+
+ # update the static table with the custom one
+ ifMSSPTable_CUSTOM:
+ self.mssp_table.update(MSSPTable_CUSTOM)
+
+ varlist=b""
+ forvariable,valueinself.mssp_table.items():
+ ifcallable(value):
+ value=value()
+ ifutils.is_iter(value):
+ forpartvalinvalue:
+ varlist+=(
+ MSSP_VAR
+ +bytes(str(variable),"utf-8")
+ +MSSP_VAL
+ +bytes(str(partval),"utf-8")
+ )
+ else:
+ varlist+=(
+ MSSP_VAR+bytes(str(variable),"utf-8")+MSSP_VAL+bytes(str(value),"utf-8")
+ )
+
+ # send to crawler by subnegotiation
+ self.protocol.requestNegotiation(MSSP,varlist)
+ self.protocol.handshake_done()
+"""
+MXP - Mud eXtension Protocol.
+
+Partial implementation of the MXP protocol.
+The MXP protocol allows more advanced formatting options for telnet clients
+that supports it (mudlet, zmud, mushclient are a few)
+
+This only implements the SEND tag.
+
+More information can be found on the following links:
+http://www.zuggsoft.com/zmud/mxp.htm
+http://www.mushclient.com/mushclient/mxp.htm
+http://www.gammon.com.au/mushclient/addingservermxp.htm
+
+"""
+importre
+
+LINKS_SUB=re.compile(r"\|lc(.*?)\|lt(.*?)\|le",re.DOTALL)
+URL_SUB=re.compile(r"\|lu(.*?)\|lt(.*?)\|le",re.DOTALL)
+
+# MXP Telnet option
+MXP=bytes([91])# b"\x5b"
+
+MXP_TEMPSECURE="\x1B[4z"
+MXP_SEND=MXP_TEMPSECURE+'<SEND HREF="\\1">'+"\\2"+MXP_TEMPSECURE+"</SEND>"
+MXP_URL=MXP_TEMPSECURE+'<A HREF="\\1">'+"\\2"+MXP_TEMPSECURE+"</A>"
+
+
+
[docs]defmxp_parse(text):
+ """
+ Replaces links to the correct format for MXP.
+
+ Args:
+ text (str): The text to parse.
+
+ Returns:
+ parsed (str): The parsed text.
+
+ """
+ text=text.replace("&","&").replace("<","<").replace(">",">")
+
+ text=LINKS_SUB.sub(MXP_SEND,text)
+ text=URL_SUB.sub(MXP_URL,text)
+ returntext
[docs]def__init__(self,protocol):
+ """
+ Initializes the protocol by checking if the client supports it.
+
+ Args:
+ protocol (Protocol): The active protocol instance.
+
+ """
+ self.protocol=protocol
+ self.protocol.protocol_flags["MXP"]=False
+ self.protocol.will(MXP).addCallbacks(self.do_mxp,self.no_mxp)
+
+
[docs]defno_mxp(self,option):
+ """
+ Called when the Client reports to not support MXP.
+
+ Args:
+ option (Option): Not used.
+
+ """
+ self.protocol.protocol_flags["MXP"]=False
+ self.protocol.handshake_done()
+
+
[docs]defdo_mxp(self,option):
+ """
+ Called when the Client reports to support MXP.
+
+ Args:
+ option (Option): Not used.
+
+ """
+ self.protocol.protocol_flags["MXP"]=True
+ self.protocol.requestNegotiation(MXP,b"")
+ self.protocol.handshake_done()
+"""
+
+NAWS - Negotiate About Window Size
+
+This implements the NAWS telnet option as per
+https://www.ietf.org/rfc/rfc1073.txt
+
+NAWS allows telnet clients to report their current window size to the
+client and update it when the size changes
+
+"""
+fromcodecsimportencodeascodecs_encode
+fromdjango.confimportsettings
+
+NAWS=bytes([31])# b"\x1f"
+IS=bytes([0])# b"\x00"
+
+# default taken from telnet specification
+DEFAULT_WIDTH=settings.CLIENT_DEFAULT_WIDTH
+DEFAULT_HEIGHT=settings.CLIENT_DEFAULT_HEIGHT
+
+# try to get the customized mssp info, if it exists.
+
+
+
[docs]classNaws:
+ """
+ Implements the NAWS protocol. Add this to a variable on the telnet
+ protocol to set it up.
+
+ """
+
+
[docs]def__init__(self,protocol):
+ """
+ initialize NAWS by storing protocol on ourselves and calling
+ the client to see if it supports NAWS.
+
+ Args:
+ protocol (Protocol): The active protocol instance.
+
+ """
+ self.naws_step=0
+ self.protocol=protocol
+ self.protocol.protocol_flags["SCREENWIDTH"]={
+ 0:DEFAULT_WIDTH
+ }# windowID (0 is root):width
+ self.protocol.protocol_flags["SCREENHEIGHT"]={0:DEFAULT_HEIGHT}# windowID:width
+ self.protocol.negotiationMap[NAWS]=self.negotiate_sizes
+ self.protocol.do(NAWS).addCallbacks(self.do_naws,self.no_naws)
+
+
[docs]defno_naws(self,option):
+ """
+ Called when client is not reporting NAWS. This is the normal
+ operation.
+
+ Args:
+ option (Option): Not used.
+
+ """
+ self.protocol.handshake_done()
+
+
[docs]defdo_naws(self,option):
+ """
+ Client wants to negotiate all the NAWS information.
+
+ Args:
+ option (Option): Not used.
+
+ """
+ self.protocol.handshake_done()
+
+
[docs]defnegotiate_sizes(self,options):
+ """
+ Step through the NAWS handshake.
+
+ Args:
+ option (list): The incoming NAWS options.
+
+ """
+ iflen(options)==4:
+ # NAWS is negotiated with 16bit words
+ width=options[0]+options[1]
+ self.protocol.protocol_flags["SCREENWIDTH"][0]=int(codecs_encode(width,"hex"),16)
+ height=options[2]+options[3]
+ self.protocol.protocol_flags["SCREENHEIGHT"][0]=int(codecs_encode(height,"hex"),16)
+"""
+This module implements the main Evennia server process, the core of
+the game engine.
+
+This module should be started with the 'twistd' executable since it
+sets up all the networking features. (this is done automatically
+by game/evennia.py).
+
+"""
+importsys
+importos
+importtime
+
+fromos.pathimportdirname,abspath
+fromtwisted.applicationimportinternet,service
+fromtwisted.internet.taskimportLoopingCall
+fromtwisted.internetimportprotocol,reactor
+fromtwisted.python.logimportILogObserver
+
+importdjango
+
+django.setup()
+fromdjango.confimportsettings
+fromdjango.dbimportconnection
+
+importevennia
+
+evennia._init()
+
+fromevennia.utils.utilsimportget_evennia_version,mod_import,make_iter,class_from_module
+fromevennia.server.portal.portalsessionhandlerimportPORTAL_SESSIONS
+fromevennia.utilsimportlogger
+fromevennia.server.webserverimportEvenniaReverseProxyResource
+
+
+# we don't need a connection to the database so close it right away
+try:
+ connection.close()
+exceptException:
+ pass
+
+PORTAL_SERVICES_PLUGIN_MODULES=[
+ mod_import(module)formoduleinmake_iter(settings.PORTAL_SERVICES_PLUGIN_MODULES)
+]
+LOCKDOWN_MODE=settings.LOCKDOWN_MODE
+
+# -------------------------------------------------------------
+# Evennia Portal settings
+# -------------------------------------------------------------
+
+VERSION=get_evennia_version()
+
+SERVERNAME=settings.SERVERNAME
+
+PORTAL_RESTART=os.path.join(settings.GAME_DIR,"server","portal.restart")
+
+TELNET_PORTS=settings.TELNET_PORTS
+SSL_PORTS=settings.SSL_PORTS
+SSH_PORTS=settings.SSH_PORTS
+WEBSERVER_PORTS=settings.WEBSERVER_PORTS
+WEBSOCKET_CLIENT_PORT=settings.WEBSOCKET_CLIENT_PORT
+
+TELNET_INTERFACES=["127.0.0.1"]ifLOCKDOWN_MODEelsesettings.TELNET_INTERFACES
+SSL_INTERFACES=["127.0.0.1"]ifLOCKDOWN_MODEelsesettings.SSL_INTERFACES
+SSH_INTERFACES=["127.0.0.1"]ifLOCKDOWN_MODEelsesettings.SSH_INTERFACES
+WEBSERVER_INTERFACES=["127.0.0.1"]ifLOCKDOWN_MODEelsesettings.WEBSERVER_INTERFACES
+WEBSOCKET_CLIENT_INTERFACE="127.0.0.1"ifLOCKDOWN_MODEelsesettings.WEBSOCKET_CLIENT_INTERFACE
+WEBSOCKET_CLIENT_URL=settings.WEBSOCKET_CLIENT_URL
+
+TELNET_ENABLED=settings.TELNET_ENABLEDandTELNET_PORTSandTELNET_INTERFACES
+SSL_ENABLED=settings.SSL_ENABLEDandSSL_PORTSandSSL_INTERFACES
+SSH_ENABLED=settings.SSH_ENABLEDandSSH_PORTSandSSH_INTERFACES
+WEBSERVER_ENABLED=settings.WEBSERVER_ENABLEDandWEBSERVER_PORTSandWEBSERVER_INTERFACES
+WEBCLIENT_ENABLED=settings.WEBCLIENT_ENABLED
+WEBSOCKET_CLIENT_ENABLED=(
+ settings.WEBSOCKET_CLIENT_ENABLEDandWEBSOCKET_CLIENT_PORTandWEBSOCKET_CLIENT_INTERFACE
+)
+
+AMP_HOST=settings.AMP_HOST
+AMP_PORT=settings.AMP_PORT
+AMP_INTERFACE=settings.AMP_INTERFACE
+AMP_ENABLED=AMP_HOSTandAMP_PORTandAMP_INTERFACE
+
+INFO_DICT={
+ "servername":SERVERNAME,
+ "version":VERSION,
+ "errors":"",
+ "info":"",
+ "lockdown_mode":"",
+ "amp":"",
+ "telnet":[],
+ "telnet_ssl":[],
+ "ssh":[],
+ "webclient":[],
+ "webserver_proxy":[],
+ "webserver_internal":[],
+}
+
+try:
+ WEB_PLUGINS_MODULE=mod_import(settings.WEB_PLUGINS_MODULE)
+exceptImportError:
+ WEB_PLUGINS_MODULE=None
+ INFO_DICT["errors"]=(
+ "WARNING: settings.WEB_PLUGINS_MODULE not found - "
+ "copy 'evennia/game_template/server/conf/web_plugins.py to "
+ "mygame/server/conf."
+ )
+
+
+_MAINTENANCE_COUNT=0
+
+
+def_portal_maintenance():
+ """
+ Repeated maintenance tasks for the portal.
+
+ """
+ global_MAINTENANCE_COUNT
+
+ _MAINTENANCE_COUNT+=1
+
+ if_MAINTENANCE_COUNT%(60*7)==0:
+ # drop database connection every 7 hrs to avoid default timeouts on MySQL
+ # (see https://github.com/evennia/evennia/issues/1376)
+ connection.close()
+
+
+# -------------------------------------------------------------
+# Portal Service object
+# -------------------------------------------------------------
+
+
+
[docs]classPortal(object):
+
+ """
+ The main Portal server handler. This object sets up the database
+ and tracks and interlinks all the twisted network services that
+ make up Portal.
+
+ """
+
+
[docs]def__init__(self,application):
+ """
+ Setup the server.
+
+ Args:
+ application (Application): An instantiated Twisted application
+
+ """
+ sys.path.append(".")
+
+ # create a store of services
+ self.services=service.MultiService()
+ self.services.setServiceParent(application)
+ self.amp_protocol=None# set by amp factory
+ self.sessions=PORTAL_SESSIONS
+ self.sessions.portal=self
+ self.process_id=os.getpid()
+
+ self.server_process_id=None
+ self.server_restart_mode="shutdown"
+ self.server_info_dict={}
+
+ self.start_time=time.time()
+
+ self.maintenance_task=LoopingCall(_portal_maintenance)
+ self.maintenance_task.start(60,now=True)# call every minute
+
+ # in non-interactive portal mode, this gets overwritten by
+ # cmdline sent by the evennia launcher
+ self.server_twistd_cmd=self._get_backup_server_twistd_cmd()
+
+ # set a callback if the server is killed abruptly,
+ # by Ctrl-C, reboot etc.
+ reactor.addSystemEventTrigger(
+ "before","shutdown",self.shutdown,_reactor_stopping=True,_stop_server=True
+ )
+
+ def_get_backup_server_twistd_cmd(self):
+ """
+ For interactive Portal mode there is no way to get the server cmdline from the launcher, so
+ we need to guess it here (it's very likely to not change)
+
+ Returns:
+ server_twistd_cmd (list): An instruction for starting the server, to pass to Popen.
+
+ """
+ server_twistd_cmd=[
+ "twistd",
+ "--python={}".format(os.path.join(dirname(dirname(abspath(__file__))),"server.py")),
+ ]
+ ifos.name!="nt":
+ gamedir=os.getcwd()
+ server_twistd_cmd.append(
+ "--pidfile={}".format(os.path.join(gamedir,"server","server.pid"))
+ )
+ returnserver_twistd_cmd
+
+
[docs]defget_info_dict(self):
+ """
+ Return the Portal info, for display.
+
+ """
+ returnINFO_DICT
+
+
[docs]defshutdown(self,_reactor_stopping=False,_stop_server=False):
+ """
+ Shuts down the server from inside it.
+
+ Args:
+ _reactor_stopping (bool, optional): This is set if server
+ is already in the process of shutting down; in this case
+ we don't need to stop it again.
+ _stop_server (bool, optional): Only used in portal-interactive mode;
+ makes sure to stop the Server cleanly.
+
+ Note that restarting (regardless of the setting) will not work
+ if the Portal is currently running in daemon mode. In that
+ case it always needs to be restarted manually.
+
+ """
+ if_reactor_stoppingandhasattr(self,"shutdown_complete"):
+ # we get here due to us calling reactor.stop below. No need
+ # to do the shutdown procedure again.
+ return
+
+ self.sessions.disconnect_all()
+ if_stop_server:
+ self.amp_protocol.stop_server(mode="shutdown")
+ ifnot_reactor_stopping:
+ # shutting down the reactor will trigger another signal. We set
+ # a flag to avoid loops.
+ self.shutdown_complete=True
+ reactor.callLater(0,reactor.stop)
+
+
+# -------------------------------------------------------------
+#
+# Start the Portal proxy server and add all active services
+#
+# -------------------------------------------------------------
+
+
+# twistd requires us to define the variable 'application' so it knows
+# what to execute from.
+application=service.Application("Portal")
+
+# custom logging
+
+if"--nodaemon"notinsys.argv:
+ logfile=logger.WeeklyLogFile(
+ os.path.basename(settings.PORTAL_LOG_FILE),
+ os.path.dirname(settings.PORTAL_LOG_FILE),
+ day_rotation=settings.PORTAL_LOG_DAY_ROTATION,
+ max_size=settings.PORTAL_LOG_MAX_SIZE,
+ )
+ application.setComponent(ILogObserver,logger.PortalLogObserver(logfile).emit)
+
+# The main Portal server program. This sets up the database
+# and is where we store all the other services.
+PORTAL=Portal(application)
+
+ifLOCKDOWN_MODE:
+
+ INFO_DICT["lockdown_mode"]=" LOCKDOWN_MODE active: Only local connections."
+
+ifAMP_ENABLED:
+
+ # The AMP protocol handles the communication between
+ # the portal and the mud server. Only reason to ever deactivate
+ # it would be during testing and debugging.
+
+ fromevennia.server.portalimportamp_server
+
+ INFO_DICT["amp"]="amp: %s"%AMP_PORT
+
+ factory=amp_server.AMPServerFactory(PORTAL)
+ amp_service=internet.TCPServer(AMP_PORT,factory,interface=AMP_INTERFACE)
+ amp_service.setName("PortalAMPServer")
+ PORTAL.services.addService(amp_service)
+
+
+# We group all the various services under the same twisted app.
+# These will gradually be started as they are initialized below.
+
+ifTELNET_ENABLED:
+
+ # Start telnet game connections
+
+ fromevennia.server.portalimporttelnet
+
+ _telnet_protocol=class_from_module(settings.TELNET_PROTOCOL_CLASS)
+
+ forinterfaceinTELNET_INTERFACES:
+ ifacestr=""
+ ifinterfacenotin("0.0.0.0","::")orlen(TELNET_INTERFACES)>1:
+ ifacestr="-%s"%interface
+ forportinTELNET_PORTS:
+ pstring="%s:%s"%(ifacestr,port)
+ factory=telnet.TelnetServerFactory()
+ factory.noisy=False
+ factory.protocol=_telnet_protocol
+ factory.sessionhandler=PORTAL_SESSIONS
+ telnet_service=internet.TCPServer(port,factory,interface=interface)
+ telnet_service.setName("EvenniaTelnet%s"%pstring)
+ PORTAL.services.addService(telnet_service)
+
+ INFO_DICT["telnet"].append("telnet%s: %s"%(ifacestr,port))
+
+
+ifSSL_ENABLED:
+
+ # Start Telnet+SSL game connection (requires PyOpenSSL).
+
+ fromevennia.server.portalimporttelnet_ssl
+
+ _ssl_protocol=class_from_module(settings.SSL_PROTOCOL_CLASS)
+
+ forinterfaceinSSL_INTERFACES:
+ ifacestr=""
+ ifinterfacenotin("0.0.0.0","::")orlen(SSL_INTERFACES)>1:
+ ifacestr="-%s"%interface
+ forportinSSL_PORTS:
+ pstring="%s:%s"%(ifacestr,port)
+ factory=protocol.ServerFactory()
+ factory.noisy=False
+ factory.sessionhandler=PORTAL_SESSIONS
+ factory.protocol=_ssl_protocol
+
+ ssl_context=telnet_ssl.getSSLContext()
+ ifssl_context:
+ ssl_service=internet.SSLServer(
+ port,factory,telnet_ssl.getSSLContext(),interface=interface
+ )
+ ssl_service.setName("EvenniaSSL%s"%pstring)
+ PORTAL.services.addService(ssl_service)
+
+ INFO_DICT["telnet_ssl"].append("telnet+ssl%s: %s"%(ifacestr,port))
+ else:
+ INFO_DICT["telnet_ssl"].append(
+ "telnet+ssl%s: %s (deactivated - keys/cert unset)"%(ifacestr,port)
+ )
+
+
+ifSSH_ENABLED:
+
+ # Start SSH game connections. Will create a keypair in
+ # evennia/game if necessary.
+
+ fromevennia.server.portalimportssh
+
+ _ssh_protocol=class_from_module(settings.SSH_PROTOCOL_CLASS)
+
+ forinterfaceinSSH_INTERFACES:
+ ifacestr=""
+ ifinterfacenotin("0.0.0.0","::")orlen(SSH_INTERFACES)>1:
+ ifacestr="-%s"%interface
+ forportinSSH_PORTS:
+ pstring="%s:%s"%(ifacestr,port)
+ factory=ssh.makeFactory(
+ {"protocolFactory":_ssh_protocol,
+ "protocolArgs":(),"sessions":PORTAL_SESSIONS}
+ )
+ factory.noisy=False
+ ssh_service=internet.TCPServer(port,factory,interface=interface)
+ ssh_service.setName("EvenniaSSH%s"%pstring)
+ PORTAL.services.addService(ssh_service)
+
+ INFO_DICT["ssh"].append("ssh%s: %s"%(ifacestr,port))
+
+
+ifWEBSERVER_ENABLED:
+ fromevennia.server.webserverimportWebsite
+
+ # Start a reverse proxy to relay data to the Server-side webserver
+
+ websocket_started=False
+ _websocket_protocol=class_from_module(settings.WEBSOCKET_PROTOCOL_CLASS)
+ forinterfaceinWEBSERVER_INTERFACES:
+ ifacestr=""
+ ifinterfacenotin("0.0.0.0","::")orlen(WEBSERVER_INTERFACES)>1:
+ ifacestr="-%s"%interface
+ forproxyport,serverportinWEBSERVER_PORTS:
+ web_root=EvenniaReverseProxyResource("127.0.0.1",serverport,"")
+ webclientstr=""
+ ifWEBCLIENT_ENABLED:
+ # create ajax client processes at /webclientdata
+ fromevennia.server.portalimportwebclient_ajax
+
+ ajax_webclient=webclient_ajax.AjaxWebClient()
+ ajax_webclient.sessionhandler=PORTAL_SESSIONS
+ web_root.putChild(b"webclientdata",ajax_webclient)
+ webclientstr="webclient (ajax only)"
+
+ ifWEBSOCKET_CLIENT_ENABLEDandnotwebsocket_started:
+ # start websocket client port for the webclient
+ # we only support one websocket client
+ fromevennia.server.portalimportwebclient# noqa
+ fromautobahn.twisted.websocketimportWebSocketServerFactory
+
+ w_interface=WEBSOCKET_CLIENT_INTERFACE
+ w_ifacestr=""
+ ifw_interfacenotin("0.0.0.0","::")orlen(WEBSERVER_INTERFACES)>1:
+ w_ifacestr="-%s"%w_interface
+ port=WEBSOCKET_CLIENT_PORT
+
+
[docs]classWebsocket(WebSocketServerFactory):
+ "Only here for better naming in logs"
+ pass
+
+ factory=Websocket()
+ factory.noisy=False
+ factory.protocol=_websocket_protocol
+ factory.sessionhandler=PORTAL_SESSIONS
+ websocket_service=internet.TCPServer(port,factory,interface=w_interface)
+ websocket_service.setName("EvenniaWebSocket%s:%s"%(w_ifacestr,port))
+ PORTAL.services.addService(websocket_service)
+ websocket_started=True
+ webclientstr="webclient-websocket%s: %s"%(w_ifacestr,port)
+ INFO_DICT["webclient"].append(webclientstr)
+
+ ifWEB_PLUGINS_MODULE:
+ try:
+ web_root=WEB_PLUGINS_MODULE.at_webproxy_root_creation(web_root)
+ exceptException:
+ # Legacy user has not added an at_webproxy_root_creation function in existing
+ # web plugins file
+ INFO_DICT["errors"]=(
+ "WARNING: WEB_PLUGINS_MODULE is enabled but at_webproxy_root_creation() "
+ "not found copy 'evennia/game_template/server/conf/web_plugins.py to "
+ "mygame/server/conf."
+ )
+ web_root=Website(web_root,logPath=settings.HTTP_LOG_FILE)
+ web_root.is_portal=True
+ proxy_service=internet.TCPServer(proxyport,web_root,interface=interface)
+ proxy_service.setName("EvenniaWebProxy%s:%s"%(ifacestr,proxyport))
+ PORTAL.services.addService(proxy_service)
+ INFO_DICT["webserver_proxy"].append("webserver-proxy%s: %s"%(ifacestr,proxyport))
+ INFO_DICT["webserver_internal"].append("webserver: %s"%serverport)
+
+
+forplugin_moduleinPORTAL_SERVICES_PLUGIN_MODULES:
+ # external plugin services to start
+ ifplugin_module:
+ plugin_module.start_plugin_services(PORTAL)
+
+
+
+
\ No newline at end of file
diff --git a/docs/0.9.5/_modules/evennia/server/portal/portalsessionhandler.html b/docs/0.9.5/_modules/evennia/server/portal/portalsessionhandler.html
index 20688dc9aa..91d3b876a2 100644
--- a/docs/0.9.5/_modules/evennia/server/portal/portalsessionhandler.html
+++ b/docs/0.9.5/_modules/evennia/server/portal/portalsessionhandler.html
@@ -40,7 +40,8 @@
Source code for evennia.server.portal.portalsessionhandler
"""
-Sessionhandler for portal sessions
+Sessionhandler for portal sessions.
+
"""
@@ -48,8 +49,11 @@
fromcollectionsimportdeque,namedtuplefromtwisted.internetimportreactorfromdjango.confimportsettings
-fromevennia.server.sessionhandlerimportSessionHandler,PCONN,PDISCONN,PCONNSYNC,PDISCONNALL
+fromevennia.server.sessionhandlerimportSessionHandler
+fromevennia.server.portal.ampimportPCONN,PDISCONN,PCONNSYNC,PDISCONNALLfromevennia.utils.loggerimportlog_trace
+fromevennia.utils.utilsimportclass_from_module
+fromdjango.utils.translationimportgettextas_# module import_MOD_IMPORT=None
@@ -74,6 +78,9 @@
# Portal-SessionHandler class# -------------------------------------------------------------
+DOS_PROTECTION_MSG=_("{servername} DoS protection is active."
+ "You are queued to connect in {num} seconds ...")
+
[docs]defgenerate_sessid(self):
+ """
+ Simply generates a sessid that's guaranteed to be unique for this Portal run.
+
+ Returns:
+ sessid
+
+ """
+ self.latest_sessid+=1
+ ifself.latest_sessidinself:
+ returnself.generate_sessid()
+ returnself.latest_sessid
+
[docs]defconnect(self,session):""" Called by protocol at first connect. This adds a not-yet
@@ -132,22 +152,17 @@
ifnotsession.sessid:# if the session already has a sessid (e.g. being inherited in the# case of a webclient auto-reconnect), keep it
- self.latest_sessid+=1
- session.sessid=self.latest_sessid
+ session.sessid=self.generate_sessid()session.server_connected=False_CONNECTION_QUEUE.appendleft(session)iflen(_CONNECTION_QUEUE)>1:session.data_out(
- text=[
- [
- "%s DoS protection is active. You are queued to connect in %g seconds ..."
- %(
- settings.SERVERNAME,
- len(_CONNECTION_QUEUE)*_MIN_TIME_BETWEEN_CONNECTS,
- )
- ],
+ text=(
+ (DOS_PROTECTION_MSG.format(
+ servername=settings.SERVERNAME,
+ num=len(_CONNECTION_QUEUE)*_MIN_TIME_BETWEEN_CONNECTS),),{},
- ]
+ ))now=time.time()if(
@@ -247,6 +262,7 @@
[docs]defdisconnect_all(self):""" Disconnect all sessions, informing the Server.
+
"""def_callback(result,sessionhandler):
@@ -504,7 +520,8 @@
log_trace()
diff --git a/docs/0.9.5/_modules/evennia/server/portal/ssh.html b/docs/0.9.5/_modules/evennia/server/portal/ssh.html
index d3ba43a5fe..85aa2e316a 100644
--- a/docs/0.9.5/_modules/evennia/server/portal/ssh.html
+++ b/docs/0.9.5/_modules/evennia/server/portal/ssh.html
@@ -84,10 +84,9 @@
fromtwisted.pythonimportcomponentsfromdjango.confimportsettings
-fromevennia.serverimportsessionfromevennia.accounts.modelsimportAccountDBfromevennia.utilsimportansi
-fromevennia.utils.utilsimportto_str
+fromevennia.utils.utilsimportto_str,class_from_module_RE_N=re.compile(r"\|n$")_RE_SCREENREADER_REGEX=re.compile(
@@ -103,29 +102,32 @@
CTRL_BACKSLASH="\x1c"CTRL_L="\x0c"
-_NO_AUTOGEN="""
-Evennia could not generate SSH private- and public keys ({{err}})
+_NO_AUTOGEN=f"""
+Evennia could not generate SSH private- and public keys ({{err}})Using conch default keys instead.If this error persists, create the keys manually (using the tools for your OS)and put them here:
-{}
-{}
-""".format(
- _PRIVATE_KEY_FILE,_PUBLIC_KEY_FILE
-)
+{_PRIVATE_KEY_FILE}
+{_PUBLIC_KEY_FILE}
+"""
+
+_BASE_SESSION_CLASS=class_from_module(settings.BASE_SESSION_CLASS)# not used atm
[docs]classSSHServerFactory(protocol.ServerFactory):
- "This is only to name this better in logs"
+ """
+ This is only to name this better in logs
+
+ """noisy=False
[docs]classSshProtocol(Manhole,_BASE_SESSION_CLASS):""" Each account connecting over ssh gets this protocol assigned to them. All communication between game and account goes through
@@ -318,18 +320,18 @@
text (str): The first argument is always the text string to send. No other arguments are considered. Keyword Args:
- options (dict): Send-option flags:
+ options (dict): Send-option flags (booleans)
- - mxp: Enforce MXP link support.
- - ansi: Enforce no ANSI colors.
- - xterm256: Enforce xterm256 colors, regardless of TTYPE setting.
- - nocolor: Strip all colors.
- - raw: Pass string through without any ansi processing
- (i.e. include Evennia ansi markers but do not
+ - mxp: enforce mxp link support.
+ - ansi: enforce no ansi colors.
+ - xterm256: enforce xterm256 colors, regardless of ttype setting.
+ - nocolor: strip all colors.
+ - raw: pass string through without any ansi processing
+ (i.e. include evennia ansi markers but do not convert them into ansi tokens)
- - echo: Turn on/off line echo on the client. Turn
+ - echo: turn on/off line echo on the client. turn off line echo for client, for example for password.
- Note that it must be actively turned back on again!
+ note that it must be actively turned back on again! """# print "telnet.send_text", args,kwargs # DEBUG
@@ -602,7 +604,6 @@
+"""
+This is a simple context factory for auto-creating
+SSL keys and certificates.
+
+"""
+importos
+importsys
+
+try:
+ importOpenSSL
+ fromtwisted.internetimportsslastwisted_ssl
+exceptImportErroraserror:
+ errstr="""
+{err}
+ SSL requires the PyOpenSSL library:
+ pip install pyopenssl
+ """
+ raiseImportError(errstr.format(err=error))
+
+fromdjango.confimportsettings
+fromevennia.utils.utilsimportclass_from_module
+
+_GAME_DIR=settings.GAME_DIR
+
+# messages
+
+NO_AUTOGEN="""
+
+{err}
+Evennia could not auto-generate the SSL private key. If this error
+persists, create {keyfile} yourself using third-party tools.
+"""
+
+NO_AUTOCERT="""
+
+{err}
+Evennia's SSL context factory could not automatically, create an SSL
+certificate {certfile}.
+
+A private key {keyfile} was already created. Please create {certfile}
+manually using the commands valid for your operating system, for
+example (linux, using the openssl program):
+{exestring}
+"""
+
+_TELNET_PROTOCOL_CLASS=class_from_module(settings.TELNET_PROTOCOL_CLASS)
+
+
+
[docs]classSSLProtocol(_TELNET_PROTOCOL_CLASS):
+ """
+ Communication is the same as telnet, except data transfer
+ is done with encryption.
+
+ """
+
+
[docs]defverify_SSL_key_and_cert(keyfile,certfile):
+ """
+ This function looks for RSA key and certificate in the current
+ directory. If files ssl.key and ssl.cert does not exist, they
+ are created.
+
+ """
+
+ ifnot(os.path.exists(keyfile)andos.path.exists(certfile)):
+ # key/cert does not exist. Create.
+ importsubprocess
+ fromCrypto.PublicKeyimportRSA
+ fromtwisted.conch.ssh.keysimportKey
+
+ print(" Creating SSL key and certificate ... ",end=" ")
+
+ try:
+ # create the RSA key and store it.
+ KEY_LENGTH=2048
+ rsa_key=Key(RSA.generate(KEY_LENGTH))
+ key_string=rsa_key.toString(type="OPENSSH")
+ withopen(keyfile,"w+b")asfil:
+ fil.write(key_string)
+ exceptExceptionaserr:
+ print(NO_AUTOGEN.format(err=err,keyfile=keyfile))
+ sys.exit(5)
+
+ # try to create the certificate
+ CERT_EXPIRE=365*20# twenty years validity
+ # default:
+ # openssl req -new -x509 -key ssl.key -out ssl.cert -days 7300
+ exestring="openssl req -new -x509 -key %s -out %s -days %s"%(
+ keyfile,
+ certfile,
+ CERT_EXPIRE,
+ )
+ try:
+ subprocess.call(exestring)
+ exceptOSErroraserr:
+ raiseOSError(
+ NO_AUTOCERT.format(err=err,certfile=certfile,keyfile=keyfile,exestring=exestring)
+ )
+ print("done.")
+
+
+
[docs]defgetSSLContext():
+ """
+ This is called by the portal when creating the SSL context
+ server-side.
+
+ Returns:
+ ssl_context (tuple): A key and certificate that is either
+ existing previously or or created on the fly.
+
+ """
+ keyfile=os.path.join(_GAME_DIR,"server","ssl.key")
+ certfile=os.path.join(_GAME_DIR,"server","ssl.cert")
+
+ verify_SSL_key_and_cert(keyfile,certfile)
+ returntwisted_ssl.DefaultOpenSSLContextFactory(keyfile,certfile)
+"""
+
+SUPPRESS-GO-AHEAD
+
+This supports suppressing or activating Evennia
+the GO-AHEAD telnet operation after every server reply.
+If the client sends no explicit DONT SUPRESS GO-AHEAD,
+Evennia will default to supressing it since many clients
+will fail to use it and has no knowledge of this standard.
+
+It is set as the NOGOAHEAD protocol_flag option.
+
+http://www.faqs.org/rfcs/rfc858.html
+
+"""
+
+SUPPRESS_GA=bytes([3])# b"\x03"
+
+# default taken from telnet specification
+
+# try to get the customized mssp info, if it exists.
+
+
+
[docs]classSuppressGA:
+ """
+ Implements the SUPRESS-GO-AHEAD protocol. Add this to a variable on the telnet
+ protocol to set it up.
+
+ """
+
+
[docs]def__init__(self,protocol):
+ """
+ Initialize suppression of GO-AHEADs.
+
+ Args:
+ protocol (Protocol): The active protocol instance.
+
+ """
+ self.protocol=protocol
+
+ self.protocol.protocol_flags["NOGOAHEAD"]=True
+ self.protocol.protocol_flags[
+ "NOPROMPTGOAHEAD"
+ ]=True# Used to send a GA after a prompt line only, set in TTYPE (per client)
+ # tell the client that we prefer to suppress GA ...
+ self.protocol.will(SUPPRESS_GA).addCallbacks(self.will_suppress_ga,self.wont_suppress_ga)
+
+
[docs]defwont_suppress_ga(self,option):
+ """
+ Called when client requests to not suppress GA.
+
+ Args:
+ option (Option): Not used.
+
+ """
+ self.protocol.protocol_flags["NOGOAHEAD"]=False
+ self.protocol.handshake_done()
+
+
[docs]defwill_suppress_ga(self,option):
+ """
+ Client will suppress GA
+
+ Args:
+ option (Option): Not used.
+
+ """
+ self.protocol.protocol_flags["NOGOAHEAD"]=True
+ self.protocol.handshake_done()
[docs]classTelnetProtocol(Telnet,StatefulTelnetProtocol,_BASE_SESSION_CLASS):
+ """
+ Each player connecting over telnet (ie using most traditional mud
+ clients) gets a telnet protocol instance assigned to them. All
+ communication between game and player goes through here.
+
+ """
+
+
[docs]defdataReceived(self,data):
+ """
+ Unused by default, but a good place to put debug printouts
+ of incoming data.
+
+ """
+ # print(f"telnet dataReceived: {data}")
+ try:
+ super().dataReceived(data)
+ exceptValueErroraserr:
+ fromevennia.utilsimportlogger
+ logger.log_err(f"Malformed telnet input: {err}")
+
+
[docs]defconnectionMade(self):
+ """
+ This is called when the connection is first established.
+
+ """
+ # important in order to work normally with standard telnet
+ self.do(LINEMODE).addErrback(self._wont_linemode)
+ # initialize the session
+ self.line_buffer=b""
+ client_address=self.transport.client
+ client_address=client_address[0]ifclient_addresselseNone
+ # this number is counted down for every handshake that completes.
+ # when it reaches 0 the portal/server syncs their data
+ self.handshakes=8# suppress-go-ahead, naws, ttype, mccp, mssp, msdp, gmcp, mxp
+
+ self.init_session(self.protocol_key,client_address,self.factory.sessionhandler)
+ self.protocol_flags["ENCODING"]=settings.ENCODINGS[0]ifsettings.ENCODINGSelse"utf-8"
+ # add this new connection to sessionhandler so
+ # the Server becomes aware of it.
+ self.sessionhandler.connect(self)
+ # change encoding to ENCODINGS[0] which reflects Telnet default encoding
+
+ # suppress go-ahead
+ self.sga=suppress_ga.SuppressGA(self)
+ # negotiate client size
+ self.naws=naws.Naws(self)
+ # negotiate ttype (client info)
+ # Obs: mudlet ttype does not seem to work if we start mccp before ttype. /Griatch
+ self.ttype=ttype.Ttype(self)
+ # negotiate mccp (data compression) - turn this off for wireshark analysis
+ self.mccp=Mccp(self)
+ # negotiate mssp (crawler communication)
+ self.mssp=mssp.Mssp(self)
+ # oob communication (MSDP, GMCP) - two handshake calls!
+ self.oob=telnet_oob.TelnetOOB(self)
+ # mxp support
+ self.mxp=Mxp(self)
+
+ fromevennia.utils.utilsimportdelay
+
+ # timeout the handshakes in case the client doesn't reply at all
+ self._handshake_delay=delay(2,callback=self.handshake_done,timeout=True)
+
+ # TCP/IP keepalive watches for dead links
+ self.transport.setTcpKeepAlive(1)
+ # The TCP/IP keepalive is not enough for some networks;
+ # we have to complement it with a NOP keep-alive.
+ self.protocol_flags["NOPKEEPALIVE"]=True
+ self.nop_keep_alive=None
+ self.toggle_nop_keepalive()
+
+ def_wont_linemode(self,*args):
+ """
+ Client refuses do(linemode). This is common for MUD-specific
+ clients, but we must ask for the sake of raw telnet. We ignore
+ this error.
+
+ """
+ pass
+
+ def_send_nop_keepalive(self):
+ """
+ Send NOP keepalive unless flag is set
+
+ """
+ ifself.protocol_flags.get("NOPKEEPALIVE"):
+ self._write(IAC+NOP)
+
+
[docs]deftoggle_nop_keepalive(self):
+ """
+ Allow to toggle the NOP keepalive for those sad clients that
+ can't even handle a NOP instruction. This is turned off by the
+ protocol_flag NOPKEEPALIVE (settable e.g. by the default
+ `option` command).
+
+ """
+ ifself.nop_keep_aliveandself.nop_keep_alive.running:
+ self.nop_keep_alive.stop()
+ else:
+ self.nop_keep_alive=LoopingCall(self._send_nop_keepalive)
+ self.nop_keep_alive.start(30,now=False)
+
+
[docs]defhandshake_done(self,timeout=False):
+ """
+ This is called by all telnet extensions once they are finished.
+ When all have reported, a sync with the server is performed.
+ The system will force-call this sync after a small time to handle
+ clients that don't reply to handshakes at all.
+
+ """
+ iftimeout:
+ ifself.handshakes>0:
+ self.handshakes=0
+ self.sessionhandler.sync(self)
+ else:
+ self.handshakes-=1
+ ifself.handshakes<=0:
+ # do the sync
+ self.sessionhandler.sync(self)
+
+
[docs]defat_login(self):
+ """
+ Called when this session gets authenticated by the server.
+
+ """
+ pass
+
+
[docs]defenableRemote(self,option):
+ """
+ This sets up the remote-activated options we allow for this protocol.
+
+ Args:
+ option (char): The telnet option to enable.
+
+ Returns:
+ enable (bool): If this option should be enabled.
+
+ """
+ ifoption==LINEMODE:
+ # make sure to activate line mode with local editing for all clients
+ self.requestNegotiation(
+ LINEMODE,MODE+bytes(chr(ord(LINEMODE_EDIT)+ord(LINEMODE_TRAPSIG)),"ascii")
+ )
+ returnTrue
+ else:
+ return(
+ option==ttype.TTYPE
+ oroption==naws.NAWS
+ oroption==MCCP
+ oroption==mssp.MSSP
+ oroption==suppress_ga.SUPPRESS_GA
+ )
[docs]defenableLocal(self,option):
+ """
+ Call to allow the activation of options for this protocol
+
+ Args:
+ option (char): The telnet option to enable locally.
+
+ Returns:
+ enable (bool): If this option should be enabled.
+
+ """
+ return(
+ option==LINEMODE
+ oroption==MCCP
+ oroption==ECHO
+ oroption==suppress_ga.SUPPRESS_GA
+ )
[docs]defconnectionLost(self,reason):
+ """
+ this is executed when the connection is lost for whatever
+ reason. it can also be called directly, from the disconnect
+ method
+
+ Args:
+ reason (str): Motivation for losing connection.
+
+ """
+ self.sessionhandler.disconnect(self)
+ self.transport.loseConnection()
+
+
[docs]defapplicationDataReceived(self,data):
+ """
+ Telnet method called when non-telnet-command data is coming in
+ over the telnet connection. We pass it on to the game engine
+ directly.
+
+ Args:
+ data (str): Incoming data.
+
+ """
+ ifnotdata:
+ data=[data]
+ elifdata.strip()==NULL:
+ # this is an ancient type of keepalive used by some
+ # legacy clients. There should never be a reason to send a
+ # lone NULL character so this seems to be a safe thing to
+ # support for backwards compatibility. It also stops the
+ # NULL from continuously popping up as an unknown command.
+ data=[_IDLE_COMMAND]
+ else:
+ data=_RE_LINEBREAK.split(data)
+
+ iflen(data)>2and_HTTP_REGEX.match(data[0]):
+ # guard against HTTP request on the Telnet port; we
+ # block and kill the connection.
+ self.transport.write(_HTTP_WARNING)
+ self.transport.loseConnection()
+ return
+
+ ifself.line_bufferandlen(data)>1:
+ # buffer exists, it is terminated by the first line feed
+ data[0]=self.line_buffer+data[0]
+ self.line_buffer=b""
+ # if the last data split is empty, it means all splits have
+ # line breaks, if not, it is unterminated and must be
+ # buffered.
+ self.line_buffer+=data.pop()
+ # send all data chunks
+ fordatindata:
+ self.data_in(text=dat+b"\n")
+
+ def_write(self,data):
+ """
+ Hook overloading the one used in plain telnet
+
+ """
+ data=data.replace(b"\n",b"\r\n").replace(b"\r\r\n",b"\r\n")
+ super()._write(mccp_compress(self,data))
+
+
[docs]defsendLine(self,line):
+ """
+ Hook overloading the one used by linereceiver.
+
+ Args:
+ line (str): Line to send.
+
+ """
+ line=to_bytes(line,self)
+ # escape IAC in line mode, and correctly add \r\n (the TELNET end-of-line)
+ line=line.replace(IAC,IAC+IAC)
+ line=line.replace(b"\n",b"\r\n")
+ ifnotline.endswith(b"\r\n")andself.protocol_flags.get("FORCEDENDLINE",True):
+ line+=b"\r\n"
+ ifnotself.protocol_flags.get("NOGOAHEAD",True):
+ line+=IAC+GA
+ returnself.transport.write(mccp_compress(self,line))
+
+ # Session hooks
+
+
[docs]defdisconnect(self,reason=""):
+ """
+ Generic hook for the engine to call in order to
+ disconnect this protocol.
+
+ Args:
+ reason (str, optional): Reason for disconnecting.
+
+ """
+ self.data_out(text=((reason,),{}))
+ self.connectionLost(reason)
+
+
[docs]defdata_in(self,**kwargs):
+ """
+ Data User -> Evennia
+
+ Keyword Args:
+ kwargs (any): Options from the protocol.
+
+ """
+ # from evennia.server.profiling.timetrace import timetrace # DEBUG
+ # text = timetrace(text, "telnet.data_in") # DEBUG
+
+ self.sessionhandler.data_in(self,**kwargs)
+
+
[docs]defdata_out(self,**kwargs):
+ """
+ Data Evennia -> User
+
+ Keyword Args:
+ kwargs (any): Options to the protocol
+
+ """
+ self.sessionhandler.data_out(self,**kwargs)
+
+ # send_* methods
+
+
[docs]defsend_text(self,*args,**kwargs):
+ """
+ Send text data. This is an in-band telnet operation.
+
+ Args:
+ text (str): The first argument is always the text string to send. No other arguments
+ are considered.
+ Keyword Args:
+ options (dict): Send-option flags
+
+ - mxp: Enforce MXP link support.
+ - ansi: Enforce no ANSI colors.
+ - xterm256: Enforce xterm256 colors, regardless of TTYPE.
+ - noxterm256: Enforce no xterm256 color support, regardless of TTYPE.
+ - nocolor: Strip all Color, regardless of ansi/xterm256 setting.
+ - raw: Pass string through without any ansi processing
+ (i.e. include Evennia ansi markers but do not
+ convert them into ansi tokens)
+ - echo: Turn on/off line echo on the client. Turn
+ off line echo for client, for example for password.
+ Note that it must be actively turned back on again!
+
+ """
+ text=args[0]ifargselse""
+ iftextisNone:
+ return
+
+ # handle arguments
+ options=kwargs.get("options",{})
+ flags=self.protocol_flags
+ xterm256=options.get(
+ "xterm256",flags.get("XTERM256",False)ifflags.get("TTYPE",False)elseTrue
+ )
+ useansi=options.get(
+ "ansi",flags.get("ANSI",False)ifflags.get("TTYPE",False)elseTrue
+ )
+ raw=options.get("raw",flags.get("RAW",False))
+ nocolor=options.get("nocolor",flags.get("NOCOLOR")ornot(xterm256oruseansi))
+ echo=options.get("echo",None)
+ mxp=options.get("mxp",flags.get("MXP",False))
+ screenreader=options.get("screenreader",flags.get("SCREENREADER",False))
+
+ ifscreenreader:
+ # screenreader mode cleans up output
+ text=ansi.parse_ansi(text,strip_ansi=True,xterm256=False,mxp=False)
+ text=_RE_SCREENREADER_REGEX.sub("",text)
+
+ ifoptions.get("send_prompt"):
+ # send a prompt instead.
+ prompt=text
+ ifnotraw:
+ # processing
+ prompt=ansi.parse_ansi(
+ _RE_N.sub("",prompt)+("||n"ifprompt.endswith("|")else"|n"),
+ strip_ansi=nocolor,
+ xterm256=xterm256,
+ )
+ ifmxp:
+ prompt=mxp_parse(prompt)
+ prompt=to_bytes(prompt,self)
+ prompt=prompt.replace(IAC,IAC+IAC).replace(b"\n",b"\r\n")
+ ifnotself.protocol_flags.get("NOPROMPTGOAHEAD",
+ self.protocol_flags.get("NOGOAHEAD",True)):
+ prompt+=IAC+GA
+ self.transport.write(mccp_compress(self,prompt))
+ else:
+ ifechoisnotNone:
+ # turn on/off echo. Note that this is a bit turned around since we use
+ # echo as if we are "turning off the client's echo" when telnet really
+ # handles it the other way around.
+ ifecho:
+ # by telling the client that WE WON'T echo, the client knows
+ # that IT should echo. This is the expected behavior from
+ # our perspective.
+ self.transport.write(mccp_compress(self,IAC+WONT+ECHO))
+ else:
+ # by telling the client that WE WILL echo, the client can
+ # safely turn OFF its OWN echo.
+ self.transport.write(mccp_compress(self,IAC+WILL+ECHO))
+ ifraw:
+ # no processing
+ self.sendLine(text)
+ return
+ else:
+ # we need to make sure to kill the color at the end in order
+ # to match the webclient output.
+ linetosend=ansi.parse_ansi(
+ _RE_N.sub("",text)+("||n"iftext.endswith("|")else"|n"),
+ strip_ansi=nocolor,
+ xterm256=xterm256,
+ mxp=mxp,
+ )
+ ifmxp:
+ linetosend=mxp_parse(linetosend)
+ self.sendLine(linetosend)
+
+
[docs]defsend_prompt(self,*args,**kwargs):
+ """
+ Send a prompt - a text without a line end. See send_text for argument options.
+
+ """
+ kwargs["options"].update({"send_prompt":True})
+ self.send_text(*args,**kwargs)
+
+
[docs]defsend_default(self,cmdname,*args,**kwargs):
+ """
+ Send other oob data
+
+ """
+ ifnotcmdname=="options":
+ self.oob.data_out(cmdname,*args,**kwargs)
+"""
+
+Telnet OOB (Out of band communication)
+
+OOB protocols allow for asynchronous communication between Evennia and
+compliant telnet clients. The "text" type of send command will always
+be sent "in-band", appearing in the client's main text output. OOB
+commands, by contrast, can have many forms and it is up to the client
+how and if they are handled. Examples of OOB instructions could be to
+instruct the client to play sounds or to update a graphical health
+bar.
+
+Note that in Evennia's Web client, all send commands are "OOB
+commands", (including the "text" one), there is no equivalence to
+MSDP/GMCP for the webclient since it doesn't need it.
+
+This implements the following telnet OOB communication protocols:
+
+- MSDP (Mud Server Data Protocol), as per http://tintin.sourceforge.net/msdp/
+- GMCP (Generic Mud Communication Protocol) as per
+ http://www.ironrealms.com/rapture/manual/files/FeatGMCP-txt.html#Generic_MUD_Communication_Protocol%28GMCP%29
+
+----
+
+"""
+importre
+importjson
+fromevennia.utils.utilsimportis_iter
+
+# General Telnet
+fromtwisted.conch.telnetimportIAC,SB,SE
+
+# MSDP-relevant telnet cmd/opt-codes
+MSDP=bytes([69])
+MSDP_VAR=bytes([1])
+MSDP_VAL=bytes([2])
+MSDP_TABLE_OPEN=bytes([3])
+MSDP_TABLE_CLOSE=bytes([4])
+
+MSDP_ARRAY_OPEN=bytes([5])
+MSDP_ARRAY_CLOSE=bytes([6])
+
+# GMCP
+GMCP=bytes([201])
+
+
+# pre-compiled regexes
+# returns 2-tuple
+msdp_regex_table=re.compile(
+ br"%s\s*(\w*?)\s*%s\s*%s(.*?)%s"%(MSDP_VAR,MSDP_VAL,MSDP_TABLE_OPEN,MSDP_TABLE_CLOSE)
+)
+# returns 2-tuple
+msdp_regex_array=re.compile(
+ br"%s\s*(\w*?)\s*%s\s*%s(.*?)%s"%(MSDP_VAR,MSDP_VAL,MSDP_ARRAY_OPEN,MSDP_ARRAY_CLOSE)
+)
+msdp_regex_var=re.compile(br"%s"%MSDP_VAR)
+msdp_regex_val=re.compile(br"%s"%MSDP_VAL)
+
+EVENNIA_TO_GMCP={
+ "client_options":"Core.Supports.Get",
+ "get_inputfuncs":"Core.Commands.Get",
+ "get_value":"Char.Value.Get",
+ "repeat":"Char.Repeat.Update",
+ "monitor":"Char.Monitor.Update",
+}
+
+
+# MSDP/GMCP communication handler
+
+
+
[docs]classTelnetOOB:
+ """
+ Implements the MSDP and GMCP protocols.
+ """
+
+
[docs]def__init__(self,protocol):
+ """
+ Initiates by storing the protocol on itself and trying to
+ determine if the client supports MSDP.
+
+ Args:
+ protocol (Protocol): The active protocol.
+
+ """
+ self.protocol=protocol
+ self.protocol.protocol_flags["OOB"]=False
+ self.MSDP=False
+ self.GMCP=False
+ # ask for the available protocols and assign decoders
+ # (note that handshake_done() will be called twice!)
+ self.protocol.negotiationMap[MSDP]=self.decode_msdp
+ self.protocol.negotiationMap[GMCP]=self.decode_gmcp
+ self.protocol.will(MSDP).addCallbacks(self.do_msdp,self.no_msdp)
+ self.protocol.will(GMCP).addCallbacks(self.do_gmcp,self.no_gmcp)
+ self.oob_reported={}
+
+
[docs]defno_msdp(self,option):
+ """
+ Client reports No msdp supported or wanted.
+
+ Args:
+ option (Option): Not used.
+
+ """
+ # no msdp, check GMCP
+ self.protocol.handshake_done()
+
+
[docs]defdo_msdp(self,option):
+ """
+ Client reports that it supports msdp.
+
+ Args:
+ option (Option): Not used.
+
+ """
+ self.MSDP=True
+ self.protocol.protocol_flags["OOB"]=True
+ self.protocol.handshake_done()
+
+
[docs]defno_gmcp(self,option):
+ """
+ If this is reached, it means neither MSDP nor GMCP is
+ supported.
+
+ Args:
+ option (Option): Not used.
+
+ """
+ self.protocol.handshake_done()
+
+
[docs]defdo_gmcp(self,option):
+ """
+ Called when client confirms that it can do MSDP or GMCP.
+
+ Args:
+ option (Option): Not used.
+
+ """
+ self.GMCP=True
+ self.protocol.protocol_flags["OOB"]=True
+ self.protocol.handshake_done()
+
+ # encoders
+
+
[docs]defencode_msdp(self,cmdname,*args,**kwargs):
+ """
+ Encode into a valid MSDP command.
+
+ Args:
+ cmdname (str): Name of send instruction.
+ args, kwargs (any): Arguments to OOB command.
+
+ Notes:
+ The output of this encoding will be
+ MSDP structures on these forms:
+ ::
+
+ [cmdname, [], {}] -> VAR cmdname VAL ""
+ [cmdname, [arg], {}] -> VAR cmdname VAL arg
+ [cmdname, [args],{}] -> VAR cmdname VAL ARRAYOPEN VAL arg VAL arg ... ARRAYCLOSE
+ [cmdname, [], {kwargs}] -> VAR cmdname VAL TABLEOPEN VAR key VAL val ... TABLECLOSE
+ [cmdname, [args], {kwargs}] -> VAR cmdname VAL ARRAYOPEN VAL arg VAL arg ... ARRAYCLOSE
+ VAR cmdname VAL TABLEOPEN VAR key VAL val ... TABLECLOSE
+
+ Further nesting is not supported, so if an array argument
+ consists of an array (for example), that array will be
+ json-converted to a string.
+
+ """
+ msdp_cmdname="{msdp_var}{msdp_cmdname}{msdp_val}".format(
+ msdp_var=MSDP_VAR.decode(),msdp_cmdname=cmdname,msdp_val=MSDP_VAL.decode()
+ )
+
+ ifnot(argsorkwargs):
+ returnmsdp_cmdname.encode()
+
+ # print("encode_msdp in:", cmdname, args, kwargs) # DEBUG
+
+ msdp_args=""
+ ifargs:
+ msdp_args=msdp_cmdname
+ iflen(args)==1:
+ msdp_args+=args[0]
+ else:
+ msdp_args+=(
+ "{msdp_array_open}"
+ "{msdp_args}"
+ "{msdp_array_close}".format(
+ msdp_array_open=MSDP_ARRAY_OPEN.decode(),
+ msdp_array_close=MSDP_ARRAY_CLOSE.decode(),
+ msdp_args="".join("%s%s"%(MSDP_VAL.decode(),val)forvalinargs),
+ )
+ )
+
+ msdp_kwargs=""
+ ifkwargs:
+ msdp_kwargs=msdp_cmdname
+ msdp_kwargs+=(
+ "{msdp_table_open}"
+ "{msdp_kwargs}"
+ "{msdp_table_close}".format(
+ msdp_table_open=MSDP_TABLE_OPEN.decode(),
+ msdp_table_close=MSDP_TABLE_CLOSE.decode(),
+ msdp_kwargs="".join(
+ "%s%s%s%s"%(MSDP_VAR.decode(),key,MSDP_VAL.decode(),val)
+ forkey,valinkwargs.items()
+ ),
+ )
+ )
+
+ msdp_string=msdp_args+msdp_kwargs
+
+ # print("msdp_string:", msdp_string) # DEBUG
+ returnmsdp_string.encode()
+
+
[docs]defencode_gmcp(self,cmdname,*args,**kwargs):
+ """
+ Encode into GMCP messages.
+
+ Args:
+ cmdname (str): GMCP OOB command name.
+ args, kwargs (any): Arguments to OOB command.
+
+ Notes:
+ GMCP messages will be outgoing on the following
+ form (the non-JSON cmdname at the start is what
+ IRE games use, supposedly, and what clients appear
+ to have adopted). A cmdname without Package will end
+ up in the Core package, while Core package names will
+ be stripped on the Evennia side.
+ ::
+
+ [cmd.name, [], {}] -> Cmd.Name
+ [cmd.name, [arg], {}] -> Cmd.Name arg
+ [cmd.name, [args],{}] -> Cmd.Name [args]
+ [cmd.name, [], {kwargs}] -> Cmd.Name {kwargs}
+ [cmdname, [args, {kwargs}] -> Core.Cmdname [[args],{kwargs}]
+
+ Notes:
+ There are also a few default mappings between evennia outputcmds and GMCP:
+ ::
+
+ client_options -> Core.Supports.Get
+ get_inputfuncs -> Core.Commands.Get
+ get_value -> Char.Value.Get
+ repeat -> Char.Repeat.Update
+ monitor -> Char.Monitor.Update
+
+ """
+
+ ifcmdnameinEVENNIA_TO_GMCP:
+ gmcp_cmdname=EVENNIA_TO_GMCP[cmdname]
+ elif"_"incmdname:
+ gmcp_cmdname=".".join(word.capitalize()forwordincmdname.split("_"))
+ else:
+ gmcp_cmdname="Core.%s"%cmdname.capitalize()
+
+ ifnot(argsorkwargs):
+ gmcp_string=gmcp_cmdname
+ elifargs:
+ iflen(args)==1:
+ args=args[0]
+ ifkwargs:
+ gmcp_string="%s%s"%(gmcp_cmdname,json.dumps([args,kwargs]))
+ else:
+ gmcp_string="%s%s"%(gmcp_cmdname,json.dumps(args))
+ else:# only kwargs
+ gmcp_string="%s%s"%(gmcp_cmdname,json.dumps(kwargs))
+
+ # print("gmcp string", gmcp_string) # DEBUG
+ returngmcp_string.encode()
+
+
[docs]defdecode_msdp(self,data):
+ """
+ Decodes incoming MSDP data.
+
+ Args:
+ data (str or list): MSDP data.
+
+ Notes:
+ Clients should always send MSDP data on
+ one of the following forms:
+ ::
+
+ cmdname '' -> [cmdname, [], {}]
+ cmdname val -> [cmdname, [val], {}]
+ cmdname array -> [cmdname, [array], {}]
+ cmdname table -> [cmdname, [], {table}]
+ cmdname array cmdname table -> [cmdname, [array], {table}]
+
+ Observe that all MSDP_VARS are used to identify cmdnames,
+ so if there are multiple arrays with the same cmdname
+ given, they will be merged into one argument array, same
+ for tables. Different MSDP_VARS (outside tables) will be
+ identified as separate cmdnames.
+
+ """
+ ifisinstance(data,list):
+ data=b"".join(data)
+
+ # print("decode_msdp in:", data) # DEBUG
+
+ tables={}
+ arrays={}
+ variables={}
+
+ # decode tables
+ forkey,tableinmsdp_regex_table.findall(data):
+ key=key.decode()
+ tables[key]={}ifkeynotintableselsetables[key]
+ forvarvalinmsdp_regex_var.split(table)[1:]:
+ var,val=msdp_regex_val.split(varval,1)
+ var,val=var.decode(),val.decode()
+ ifvar:
+ tables[key][var]=val
+
+ # decode arrays from all that was not a table
+ data_no_tables=msdp_regex_table.sub(b"",data)
+ forkey,arrayinmsdp_regex_array.findall(data_no_tables):
+ key=key.decode()
+ arrays[key]=[]ifkeynotinarrayselsearrays[key]
+ parts=msdp_regex_val.split(array)
+ parts=[part.decode()forpartinparts]
+ iflen(parts)==2:
+ arrays[key].append(parts[1])
+ eliflen(parts)>1:
+ arrays[key].extend(parts[1:])
+
+ # decode remainders from all that were not tables or arrays
+ data_no_tables_or_arrays=msdp_regex_array.sub(b"",data_no_tables)
+ forvarvalinmsdp_regex_var.split(data_no_tables_or_arrays):
+ # get remaining varvals after cleaning away tables/arrays. If mathcing
+ # an existing key in arrays, it will be added as an argument to that command,
+ # otherwise it will be treated as a command without argument.
+ parts=msdp_regex_val.split(varval)
+ parts=[part.decode()forpartinparts]
+ iflen(parts)==2:
+ variables[parts[0]]=parts[1]
+ eliflen(parts)>1:
+ variables[parts[0]]=parts[1:]
+
+ cmds={}
+ # merge matching table/array/variables together
+ forkey,tableintables.items():
+ args,kwargs=[],table
+ ifkeyinarrays:
+ args.extend(arrays.pop(key))
+ ifkeyinvariables:
+ args.append(variables.pop(key))
+ cmds[key]=[args,kwargs]
+
+ forkey,arrinarrays.items():
+ args,kwargs=arr,{}
+ ifkeyinvariables:
+ args.append(variables.pop(key))
+ cmds[key]=[args,kwargs]
+
+ forkey,varinvariables.items():
+ cmds[key]=[[var],{}]
+
+ # remap the 'generic msdp commands' to avoid colliding with builtins etc
+ # by prepending "msdp_"
+ lower_case={key.lower():keyforkeyincmds}
+ forremapin("list","report","reset","send","unreport"):
+ ifremapinlower_case:
+ cmds["msdp_{}".format(remap)]=cmds.pop(lower_case[remap])
+
+ # print("msdp data in:", cmds) # DEBUG
+ self.protocol.data_in(**cmds)
+
+
[docs]defdecode_gmcp(self,data):
+ """
+ Decodes incoming GMCP data on the form 'varname <structure>'.
+
+ Args:
+ data (str or list): GMCP data.
+
+ Notes:
+ Clients send data on the form "Module.Submodule.Cmdname <structure>".
+ We assume the structure is valid JSON.
+
+ The following is parsed into Evennia's formal structure:
+ ::
+
+ Core.Name -> [name, [], {}]
+ Core.Name string -> [name, [string], {}]
+ Core.Name [arg, arg,...] -> [name, [args], {}]
+ Core.Name {key:arg, key:arg, ...} -> [name, [], {kwargs}]
+ Core.Name [[args], {kwargs}] -> [name, [args], {kwargs}]
+
+ """
+ ifisinstance(data,list):
+ data=b"".join(data)
+
+ # print("decode_gmcp in:", data) # DEBUG
+ ifdata:
+ try:
+ cmdname,structure=data.split(None,1)
+ exceptValueError:
+ cmdname,structure=data,b""
+ cmdname=cmdname.replace(b".",b"_")
+ try:
+ structure=json.loads(structure)
+ exceptValueError:
+ # maybe the structure is not json-serialized at all
+ pass
+ args,kwargs=[],{}
+ ifis_iter(structure):
+ ifisinstance(structure,dict):
+ kwargs={key:valueforkey,valueinstructure.items()ifkey}
+ else:
+ args=list(structure)
+ else:
+ args=(structure,)
+ ifcmdname.lower().startswith(b"core_"):
+ # if Core.cmdname, then use cmdname
+ cmdname=cmdname[5:]
+ self.protocol.data_in(**{cmdname.lower().decode():[args,kwargs]})
+
+ # access methods
+
+
[docs]defdata_out(self,cmdname,*args,**kwargs):
+ """
+ Return a MSDP- or GMCP-valid subnegotiation across the protocol.
+
+ Args:
+ cmdname (str): OOB-command name.
+ args, kwargs (any): Arguments to OOB command.
+
+ """
+ kwargs.pop("options",None)
+
+ ifself.MSDP:
+ encoded_oob=self.encode_msdp(cmdname,*args,**kwargs)
+ self.protocol._write(IAC+SB+MSDP+encoded_oob+IAC+SE)
+
+ ifself.GMCP:
+ encoded_oob=self.encode_gmcp(cmdname,*args,**kwargs)
+ self.protocol._write(IAC+SB+GMCP+encoded_oob+IAC+SE)
+"""
+This allows for running the telnet communication over an encrypted SSL tunnel. To use it, requires a
+client supporting Telnet SSL.
+
+The protocol will try to automatically create the private key and certificate on the server side
+when starting and will warn if this was not possible. These will appear as files ssl.key and
+ssl.cert in mygame/server/.
+
+"""
+importos
+
+try:
+ fromOpenSSLimportcrypto
+ fromtwisted.internetimportsslastwisted_ssl
+exceptImportErroraserror:
+ errstr="""
+{err}
+ Telnet-SSL requires the PyOpenSSL library and dependencies:
+
+ pip install pyopenssl pycrypto enum pyasn1 service_identity
+
+ Stop and start Evennia again. If no certificate can be generated, you'll
+ get a suggestion for a (linux) command to generate this locally.
+
+ """
+ raiseImportError(errstr.format(err=error))
+
+fromdjango.confimportsettings
+fromevennia.server.portal.telnetimportTelnetProtocol
+
+_GAME_DIR=settings.GAME_DIR
+
+_PRIVATE_KEY_LENGTH=2048
+_PRIVATE_KEY_FILE=os.path.join(_GAME_DIR,"server","ssl.key")
+_PUBLIC_KEY_FILE=os.path.join(_GAME_DIR,"server","ssl-public.key")
+_CERTIFICATE_FILE=os.path.join(_GAME_DIR,"server","ssl.cert")
+_CERTIFICATE_EXPIRE=365*24*60*60*20# 20 years
+_CERTIFICATE_ISSUER={
+ "C":"EV",
+ "ST":"Evennia",
+ "L":"Evennia",
+ "O":"Evennia Security",
+ "OU":"Evennia Department",
+ "CN":"evennia",
+}
+
+# messages
+
+NO_AUTOGEN=f"""
+Evennia could not auto-generate the SSL private- and public keys ({{err}}).
+If this error persists, create them manually (using the tools for your OS). The files
+should be placed and named like this:
+{_PRIVATE_KEY_FILE}
+{_PUBLIC_KEY_FILE}
+"""
+
+NO_AUTOCERT="""
+Evennia's could not auto-generate the SSL certificate ({{err}}).
+The private key already exists here:
+{_PRIVATE_KEY_FILE}
+If this error persists, create the certificate manually (using the private key and
+the tools for your OS). The file should be placed and named like this:
+{_CERTIFICATE_FILE}
+"""
+
+
+
[docs]classSSLProtocol(TelnetProtocol):
+ """
+ Communication is the same as telnet, except data transfer
+ is done with encryption set up by the portal at start time.
+
+ """
+
+
[docs]defgetSSLContext():
+ """
+ This is called by the portal when creating the SSL context
+ server-side.
+
+ Returns:
+ ssl_context (tuple): A key and certificate that is either
+ existing previously or created on the fly.
+
+ """
+
+ ifverify_or_create_SSL_key_and_cert(_PRIVATE_KEY_FILE,_CERTIFICATE_FILE):
+ returntwisted_ssl.DefaultOpenSSLContextFactory(_PRIVATE_KEY_FILE,_CERTIFICATE_FILE)
+ else:
+ returnNone
[docs]deftest_plain_ansi(self):
+ """
+ Test that printable characters do not get mangled.
+ """
+ irc_ansi=irc.parse_ansi_to_irc(string.printable)
+ ansi_irc=irc.parse_irc_to_ansi(string.printable)
+ self.assertEqual(irc_ansi,string.printable)
+ self.assertEqual(ansi_irc,string.printable)
[docs]deftest_identity(self):
+ """
+ Test that the composition of the function and
+ its inverse gives the correct string.
+ """
+
+ s=r"|wthis|Xis|gis|Ma|C|complex|*string"
+
+ self.assertEqual(irc.parse_irc_to_ansi(irc.parse_ansi_to_irc(s)),s)
+"""
+TTYPE (MTTS) - Mud Terminal Type Standard
+
+This module implements the TTYPE telnet protocol as per
+http://tintin.sourceforge.net/mtts/. It allows the server to ask the
+client about its capabilities. If the client also supports TTYPE, it
+will return with information such as its name, if it supports colour
+etc. If the client does not support TTYPE, this will be ignored.
+
+All data will be stored on the protocol's protocol_flags dictionary,
+under the 'TTYPE' key.
+
+"""
+
+# telnet option codes
+TTYPE=bytes([24])# b"\x18"
+IS=bytes([0])# b"\x00"
+SEND=bytes([1])# b"\x01"
+
+# terminal capabilities and their codes
+MTTS=[
+ (128,"PROXY"),
+ (64,"SCREENREADER"),
+ (32,"OSC_COLOR_PALETTE"),
+ (16,"MOUSE_TRACKING"),
+ (8,"XTERM256"),
+ (4,"UTF-8"),
+ (2,"VT100"),
+ (1,"ANSI"),
+]
+
+
+
[docs]classTtype:
+ """
+ Handles ttype negotiations. Called and initiated by the
+ telnet protocol.
+
+ """
+
+
[docs]def__init__(self,protocol):
+ """
+ Initialize ttype by storing protocol on ourselves and calling
+ the client to see if it supporst ttype.
+
+ Args:
+ protocol (Protocol): The protocol instance.
+
+ Notes:
+ The `self.ttype_step` indicates how far in the data
+ retrieval we've gotten.
+
+ """
+ self.ttype_step=0
+ self.protocol=protocol
+ # we set FORCEDENDLINE for clients not supporting ttype
+ self.protocol.protocol_flags["FORCEDENDLINE"]=True
+ self.protocol.protocol_flags["TTYPE"]=False
+ # is it a safe bet to assume ANSI is always supported?
+ self.protocol.protocol_flags["ANSI"]=True
+ # setup protocol to handle ttype initialization and negotiation
+ self.protocol.negotiationMap[TTYPE]=self.will_ttype
+ # ask if client will ttype, connect callback if it does.
+ self.protocol.do(TTYPE).addCallbacks(self.will_ttype,self.wont_ttype)
+
+
[docs]defwont_ttype(self,option):
+ """
+ Callback if ttype is not supported by client.
+
+ Args:
+ option (Option): Not used.
+
+ """
+ self.protocol.protocol_flags["TTYPE"]=False
+ self.protocol.handshake_done()
+
+
[docs]defwill_ttype(self,option):
+ """
+ Handles negotiation of the ttype protocol once the client has
+ confirmed that it will respond with the ttype protocol.
+
+ Args:
+ option (Option): Not used.
+
+ Notes:
+ The negotiation proceeds in several steps, each returning a
+ certain piece of information about the client. All data is
+ stored on protocol.protocol_flags under the TTYPE key.
+
+ """
+ options=self.protocol.protocol_flags
+
+ ifoptionsandoptions.get("TTYPE",False)orself.ttype_step>3:
+ return
+
+ try:
+ option=b"".join(option).lstrip(IS).decode()
+ exceptTypeError:
+ # option is not on a suitable form for joining
+ pass
+
+ ifself.ttype_step==0:
+ # just start the request chain
+ self.protocol.requestNegotiation(TTYPE,SEND)
+
+ elifself.ttype_step==1:
+ # this is supposed to be the name of the client/terminal.
+ # For clients not supporting the extended TTYPE
+ # definition, subsequent calls will just repeat-return this.
+ try:
+ clientname=option.upper()
+ exceptAttributeError:
+ # malformed option (not a string)
+ clientname="UNKNOWN"
+
+ # use name to identify support for xterm256. Many of these
+ # only support after a certain version, but all support
+ # it since at least 4 years. We assume recent client here for now.
+ xterm256=False
+ ifclientname.startswith("MUDLET"):
+ # supports xterm256 stably since 1.1 (2010?)
+ xterm256=clientname.split("MUDLET",1)[1].strip()>="1.1"
+ # Mudlet likes GA's on a prompt line for the prompt trigger to
+ # match, if it's not wanting NOGOAHEAD.
+ ifnotself.protocol.protocol_flags["NOGOAHEAD"]:
+ self.protocol.protocol_flags["NOGOAHEAD"]=True
+ self.protocol.protocol_flags["NOPROMPTGOAHEAD"]=False
+
+ if(
+ clientname.startswith("XTERM")
+ orclientname.endswith("-256COLOR")
+ orclientname
+ in(
+ "ATLANTIS",# > 0.9.9.0 (aug 2009)
+ "CMUD",# > 3.04 (mar 2009)
+ "KILDCLIENT",# > 2.2.0 (sep 2005)
+ "MUDLET",# > beta 15 (sep 2009)
+ "MUSHCLIENT",# > 4.02 (apr 2007)
+ "PUTTY",# > 0.58 (apr 2005)
+ "BEIP",# > 2.00.206 (late 2009) (BeipMu)
+ "POTATO",# > 2.00 (maybe earlier)
+ "TINYFUGUE",# > 4.x (maybe earlier)
+ )
+ ):
+ xterm256=True
+
+ # all clients supporting TTYPE at all seem to support ANSI
+ self.protocol.protocol_flags["ANSI"]=True
+ self.protocol.protocol_flags["XTERM256"]=xterm256
+ self.protocol.protocol_flags["CLIENTNAME"]=clientname
+ self.protocol.requestNegotiation(TTYPE,SEND)
+
+ elifself.ttype_step==2:
+ # this is a term capabilities flag
+ term=option
+ tupper=term.upper()
+ # identify xterm256 based on flag
+ xterm256=(
+ tupper.endswith("-256COLOR")
+ ortupper.endswith("XTERM")# Apple Terminal, old Tintin
+ andnottupper.endswith("-COLOR")# old Tintin, Putty
+ )
+ ifxterm256:
+ self.protocol.protocol_flags["ANSI"]=True
+ self.protocol.protocol_flags["XTERM256"]=xterm256
+ self.protocol.protocol_flags["TERM"]=term
+ # request next information
+ self.protocol.requestNegotiation(TTYPE,SEND)
+
+ elifself.ttype_step==3:
+ # the MTTS bitstring identifying term capabilities
+ ifoption.startswith("MTTS"):
+ option=option[4:].strip()
+ ifoption.isdigit():
+ # a number - determine the actual capabilities
+ option=int(option)
+ support=dict(
+ (capability,True)forbitval,capabilityinMTTSifoption&bitval>0
+ )
+ self.protocol.protocol_flags.update(support)
+ else:
+ # some clients send erroneous MTTS as a string. Add directly.
+ self.protocol.protocol_flags[option.upper()]=True
+
+ self.protocol.protocol_flags["TTYPE"]=True
+ # we must sync ttype once it'd done
+ self.protocol.handshake_done()
+ self.ttype_step+=1
+
+
+
\ No newline at end of file
diff --git a/docs/0.9.5/_modules/evennia/server/portal/webclient.html b/docs/0.9.5/_modules/evennia/server/portal/webclient.html
index cb67ab8f87..f58ab9885a 100644
--- a/docs/0.9.5/_modules/evennia/server/portal/webclient.html
+++ b/docs/0.9.5/_modules/evennia/server/portal/webclient.html
@@ -58,10 +58,8 @@
importreimportjsonimporthtml
-fromtwisted.internet.protocolimportProtocolfromdjango.confimportsettings
-fromevennia.server.sessionimportSession
-fromevennia.utils.utilsimportto_str,mod_import
+fromevennia.utils.utilsimportmod_import,class_from_modulefromevennia.utils.ansiimportparse_ansifromevennia.utils.text2htmlimportparse_htmlfromautobahn.twisted.websocketimportWebSocketServerProtocol
@@ -81,12 +79,13 @@
# called when the browser is navigating away from the pageGOING_AWAY=WebSocketServerProtocol.CLOSE_STATUS_CODE_GOING_AWAY
-STATE_CLOSING=WebSocketServerProtocol.STATE_CLOSING
+_BASE_SESSION_CLASS=class_from_module(settings.BASE_SESSION_CLASS)
-
[docs]classWebSocketClient(WebSocketServerProtocol,_BASE_SESSION_CLASS):""" Implements the server-side of the Websocket connection.
+
"""# nonce value, used to prevent the webclient from erasing the
@@ -198,7 +197,7 @@
# in case anyone wants to expose this functionality later.## sendClose() under autobahn/websocket/interfaces.py
- ret=self.sendClose(CLOSE_NORMAL,reason)
+ self.sendClose(CLOSE_NORMAL,reason)
[docs]defonClose(self,wasClean,code=None,reason=None):"""
@@ -302,8 +301,6 @@
returnelse:return
- # just to be sure
- text=to_str(text)flags=self.protocol_flags
@@ -387,7 +384,6 @@
diff --git a/docs/0.9.5/_modules/evennia/server/portal/webclient_ajax.html b/docs/0.9.5/_modules/evennia/server/portal/webclient_ajax.html
index b88d29992c..e2b6ea1e90 100644
--- a/docs/0.9.5/_modules/evennia/server/portal/webclient_ajax.html
+++ b/docs/0.9.5/_modules/evennia/server/portal/webclient_ajax.html
@@ -56,6 +56,7 @@
The WebClient resource in this module will handle these requests and act as a gateway to sessions connected over the webclient.
+
"""importjsonimportre
@@ -68,7 +69,7 @@
fromdjango.confimportsettingsfromevennia.utils.ansiimportparse_ansifromevennia.utilsimportutils
-fromevennia.utils.utilsimportto_bytes,to_str
+fromevennia.utils.utilsimportto_bytesfromevennia.utils.text2htmlimportparse_htmlfromevennia.serverimportsession
@@ -264,10 +265,13 @@
returnjsonify({"msg":host_string,"csessid":csessid})
[docs]defmode_keepalive(self,request):
-
""" This is called by render_POST when the client is replying to the keepalive.
+
+ Args:
+ request (Request): Incoming request.
+
"""csessid=self.get_client_sessid(request)self.last_alive[csessid]=(time.time(),False)
@@ -543,7 +547,6 @@
diff --git a/docs/0.9.5/_modules/evennia/server/profiling/dummyrunner.html b/docs/0.9.5/_modules/evennia/server/profiling/dummyrunner.html
index d60724411d..7ee582c81c 100644
--- a/docs/0.9.5/_modules/evennia/server/profiling/dummyrunner.html
+++ b/docs/0.9.5/_modules/evennia/server/profiling/dummyrunner.html
@@ -81,8 +81,16 @@
fromtwisted.internetimportreactor,protocolfromtwisted.internet.taskimportLoopingCall
-fromdjango.confimportsettings
-fromevennia.utilsimportmod_import,time_format
+importdjango
+django.setup()
+importevennia# noqa
+evennia._init()
+
+fromdjango.confimportsettings# noqa
+fromevennia.utilsimportmod_import,time_format# noqa
+fromevennia.commands.commandimportCommand# noqa
+fromevennia.commands.cmdsetimportCmdSet# noqa
+fromevennia.utils.ansiimportstrip_ansi# noqa# Load the dummyrunner settings module
@@ -92,8 +100,10 @@
"Error: Dummyrunner could not find settings file at %s"%settings.DUMMYRUNNER_SETTINGS_MODULE)
+IDMAPPER_CACHE_MAXSIZE=settings.IDMAPPER_CACHE_MAXSIZEDATESTRING="%Y%m%d%H%M%S"
+CLIENTS=[]# Settings
@@ -112,18 +122,37 @@
# Port to use, if not specified on command lineTELNET_PORT=DUMMYRUNNER_SETTINGS.TELNET_PORTorsettings.TELNET_PORTS[0]#
-NLOGGED_IN=0
+NCONNECTED=0# client has received a connection
+NLOGIN_SCREEN=0# client has seen the login screen (server responded)
+NLOGGING_IN=0# client starting login procedure
+NLOGGED_IN=0# client has authenticated and logged in
-
-# Messages
+# time when all clients have logged_in
+TIME_ALL_LOGIN=0
+# actions since all logged in
+TOTAL_ACTIONS=0
+TOTAL_LAG_MEASURES=0
+# lag per 30s for all logged in
+TOTAL_LAG=0
+TOTAL_LAG_IN=0
+TOTAL_LAG_OUT=0INFO_STARTING="""
- Dummyrunner starting using {N} dummy account(s). If you don't see
+ Dummyrunner starting using {nclients} dummy account(s). If you don't see any connection messages, make sure that the Evennia server is running.
- Use Ctrl-C to stop/disconnect clients.
+ TELNET_PORT = {port}
+ IDMAPPER_CACHE_MAXSIZE = {idmapper_cache_size} MB
+ TIMESTEP = {timestep} (rate {rate}/s)
+ CHANCE_OF_LOGIN = {chance_of_login}% per time step
+ CHANCE_OF_ACTION = {chance_of_action}% per time step
+ -> avg rate (per client, after login): {avg_rate} cmds/s
+ -> total avg rate (after login): {avg_rate_total} cmds/s
+
+ Use Ctrl-C (or Cmd-C) to stop/disconnect all clients.
+
"""ERROR_NO_MIXIN="""
@@ -138,6 +167,7 @@
to test all commands - change PASSWORD_HASHERS to use a faster (but less safe) algorithm when creating large numbers of accounts at the same time
+ - set LOGIN_THROTTLE/CREATION_THROTTLE=None to disable it If you don't want to use the custom settings of the mixin for some reason, you can change their values manually after the import, or
@@ -209,6 +239,39 @@
"""
+
+
[docs]classCmdDummyRunnerEchoResponse(Command):
+ """
+ Dummyrunner command measuring the round-about response time
+ from sending to receiving a result.
+
+ Usage:
+ dummyrunner_echo_response <timestamp>
+
+ Responds with
+ dummyrunner_echo_response:<timestamp>,<current_time>
+
+ The dummyrunner will send this and then compare the send time
+ with the receive time on both ends.
+
+ """
+ key="dummyrunner_echo_response"
+
+
+
+ def_retry_welcome_screen(self):
+ ifnotself._connectedandnotself._ready:
+ # we have connected but not received anything for 30s.
+ # (unclear why this would be - overload?)
+ # try sending a look to get something to start with
+ self.report("?? retrying welcome screen",self.key)
+ self.sendLine(bytes("look",'utf-8'))
+ # make sure to check again later
+ reactor.callLater(30,self._retry_welcome_screen)
+
+ def_print_statistics(self):
+ globalTIME_ALL_LOGIN,TOTAL_ACTIONS
+ globalTOTAL_LAG,TOTAL_LAG_MEASURES,TOTAL_LAG_IN,TOTAL_LAG_OUT
+
+ tim=time.time()-TIME_ALL_LOGIN
+ avgrate=round(TOTAL_ACTIONS/tim)
+ lag=TOTAL_LAG/(TOTAL_LAG_MEASURESor1)
+ lag_in=TOTAL_LAG_IN/(TOTAL_LAG_MEASURESor1)
+ lag_out=TOTAL_LAG_OUT/(TOTAL_LAG_MEASURESor1)
+
+ TOTAL_ACTIONS=0
+ TOTAL_LAG=0
+ TOTAL_LAG_IN=0
+ TOTAL_LAG_OUT=0
+ TOTAL_LAG_MEASURES=0
+ TIME_ALL_LOGIN=time.time()
+
+ print(f".. running 30s average: ~{avgrate} actions/s "
+ f"lag: {lag:.2}s (in: {lag_in:.2}s, out: {lag_out:.2}s)")
+
+ reactor.callLater(30,self._print_statistics)
[docs]defdataReceived(self,data):"""
@@ -305,15 +418,67 @@
data (str): Incoming data. """
- ifnotself._connectedandnotdata.startswith(chr(255)):
- # wait until we actually get text back (not just telnet
- # negotiation)
- self._connected=True
- # start client tick
- d=LoopingCall(self.step)
- # dissipate exact step by up to +/- 0.5 second
- timestep=TIMESTEP+(-0.5+(random.random()*1.0))
- d.start(timestep,now=True).addErrback(self.error)
+ globalNLOGIN_SCREEN,NLOGGED_IN,NLOGGING_IN,NCONNECTED
+ globalTOTAL_ACTIONS,TIME_ALL_LOGIN
+ globalTOTAL_LAG,TOTAL_LAG_MEASURES,TOTAL_LAG_IN,TOTAL_LAG_OUT
+
+ ifnotdata.startswith(b"\xff"):
+ # regular text, not a telnet command
+
+ ifNCLIENTS==1:
+ print("dummy-client sees:",str(data,"utf-8"))
+
+ ifnotself._connected:
+ # waiting for connection
+ # wait until we actually get text back (not just telnet
+ # negotiation)
+ # start client tick
+ d=LoopingCall(self.step)
+ df=max(abs(TIMESTEP*0.001),min(TIMESTEP/10,0.5))
+ # dither next attempt with random time
+ timestep=TIMESTEP+(-df+(random.random()*df))
+ d.start(timestep,now=True).addErrback(self.error)
+ self.connection_attempt+=1
+
+ self._connected=True
+ NLOGIN_SCREEN+=1
+ NCONNECTED-=1
+ self.report("<- server sent login screen",self.key)
+
+ elifself._loggedin:
+ ifnotself._ready:
+ # logged in, ready to run
+ NLOGGED_IN+=1
+ NLOGGING_IN-=1
+ self._ready=True
+ self.report("== logged in",self.key)
+ ifNLOGGED_IN==NCLIENTSandnotTIME_ALL_LOGIN:
+ # all are logged in! We can start collecting statistics
+ print(".. All clients connected and logged in!")
+ TIME_ALL_LOGIN=time.time()
+ reactor.callLater(30,self._print_statistics)
+
+ elifTIME_ALL_LOGIN:
+ TOTAL_ACTIONS+=1
+
+ try:
+ data=strip_ansi(str(data,"utf-8").strip())
+ ifdata.startswith("dummyrunner_echo_response:"):
+ # handle special lag-measuring command. This returns
+ # dummyrunner_echo_response:<starttime>,<midpointtime>
+ now=time.time()
+ _,data=data.split(":",1)
+ start_time,mid_time=(float(part)forpartindata.split(",",1))
+ lag_in=mid_time-start_time
+ lag_out=now-mid_time
+ total_lag=now-start_time# full round-about time
+
+ TOTAL_LAG+=total_lag
+ TOTAL_LAG_IN+=lag_in
+ TOTAL_LAG_OUT+=lag_out
+ TOTAL_LAG_MEASURES+=1
+ exceptException:
+ pass
[docs]defstep(self):"""
@@ -362,7 +527,7 @@
all "intelligence" of the dummy client. """
- globalNLOGGED_IN
+ globalNLOGGING_IN,NLOGIN_SCREENrand=random.random()
@@ -370,11 +535,13 @@
# no commands ready. Load some.ifnotself._loggedin:
- ifrand<CHANCE_OF_LOGIN:
+ ifrand<CHANCE_OF_LOGINorNLOGGING_IN<10:
+ # lower rate of logins, but not below 1 / s# get the login commandsself._cmdlist=list(makeiter(self._login(self)))
- NLOGGED_IN+=1# this is for book-keeping
- print("connecting client %s (%i/%i)..."%(self.key,NLOGGED_IN,NCLIENTS))
+ NLOGGING_IN+=1# this is for book-keeping
+ NLOGIN_SCREEN-=1
+ self.report("-> create/login",self.key)self._loggedin=Trueelse:# no login yet, so cmdlist not yet set
@@ -388,12 +555,26 @@
# at this point we always have a list of commandsifrand<CHANCE_OF_ACTION:# send to the game
- self.sendLine(str(self._cmdlist.pop(0)))
- self.istep+=1
+ cmd=str(self._cmdlist.pop(0))
+
+ ifcmd.startswith("dummyrunner_echo_response"):
+ # we need to set the timer element as close to
+ # the send as possible
+ cmd=cmd.format(timestamp=time.time())
+
+ self.sendLine(bytes(cmd,'utf-8'))
+ self.action_started=time.time()
+ self.istep+=1
+
+ ifNCLIENTS==1:
+ print(f"dummy-client sent: {cmd}")
-
[docs]def__init__(self,actions):"Setup the factory base (shared by all clients)"
@@ -438,7 +619,7 @@
# setting up all clients (they are automatically started)factory=DummyFactory(actions)foriinrange(NCLIENTS):
- reactor.connectTCP("localhost",TELNET_PORT,factory)
+ reactor.connectTCP("127.0.0.1",TELNET_PORT,factory)# start reactorreactor.run()
diff --git a/docs/0.9.5/_modules/evennia/server/profiling/dummyrunner_settings.html b/docs/0.9.5/_modules/evennia/server/profiling/dummyrunner_settings.html
index 0d34e4faf5..d21b3a3117 100644
--- a/docs/0.9.5/_modules/evennia/server/profiling/dummyrunner_settings.html
+++ b/docs/0.9.5/_modules/evennia/server/profiling/dummyrunner_settings.html
@@ -47,45 +47,47 @@
The settings are global variables:
-TIMESTEP - time in seconds between each 'tick'
-CHANCE_OF_ACTION - chance 0-1 of action happening
-CHANCE_OF_LOGIN - chance 0-1 of login happening
-TELNET_PORT - port to use, defaults to settings.TELNET_PORT
-ACTIONS - see below
+- TIMESTEP - time in seconds between each 'tick'. 1 is a good start.
+- CHANCE_OF_ACTION - chance 0-1 of action happening. Default is 0.5.
+- CHANCE_OF_LOGIN - chance 0-1 of login happening. 0.01 is a good number.
+- TELNET_PORT - port to use, defaults to settings.TELNET_PORT
+- ACTIONS - see belowACTIONS is a tuple
-```
+```python(login_func, logout_func, (0.3, func1), (0.1, func2) ... )
+
```where the first entry is the function to call on first connect, with achance of occurring given by CHANCE_OF_LOGIN. This function is usuallyresponsible for logging in the account. The second entry is alwayscalled when the dummyrunner disconnects from the server and should
-thus issue a logout command. The other entries are tuples (chance,
+thus issue a logout command. The other entries are tuples (chance,func). They are picked randomly, their commonality based on thecumulative chance given (the chance is normalized between all optionsso if will still work also if the given chances don't add up to 1).
-Since each function can return a list of game-command strings, each
-function may result in multiple operations.
+
+The PROFILE variable define pre-made ACTION tuples for convenience.
+
+Each function should return an iterable of one or more command-call
+strings (like "look here"), so each can group multiple command operations.An action-function is called with a "client" argument which is a
-reference to the dummy client currently performing the action. It
-returns a string or a list of command strings to execute. Use the
-client object for optionally saving data between actions.
+reference to the dummy client currently performing the action.The client object has the following relevant properties and methods:- key - an optional client key. This is only used for dummyrunner output.
- Default is "Dummy-<cid>"
+ Default is "Dummy-<cid>"- cid - client id- gid - globally unique id, hashed with time stamp- istep - the current step- exits - an empty list. Can be used to store exit names- objs - an empty list. Can be used to store object names- counter() - returns a unique increasing id, hashed with time stamp
- to make it unique also between dummyrunner instances.
+ to make it unique also between dummyrunner instances.The return should either be a single command string or a tuple ofcommand strings. This list of commands will always be executed every
@@ -93,14 +95,18 @@
(no randomness) and allows for setting up a more complex chain ofcommands (such as creating an account and logging in).
----
+----"""
+importrandom
+importstring
+
# Dummy runner settings# Time between each dummyrunner "tick", in seconds. Each dummy# will be called with this frequency.
-TIMESTEP=2
+TIMESTEP=1
+# TIMESTEP = 0.025 # 40/s# Chance of a dummy actually performing an action on a given tick.# This spreads out usage randomly, like it would be in reality.
@@ -109,7 +115,7 @@
# Chance of a currently unlogged-in dummy performing its login# action every tick. This emulates not all accounts logging in# at exactly the same time.
-CHANCE_OF_LOGIN=1.0
+CHANCE_OF_LOGIN=0.01# Which telnet port to connect to. If set to None, uses the first# default telnet port of the running server.
@@ -120,9 +126,10 @@
# some convenient templates
-DUMMY_NAME="Dummy-%s"
-DUMMY_PWD="password-%s"
-START_ROOM="testing_room_start_%s"
+DUMMY_NAME="Dummy_{gid}"
+DUMMY_PWD=(''.join(random.choice(string.ascii_letters+string.digits)
+ for_inrange(20))+"-{gid}")
+START_ROOM="testing_room_start_{gid}"ROOM_TEMPLATE="testing_room_%s"EXIT_TEMPLATE="exit_%s"OBJ_TEMPLATE="testing_obj_%s"
@@ -135,42 +142,45 @@
# login/logout
-
[docs]defc_login(client):"logins to the game"# we always use a new client name
- cname=DUMMY_NAME%client.gid
- cpwd=DUMMY_PWD%client.gid
+ cname=DUMMY_NAME.format(gid=client.gid)
+ cpwd=DUMMY_PWD.format(gid=client.gid)
+ room_name=START_ROOM.format(gid=client.gid)
- # set up for digging a first room (to move to and keep the
- # login room clean)
- roomname=ROOM_TEMPLATE%client.counter()
- exitname1=EXIT_TEMPLATE%client.counter()
- exitname2=EXIT_TEMPLATE%client.counter()
- client.exits.extend([exitname1,exitname2])
+ # we assign the dummyrunner cmdsert to ourselves so # we can use special commands
+ add_cmdset=(
+ "py from evennia.server.profiling.dummyrunner import DummyRunnerCmdSet;"
+ "self.cmdset.add(DummyRunnerCmdSet, persistent=False)"
+ )
+ # create character, log in, then immediately dig a new location and
+ # teleport it (to keep the login room clean)cmds=(
- "create %s%s"%(cname,cpwd),
- "connect %s%s"%(cname,cpwd),
- "@dig %s"%START_ROOM%client.gid,
- "@teleport %s"%START_ROOM%client.gid,
- "@dig %s = %s, %s"%(roomname,exitname1,exitname2),
+ f"create {cname}{cpwd}",
+ f"connect {cname}{cpwd}",
+ f"dig {room_name}",
+ f"teleport {room_name}",
+ add_cmdset,)returncmds
[docs]defc_creates_obj(client):
@@ -222,10 +232,10 @@
objname=OBJ_TEMPLATE%client.counter()client.objs.append(objname)cmds=(
- "@create %s"%objname,
- '@desc %s = "this is a test object'%objname,
- "@set %s/testattr = this is a test attribute value."%objname,
- "@set %s/testattr2 = this is a second test attribute."%objname,
+ "create %s"%objname,
+ 'desc %s = "this is a test object'%objname,
+ "set %s/testattr = this is a test attribute value."%objname,
+ "set %s/testattr2 = this is a second test attribute."%objname,)returncmds
@@ -234,16 +244,14 @@
"creates example button, storing name on client"objname=TOBJ_TEMPLATE%client.counter()client.objs.append(objname)
- cmds=("@create %s:%s"%(objname,TOBJ_TYPECLASS),"@desc %s = test red button!"%objname)
+ cmds=("create %s:%s"%(objname,TOBJ_TYPECLASS),"desc %s = test red button!"%objname)returncmds
[docs]defc_moves(client):"moves to a previously created room, using the stored exits"cmds=client.exits# try all exits - finally one will work
- return"look"ifnotcmdselsecmds
+ return("look",)ifnotcmdselsecmds
[docs]defc_moves_n(client):"move through north exit if available"
- return"north"
+ return("north",)
[docs]defc_moves_s(client):"move through south exit if available"
- return"south"
+ return("south",)
-# Action tuple (required)
-#
-# This is a tuple of client action functions. The first element is the
-# function the client should use to log into the game and move to
-# STARTROOM . The second element is the logout command, for cleanly
-# exiting the mud. The following elements are 2-tuples of (probability,
-# action_function). The probablities should normally sum up to 1,
-# otherwise the system will normalize them.
+
[docs]defc_measure_lag(client):
+ """
+ Special dummyrunner command, injected in c_login. It measures
+ response time. Including this in the ACTION tuple will give more
+ dummyrunner output about just how fast commands are being processed.
+
+ The dummyrunner will treat this special and inject the
+ {timestamp} just before sending.
+
+ """
+ return("dummyrunner_echo_response {timestamp}",)
@@ -137,10 +135,10 @@
self.assertEqual(c_creates_obj(self.client),(
- "@create %s"%objname,
- '@desc %s = "this is a test object'%objname,
- "@set %s/testattr = this is a test attribute value."%objname,
- "@set %s/testattr2 = this is a second test attribute."%objname,
+ "create %s"%objname,
+ 'desc %s = "this is a test object'%objname,
+ "set %s/testattr = this is a test attribute value."%objname,
+ "set %s/testattr2 = this is a second test attribute."%objname,),)self.assertEqual(self.client.objs,[objname])
@@ -151,7 +149,7 @@
typeclass_name="contrib.tutorial_examples.red_button.RedButton"self.assertEqual(c_creates_button(self.client),
- ("@create %s:%s"%(objname,typeclass_name),"@desc %s = test red button!"%objname),
+ ("create %s:%s"%(objname,typeclass_name),"desc %s = test red button!"%objname),)self.assertEqual(self.client.objs,[objname])self.clear_client_lists()
"""
-This module implements the main Evennia server process, the core of
-the game engine.
+This module implements the main Evennia server process, the core of the game
+engine.
-This module should be started with the 'twistd' executable since it
-sets up all the networking features. (this is done automatically
-by evennia/server/server_runner.py).
+This module should be started with the 'twistd' executable since it sets up all
+the networking features. (this is done automatically by
+evennia/server/server_runner.py)."""importtime
@@ -63,6 +63,7 @@
django.setup()importevennia
+importimportlibevennia._init()
@@ -73,11 +74,9 @@
fromevennia.accounts.modelsimportAccountDBfromevennia.scripts.modelsimportScriptDBfromevennia.server.modelsimportServerConfig
-fromevennia.serverimportinitial_setupfromevennia.utils.utilsimportget_evennia_version,mod_import,make_iterfromevennia.utilsimportlogger
-fromevennia.commsimportchannelhandlerfromevennia.server.sessionhandlerimportSESSIONSfromdjango.utils.translationimportgettextas_
@@ -184,12 +183,6 @@
if_MAINTENANCE_COUNT%5==0:# check cache size every 5 minutes_FLUSH_CACHE(_IDMAPPER_CACHE_MAXSIZE)
- if_MAINTENANCE_COUNT%60==0:
- # validate scripts every hour
- evennia.ScriptDB.objects.validate()
- if_MAINTENANCE_COUNT%61==0:
- # validate channels off-sync with scripts
- evennia.CHANNEL_HANDLER.update()if_MAINTENANCE_COUNT%(60*7)==0:# drop database connection every 7 hrs to avoid default timeouts on MySQL# (see https://github.com/evennia/evennia/issues/1376)
@@ -216,12 +209,13 @@
# ------------------------------------------------------------
-
[docs]classEvennia:""" The main Evennia server handler. This object sets up the database and tracks and interlinks all the twisted network services that make up evennia.
+
"""
[docs]def__init__(self,application):
@@ -246,12 +240,6 @@
self.start_time=time.time()
- # initialize channelhandler
- try:
- channelhandler.CHANNELHANDLER.update()
- exceptOperationalError:
- print("channelhandler couldn't update - db not set up")
-
# wrap the SIGINT handler to make sure we empty the threadpool# even when we reload and we have long-running requests in queue.# this is necessary over using Twisted's signal handler.
@@ -297,6 +285,7 @@
This allows for changing default cmdset locations and default typeclasses in the settings file and have them auto-update all already existing objects.
+
"""globalINFO_DICT
@@ -386,6 +375,7 @@
Once finished the last_initial_setup_step is set to -1. """globalINFO_DICT
+ initial_setup=importlib.import_module(settings.INITIAL_SETUP_MODULE)last_initial_setup_step=ServerConfig.objects.conf("last_initial_setup_step")ifnotlast_initial_setup_step:# None is only returned if the config does not exist,
@@ -444,18 +434,17 @@
""" Shuts down the server from inside it.
- Keyword Args:
- mode (str): Sets the server restart mode:
- - 'reload': server restarts, no "persistent" scripts
- are stopped, at_reload hooks called.
- - 'reset' - server restarts, non-persistent scripts stopped,
- at_shutdown hooks called but sessions will not
- be disconnected.
- -'shutdown' - like reset, but server will not auto-restart.
- _reactor_stopping: This is set if server is stopped by a kill
- command OR this method was already called
- once - in both cases the reactor is dead/stopping already.
-
+ mode - sets the server restart mode.
+ - 'reload' - server restarts, no "persistent" scripts
+ are stopped, at_reload hooks called.
+ - 'reset' - server restarts, non-persistent scripts stopped,
+ at_shutdown hooks called but sessions will not
+ be disconnected.
+ - 'shutdown' - like reset, but server will not auto-restart.
+ _reactor_stopping - this is set if server is stopped by a kill
+ command OR this method was already called
+ once - in both cases the reactor is
+ dead/stopping already. """if_reactor_stoppingandhasattr(self,"shutdown_complete"):# this means we have already passed through this method
@@ -472,9 +461,9 @@
yield[o.at_server_reload()foroinObjectDB.get_all_cached_instances()]yield[p.at_server_reload()forpinAccountDB.get_all_cached_instances()]yield[
- (s.pause(manual_pause=False),s.at_server_reload())
+ (s._pause_task(auto_pause=True),s.at_server_reload())forsinScriptDB.get_all_cached_instances()
- ifs.idand(s.is_activeors.attributes.has("_manual_pause"))
+ ifs.idands.is_active]yieldself.sessions.all_sessions_portal_sync()self.at_server_reload_stop()
@@ -498,11 +487,9 @@
]yieldObjectDB.objects.clear_all_sessids()yield[
- (
- s.pause(manual_pause=s.attributes.get("_manual_pause",False)),
- s.at_server_shutdown(),
- )
+ (s._pause_task(auto_pause=True),s.at_server_shutdown())forsinScriptDB.get_all_cached_instances()
+ ifs.idands.is_active]ServerConfig.objects.conf("server_restart_mode","reset")self.at_server_cold_stop()
@@ -527,7 +514,10 @@
ServerConfig.objects.conf("runtime",_GAMETIME_MODULE.runtime())
[docs]defget_info_dict(self):
- "Return the server info, for display."
+ """
+ Return the server info, for display.
+
+ """returnINFO_DICT
# server start/stop hooks
@@ -536,6 +526,7 @@
""" This is called every time the server starts up, regardless of how it was shut down.
+
"""ifSERVER_STARTSTOP_MODULE:SERVER_STARTSTOP_MODULE.at_server_start()
@@ -544,6 +535,7 @@
""" This is called just before a server is shut down, regardless of it is fore a reload, reset or shutdown.
+
"""ifSERVER_STARTSTOP_MODULE:SERVER_STARTSTOP_MODULE.at_server_stop()
@@ -551,6 +543,7 @@
[docs]defat_server_reload_start(self):""" This is called only when server starts back up after a reload.
+
"""ifSERVER_STARTSTOP_MODULE:SERVER_STARTSTOP_MODULE.at_server_reload_start()
@@ -561,7 +554,7 @@
after reconnecting. Args:
- mode (str): One of reload, reset or shutdown.
+ mode (str): One of 'reload', 'reset' or 'shutdown'. """
@@ -573,9 +566,8 @@
TICKER_HANDLER.restore(mode=="reload")
- # after sync is complete we force-validate all scripts
- # (this also starts any that didn't yet start)
- ScriptDB.objects.validate(init_mode=mode)
+ # Un-pause all scripts, stop non-persistent timers
+ ScriptDB.objects.update_scripts_after_server_start()# start the task handlerfromevennia.scripts.taskhandlerimportTASK_HANDLER
@@ -591,11 +583,10 @@
god_account=AccountDB.objects.get(id=1)# mudinfomudinfo_chan=settings.CHANNEL_MUDINFO
- ifnotmudinfo_chan:
- raiseRuntimeError("settings.CHANNEL_MUDINFO must be defined.")
- ifnotChannelDB.objects.filter(db_key=mudinfo_chan["key"]):
- channel=create_channel(**mudinfo_chan)
- channel.connect(god_account)
+ ifmudinfo_chan:
+ ifnotChannelDB.objects.filter(db_key=mudinfo_chan["key"]):
+ channel=create_channel(**mudinfo_chan)
+ channel.connect(god_account)# connectinfoconnectinfo_chan=settings.CHANNEL_MUDINFOifconnectinfo_chan:
@@ -613,6 +604,7 @@
[docs]defat_server_reload_stop(self):""" This is called only time the server stops before a reload.
+
"""ifSERVER_STARTSTOP_MODULE:SERVER_STARTSTOP_MODULE.at_server_reload_stop()
@@ -621,6 +613,7 @@
""" This is called only when the server starts "cold", i.e. after a shutdown or a reset.
+
"""# We need to do this just in case the server was killed in a way where# the normal cleanup operations did not have time to run.
@@ -632,7 +625,7 @@
fromevennia.scripts.modelsimportScriptDBforscriptinScriptDB.objects.filter(db_persistent=False):
- script.stop()
+ script._stop_task()ifGUEST_ENABLED:forguestinAccountDB.objects.all().filter(
@@ -648,6 +641,7 @@
[docs]defat_server_cold_stop(self):""" This is called only when the server goes down due to a shutdown or reset.
+
"""ifSERVER_STARTSTOP_MODULE:SERVER_STARTSTOP_MODULE.at_server_cold_stop()
-
diff --git a/docs/0.9.5/_modules/evennia/server/serversession.html b/docs/0.9.5/_modules/evennia/server/serversession.html
index 0e2e3de87c..ae0b7ba1f9 100644
--- a/docs/0.9.5/_modules/evennia/server/serversession.html
+++ b/docs/0.9.5/_modules/evennia/server/serversession.html
@@ -47,142 +47,22 @@
It is stored on the Server side (as opposed to protocol-specific sessions whichare stored on the Portal side)"""
-importweakrefimporttimefromdjango.utilsimporttimezonefromdjango.confimportsettingsfromevennia.comms.modelsimportChannelDBfromevennia.utilsimportlogger
-fromevennia.utils.utilsimportmake_iter,lazy_property
+fromevennia.utils.utilsimportmake_iter,lazy_property,class_from_modulefromevennia.commands.cmdsethandlerimportCmdSetHandler
-fromevennia.server.sessionimportSessionfromevennia.scripts.monitorhandlerimportMONITOR_HANDLER
+fromevennia.typeclasses.attributesimportAttributeHandler,InMemoryAttributeBackend,DbHolder_GA=object.__getattribute___SA=object.__setattr___ObjectDB=None_ANSI=None
-# i18n
-fromdjango.utils.translationimportgettextas_
-
-# Handlers for Session.db/ndb operation
-
-
-
[docs]classNDbHolder(object):
- """Holder for allowing property access of attributes"""
-
-
[docs]classNAttributeHandler(object):
- """
- NAttributeHandler version without recache protection.
- This stand-alone handler manages non-database saving.
- It is similar to `AttributeHandler` and is used
- by the `.ndb` handler in the same way as `.db` does
- for the `AttributeHandler`.
- """
-
-
[docs]def__init__(self,obj):
- """
- Initialized on the object
- """
- self._store={}
- self.obj=weakref.proxy(obj)
-
-
[docs]defhas(self,key):
- """
- Check if object has this attribute or not.
-
- Args:
- key (str): The Nattribute key to check.
-
- Returns:
- has_nattribute (bool): If Nattribute is set or not.
-
- """
- returnkeyinself._store
-
-
[docs]defget(self,key,default=None):
- """
- Get the named key value.
-
- Args:
- key (str): The Nattribute key to get.
-
- Returns:
- the value of the Nattribute.
-
- """
- returnself._store.get(key,default)
-
-
[docs]defadd(self,key,value):
- """
- Add new key and value.
-
- Args:
- key (str): The name of Nattribute to add.
- value (any): The value to store.
-
- """
- self._store[key]=value
-
-
[docs]defremove(self,key):
- """
- Remove Nattribute from storage.
-
- Args:
- key (str): The name of the Nattribute to remove.
-
- """
- ifkeyinself._store:
- delself._store[key]
-
-
[docs]defclear(self):
- """
- Remove all NAttributes from handler.
-
- """
- self._store={}
-
-
[docs]defall(self,return_tuples=False):
- """
- List the contents of the handler.
-
- Args:
- return_tuples (bool, optional): Defines if the Nattributes
- are returns as a list of keys or as a list of `(key, value)`.
-
- Returns:
- nattributes (list): A list of keys `[key, key, ...]` or a
- list of tuples `[(key, value), ...]` depending on the
- setting of `return_tuples`.
-
- """
- ifreturn_tuples:
- return[(key,value)for(key,value)inself._store.items()ifnotkey.startswith("_")]
- return[keyforkeyinself._storeifnotkey.startswith("_")]
[docs]classServerSession(_BASE_SESSION_CLASS):""" This class represents an account's session and is a template for individual protocols to communicate with Evennia.
@@ -202,7 +82,10 @@
"""
[docs]def__init__(self):
- """Initiate to avoid AttributeErrors down the line"""
+ """
+ Initiate to avoid AttributeErrors down the line
+
+ """self.puppet=Noneself.account=Noneself.cmdset_storage_string=""
@@ -216,6 +99,10 @@
cmdset_storage=property(__cmdset_storage_get,__cmdset_storage_set)
+ @property
+ defid(self):
+ returnself.sessid
+
[docs]defat_sync(self):""" This is called whenever a session has been resynced with the
@@ -385,7 +272,7 @@
Update the protocol_flags and sync them with Portal. Keyword Args:
- any: A key:value pair to set in the
+ protocol_flag (any): A key and value to set in the protocol_flags dictionary. Notes:
@@ -417,14 +304,13 @@
the respective inputfuncs. Keyword Args:
- any: Incoming data from protocol on
+ kwargs (any): Incoming data from protocol on the form `{"commandname": ((args), {kwargs}),...}` Notes: This method is here in order to give the user a single place to catch and possibly process all incoming data from the client. It should usually always end by sending this data off to `self.sessionhandler.call_inputfuncs(self, **kwargs)`.
-
"""self.sessionhandler.call_inputfuncs(self,**kwargs)
@@ -434,7 +320,9 @@
Args: text (str): String input.
- kwargs (str or tuple): Send-commands identified
+
+ Keyword Args:
+ any (str or tuple): Send-commands identified by their keys. Or "options", carrying options for the protocol(s).
@@ -473,7 +361,10 @@
self.sessionhandler.data_in(sessionorself,**kwargs)
ndb=property(ndb_get,ndb_set,ndb_del)
@@ -575,7 +470,10 @@
# Mock access method for the session (there is no lock info# at this stage, so we just present a uniform API)
[docs]defaccess(self,*args,**kwargs):
- """Dummy method to mimic the logged-in API."""
+ """
+ Dummy method to mimic the logged-in API.
+
+ """returnTrue
[docs]classSession:""" This class represents a player's session and is a template for both portal- and server-side sessions.
@@ -76,26 +76,6 @@
"""
- # names of attributes that should be affected by syncing.
- _attrs_to_sync=(
- "protocol_key",
- "address",
- "suid",
- "sessid",
- "uid",
- "csessid",
- "uname",
- "logged_in",
- "puid",
- "conn_time",
- "cmd_last",
- "cmd_last_visible",
- "cmd_total",
- "protocol_flags",
- "server_data",
- "cmdset_storage_string",
- )
-
[docs]definit_session(self,protocol_key,address,sessionhandler):""" Initialize the Session. This should be called by the protocol when
@@ -162,9 +142,9 @@
the keys given by self._attrs_to_sync. """
- returndict(
- (key,value)forkey,valueinself.__dict__.items()ifkeyinself._attrs_to_sync
- )
diff --git a/docs/0.9.5/_modules/evennia/server/sessionhandler.html b/docs/0.9.5/_modules/evennia/server/sessionhandler.html
index 0a67cc4cc7..7a61d84708 100644
--- a/docs/0.9.5/_modules/evennia/server/sessionhandler.html
+++ b/docs/0.9.5/_modules/evennia/server/sessionhandler.html
@@ -45,12 +45,12 @@
There are two similar but separate stores of sessions:
- - ServerSessionHandler - this stores generic game sessions
- for the game. These sessions has no knowledge about
- how they are connected to the world.
- - PortalSessionHandler - this stores sessions created by
- twisted protocols. These are dumb connectors that
- handle network communication but holds no game info.
+- ServerSessionHandler - this stores generic game sessions
+ for the game. These sessions has no knowledge about
+ how they are connected to the world.
+- PortalSessionHandler - this stores sessions created by
+ twisted protocols. These are dumb connectors that
+ handle network communication but holds no game info."""importtime
@@ -59,18 +59,19 @@
fromevennia.commands.cmdhandlerimportCMD_LOGINSTARTfromevennia.utils.loggerimportlog_tracefromevennia.utils.utilsimport(
- variable_from_module,is_iter,make_iter,delay,callables_from_module,
+ class_from_module,)
+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
+fromdjango.utils.translationimportgettextas_
-_INLINEFUNC_ENABLED=settings.INLINEFUNC_ENABLED
+_FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED=settings.FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED# delayed imports_AccountDB=None
@@ -79,7 +80,7 @@
_ScriptDB=None_OOB_HANDLER=None
-_ERR_BAD_UTF8="Your client sent an incorrect UTF-8 sequence."
+_ERR_BAD_UTF8=_("Your client sent an incorrect UTF-8 sequence.")
[docs]classDummySession(object):
@@ -88,28 +89,6 @@
DUMMYSESSION=DummySession()
-# AMP signals
-PCONN=chr(1)# portal session connect
-PDISCONN=chr(2)# portal session disconnect
-PSYNC=chr(3)# portal session sync
-SLOGIN=chr(4)# server session login
-SDISCONN=chr(5)# server session disconnect
-SDISCONNALL=chr(6)# server session disconnect all
-SSHUTD=chr(7)# server shutdown
-SSYNC=chr(8)# server session sync
-SCONN=chr(11)# server portal connection (for bots)
-PCONNSYNC=chr(12)# portal post-syncing session
-PDISCONNALL=chr(13)# portal session discnnect all
-SRELOAD=chr(14)# server reloading (have portal start a new server)
-SSTART=chr(15)# server start (portal must already be running anyway)
-PSHUTD=chr(16)# portal (+server) shutdown
-SSHUTD=chr(17)# server shutdown
-PSTATUS=chr(18)# ping server or portal status
-SRESET=chr(19)# server shutdown in reset mode
-
-# i18n
-fromdjango.utils.translationimportgettextas_
-
_SERVERNAME=settings.SERVERNAME_MULTISESSION_MODE=settings.MULTISESSION_MODE_IDLE_TIMEOUT=settings.IDLE_TIMEOUT
@@ -117,6 +96,8 @@
_MAX_SERVER_COMMANDS_PER_SECOND=100.0_MAX_SESSION_COMMANDS_PER_SECOND=5.0_MODEL_MAP=None
+_FUNCPARSER=None
+
# input handlers
@@ -133,8 +114,7 @@
global_ServerSession,_AccountDB,_ServerConfig,_ScriptDBifnot_ServerSession:# we allow optional arbitrary serversession class for overloading
- modulename,classname=settings.SERVER_SESSION_CLASS.rsplit(".",1)
- _ServerSession=variable_from_module(modulename,classname)
+ _ServerSession=class_from_module(settings.SERVER_SESSION_CLASS)ifnot_AccountDB:fromevennia.accounts.modelsimportAccountDBas_AccountDBifnot_ServerConfig:
@@ -142,10 +122,10 @@
ifnot_ScriptDB:fromevennia.scripts.modelsimportScriptDBas_ScriptDB# including once to avoid warnings in Python syntax checkers
- assert_ServerSession
- assert_AccountDB
- assert_ServerConfig
- assert_ScriptDB
+ assert_ServerSession,"ServerSession class could not load"
+ assert_AccountDB,"AccountDB class could not load"
+ assert_ServerConfig,"ServerConfig class could not load"
+ assert_ScriptDB,"ScriptDB class c ould not load"# -----------------------------------------------------------
@@ -160,24 +140,36 @@
"""def__getitem__(self,key):
- "Clean out None-sessions automatically."
+ """
+ Clean out None-sessions automatically.
+
+ """ifNoneinself:delself[None]returnsuper().__getitem__(key)
[docs]defget(self,key,default=None):
- "Clean out None-sessions automatically."
+ """
+ Clean out None-sessions automatically.
+
+ """ifNoneinself:delself[None]returnsuper().get(key,default)
def__setitem__(self,key,value):
- "Don't assign None sessions"
+ """
+ Don't assign None sessions"
+
+ """ifkeyisnotNone:super().__setitem__(key,value)def__contains__(self,key):
- "None-keys are not accepted."
+ """
+ None-keys are not accepted.
+
+ """returnFalseifkeyisNoneelsesuper().__contains__(key)
[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.
- kwargs (dict) Each keyword represents a
- send-instruction, with the keyword itself being the name
- of the instruction (like "text"). Suitable values for each
- keyword are:
- ::
-
- arg -> [[arg], {}]
- [args] -> [[args], {}]
- {kwargs} -> [[], {kwargs}]
- [args, {kwargs}] -> [[arg], {kwargs}]
- [[args], {kwargs}] -> [[args], {kwargs}]
+ kwargs (dict) Each keyword represents a send-instruction, with the keyword itself being
+ the name of the instruction (like "text"). Suitable values for each keyword are:
+ - arg -> [[arg], {}]
+ - [args] -> [[args], {}]
+ - {kwargs} -> [[], {kwargs}]
+ - [args, {kwargs}] -> [[arg], {kwargs}]
+ - [[args], {kwargs}] -> [[args], {kwargs}] 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
- applied.
+ where the keys, args and kwargs have all been converted to
+ 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_MODULES,
+ raise_errors=True)
+
options=kwargs.pop("options",None)or{}raw=options.get("raw",False)strip_inlinefunc=options.get("strip_inlinefunc",False)
@@ -253,7 +248,10 @@
returndatadef_validate(data):
- "Helper function to convert data to AMP-safe (picketable) values"
+ """
+ Helper function to convert data to AMP-safe (picketable) values"
+
+ """ifisinstance(data,dict):newdict={}forkey,partindata.items():
@@ -264,9 +262,11 @@
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_ENABLED
+ andnotrawandisinstance(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(
@@ -314,14 +314,11 @@
[docs]classServerSessionHandler(SessionHandler):"""
- This object holds the stack of sessions active in the game at
- any time.
+ This object holds the stack of sessions active in the game at any time.
- A session register with the handler in two steps, first by
- registering itself with the connect() method. This indicates an
- non-authenticated session. Whenever the session is authenticated
- the session together with the related account is sent to the login()
- method.
+ A session register with the handler in two steps, first by registering itself with the connect()
+ method. This indicates an non-authenticated session. Whenever the session is authenticated the
+ session together with the related account is sent to the login() method. """
@@ -366,7 +363,7 @@
sess.load_sync_data(portalsessiondata)sess.at_sync()# validate all scripts
- _ScriptDB.objects.validate()
+ # _ScriptDB.objects.validate()self[sess.sessid]=sessifsess.logged_inandsess.uid:
@@ -494,7 +491,7 @@
"""self.server.amp_protocol.send_AdminServer2Portal(
- DUMMYSESSION,operation=SCONN,protocol_path=protocol_path,config=configdict
+ DUMMYSESSION,operation=amp.SCONN,protocol_path=protocol_path,config=configdict)
[docs]defportal_restart_server(self):
@@ -502,14 +499,14 @@
Called by server when reloading. We tell the portal to start a new server instance. """
- self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION,operation=SRELOAD)
[docs]defportal_reset_server(self):""" Called by server when reloading. We tell the portal to start a new server instance. """
- self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION,operation=SRESET)
[docs]deflogin(self,session,account,force=False,testmode=False):"""
- Log in the previously unloggedin session and the account we by
- now should know is connected to it. After this point we assume
- the session to be logged in one way or another.
+ Log in the previously unloggedin session and the account we by now should know is connected
+ to it. After this point we assume the session to be logged in one way or another. Args: session (Session): The Session to authenticate.
@@ -565,7 +561,7 @@
# sync the portal to the sessionifnottestmode:self.server.amp_protocol.send_AdminServer2Portal(
- session,operation=SLOGIN,sessiondata={"logged_in":True,"uid":session.uid}
+ session,operation=amp.SLOGIN,sessiondata={"logged_in":True,"uid":session.uid})account.at_post_login(session=session)ifnsess<2:
@@ -610,7 +606,7 @@
ifsync_portal:# inform portal that session should be closed.self.server.amp_protocol.send_AdminServer2Portal(
- session,operation=SDISCONN,reason=reason
+ session,operation=amp.SDISCONN,reason=reason)
[docs]defdisconnect_all_sessions(self,reason="You have been disconnected."):
@@ -661,7 +657,7 @@
delsession# tell portal to disconnect all sessionsself.server.amp_protocol.send_AdminServer2Portal(
- DUMMYSESSION,operation=SDISCONNALL,reason=reason
+ DUMMYSESSION,operation=amp.SDISCONNALL,reason=reason)
[docs]defdisconnect_duplicate_sessions(
@@ -680,7 +676,8 @@
# mean connecting from the same host would not catch duplicatessid=id(curr_session)doublet_sessions=[
- sessforsessinself.values()ifsess.logged_inandsess.uid==uidandid(sess)!=sid
+ sessforsessinself.values()
+ ifsess.logged_inandsess.uid==uidandid(sess)!=sid]forsessionindoublet_sessions:
@@ -790,8 +787,8 @@
puppet (Object): Object puppeted Returns.
- sessions (Session or list): Can be more than one of Object is controlled by
- more than one Session (MULTISESSION_MODE > 1).
+ sessions (Session or list): Can be more than one of Object is controlled by more than
+ one Session (MULTISESSION_MODE > 1). """sessions=puppet.sessid.get()
@@ -804,8 +801,9 @@
Given a client identification hash (for session types that offer them) return all sessions with a matching hash.
- Args:
+ Args csessid (str): The session hash.
+
Returns: sessions (list): The sessions with matching .csessid, if any.
@@ -868,9 +866,9 @@
[docs]defcall_inputfuncs(self,session,**kwargs):"""
- Split incoming data into its inputfunc counterparts.
- This should be called by the serversession.data_in
- as `sessionhandler.call_inputfunc(self, **kwargs)`.
+ Split incoming data into its inputfunc counterparts. This should be
+ called by the `serversession.data_in` as
+ `sessionhandler.call_inputfunc(self, **kwargs)`. We also intercept OOB communication here.
@@ -878,8 +876,8 @@
sessions (Session): Session. Keyword Args:
- kwargs (any): Incoming data from protocol on
- the form `{"commandname": ((args), {kwargs}),...}`
+ any (tuple): Incoming data from protocol, each
+ on the form `commandname=((args), {kwargs})`. """
@@ -900,7 +898,11 @@
log_trace()
-SESSION_HANDLER=ServerSessionHandler()
+# import class from settings
+_SESSION_HANDLER_CLASS=class_from_module(settings.SERVER_SESSION_HANDLER_CLASS)
+
+# Instantiate class. These globals are used to provide singleton-like behavior.
+SESSION_HANDLER=_SESSION_HANDLER_CLASS()SESSIONS=SESSION_HANDLER# legacy
@@ -939,7 +941,6 @@
[docs]classThrottle:""" Keeps a running count of failed actions per IP address.
@@ -53,28 +55,58 @@
This version of the throttle is usable by both the terminal server as well as the web server, imposes limits on memory consumption by using deques
- with length limits instead of open-ended lists, and removes sparse keys when
- no recent failures have been recorded.
+ with length limits instead of open-ended lists, and uses native Django
+ caches for automatic key eviction and persistence configurability. """
- error_msg="Too many failed attempts; you must wait a few minutes before trying again."
+ error_msg=_("Too many failed attempts; you must wait a few minutes before trying again.")
[docs]def__init__(self,**kwargs):""" Allows setting of throttle parameters. Keyword Args:
- limit (int): Max number of failures before imposing limiter
+ name (str): Name of this throttle.
+ limit (int): Max number of failures before imposing limiter. If `None`,
+ the throttle is disabled. timeout (int): number of timeout seconds after max number of tries has been reached. cache_size (int): Max number of attempts to record per IP within a rolling window; this is NOT the same as the limit after which the throttle is imposed! """
- self.storage=defaultdict(deque)
- self.cache_size=self.limit=kwargs.get("limit",5)
+ try:
+ self.storage=caches['throttle']
+ exceptException:
+ logger.log_trace("Throttle: Errors encountered; using default cache.")
+ self.storage=caches['default']
+
+ self.name=kwargs.get('name','undefined-throttle')
+ self.limit=kwargs.get("limit",5)
+ self.cache_size=kwargs.get('cache_size',self.limit)self.timeout=kwargs.get("timeout",5*60)
+
[docs]defget_cache_key(self,*args,**kwargs):
+ """
+ Creates a 'prefixed' key containing arbitrary terms to prevent key
+ collisions in the same namespace.
+
+ """
+ return'-'.join((self.name,*args))
+
+
[docs]deftouch(self,key,*args,**kwargs):
+ """
+ Refreshes the timeout on a given key and ensures it is recorded in the
+ key register.
+
+ Args:
+ key(str): Key of entry to renew.
+
+ """
+ cache_key=self.get_cache_key(key)
+ ifself.storage.touch(cache_key,self.timeout):
+ self.record_key(key)
+
[docs]defget(self,ip=None):""" Convenience function that returns the storage table, or part of.
@@ -91,9 +123,18 @@
"""ifip:
- returnself.storage.get(ip,deque(maxlen=self.cache_size))
+ cache_key=self.get_cache_key(str(ip))
+ returnself.storage.get(cache_key,deque(maxlen=self.cache_size))else:
- returnself.storage
[docs]defupdate(self,ip,failmsg="Exceeded threshold."):"""
@@ -108,14 +149,17 @@
None """
+ cache_key=self.get_cache_key(ip)
+
# Get current statuspreviously_throttled=self.check(ip)
- # Enforce length limits
- ifnotself.storage[ip].maxlen:
- self.storage[ip]=deque(maxlen=self.cache_size)
+ # Get previous failures, if any
+ entries=self.storage.get(cache_key,[])
+ entries.append(time.time())
- self.storage[ip].append(time.time())
+ # Store updated record
+ self.storage.set(cache_key,deque(entries,maxlen=self.cache_size),self.timeout)# See if this update caused a change in statuscurrently_throttled=self.check(ip)
@@ -123,9 +167,63 @@
# If this makes it engage, log a single activation eventifnotpreviously_throttledandcurrently_throttled:logger.log_sec(
- "Throttle Activated: %s (IP: %s, %i hits in %i seconds.)"
- %(failmsg,ip,self.limit,self.timeout)
- )
[docs]defremove(self,ip,*args,**kwargs):
+ """
+ Clears data stored for an IP from the throttle.
+
+ Args:
+ ip(str): IP to clear.
+
+ """
+ exists=self.get(ip)
+ ifnotexists:
+ returnFalse
+
+ cache_key=self.get_cache_key(ip)
+ self.storage.delete(cache_key)
+ self.unrecord_ip(ip)
+
+ # Return True if NOT exists
+ returnnotbool(self.get(ip))
+
+
[docs]defrecord_ip(self,ip,*args,**kwargs):
+ """
+ Tracks keys as they are added to the cache (since there is no way to
+ get a list of keys after-the-fact).
+
+ Args:
+ ip(str): IP being added to cache. This should be the original
+ IP, not the cache-prefixed key.
+
+ """
+ keys_key=self.get_cache_key('keys')
+ keys=self.storage.get(keys_key,set())
+ keys.add(ip)
+ self.storage.set(keys_key,keys,self.timeout)
+ returnTrue
+
+
[docs]defunrecord_ip(self,ip,*args,**kwargs):
+ """
+ Forces removal of a key from the key registry.
+
+ Args:
+ ip(str): IP to remove from list of keys.
+
+ """
+ keys_key=self.get_cache_key('keys')
+ keys=self.storage.get(keys_key,set())
+ try:
+ keys.remove(ip)
+ self.storage.set(keys_key,keys,self.timeout)
+ returnTrue
+ exceptKeyError:
+ returnFalse
[docs]defcheck(self,ip):"""
@@ -141,19 +239,26 @@
False otherwise. """
+ ifself.limitisNone:
+ # throttle is disabled
+ returnFalse
+
now=time.time()ip=str(ip)
+ cache_key=self.get_cache_key(ip)
+
# checking mode
- latest_fails=self.storage[ip]
+ latest_fails=self.storage.get(cache_key)iflatest_failsandlen(latest_fails)>=self.limit:# too many fails recentlyifnow-latest_fails[-1]<self.timeout:# too soon - timeout in play
+ self.touch(cache_key)returnTrueelse:# timeout has passed. clear faillist
- delself.storage[ip]
+ self.remove(ip)returnFalseelse:returnFalse
diff --git a/docs/0.9.5/_modules/evennia/server/validators.html b/docs/0.9.5/_modules/evennia/server/validators.html
index a396964bcc..ff1c97a4c6 100644
--- a/docs/0.9.5/_modules/evennia/server/validators.html
+++ b/docs/0.9.5/_modules/evennia/server/validators.html
@@ -126,8 +126,8 @@
"""return_(
- "%s From a terminal client, you can also use a phrase of multiple words if "
- "you enclose the password in double quotes."%self.policy
+ "{policy} From a terminal client, you can also use a phrase of multiple words if "
+ "you enclose the password in double quotes.".format(policy=self.policy))
@@ -166,7 +166,6 @@
[docs]classTagForm(forms.ModelForm):
- """
- This form overrides the base behavior of the ModelForm that would be used for a
- Tag-through-model. Since the through-models only have access to the foreignkeys of the Tag and
- the Object that they're attached to, we need to spoof the behavior of it being a form that would
- correspond to its tag, or the creation of a tag. Instead of being saved, we'll call to the
- Object's handler, which will handle the creation, change, or deletion of a tag for us, as well
- as updating the handler's cache so that all changes are instantly updated in-game.
- """
-
- tag_key=forms.CharField(
- label="Tag Name",required=True,help_text="This is the main key identifier"
- )
- tag_category=forms.CharField(
- label="Category",
- help_text="Used for grouping tags. Unset (default) gives a category of None",
- required=False,
- )
- tag_type=forms.CharField(
- label="Type",
- help_text='Internal use. Either unset, "alias" or "permission"',
- required=False,
- )
- tag_data=forms.CharField(
- label="Data",
- help_text="Usually unused. Intended for eventual info about the tag itself",
- required=False,
- )
-
-
[docs]def__init__(self,*args,**kwargs):
- """
- If we have a tag, then we'll prepopulate our instance with the fields we'd expect it
- to have based on the tag. tag_key, tag_category, tag_type, and tag_data all refer to
- the corresponding tag fields. The initial data of the form fields will similarly be
- populated.
- """
- super().__init__(*args,**kwargs)
- tagkey=None
- tagcategory=None
- tagtype=None
- tagdata=None
- ifhasattr(self.instance,"tag"):
- tagkey=self.instance.tag.db_key
- tagcategory=self.instance.tag.db_category
- tagtype=self.instance.tag.db_tagtype
- tagdata=self.instance.tag.db_data
- self.fields["tag_key"].initial=tagkey
- self.fields["tag_category"].initial=tagcategory
- self.fields["tag_type"].initial=tagtype
- self.fields["tag_data"].initial=tagdata
- self.instance.tag_key=tagkey
- self.instance.tag_category=tagcategory
- self.instance.tag_type=tagtype
- self.instance.tag_data=tagdata
-
-
[docs]defsave(self,commit=True):
- """
- One thing we want to do here is the or None checks, because forms are saved with an empty
- string rather than null from forms, usually, and the Handlers may handle empty strings
- differently than None objects. So for consistency with how things are handled in game,
- we'll try to make sure that empty form fields will be None, rather than ''.
- """
- # we are spoofing a tag for the Handler that will be called
- # instance = super().save(commit=False)
- instance=self.instance
- instance.tag_key=self.cleaned_data["tag_key"]
- instance.tag_category=self.cleaned_data["tag_category"]orNone
- instance.tag_type=self.cleaned_data["tag_type"]orNone
- instance.tag_data=self.cleaned_data["tag_data"]orNone
- returninstance
-
-
-
[docs]classTagFormSet(forms.BaseInlineFormSet):
- """
- The Formset handles all the inline forms that are grouped together on the change page of the
- corresponding object. All the tags will appear here, and we'll save them by overriding the
- formset's save method. The forms will similarly spoof their save methods to return an instance
- which hasn't been saved to the database, but have the relevant fields filled out based on the
- contents of the cleaned form. We'll then use that to call to the handler of the corresponding
- Object, where the handler is an AliasHandler, PermissionsHandler, or TagHandler, based on the
- type of tag.
- """
-
-
[docs]defsave(self,commit=True):
- defget_handler(finished_object):
- related=getattr(finished_object,self.related_field)
- try:
- tagtype=finished_object.tag_type
- exceptAttributeError:
- tagtype=finished_object.tag.db_tagtype
- iftagtype=="alias":
- handler_name="aliases"
- eliftagtype=="permission":
- handler_name="permissions"
- else:
- handler_name="tags"
- returngetattr(related,handler_name)
-
- instances=super().save(commit=False)
- # self.deleted_objects is a list created when super of save is called, we'll remove those
- forobjinself.deleted_objects:
- handler=get_handler(obj)
- handler.remove(obj.tag_key,category=obj.tag_category)
- forinstanceininstances:
- handler=get_handler(instance)
- handler.add(instance.tag_key,category=instance.tag_category,data=instance.tag_data)
-
-
-
[docs]classTagInline(admin.TabularInline):
- """
- A handler for inline Tags. This class should be subclassed in the admin of your models,
- and the 'model' and 'related_field' class attributes must be set. model should be the
- through model (ObjectDB_db_tag', for example), while related field should be the name
- of the field on that through model which points to the model being used: 'objectdb',
- 'msg', 'accountdb', etc.
- """
-
- # Set this to the through model of your desired M2M when subclassing.
- model=None
- form=TagForm
- formset=TagFormSet
- related_field=None# Must be 'objectdb', 'accountdb', 'msg', etc. Set when subclassing
- # raw_id_fields = ('tag',)
- # readonly_fields = ('tag',)
- extra=0
-
-
[docs]defget_formset(self,request,obj=None,**kwargs):
- """
- get_formset has to return a class, but we need to make the class that we return
- know about the related_field that we'll use. Returning the class itself rather than
- a proxy isn't threadsafe, since it'd be the base class and would change if multiple
- people used the admin at the same time
- """
- formset=super().get_formset(request,obj,**kwargs)
-
- classProxyFormset(formset):
- pass
-
- ProxyFormset.related_field=self.related_field
- returnProxyFormset
-
-
-
[docs]classAttributeForm(forms.ModelForm):
- """
- This form overrides the base behavior of the ModelForm that would be used for a Attribute-through-model.
- Since the through-models only have access to the foreignkeys of the Attribute and the Object that they're
- attached to, we need to spoof the behavior of it being a form that would correspond to its Attribute,
- or the creation of an Attribute. Instead of being saved, we'll call to the Object's handler, which will handle
- the creation, change, or deletion of an Attribute for us, as well as updating the handler's cache so that all
- changes are instantly updated in-game.
- """
-
- attr_key=forms.CharField(
- label="Attribute Name",required=False,initial="Enter Attribute Name Here"
- )
- attr_category=forms.CharField(
- label="Category",help_text="type of attribute, for sorting",required=False,max_length=128
- )
- attr_value=PickledFormField(label="Value",help_text="Value to pickle/save",required=False)
- attr_type=forms.CharField(
- label="Type",
- help_text='Internal use. Either unset (normal Attribute) or "nick"',
- required=False,
- max_length=16,
- )
- attr_lockstring=forms.CharField(
- label="Locks",
- required=False,
- help_text="Lock string on the form locktype:lockdef;lockfunc:lockdef;...",
- widget=forms.Textarea(attrs={"rows":1,"cols":8}),
- )
-
-
[docs]def__init__(self,*args,**kwargs):
- """
- If we have an Attribute, then we'll prepopulate our instance with the fields we'd expect it
- to have based on the Attribute. attr_key, attr_category, attr_value, attr_type,
- and attr_lockstring all refer to the corresponding Attribute fields. The initial data of the form fields will
- similarly be populated.
-
- """
- super().__init__(*args,**kwargs)
- attr_key=None
- attr_category=None
- attr_value=None
- attr_type=None
- attr_lockstring=None
- ifhasattr(self.instance,"attribute"):
- attr_key=self.instance.attribute.db_key
- attr_category=self.instance.attribute.db_category
- attr_value=self.instance.attribute.db_value
- attr_type=self.instance.attribute.db_attrtype
- attr_lockstring=self.instance.attribute.db_lock_storage
- self.fields["attr_key"].initial=attr_key
- self.fields["attr_category"].initial=attr_category
- self.fields["attr_type"].initial=attr_type
- self.fields["attr_value"].initial=attr_value
- self.fields["attr_lockstring"].initial=attr_lockstring
- self.instance.attr_key=attr_key
- self.instance.attr_category=attr_category
- self.instance.attr_value=attr_value
-
- # prevent from being transformed to str
- ifisinstance(attr_value,(set,_SaverSet)):
- self.fields["attr_value"].disabled=True
-
- self.instance.deserialized_value=from_pickle(attr_value)
- self.instance.attr_type=attr_type
- self.instance.attr_lockstring=attr_lockstring
-
-
[docs]defsave(self,commit=True):
- """
- One thing we want to do here is the or None checks, because forms are saved with an empty
- string rather than null from forms, usually, and the Handlers may handle empty strings
- differently than None objects. So for consistency with how things are handled in game,
- we'll try to make sure that empty form fields will be None, rather than ''.
- """
- # we are spoofing an Attribute for the Handler that will be called
- instance=self.instance
- instance.attr_key=self.cleaned_data["attr_key"]or"no_name_entered_for_attribute"
- instance.attr_category=self.cleaned_data["attr_category"]orNone
- instance.attr_value=self.cleaned_data["attr_value"]
- # convert the serialized string value into an object, if necessary, for AttributeHandler
- instance.attr_value=from_pickle(instance.attr_value)
- instance.attr_type=self.cleaned_data["attr_type"]orNone
- instance.attr_lockstring=self.cleaned_data["attr_lockstring"]
- returninstance
-
-
[docs]defclean_attr_value(self):
- """
- Prevent certain data-types from being cleaned due to literal_eval
- failing on them. Otherwise they will be turned into str.
-
- """
- data=self.cleaned_data["attr_value"]
- initial=self.instance.attr_value
- ifisinstance(initial,(set,_SaverSet,datetime)):
- returninitial
- returndata
-
-
-
[docs]classAttributeFormSet(forms.BaseInlineFormSet):
- """
- Attribute version of TagFormSet, as above.
- """
-
-
[docs]defsave(self,commit=True):
- defget_handler(finished_object):
- related=getattr(finished_object,self.related_field)
- try:
- attrtype=finished_object.attr_type
- exceptAttributeError:
- attrtype=finished_object.attribute.db_attrtype
- ifattrtype=="nick":
- handler_name="nicks"
- else:
- handler_name="attributes"
- returngetattr(related,handler_name)
-
- instances=super().save(commit=False)
- forobjinself.deleted_objects:
- # self.deleted_objects is a list created when super of save is called, we'll remove those
- handler=get_handler(obj)
- handler.remove(obj.attr_key,category=obj.attr_category)
-
- forinstanceininstances:
- handler=get_handler(instance)
-
- value=instance.attr_value
-
- try:
- handler.add(
- instance.attr_key,
- value,
- category=instance.attr_category,
- strattr=False,
- lockstring=instance.attr_lockstring,
- )
- except(TypeError,ValueError):
- # catch errors in nick templates and continue
- traceback.print_exc()
- continue
-
-
-
[docs]classAttributeInline(admin.TabularInline):
- """
- A handler for inline Attributes. This class should be subclassed in the admin of your models,
- and the 'model' and 'related_field' class attributes must be set. model should be the
- through model (ObjectDB_db_tag', for example), while related field should be the name
- of the field on that through model which points to the model being used: 'objectdb',
- 'msg', 'accountdb', etc.
- """
-
- # Set this to the through model of your desired M2M when subclassing.
- model=None
- form=AttributeForm
- formset=AttributeFormSet
- related_field=None# Must be 'objectdb', 'accountdb', 'msg', etc. Set when subclassing
- # raw_id_fields = ('attribute',)
- # readonly_fields = ('attribute',)
- extra=0
-
-
[docs]defget_formset(self,request,obj=None,**kwargs):
- """
- get_formset has to return a class, but we need to make the class that we return
- know about the related_field that we'll use. Returning the class itself rather than
- a proxy isn't threadsafe, since it'd be the base class and would change if multiple
- people used the admin at the same time
- """
- formset=super().get_formset(request,obj,**kwargs)
-
- classProxyFormset(formset):
- pass
-
- ProxyFormset.related_field=self.related_field
- returnProxyFormset
[docs]classIAttribute:""" Attributes are things that are specific to different types of objects. For example, a drink container needs to store its fill level, whereas an exit
@@ -94,6 +95,115 @@
- category (str): Optional character string for grouping the Attribute.
+ This class is an API/Interface/Abstract base class; do not instantiate it directly.
+ """
+
+
[docs]defaccess(self,accessing_obj,access_type="read",default=False,**kwargs):
+ """
+ Determines if another object has permission to access.
+
+ Args:
+ accessing_obj (object): Entity trying to access this one.
+ access_type (str, optional): Type of access sought, see
+ the lock documentation.
+ default (bool, optional): What result to return if no lock
+ of access_type was found. The default, `False`, means a lockdown
+ policy, only allowing explicit access.
+ kwargs (any, optional): Not used; here to make the API consistent with
+ other access calls.
+
+ Returns:
+ result (bool): If the lock was passed or not.
+
+ """
+ result=self.locks.check(accessing_obj,access_type=access_type,default=default)
+ returnresult
[docs]classInMemoryAttribute(IAttribute):
+ """
+ This Attribute is used purely for NAttributes/NAttributeHandler. It has no database backend.
+
+ """
+
+ # Primary Key has no meaning for an InMemoryAttribute. This merely serves to satisfy other code.
+
+
[docs]def__init__(self,pk,**kwargs):
+ """
+ Create an Attribute that exists only in Memory.
+
+ Args:
+ pk (int): This is a fake 'primary key' / id-field. It doesn't actually have to be
+ unique, but is fed an incrementing number from the InMemoryBackend by default. This
+ is needed only so Attributes can be sorted. Some parts of the API also see the lack
+ of a .pk field as a sign that the Attribute was deleted.
+ **kwargs: Other keyword arguments are used to construct the actual Attribute.
+
+ """
+ self.id=pk
+ self.pk=pk
+
+ # Copy all kwargs to local properties. We use db_ for compatability here.
+ forkey,valueinkwargs.items():
+ # Value and locks are special. We must call the wrappers.
+ ifkey=="value":
+ self.value=value
+ elifkey=="lock_storage":
+ self.lock_storage=value
+ else:
+ setattr(self,f"db_{key}",value)
[docs]classAttribute(IAttribute,SharedMemoryModel):
+ """
+ This attribute is stored via Django. Most Attributes will be using this class.
+
"""#
@@ -150,34 +260,9 @@
# Database manager# objects = managers.AttributeManager()
-
-
classMeta(object):"Define Django meta options"
- verbose_name="Evennia Attribute"
-
- # read-only wrappers
- key=property(lambdaself:self.db_key)
- strvalue=property(lambdaself:self.db_strvalue)
- category=property(lambdaself:self.db_category)
- model=property(lambdaself:self.db_model)
- attrtype=property(lambdaself:self.db_attrtype)
- date_created=property(lambdaself:self.db_date_created)
-
- def__lock_storage_get(self):
- returnself.db_lock_storage
-
- def__lock_storage_set(self,value):
- self.db_lock_storage=value
- self.save(update_fields=["db_lock_storage"])
-
- def__lock_storage_del(self):
- self.db_lock_storage=""
- self.save(update_fields=["db_lock_storage"])
-
- lock_storage=property(__lock_storage_get,__lock_storage_set,__lock_storage_del)
+ verbose_name="Attribute"# Wrapper properties to easily set database fields. These are# @property decorators that allows to access these fields using
@@ -187,9 +272,23 @@
# value = self.attr and del self.attr respectively (where self# is the object in question).
+ # lock_storage wrapper. Overloaded for saving to database.
+ def__lock_storage_get(self):
+ returnself.db_lock_storage
+
+ def__lock_storage_set(self,value):
+ super().__lock_storage_set(value)
+ self.save(update_fields=["db_lock_storage"])
+
+ def__lock_storage_del(self):
+ super().__lock_storage_del()
+ self.save(update_fields=["db_lock_storage"])
+
+ lock_storage=property(__lock_storage_get,__lock_storage_set,__lock_storage_del)
+
# value property (wraps db_value)
- # @property
- def__value_get(self):
+ @property
+ defvalue(self):""" Getter. Allows for `value = self.value`. We cannot cache here since it makes certain cases (such
@@ -198,115 +297,164 @@
"""returnfrom_pickle(self.db_value,db_obj=self)
- # @value.setter
- def__value_set(self,new_value):
+ @value.setter
+ defvalue(self,new_value):""" Setter. Allows for self.value = value. We cannot cache here, see self.__value_get. """self.db_value=to_pickle(new_value)
- # print("value_set, self.db_value:", repr(self.db_value)) # DEBUGself.save(update_fields=["db_value"])
- # @value.deleter
- def__value_del(self):
+ @value.deleter
+ defvalue(self):"""Deleter. Allows for del attr.value. This removes the entire attribute."""
- self.delete()
-
- value=property(__value_get,__value_set,__value_del)
-
- #
- #
- # Attribute methods
- #
- #
-
- def__str__(self):
- returnsmart_str("%s[category=%s](#%s)"%(self.db_key,self.db_category,self.id))
-
- def__repr__(self):
- return"%s[category=%s](#%s)"%(self.db_key,self.db_category,self.id)
-
-
[docs]defaccess(self,accessing_obj,access_type="attrread",default=False,**kwargs):
- """
- Determines if another object has permission to access.
-
- Args:
- accessing_obj (object): Entity trying to access this one.
- access_type (str, optional): Type of access sought, see
- the lock documentation.
- default (bool, optional): What result to return if no lock
- of access_type was found. The default, `False`, means a lockdown
- policy, only allowing explicit access.
- kwargs (any, optional): Not used; here to make the API consistent with
- other access calls.
-
- Returns:
- result (bool): If the lock was passed or not.
-
- """
- result=self.locks.check(accessing_obj,access_type=access_type,default=default)
- returnresult
[docs]classIAttributeBackend:"""
- Handler for adding Attributes to the object.
+ Abstract interface for the backends used by the Attribute Handler.
+
+ All Backends must implement this base class. """
- _m2m_fieldname="db_attributes"_attrcreate="attrcreate"_attredit="attredit"_attrread="attrread"
- _attrtype=None
+ _attrclass=None
-
[docs]def__init__(self,handler,attrtype):
+ self.handler=handler
+ self.obj=handler.obj
+ self._attrtype=attrtype
+ self._objid=handler.obj.idself._cache={}# store category names fully cachedself._catcache={}# full cache was run on all attributesself._cache_complete=False
- def_query_all(self):
- "Fetch all Attributes on this object"
- query={
- "%s__id"%self._model:self._objid,
- "attribute__db_model__iexact":self._model,
- "attribute__db_attrtype":self._attrtype,
- }
- return[
- conn.attribute
- forconningetattr(self.obj,self._m2m_fieldname).through.objects.filter(**query)
- ]
+
[docs]defquery_all(self):
+ """
+ Fetch all Attributes from this object.
- def_fullcache(self):
+ Returns:
+ attrlist (list): A list of Attribute objects.
+ """
+ raiseNotImplementedError()
+
+
[docs]defquery_key(self,key,category):
+ """
+
+ Args:
+ key (str): The key of the Attribute being searched for.
+ category (str or None): The category of the desired Attribute.
+
+ Returns:
+ attribute (IAttribute): A single Attribute.
+ """
+ raiseNotImplementedError()
+
+
[docs]defquery_category(self,category):
+ """
+ Returns every matching Attribute as a list, given a category.
+
+ This method calls up whatever storage the backend uses.
+
+ Args:
+ category (str or None): The category to query.
+
+ Returns:
+ attrs (list): The discovered Attributes.
+ """
+ raiseNotImplementedError()
+
+ def_full_cache(self):"""Cache all attributes of this object"""ifnot_TYPECLASS_AGGRESSIVE_CACHE:return
- attrs=self._query_all()
- self._cache=dict(
- (
- "%s-%s"
- %(
- to_str(attr.db_key).lower(),
- attr.db_category.lower()ifattr.db_categoryisnotNoneelseNone,
- ),
- attr,
- )
+ attrs=self.query_all()
+ self._cache={
+ f"{to_str(attr.key).lower()}-{attr.category.lower()ifattr.categoryelseNone}":attrforattrinattrs
- )
+ }self._cache_complete=True
- def_getcache(self,key=None,category=None):
+ def_get_cache_key(self,key,category):
+ """
+ Fetch cache key.
+
+ Args:
+ key (str): The key of the Attribute being searched for.
+ category (str or None): The category of the desired Attribute.
+
+ Returns:
+ attribute (IAttribute): A single Attribute.
+ """
+ cachekey="%s-%s"%(key,category)
+ cachefound=False
+ try:
+ attr=_TYPECLASS_AGGRESSIVE_CACHEandself._cache[cachekey]
+ cachefound=True
+ exceptKeyError:
+ attr=None
+
+ ifattrand(nothasattr(attr,"pk")andattr.pkisNone):
+ # clear out Attributes deleted from elsewhere. We must search this anew.
+ attr=None
+ cachefound=False
+ delself._cache[cachekey]
+ ifcachefoundand_TYPECLASS_AGGRESSIVE_CACHE:
+ ifattr:
+ return[attr]# return cached entity
+ else:
+ return[]# no such attribute: return an empty list
+ else:
+ conn=self.query_key(key,category)
+ ifconn:
+ attr=conn[0].attribute
+ if_TYPECLASS_AGGRESSIVE_CACHE:
+ self._cache[cachekey]=attr
+ return[attr]ifattr.pkelse[]
+ else:
+ # There is no such attribute. We will explicitly save that
+ # in our cache to avoid firing another query if we try to
+ # retrieve that (non-existent) attribute again.
+ if_TYPECLASS_AGGRESSIVE_CACHE:
+ self._cache[cachekey]=None
+ return[]
+
+ def_get_cache_category(self,category):
+ """
+ Retrieves Attribute list (by category) from cache.
+
+ Args:
+ category (str or None): The category to query.
+
+ Returns:
+ attrs (list): The discovered Attributes.
+ """
+ catkey="-%s"%category
+ if_TYPECLASS_AGGRESSIVE_CACHEandcatkeyinself._catcache:
+ return[attrforkey,attrinself._cache.items()ifkey.endswith(catkey)andattr]
+ else:
+ # we have to query to make this category up-date in the cache
+ attrs=self.query_category(category)
+ if_TYPECLASS_AGGRESSIVE_CACHE:
+ forattrinattrs:
+ ifattr.pk:
+ cachekey="%s-%s"%(attr.key,category)
+ self._cache[cachekey]=attr
+ # mark category cache as up-to-date
+ self._catcache[catkey]=True
+ returnattrs
+
+ def_get_cache(self,key=None,category=None):""" Retrieve from cache or database (always caches)
@@ -332,85 +480,31 @@
key=key.strip().lower()ifkeyelseNonecategory=category.strip().lower()ifcategoryisnotNoneelseNoneifkey:
- cachekey="%s-%s"%(key,category)
- cachefound=False
- try:
- attr=_TYPECLASS_AGGRESSIVE_CACHEandself._cache[cachekey]
- cachefound=True
- exceptKeyError:
- attr=None
+ returnself._get_cache_key(key,category)
+ returnself._get_cache_category(category)
- ifattrand(nothasattr(attr,"pk")andattr.pkisNone):
- # clear out Attributes deleted from elsewhere. We must search this anew.
- attr=None
- cachefound=False
- delself._cache[cachekey]
- ifcachefoundand_TYPECLASS_AGGRESSIVE_CACHE:
- ifattr:
- return[attr]# return cached entity
- else:
- return[]# no such attribute: return an empty list
- else:
- query={
- "%s__id"%self._model:self._objid,
- "attribute__db_model__iexact":self._model,
- "attribute__db_attrtype":self._attrtype,
- "attribute__db_key__iexact":key.lower(),
- "attribute__db_category__iexact":category.lower()ifcategoryelseNone,
- }
- ifnotself.obj.pk:
- return[]
- conn=getattr(self.obj,self._m2m_fieldname).through.objects.filter(**query)
- ifconn:
- attr=conn[0].attribute
- if_TYPECLASS_AGGRESSIVE_CACHE:
- self._cache[cachekey]=attr
- return[attr]ifattr.pkelse[]
- else:
- # There is no such attribute. We will explicitly save that
- # in our cache to avoid firing another query if we try to
- # retrieve that (non-existent) attribute again.
- if_TYPECLASS_AGGRESSIVE_CACHE:
- self._cache[cachekey]=None
- return[]
- else:
- # only category given (even if it's None) - we can't
- # assume the cache to be complete unless we have queried
- # for this category before
- catkey="-%s"%category
- if_TYPECLASS_AGGRESSIVE_CACHEandcatkeyinself._catcache:
- return[attrforkey,attrinself._cache.items()ifkey.endswith(catkey)andattr]
- else:
- # we have to query to make this category up-date in the cache
- query={
- "%s__id"%self._model:self._objid,
- "attribute__db_model__iexact":self._model,
- "attribute__db_attrtype":self._attrtype,
- "attribute__db_category__iexact":category.lower()ifcategoryelseNone,
- }
- attrs=[
- conn.attribute
- forconningetattr(self.obj,self._m2m_fieldname).through.objects.filter(
- **query
- )
- ]
- if_TYPECLASS_AGGRESSIVE_CACHE:
- forattrinattrs:
- ifattr.pk:
- cachekey="%s-%s"%(attr.db_key,category)
- self._cache[cachekey]=attr
- # mark category cache as up-to-date
- self._catcache[catkey]=True
- returnattrs
+
[docs]defget(self,key=None,category=None):
+ """
+ Frontend for .get_cache. Retrieves Attribute(s).
- def_setcache(self,key,category,attr_obj):
+ Args:
+ key (str, optional): Attribute key to query for
+ category (str, optional): Attribiute category
+
+ Returns:
+ args (list): Returns a list of zero or more matches
+ found from cache or database.
+ """
+ returnself._get_cache(key,category)
+
+ def_set_cache(self,key,category,attr_obj):""" Update cache. Args: key (str): A cleaned key string category (str or None): A cleaned category name
- attr_obj (Attribute): The newly saved attribute
+ attr_obj (IAttribute): The newly saved attribute """ifnot_TYPECLASS_AGGRESSIVE_CACHE:
@@ -424,7 +518,7 @@
self._catcache.pop(catkey,None)self._cache_complete=False
- def_delcache(self,key,category):
+ def_delete_cache(self,key,category):""" Remove attribute from cache
@@ -447,7 +541,7 @@
self._catcache.pop(catkey,None)self._cache_complete=False
-
[docs]defreset_cache(self):""" Reset cache from the outside. """
@@ -455,6 +549,435 @@
self._cache={}self._catcache={}
+
[docs]defdo_create_attribute(self,key,category,lockstring,value,strvalue):
+ """
+ Does the hard work of actually creating Attributes, whatever is needed.
+
+ Args:
+ key (str): The Attribute's key.
+ category (str or None): The Attribute's category, or None
+ lockstring (str): Any locks for the Attribute.
+ value (obj): The Value of the Attribute.
+ strvalue (bool): Signifies if this is a strvalue Attribute. Value MUST be a string or
+ this will lead to Trouble. Ignored for InMemory attributes.
+
+ Returns:
+ attr (IAttribute): The new Attribute.
+ """
+ raiseNotImplementedError()
+
+
[docs]defcreate_attribute(self,key,category,lockstring,value,strvalue=False,cache=True):
+ """
+ Creates Attribute (using the class specified for the backend), (optionally) caches it, and
+ returns it.
+
+ This MUST actively save the Attribute to whatever database backend is used, AND
+ call self.set_cache(key, category, new_attrobj)
+
+ Args:
+ key (str): The Attribute's key.
+ category (str or None): The Attribute's category, or None
+ lockstring (str): Any locks for the Attribute.
+ value (obj): The Value of the Attribute.
+ strvalue (bool): Signifies if this is a strvalue Attribute. Value MUST be a string or
+ this will lead to Trouble. Ignored for InMemory attributes.
+ cache (bool): Whether to cache the new Attribute
+
+ Returns:
+ attr (IAttribute): The new Attribute.
+ """
+ attr=self.do_create_attribute(key,category,lockstring,value,strvalue)
+ ifcache:
+ self._set_cache(key,category,attr)
+ returnattr
+
+
[docs]defdo_update_attribute(self,attr,value):
+ """
+ Simply sets a new Value to an Attribute.
+
+ Args:
+ attr (IAttribute): The Attribute being changed.
+ value (obj): The Value for the Attribute.
+
+ """
+ raiseNotImplementedError()
+
+
[docs]defdo_batch_update_attribute(self,attr_obj,category,lock_storage,new_value,strvalue):
+ """
+ Called opnly by batch add. For the database backend, this is a method
+ of updating that can alter category and lock-storage.
+
+ Args:
+ attr_obj (IAttribute): The Attribute being altered.
+ category (str or None): The attribute's (new) category.
+ lock_storage (str): The attribute's new locks.
+ new_value (obj): The Attribute's new value.
+ strvalue (bool): Signifies if this is a strvalue Attribute. Value MUST be a string or
+ this will lead to Trouble. Ignored for InMemory attributes.
+ """
+ raiseNotImplementedError()
+
+
[docs]defdo_batch_finish(self,attr_objs):
+ """
+ Called after batch_add completed. Used for handling database operations
+ and/or caching complications.
+
+ Args:
+ attr_objs (list of IAttribute): The Attributes created/updated thus far.
+
+ """
+ raiseNotImplementedError()
+
+
[docs]defbatch_add(self,*args,**kwargs):
+ """
+ Batch-version of `.add()`. This is more efficient than repeat-calling
+ `.add` when having many Attributes to add.
+
+ Args:
+ *args (tuple): Tuples of varying length representing the
+ Attribute to add to this object. Supported tuples are
+
+ - (key, value)
+ - (key, value, category)
+ - (key, value, category, lockstring)
+ - (key, value, category, lockstring, default_access)
+
+ Raises:
+ RuntimeError: If trying to pass a non-iterable as argument.
+
+ Notes:
+ The indata tuple order matters, so if you want a lockstring but no
+ category, set the category to `None`. This method does not have the
+ ability to check editing permissions and is mainly used internally.
+ It does not use the normal `self.add` but applies the Attributes
+ directly to the database.
+
+ """
+ new_attrobjs=[]
+ strattr=kwargs.get("strattr",False)
+ fortupinargs:
+ ifnotis_iter(tup)orlen(tup)<2:
+ raiseRuntimeError("batch_add requires iterables as arguments (got %r)."%tup)
+ ntup=len(tup)
+ keystr=str(tup[0]).strip().lower()
+ new_value=tup[1]
+ category=str(tup[2]).strip().lower()ifntup>2andtup[2]isnotNoneelseNone
+ lockstring=tup[3]ifntup>3else""
+
+ attr_objs=self._get_cache(keystr,category)
+
+ ifattr_objs:
+ attr_obj=attr_objs[0]
+ # update an existing attribute object
+ self.do_batch_update_attribute(attr_obj,category,lockstring,new_value,strattr)
+ else:
+ new_attr=self.do_create_attribute(
+ keystr,category,lockstring,new_value,strvalue=strattr
+ )
+ new_attrobjs.append(new_attr)
+ ifnew_attrobjs:
+ self.do_batch_finish(new_attrobjs)
+
+
[docs]defdo_delete_attribute(self,attr):
+ """
+ Does the hard work of actually deleting things.
+
+ Args:
+ attr (IAttribute): The attribute to delete.
+ """
+ raiseNotImplementedError()
+
+
[docs]defdelete_attribute(self,attr):
+ """
+ Given an Attribute, deletes it. Also remove it from cache.
+
+ Args:
+ attr (IAttribute): The attribute to delete.
+ """
+ ifnotattr:
+ return
+ self._delete_cache(attr.key,attr.category)
+ self.do_delete_attribute(attr)
+
+
[docs]defupdate_attribute(self,attr,value):
+ """
+ Simply updates an Attribute.
+
+ Args:
+ attr (IAttribute): The attribute to delete.
+ value (obj): The new value.
+ """
+ self.do_update_attribute(attr,value)
+
+
[docs]defdo_batch_delete(self,attribute_list):
+ """
+ Given a list of attributes, deletes them all.
+ The default implementation is fine, but this is overridable since some databases may allow
+ for a better method.
+
+ Args:
+ attribute_list (list of IAttribute):
+ """
+ forattributeinattribute_list:
+ self.delete_attribute(attribute)
+
+
[docs]defclear_attributes(self,category,accessing_obj,default_access):
+ """
+ Remove all Attributes on this object.
+
+ Args:
+ category (str, optional): If given, clear only Attributes
+ of this category.
+ accessing_obj (object, optional): If given, check the
+ `attredit` lock on each Attribute before continuing.
+ default_access (bool, optional): Use this permission as
+ fallback if `access_obj` is given but there is no lock of
+ type `attredit` on the Attribute in question.
+
+ """
+ category=category.strip().lower()ifcategoryisnotNoneelseNone
+
+ ifnotself._cache_complete:
+ self._full_cache()
+
+ ifcategoryisnotNone:
+ attrs=[attrforattrinself._cache.values()ifattr.category==category]
+ else:
+ attrs=self._cache.values()
+
+ ifaccessing_obj:
+ self.do_batch_delete(
+ [
+ attr
+ forattrinattrs
+ ifattr.access(accessing_obj,self._attredit,default=default_access)
+ ]
+ )
+ else:
+ # have to cast the results to a list or we'll get a RuntimeError for removing from the
+ # dict we're iterating
+ self.do_batch_delete(list(attrs))
+ self.reset_cache()
+
+
[docs]defget_all_attributes(self):
+ """
+ Simply returns all Attributes of this object, sorted by their IDs.
+
+ Returns:
+ attributes (list of IAttribute)
+ """
+ if_TYPECLASS_AGGRESSIVE_CACHE:
+ ifnotself._cache_complete:
+ self._full_cache()
+ returnsorted([attrforattrinself._cache.values()ifattr],key=lambdao:o.id)
+ else:
+ returnsorted([attrforattrinself.query_all()ifattr],key=lambdao:o.id)
+
+
+
[docs]classInMemoryAttributeBackend(IAttributeBackend):
+ """
+ This Backend for Attributes stores NOTHING in the database. Everything is kept in memory, and
+ normally lost on a crash, reload, shared memory flush, etc. It generates IDs for the Attributes
+ it manages, but these are of little importance beyond sorting and satisfying the caching logic
+ to know an Attribute hasn't been deleted out from under the cache's nose.
+
+ """
+
+ _attrclass=InMemoryAttribute
+
+
[docs]defdo_batch_update_attribute(self,attr_obj,category,lock_storage,new_value,strvalue):
+ """
+ No need to bother saving anything. Just set some values.
+ """
+ attr_obj.db_category=category
+ attr_obj.db_lock_storage=lock_storageiflock_storageelse""
+ attr_obj.value=new_value
+
+
[docs]defdo_batch_finish(self,attr_objs):
+ """
+ Nothing to do here for In-Memory.
+
+ Args:
+ attr_objs (list of IAttribute): The Attributes created/updated thus far.
+ """
+ pass
+
+
[docs]defdo_delete_attribute(self,attr):
+ """
+ Removes the Attribute from local storage. Once it's out of the cache, garbage collection
+ will handle the rest.
+
+ Args:
+ attr (IAttribute): The attribute to delete.
+ """
+ delself._storage[(attr.key,attr.category)]
+ self._category_storage[attr.category].remove(attr)
[docs]defdo_batch_update_attribute(self,attr_obj,category,lock_storage,new_value,strvalue):
+ attr_obj.db_category=category
+ attr_obj.db_lock_storage=lock_storageiflock_storageelse""
+ ifstrvalue:
+ # store as a simple string (will not notify OOB handlers)
+ attr_obj.db_strvalue=new_value
+ attr_obj.value=None
+ else:
+ # store normally (this will also notify OOB handlers)
+ attr_obj.value=new_value
+ attr_obj.db_strvalue=None
+ attr_obj.save(update_fields=["db_strvalue","db_value","db_category","db_lock_storage"])
+
+
[docs]defdo_batch_finish(self,attr_objs):
+ # Add new objects to m2m field all at once
+ getattr(self.obj,self._m2m_fieldname).add(*attr_objs)
+
+
[docs]defdo_delete_attribute(self,attr):
+ try:
+ attr.delete()
+ exceptAssertionError:
+ # This could happen if the Attribute has already been deleted.
+ pass
+
+
+
[docs]classAttributeHandler:
+ """
+ Handler for adding Attributes to the object.
+ """
+
+ _attrcreate="attrcreate"
+ _attredit="attredit"
+ _attrread="attrread"
+ _attrtype=None
+
+
[docs]def__init__(self,obj,backend_class):
+ """
+ Setup the AttributeHandler.
+
+ Args:
+ obj (TypedObject): An Account, Object, Channel, ServerSession (not technically a typed
+ object), etc. backend_class (IAttributeBackend class): The class of the backend to
+ use.
+ """
+ self.obj=obj
+ self.backend=backend_class(self,self._attrtype)
+
[docs]defhas(self,key=None,category=None):""" Checks if the given Attribute (or list of Attributes) exists on
@@ -476,7 +999,7 @@
category=category.strip().lower()ifcategoryisnotNoneelseNoneforkeystrinmake_iter(key):keystr=key.strip().lower()
- ret.extend(bool(attr)forattrinself._getcache(keystr,category))
+ ret.extend(bool(attr)forattrinself.backend.get(keystr,category))returnret[0]iflen(ret)==1elseret
[docs]defget(
@@ -516,7 +1039,8 @@
looked-after Attribute. default_access (bool, optional): If no `attrread` lock is set on object, this determines if the lock should then be passed or not.
- return_list (bool, optional):
+ return_list (bool, optional): Always return a list, also if there is only
+ one or zero matches found. Returns: result (any or list): One or more matches for keys and/or
@@ -534,7 +1058,7 @@
ret=[]forkeystrinmake_iter(key):# it's okay to send a None key
- attr_objs=self._getcache(keystr,category)
+ attr_objs=self.backend.get(keystr,category)ifattr_objs:ret.extend(attr_objs)elifraise_exception:
@@ -599,35 +1123,16 @@
returncategory=category.strip().lower()ifcategoryisnotNoneelseNone
-
keystr=key.strip().lower()
- attr_obj=self._getcache(key,category)
+ attr_obj=self.backend.get(key,category)ifattr_obj:# update an existing attribute objectattr_obj=attr_obj[0]
- ifstrattr:
- # store as a simple string (will not notify OOB handlers)
- attr_obj.db_strvalue=value
- attr_obj.save(update_fields=["db_strvalue"])
- else:
- # store normally (this will also notify OOB handlers)
- attr_obj.value=value
+ self.backend.update_attribute(attr_obj,value)else:# create a new Attribute (no OOB handlers can be notified)
- kwargs={
- "db_key":keystr,
- "db_category":category,
- "db_model":self._model,
- "db_attrtype":self._attrtype,
- "db_value":Noneifstrattrelseto_pickle(value),
- "db_strvalue":valueifstrattrelseNone,
- }
- new_attr=Attribute(**kwargs)
- new_attr.save()
- getattr(self.obj,self._m2m_fieldname).add(new_attr)
- # update cache
- self._setcache(keystr,category,new_attr)
[docs]definitialize_nick_templates(pattern,replacement,pattern_is_regex=False):""" 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.
+ pattern (str): The pattern to be used for nick recognition. This will
+ be parsed for shell patterns into a regex, unless `pattern_is_regex`
+ is `True`, in which case it must be an already valid regex string. In
+ this case, instead of `$N`, numbered arguments must instead be given
+ as matching groups named as `argN`, such as `(?P<arg1>.+?)`.
+ replacement (str): The template to be used to replace the string
+ matched by the pattern. This can contain `$N` markers and is never
+ parsed into a regex.
+ pattern_is_regex (bool): If set, `pattern` is a full regex string
+ instead of containing shell patterns. Returns:
- (regex, str): Regex to match against strings and a template
- Template with markers `{arg1}`, `{arg2}`, etc for
- replacement using the standard `.format` method.
+ regex, template (str): Regex to match against strings and template
+ with markers ``{arg1}, {arg2}``, etc for replacement using the standard
+ `.format` method. Raises:
- attributes.NickTemplateInvalid: If the in/out template does not have a matching
- number of $args.
+ evennia.typecalasses.attributes.NickTemplateInvalid: If the in/out
+ template does not have a matching number of `$args`.
+
+ Examples:
+ - `pattern` (shell syntax): `"grin $1"`
+ - `pattern` (regex): `"grin (?P<arg1.+?>)"`
+ - `replacement`: `"emote gives a wicked grin to $1"` """
- # create the regex for in_template
- regex_string=fnmatch.translate(in_template)
- # we must account for a possible line break coming over the wire
+ # create the regex from the pattern
+ ifpattern_is_regex:
+ # Note that for a regex we can't validate in the way we do for the shell
+ # pattern, since you may have complex OR statements or optional arguments.
- # NOTE-PYTHON3: fnmatch.translate format changed since Python2
- regex_string=regex_string[:-2]+r"(?:[\n\r]*?)\Z"
+ # Explicit regex given from the onset - this already contains argN
+ # groups. we need to split out any | - separated parts so we can
+ # attach the line-break/ending extras all regexes require.
+ pattern_regex_string=r"|".join(
+ or_part+r"(?:[\n\r]*?)\Z"
+ foror_partin_RE_OR.split(pattern))
- # validate the templates
- regex_args=[match.group(2)formatchin_RE_NICK_ARG.finditer(regex_string)]
- temp_args=[match.group(2)formatchin_RE_NICK_TEMPLATE_ARG.finditer(out_template)]
- ifset(regex_args)!=set(temp_args):
- # We don't have the same $-tags in input/output.
- raiseNickTemplateInvalid
+ else:
+ # Shell pattern syntax - convert $N to argN groups
+ # for the shell pattern we make sure we have matching $N on both sides
+ pattern_args=[match.group(1)formatchin_RE_NICK_RAW_ARG.finditer(pattern)]
+ replacement_args=[
+ match.group(1)formatchin_RE_NICK_RAW_ARG.finditer(replacement)]
+ ifset(pattern_args)!=set(replacement_args):
+ # We don't have the same amount of argN/$N tags in input/output.
+ raiseNickTemplateInvalid("Nicks: Both in/out-templates must contain the same $N tags.")
- regex_string=_RE_NICK_SPACE.sub(r"\\s+",regex_string)
- regex_string=_RE_NICK_ARG.sub(lambdam:"(?P<arg%s>.+?)"%m.group(2),regex_string)
- template_string=_RE_NICK_TEMPLATE_ARG.sub(lambdam:"{arg%s}"%m.group(2),out_template)
+ # generate regex from shell pattern
+ pattern_regex_string=fnmatch.translate(pattern)
+ pattern_regex_string=_RE_NICK_SPACE.sub(r"\\s+",pattern_regex_string)
+ pattern_regex_string=_RE_NICK_ARG.sub(
+ lambdam:"(?P<arg%s>.+?)"%m.group(2),pattern_regex_string)
+ # we must account for a possible line break coming over the wire
+ pattern_regex_string=pattern_regex_string[:-2]+r"(?:[\n\r]*?)\Z"
- returnregex_string,template_string
+ # map the replacement to match the arg1 group-names, to make replacement easy
+ replacement_string=_RE_NICK_RAW_ARG.sub(lambdam:"{arg%s}"%m.group(2),replacement)
+
+ returnpattern_regex_string,replacement_string
[docs]defparse_nick_template(string,template_regex,outtemplate):
@@ -926,17 +1421,21 @@
Parse a text using a template and map it to another template Args:
- string (str): The input string to processj
+ string (str): The input string to process 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.
+ etc to match the template-regex. Un-found $N-markers (possible if
+ the regex has optional matching groups) are replaced with empty
+ strings. """match=template_regex.match(string)ifmatch:
- returnTrue,outtemplate.format(**match.groupdict())
+ matchdict={key:valueifvalueisnotNoneelse""
+ forkey,valueinmatch.groupdict().items()}
+ returnTrue,outtemplate.format(**matchdict)returnFalse,string
@@ -985,6 +1484,9 @@
a string. kwargs (any, optional): These are passed on to `AttributeHandler.get`.
+ Returns:
+ str or tuple: The nick replacement string or nick tuple.
+
"""ifreturn_tupleor"return_obj"inkwargs:returnsuper().get(key=key,category=category,**kwargs)
@@ -998,24 +1500,46 @@
)returnNone
-
[docs]defadd(self,pattern,replacement,category="inputline",pattern_is_regex=False,**kwargs):"""
- Add a new nick.
+ Add a new nick, a mapping pattern -> replacement. Args:
- key (str): A key (or template) for the nick to match for.
- replacement (str): The string (or template) to replace `key` with (the "nickname").
+ pattern (str): A pattern to match for. This will be parsed for
+ shell patterns using the `fnmatch` library and can contain
+ `$N`-markers to indicate the locations of arguments to catch. If
+ `pattern_is_regex=True`, this must instead be a valid regular
+ expression and the `$N`-markers must be named `argN` that matches
+ numbered regex groups (see examples).
+ replacement (str): The string (or template) to replace `key` with
+ (the "nickname"). This may contain `$N` markers to indicate where to
+ place the argument-matches category (str, optional): the category within which to retrieve the nick. The "inputline" means replacing data sent by the user.
- kwargs (any, optional): These are passed on to `AttributeHandler.get`.
+ pattern_is_regex (bool): If `True`, the `pattern` will be parsed as a
+ raw regex string. Instead of using `$N` markers in this string, one
+ then must mark numbered arguments as a named regex-groupd named `argN`.
+ For example, `(?P<arg1>.+?)` will match the behavior of using `$1`
+ in the shell pattern.
+ **kwargs (any, optional): These are passed on to `AttributeHandler.get`.
+
+ Notes:
+ For most cases, the shell-pattern is much shorter and easier. The
+ regex pattern form can be useful for more complex matchings though,
+ for example in order to add optional arguments, such as with
+ `(?P<argN>.*?)`.
+
+ Example:
+ - pattern (default shell syntax): `"gr $1 at $2"`
+ - pattern (with pattern_is_regex=True): `r"gr (?P<arg1>.+?) at (?P<arg2>.+?)"`
+ - replacement: `"emote With a flourish, $1 grins at $2."` """
- ifcategory=="channel":
- nick_regex,nick_template=initialize_nick_templates(key+" $1",replacement+" $1")
- else:
- nick_regex,nick_template=initialize_nick_templates(key,replacement)
- super().add(key,(nick_regex,nick_template,key,replacement),category=category,**kwargs)
[docs]classNAttributeHandler(object):
- """
- This stand-alone handler manages non-database saving.
- It is similar to `AttributeHandler` and is used
- by the `.ndb` handler in the same way as `.db` does
- for the `AttributeHandler`.
- """
-
-
[docs]def__init__(self,obj):
- """
- Initialized on the object
- """
- self._store={}
- self.obj=weakref.proxy(obj)
-
-
[docs]defhas(self,key):
- """
- Check if object has this attribute or not.
-
- Args:
- key (str): The Nattribute key to check.
-
- Returns:
- has_nattribute (bool): If Nattribute is set or not.
-
- """
- returnkeyinself._store
-
-
[docs]defget(self,key):
- """
- Get the named key value.
-
- Args:
- key (str): The Nattribute key to get.
-
- Returns:
- the value of the Nattribute.
-
- """
- returnself._store.get(key,None)
-
-
[docs]defadd(self,key,value):
- """
- Add new key and value.
-
- Args:
- key (str): The name of Nattribute to add.
- value (any): The value to store.
-
- """
- self._store[key]=value
-
-
[docs]defremove(self,key):
- """
- Remove Nattribute from storage.
-
- Args:
- key (str): The name of the Nattribute to remove.
-
- """
- ifkeyinself._store:
- delself._store[key]
-
-
[docs]defclear(self):
- """
- Remove all NAttributes from handler.
-
- """
- self._store={}
-
-
[docs]defall(self,return_tuples=False):
- """
- List the contents of the handler.
-
- Args:
- return_tuples (bool, optional): Defines if the Nattributes
- are returns as a list of keys or as a list of `(key, value)`.
-
- Returns:
- nattributes (list): A list of keys `[key, key, ...]` or a
- list of tuples `[(key, value), ...]` depending on the
- setting of `return_tuples`.
-
- """
- ifreturn_tuples:
- return[(key,value)for(key,value)inself._store.items()ifnotkey.startswith("_")]
- return[keyforkeyinself._storeifnotkey.startswith("_")]
-
diff --git a/docs/0.9.5/_modules/evennia/typeclasses/managers.html b/docs/0.9.5/_modules/evennia/typeclasses/managers.html
index c41f400c71..1a98c2e99b 100644
--- a/docs/0.9.5/_modules/evennia/typeclasses/managers.html
+++ b/docs/0.9.5/_modules/evennia/typeclasses/managers.html
@@ -75,14 +75,12 @@
self,key=None,category=None,value=None,strvalue=None,obj=None,attrtype=None,**kwargs):"""
- Return Attribute objects by key, by category, by value, by
- `strvalue`, by object (it is stored on) or with a combination of
- those criteria.
+ Return Attribute objects by key, by category, by value, by strvalue, by
+ object (it is stored on) or with a combination of those criteria. Args:
- key (str, optional): The attribute's key to search for.
- category (str, optional): The category of the attribute(s)
- to search for.
+ key (str, optional): The attribute's key to search for
+ category (str, optional): The category of the attribute(s) to search for. value (str, optional): The attribute value to search for. Note that this is not a very efficient operation since it will query for a pickled entity. Mutually exclusive to
@@ -93,13 +91,13 @@
precedence if given. obj (Object, optional): On which object the Attribute to search for is.
- attrtype (str, optional): An attribute-type to search for.
+ attrype (str, optional): An attribute-type to search for. By default this is either `None` (normal Attributes) or `"nick"`.
- kwargs (any): Currently unused. Reserved for future use.
+ **kwargs (any): Currently unused. Reserved for future use. Returns:
- attributes (list): The matching Attributes.
+ list: The matching Attributes. """dbmodel=self.model.__dbclass__.__name__.lower()
@@ -217,7 +215,7 @@
to search for. obj (Object, optional): On which object the Tag to search for is.
- tagtype (str, optional): One of None (normal tags),
+ tagtype (str, optional): One of `None` (normal tags), "alias" or "permission" global_search (bool, optional): Include all possible tags, not just tags on this object
@@ -620,7 +618,7 @@
forparentin(parentforparentinparentsifhasattr(parent,"path")):query=query|Q(db_typeclass_path__exact=parent.path)# actually query the database
- returnself.filter(query)
+ returnsuper().filter(query)classTypeclassManager(TypedObjectManager):
@@ -638,13 +636,21 @@
Search by supplying a string with optional extra search criteria to aid the query. Args:
- query (str): A search criteria that accepts extra search criteria on the
+ query (str): A search criteria that accepts extra search criteria on the following
+ forms:
+
+ [key|alias|#dbref...]
+ [tag==<tagstr>[:category]...]
+ [attr==<key>:<value>:category...]
+
+ All three can be combined in the same query, separated by spaces.
- following forms: [key|alias|#dbref...] [tag==<tagstr>[:category]...] [attr==<key>:<value>:category...]
- " != " != " Returns:
- matches (queryset): A queryset result matching all queries exactly. If wanting to use spaces or
- ==, != in tags or attributes, enclose them in quotes.
+ matches (queryset): A queryset result matching all queries exactly. If wanting to use
+ spaces or ==, != in tags or attributes, enclose them in quotes.
+
+ Example:
+ house = smart_search("key=foo alias=bar tag=house:building tag=magic attr=color:red") Note: The flexibility of this method is limited by the input line format. Tag/attribute
@@ -926,7 +932,6 @@
-
diff --git a/docs/0.9.5/_modules/evennia/typeclasses/models.html b/docs/0.9.5/_modules/evennia/typeclasses/models.html
index de1abcbf76..58666ce271 100644
--- a/docs/0.9.5/_modules/evennia/typeclasses/models.html
+++ b/docs/0.9.5/_modules/evennia/typeclasses/models.html
@@ -65,8 +65,6 @@
This module also contains the Managers for the respective models; inherit fromthese to create custom managers.
-----
-
"""fromdjango.db.modelsimportsignals
@@ -79,7 +77,13 @@
fromdjango.utils.encodingimportsmart_strfromdjango.utils.textimportslugify
-fromevennia.typeclasses.attributesimportAttribute,AttributeHandler,NAttributeHandler
+fromevennia.typeclasses.attributesimport(
+ Attribute,
+ AttributeHandler,
+ ModelAttributeBackend,
+ InMemoryAttributeBackend,
+)
+fromevennia.typeclasses.attributesimportDbHolderfromevennia.typeclasses.tagsimportTag,TagHandler,AliasHandler,PermissionHandlerfromevennia.utils.idmapper.modelsimportSharedMemoryModel,SharedMemoryModelBase
@@ -106,6 +110,7 @@
defcall_at_first_save(sender,instance,created,**kwargs):""" Receives a signal just after the object is saved.
+
"""ifcreated:instance.at_first_save()
@@ -114,6 +119,7 @@
defremove_attributes_on_delete(sender,instance,**kwargs):""" Wipe object's Attributes when it's deleted
+
"""instance.db_attributes.all().delete()
@@ -135,6 +141,7 @@
Metaclass which should be set for the root of model proxies that don't define any new fields, like Object, Script etc. This is the basis for the typeclassing system.
+
"""def__new__(cls,name,bases,attrs):
@@ -203,35 +210,6 @@
returnnew_class
-classDbHolder(object):
- """
- Holder for allowing property access of attributes.
-
- """
-
- def__init__(self,obj,name,manager_name="attributes"):
- _SA(self,name,_GA(obj,manager_name))
- _SA(self,"name",name)
-
- def__getattribute__(self,attrname):
- ifattrname=="all":
- # we allow to overload our default .all
- attr=_GA(self,_GA(self,"name")).get("all")
- returnattrifattrelse_GA(self,"all")
- return_GA(self,_GA(self,"name")).get(attrname)
-
- def__setattr__(self,attrname,value):
- _GA(self,_GA(self,"name")).add(attrname,value)
-
- def__delattr__(self,attrname):
- _GA(self,_GA(self,"name")).remove(attrname)
-
- defget_all(self):
- return_GA(self,_GA(self,"name")).all()
-
- all=property(get_all)
-
-
## Main TypedObject abstraction#
@@ -245,15 +223,16 @@
mechanics for managing connected attributes. The TypedObject has the following properties:
- key - main name
- name - alias for key
- typeclass_path - the path to the decorating typeclass
- typeclass - auto-linked typeclass
- date_created - time stamp of object creation
- permissions - perm strings
- dbref - #id of object
- db - persistent attribute storage
- ndb - non-persistent attribute storage
+
+ - key - main name
+ - name - alias for key
+ - typeclass_path - the path to the decorating typeclass
+ - typeclass - auto-linked typeclass
+ - date_created - time stamp of object creation
+ - permissions - perm strings
+ - dbref - #id of object
+ - db - persistent attribute storage
+ - ndb - non-persistent attribute storage """
@@ -274,7 +253,8 @@
"typeclass",max_length=255,null=True,
- help_text="this defines what 'type' of entity this is. This variable holds a Python path to a module with a valid Evennia Typeclass.",
+ help_text="this defines what 'type' of entity this is. This variable holds "
+ "a Python path to a module with a valid Evennia Typeclass.",db_index=True,)# Creation date. This is not changed once the object is created.
@@ -283,16 +263,20 @@
db_lock_storage=models.TextField("locks",blank=True,
- help_text="locks limit access to an entity. A lock is defined as a 'lock string' on the form 'type:lockfunctions', defining what functionality is locked and how to determine access. Not defining a lock means no access is granted.",
+ help_text="locks limit access to an entity. A lock is defined as a 'lock string' "
+ "on the form 'type:lockfunctions', defining what functionality is locked and "
+ "how to determine access. Not defining a lock means no access is granted.",)# many2many relationshipsdb_attributes=models.ManyToManyField(Attribute,
- help_text="attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).",
+ help_text="attributes on this object. An attribute can hold any pickle-able "
+ "python object (see docs for special cases).",)db_tags=models.ManyToManyField(Tag,
- help_text="tags on this object. Tags are simple string markers to identify, group and alias objects.",
+ help_text="tags on this object. Tags are simple string markers to identify, "
+ "group and alias objects.",)# Database manager
@@ -355,8 +339,10 @@
than use the one in the model. Args:
- *args: Passed through to parent.
- **kwargs: Passed through to parent.
+ Passed through to parent.
+
+ Keyword Args:
+ Passed through to parent. Notes: The loading mechanism will attempt the following steps:
@@ -383,7 +369,7 @@
# initialize all handlers in a lazy fashion
[docs]classMeta:""" Django setup info. """
@@ -680,7 +666,7 @@
superuser lock bypass (be careful with this one). Keyword Args:
- kwargs (any): Ignored, but is there to make the api
+ kwar (any): Ignored, but is there to make the api consistent with the object-typeclass method access, which use it to feed to its hook methods.
@@ -765,22 +751,22 @@
# Attribute storage#
- # @property db
- def__db_get(self):
+ @property
+ defdb(self):""" Attribute handler wrapper. Allows for the syntax
- ::
+ ```python obj.db.attrname = value
- and
+ # and value = obj.db.attrname
- and
+ # and del obj.db.attrname
- and
+ # and all_attr = obj.db.all()
-
- (unless there is an attribute named 'all', in which case that will be
- returned instead).
+ # (unless there is an attribute
+ # named 'all', in which case that will be returned instead).
+ ``` """try:
@@ -789,44 +775,29 @@
self._db_holder=DbHolder(self,"attributes")returnself._db_holder
- # @db.setter
- def__db_set(self,value):
- """Stop accidentally replacing the db object"""
+ @db.setter
+ defdb(self,value):
+ "Stop accidentally replacing the db object"string="Cannot assign directly to db object! "string+="Use db.attr=value instead."raiseException(string)
- # @db.deleter
- def__db_del(self):
- """Stop accidental deletion."""
+ @db.deleter
+ defdb(self):
+ "Stop accidental deletion."raiseException("Cannot delete the db object!")
- db=property(__db_get,__db_set,__db_del)
-
## Non-persistent (ndb) storage#
- # @property ndb
- def__ndb_get(self):
+ @property
+ defndb(self):"""
- A non-attr_obj store (NonDataBase). Everything stored to this is
- guaranteed to be cleared when a server is shutdown. Syntax is same as
- for the `.db` property, e.g.
- ::
-
- obj.ndb.attrname = value
- and
- value = obj.ndb.attrname
- and
- del obj.ndb.attrname
- and
- all_attr = obj.ndb.all()
-
- What makes this preferable over just assigning properties directly on
- the object is that Evennia can track caching for these properties and
- for example avoid wiping objects with set `.ndb` data on cache flushes.
-
+ A non-attr_obj store (ndb: NonDataBase). Everything stored
+ to this is guaranteed to be cleared when a server is shutdown.
+ Syntax is same as for the _get_db_holder() method and
+ property, e.g. obj.ndb.attr = value etc. """try:returnself._ndb_holder
@@ -834,20 +805,18 @@
self._ndb_holder=DbHolder(self,"nattrhandler",manager_name="nattributes")returnself._ndb_holder
- # @db.setter
- def__ndb_set(self,value):
+ @ndb.setter
+ defndb(self,value):"Stop accidentally replacing the ndb object"string="Cannot assign directly to ndb object! "string+="Use ndb.attr=value instead."raiseException(string)
- # @db.deleter
- def__ndb_del(self):
+ @ndb.deleter
+ defndb(self):"Stop accidental deletion."raiseException("Cannot delete the ndb object!")
- ndb=property(__ndb_get,__ndb_set,__ndb_del)
-
[docs]defget_display_name(self,looker,**kwargs):""" Displays the name of the object in a viewer-aware manner.
@@ -931,37 +900,32 @@
[docs]@classmethoddefweb_get_create_url(cls):"""
-
Returns the URI path for a View that allows users to create new instances of this object.
+ ex. Chargen = '/characters/create/'
+
+ For this to work, the developer must have defined a named view somewhere
+ in urls.py that follows the format 'modelname-action', so in this case
+ a named view of 'character-create' would be referenced by this method.
+
+ ex.
+ url(r'characters/create/', ChargenView.as_view(), name='character-create')
+
+ If no View has been created and defined in urls.py, returns an
+ HTML anchor.
+
+ This method is naive and simply returns a path. Securing access to
+ the actual view and limiting who can create new objects is the
+ developer's responsibility.
+
Returns: path (str): URI path to object creation page, if defined.
- Examples:
- ::
-
- Chargen = '/characters/create/'
-
- For this to work, the developer must have defined a named view somewhere
- in urls.py that follows the format 'modelname-action', so in this case
- a named view of 'character-create' would be referenced by this method.
- ::
-
- url(r'characters/create/', ChargenView.as_view(), name='character-create')
-
- If no View has been created and defined in urls.py, returns an
- HTML anchor.
-
- Notes:
- This method is naive and simply returns a path. Securing access to
- the actual view and limiting who can create new objects is the
- developer's responsibility.
-
"""try:returnreverse("%s-create"%slugify(cls._meta.verbose_name))
- except:
+ exceptException:return"#"
[docs]defweb_get_detail_url(self):
@@ -973,21 +937,24 @@
path (str): URI path to object detail page, if defined. Examples:
- ::
- Oscar (Character) = '/characters/oscar/1/'
+ ```python
+ Oscar (Character) = '/characters/oscar/1/'
+ ``` For this to work, the developer must have defined a named view somewhere in urls.py that follows the format 'modelname-action', so in this case a named view of 'character-detail' would be referenced by this method.
- ::
+
+ ```python
+ url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/$', CharDetailView.as_view(), name='character-detail')
+ ``` If no View has been created and defined in urls.py, returns an HTML anchor.
- Notes: This method is naive and simply returns a path. Securing access to the actual view and limiting who can view this object is the developer's responsibility.
@@ -998,7 +965,7 @@
"%s-detail"%slugify(self._meta.verbose_name),kwargs={"pk":self.pk,"slug":slugify(self.name)},)
- except:
+ exceptException:return"#"
[docs]defweb_get_puppet_url(self):
@@ -1007,7 +974,7 @@
object. Returns:
- path (str): URI path to object puppet page, if defined.
+ str: URI path to object puppet page, if defined. Examples: ::
@@ -1025,7 +992,6 @@
If no View has been created and defined in urls.py, returns an HTML anchor.
- Notes: This method is naive and simply returns a path. Securing access to the actual view and limiting who can view this object is the developer's responsibility.
@@ -1037,7 +1003,7 @@
"%s-puppet"%slugify(self._meta.verbose_name),kwargs={"pk":self.pk,"slug":slugify(self.name)},)
- except:
+ exceptException:return"#"
[docs]defweb_get_update_url(self):
@@ -1046,12 +1012,13 @@
object. Returns:
- path (str): URI path to object update page, if defined.
+ str: URI path to object update page, if defined. Examples:
- ::
- Oscar (Character) = '/characters/oscar/1/change/'
+ ```python
+ Oscar (Character) = '/characters/oscar/1/change/'
+ ``` For this to work, the developer must have defined a named view somewhere in urls.py that follows the format 'modelname-action', so in this case
@@ -1059,23 +1026,23 @@
:: url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/change/$',
- CharUpdateView.as_view(), name='character-update')
+ CharUpdateView.as_view(), name='character-update') If no View has been created and defined in urls.py, returns an HTML anchor.
- Notes: This method is naive and simply returns a path. Securing access to the actual view and limiting who can modify objects is the developer's responsibility.
+
"""try:returnreverse("%s-update"%slugify(self._meta.verbose_name),kwargs={"pk":self.pk,"slug":slugify(self.name)},)
- except:
+ exceptException:return"#"
[docs]defweb_get_delete_url(self):
@@ -1086,25 +1053,26 @@
path (str): URI path to object deletion page, if defined. Examples:
- ::
- Oscar (Character) = '/characters/oscar/1/delete/'
+ ```python
+ Oscar (Character) = '/characters/oscar/1/delete/'
+ ```
- For this to work, the developer must have defined a named view somewhere
- in urls.py that follows the format 'modelname-action', so in this case
- a named view of 'character-detail' would be referenced by this method.
+ For this to work, the developer must have defined a named view
+ somewhere in urls.py that follows the format 'modelname-action', so
+ in this case a named view of 'character-detail' would be referenced
+ by this method. :: url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/delete/$',
- CharDeleteView.as_view(), name='character-delete')
+ CharDeleteView.as_view(), name='character-delete')
- If no View has been created and defined in urls.py, returns an
- HTML anchor.
+ If no View has been created and defined in urls.py, returns an HTML
+ anchor.
- Notes: This method is naive and simply returns a path. Securing access to
- the actual view and limiting who can delete this object is the developer's
- responsibility.
+ the actual view and limiting who can delete this object is the
+ developer's responsibility. """
@@ -1113,7 +1081,7 @@
"%s-delete"%slugify(self._meta.verbose_name),kwargs={"pk":self.pk,"slug":slugify(self.name)},)
- except:
+ exceptException:return"#"
# Used by Django Sites/Admin
@@ -1155,7 +1123,6 @@
def_query_all(self):
- "Get all tags for this objects"
+ """
+ Get all tags for this object.
+
+ """query={"%s__id"%self._model:self._objid,"tag__db_model":self._model,
@@ -178,7 +181,10 @@
]def_fullcache(self):
- "Cache all tags of this object"
+ """
+ Cache all tags of this object.
+
+ """ifnot_TYPECLASS_AGGRESSIVE_CACHE:returntags=self._query_all()
@@ -318,6 +324,7 @@
[docs]defreset_cache(self):""" Reset the cache from the outside.
+
"""self._cache_complete=Falseself._cache={}
@@ -360,6 +367,40 @@
getattr(self.obj,self._m2m_fieldname).add(tagobj)self._setcache(tagstr,category,tagobj)
+
[docs]defhas(self,tag=None,category=None,return_list=False):
+ """
+ Checks if the given Tag (or list of Tags) exists on the object.
+
+ Args:
+ tag (str or iterable): The Tag key or tags to check for.
+ If `None`, search by category.
+ category (str, optional): Limit the check to Tags with this
+ category (note, that `None` is the default category).
+
+ Returns:
+ has_tag (bool or list): If the Tag exists on this object or not.
+ If `tag` was given as an iterable then the return is a list of booleans.
+
+ Raises:
+ ValueError: If neither `tag` nor `category` is given.
+
+ """
+ ret=[]
+ category=category.strip().lower()ifcategoryisnotNoneelseNone
+ iftag:
+ fortag_strinmake_iter(tag):
+ tag_str=tag_str.strip().lower()
+ ret.extend(bool(tag)fortaginself._getcache(tag_str,category))
+ elifcategory:
+ ret.extend(bool(tag)fortaginself._getcache(category=category))
+ else:
+ raiseValueError("Either tag or category must be provided.")
+
+ ifreturn_list:
+ returnret
+
+ returnret[0]iflen(ret)==1elseret
+
[docs]defget(self,key=None,default=None,category=None,return_tagobj=False,return_list=False):""" Get the tag for the given key, category or combination of the two.
@@ -490,8 +531,9 @@
Batch-add tags from a list of tuples. Args:
- *args (tuple or str): Each argument should be a `tagstr` keys or tuple `(keystr, category)` or
- `(keystr, category, data)`. It's possible to mix input types.
+ *args (tuple or str): Each argument should be a `tagstr` keys or tuple
+ `(keystr, category)` or `(keystr, category, data)`. It's possible to mix input
+ types. Notes: This will generate a mimimal number of self.add calls,
@@ -572,7 +614,6 @@
diff --git a/docs/0.9.5/_modules/evennia/utils/ansi.html b/docs/0.9.5/_modules/evennia/utils/ansi.html
index 9ef4a26ebc..6924b4fe5f 100644
--- a/docs/0.9.5/_modules/evennia/utils/ansi.html
+++ b/docs/0.9.5/_modules/evennia/utils/ansi.html
@@ -42,18 +42,64 @@
"""ANSI - Gives colour to text.
-Use the codes defined in ANSIPARSER in your text to apply colour to text
-according to the ANSI standard.
+Use the codes defined in the *ANSIParser* class to apply colour to text. The
+`parse_ansi` function in this module parses text for markup and `strip_ansi`
+removes it.
-Examples:
+You should usually not need to call `parse_ansi` explicitly; it is run by
+Evennia just before returning data to/from the user. Alternative markup is
+possible by overriding the parser class (see also contrib/ for deprecated
+markup schemes).
+
+
+Supported standards:
+
+- ANSI 8 bright and 8 dark fg (foreground) colors
+- ANSI 8 dark bg (background) colors
+- 'ANSI' 8 bright bg colors 'faked' with xterm256 (bright bg not included in ANSI standard)
+- Xterm256 - 255 fg/bg colors + 26 greyscale fg/bg colors
+
+## Markup
+
+ANSI colors: `r` ed, `g` reen, `y` ellow, `b` lue, `m` agenta, `c` yan, `n` ormal (no color).
+Capital letters indicate the 'dark' variant.
+
+- `|r` fg bright red
+- `|R` fg dark red
+- `|[r` bg bright red
+- `|[R` bg dark red
+- `|[R|g` bg dark red, fg bright green```python"This is |rRed text|n and this is normal again."
+
```
-Mostly you should not need to call `parse_ansi()` explicitly; it is run by
-Evennia just before returning data to/from the user. Depreciated example forms
-are available by extending the ansi mapping.
+Xterm256 colors are given as RGB (Red-Green-Blue), with values 0-5:
+
+- `|500` fg bright red
+- `|050` fg bright green
+- `|005` fg bright blue
+- `|110` fg dark brown
+- `|425` fg pink
+- `|[431` bg orange
+
+Xterm256 greyscale:
+
+- `|=a` fg black
+- `|=g` fg dark grey
+- `|=o` fg middle grey
+- `|=v` fg bright grey
+- `|=z` fg white
+- `|[=r` bg middle grey
+
+```python
+"This is |500Red text|n and this is normal again."
+"This is |[=jText on dark grey background"
+
+```
+
+----"""importfunctools
@@ -121,9 +167,11 @@
[docs]classANSIParser(object):"""
- A class that parses ANSI markup to ANSI command sequences.
+ A class that parses ANSI markup
+ to ANSI command sequences
- We also allow to escape colour codes by prepending with an extra `|`.
+ We also allow to escape colour codes
+ by prepending with an extra `|`. """
@@ -216,6 +264,7 @@
ansi_xterm256_bright_bg_map+=settings.COLOR_ANSI_XTERM256_BRIGHT_BG_EXTRA_MAPmxp_re=r"\|lc(.*?)\|lt(.*?)\|le"
+ mxp_url_re=r"\|lu(.*?)\|lt(.*?)\|le"# prepare regex matchingbrightbg_sub=re.compile(
@@ -230,6 +279,7 @@
# xterm256_sub = re.compile(r"|".join([tup[0] for tup in xterm256_map]), re.DOTALL)ansi_sub=re.compile(r"|".join([re.escape(tup[0])fortupinansi_map]),re.DOTALL)mxp_sub=re.compile(mxp_re,re.DOTALL)
+ mxp_url_sub=re.compile(mxp_url_re,re.DOTALL)# used by regex replacer to correctly map ansi sequencesansi_map_dict=dict(ansi_map)
@@ -330,8 +380,9 @@
colval=16+(red*36)+(green*6)+bluereturn"\033[%s8;5;%sm"%(3+int(background),colval)
- # replaced since some clients (like Potato) does not accept codes with leading zeroes, see issue #1024.
- # return "\033[%s8;5;%s%s%sm" % (3 + int(background), colval // 100, (colval % 100) // 10, colval%10)
+ # replaced since some clients (like Potato) does not accept codes with leading zeroes,
+ # see issue #1024.
+ # return "\033[%s8;5;%s%s%sm" % (3 + int(background), colval // 100, (colval % 100) // 10, colval%10) # noqaelse:# xterm256 not supported, convert the rgb value to ansi instead
@@ -416,7 +467,9 @@
string (str): The processed string. """
- returnself.mxp_sub.sub(r"\2",string)
+ string=self.mxp_sub.sub(r"\2",string)
+ string=self.mxp_url_sub.sub(r"\1",string)# replace with url verbatim
+ returnstring
[docs]defparse_ansi(self,string,strip_ansi=False,xterm256=False,mxp=False):"""
@@ -722,7 +775,8 @@
"""
- # A compiled Regex for the format mini-language: https://docs.python.org/3/library/string.html#formatspec
+ # A compiled Regex for the format mini-language:
+ # https://docs.python.org/3/library/string.html#formatspecre_format=re.compile(r"(?i)(?P<just>(?P<fill>.)?(?P<align>\<|\>|\=|\^))?(?P<sign>\+|\-| )?(?P<alt>\#)?"r"(?P<zero>0)?(?P<width>\d+)?(?P<grouping>\_|\,)?(?:\.(?P<precision>\d+))?"
@@ -795,12 +849,14 @@
Current features supported: fill, align, width. Args:
- format_spec (str): The format specification passed by f-string or str.format(). This is a string such as
- "0<30" which would mean "left justify to 30, filling with zeros". The full specification can be found
- at https://docs.python.org/3/library/string.html#formatspec
+ format_spec (str): The format specification passed by f-string or str.format(). This is
+ a string such as "0<30" which would mean "left justify to 30, filling with zeros".
+ The full specification can be found at
+ https://docs.python.org/3/library/string.html#formatspec Returns: ansi_str (str): The formatted ANSIString's .raw() form, for display.
+
"""# This calls the compiled regex stored on ANSIString's class to analyze the format spec.# It returns a dictionary.
@@ -1060,7 +1116,7 @@
current_index=0result=tuple()forsectioninparent_result:
- result+=(self[current_index:current_index+len(section)],)
+ result+=(self[current_index:current_index+len(section)],)current_index+=len(section)returnresult
@@ -1180,7 +1236,7 @@
start=next+bylenmaxsplit-=1# NB. if it's already < 0, it stays < 0
- res.append(self[start:len(self)])
+ res.append(self[start:len(self)])ifdrop_spaces:return[partforpartinresifpart!=""]returnres
@@ -1223,7 +1279,7 @@
ifnext<0:break# Get character codes after the index as well.
- res.append(self[next+bylen:end])
+ res.append(self[next+bylen:end])end=nextmaxsplit-=1# NB. if it's already < 0, it stays < 0
@@ -1277,7 +1333,7 @@
ic-=1ir2-=1rstripped=rstripped[::-1]
- returnANSIString(lstripped+raw[ir1:ir2+1]+rstripped)
+ returnANSIString(lstripped+raw[ir1:ir2+1]+rstripped)
"""This module contains the core methods for the Batch-command- and
-Batch-code-processors respectively. In short, these are two different
-ways to build a game world using a normal text-editor without having
-to do so 'on the fly' in-game. They also serve as an automatic backup
-so you can quickly recreate a world also after a server reset. The
-functions in this module is meant to form the backbone of a system
-called and accessed through game commands.
+Batch-code-processors respectively. In short, these are two different ways to
+build a game world using a normal text-editor without having to do so 'on the
+fly' in-game. They also serve as an automatic backup so you can quickly
+recreate a world also after a server reset. The functions in this module is
+meant to form the backbone of a system called and accessed through game
+commands.
-The Batch-command processor is the simplest. It simply runs a list of
-in-game commands in sequence by reading them from a text file. The
-advantage of this is that the builder only need to remember the normal
-in-game commands. They are also executing with full permission checks
-etc, making it relatively safe for builders to use. The drawback is
-that in-game there is really a builder-character walking around
-building things, and it can be important to create rooms and objects
-in the right order, so the character can move between them. Also
-objects that affects players (such as mobs, dark rooms etc) will
-affect the building character too, requiring extra care to turn
-off/on.
+The Batch-command processor is the simplest. It simply runs a list of in-game
+commands in sequence by reading them from a text file. The advantage of this is
+that the builder only need to remember the normal in-game commands. They are
+also executing with full permission checks etc, making it relatively safe for
+builders to use. The drawback is that in-game there is really a
+builder-character walking around building things, and it can be important to
+create rooms and objects in the right order, so the character can move between
+them. Also objects that affects players (such as mobs, dark rooms etc) will
+affect the building character too, requiring extra care to turn off/on.The Batch-code processor is a more advanced system that accepts fullPython code, executing in chunks. The advantage of this is much more
@@ -72,8 +70,7 @@
recommended that the batch-code processor is limited only tosuperusers or highly trusted staff.
-Batch-Command processor file syntax
------------------------------------
+# Batch-command processor file syntaxThe batch-command processor accepts 'batchcommand files' e.g`batch.ev`, containing a sequence of valid Evennia commands in a
@@ -81,31 +78,39 @@
had been run at the game prompt.Each Evennia command must be delimited by a line comment to mark its
-end. This way entire game worlds can be created and planned offline; it is
+end.
+
+::
+
+ look
+ # delimiting comment
+ create/drop box
+ # another required comment
+
+One can also inject another batchcmdfile:
+
+::
+
+ #INSERT path.batchcmdfile
+
+This way entire game worlds can be created and planned offline; it isespecially useful in order to create long room descriptions where areal offline text editor is often much better than any online texteditor or prompt.
-There is only one batchcommand-specific entry to use in a batch-command
-files (all others are just like in-game commands):
+## Example of batch.ev file:
-- `#INSERT path.batchcmdfile` - this as the first entry on a line will
- import and run a batch.ev file in this position, as if it was
- written in this file.
-
-
-Example of batch.ev file::: # batch file # all lines starting with # are comments; they also indicate # that a command definition is over.
- @create box
+ create box # this comment ends the @create command.
- @set box/desc = A large box.
+ set box/desc = A large box. Inside are some scattered piles of clothing.
@@ -117,25 +122,22 @@
# is ignored. An empty line in the command definition is parsed as a \n # (so two empty lines becomes a new paragraph).
- @teleport #221
+ teleport #221 # (Assuming #221 is a warehouse or something.) # (remember, this comment ends the @teleport command! Don'f forget it) # Example of importing another file at this point.
- #INSERT examples.batch
+ #IMPORT examples.batch
- @drop box
+ drop box # Done, the box is in the warehouse! (this last comment is not necessary to
- # close the @drop command since it's the end of the file)
-
+ # close the drop command since it's the end of the file)An example batch file is `contrib/examples/batch_example.ev`.
-
-Batch-Code processor file syntax
---------------------------------
+# Batch-code processor file syntaxThe Batch-code processor accepts full python modules (e.g. `batch.py`)that looks identical to normal Python files. The difference from
@@ -169,13 +171,14 @@
Importing works as normal. The following variables are automaticallymade available in the script namespace.
-- `caller` - The object executing the batchscript
+- `caller` - The object executing the batchscript- `DEBUG` - This is a boolean marking if the batchprocessor is running
- in debug mode. It can be checked to e.g. delete created objects
- when running a CODE block multiple times during testing.
- (avoids creating a slew of same-named db objects)
+ in debug mode. It can be checked to e.g. delete created objects
+ when running a CODE block multiple times during testing.
+ (avoids creating a slew of same-named db objects)
+
+## Example batch.py file
-Example batch.py file::: #HEADER
@@ -204,8 +207,6 @@
script = create.create_script()
-----
-
"""importreimportcodecs
@@ -243,7 +244,7 @@
file_ending (str): The file ending of this file (.ev or .py) Returns:
- str: The text content of the batch file.
+ text (str): The text content of the batch file. Raises: IOError: If problems reading file.
@@ -290,22 +291,30 @@
[docs]defparse_file(self,pythonpath):"""
- This parses the lines of a batchfile according to the following
- rules:
+ This parses the lines of a batch-command-file.
- 1. `#` at the beginning of a line marks the end of the command before
- it. It is also a comment and any number of # can exist on
- subsequent lines (but not inside comments).
- 2. `#INSERT` at the beginning of a line imports another
- batch-cmd file file and pastes it into the batch file as if
- it was written there.
- 3. Commands are placed alone at the beginning of a line and their
- arguments are considered to be everything following (on any
- number of lines) until the next comment line beginning with #.
- 4. Newlines are ignored in command definitions
- 5. A completely empty line in a command line definition is condered
- a newline (so two empty lines is a paragraph).
- 6. Excess spaces and indents inside arguments are stripped.
+ Args:
+ pythonpath (str): The dot-python path to the file.
+
+ Returns:
+ list: A list of all parsed commands with arguments, as strings.
+
+ Notes:
+ Parsing follows the following rules:
+
+ 1. A `#` at the beginning of a line marks the end of the command before
+ it. It is also a comment and any number of # can exist on
+ subsequent lines (but not inside comments).
+ 2. #INSERT at the beginning of a line imports another
+ batch-cmd file file and pastes it into the batch file as if
+ it was written there.
+ 3. Commands are placed alone at the beginning of a line and their
+ arguments are considered to be everything following (on any
+ number of lines) until the next comment line beginning with #.
+ 4. Newlines are ignored in command definitions
+ 5. A completely empty line in a command line definition is condered
+ a newline (so two empty lines is a paragraph).
+ 6. Excess spaces and indents inside arguments are stripped. """
@@ -316,7 +325,7 @@
try:path=match.group(1)return"\n#\n".join(self.parse_file(path))
- exceptIOErroraserr:
+ exceptIOError:raiseIOError("#INSERT {} failed.".format(path))text=_RE_INSERT.sub(replace_insert,text)
@@ -354,21 +363,23 @@
[docs]defparse_file(self,pythonpath):"""
- This parses the lines of a batchfile according to the following
- rules:
+ This parses the lines of a batch-code file Args: pythonpath (str): The dot-python path to the file. Returns:
- codeblocks (list): A list of all #CODE blocks, each with
- prepended #HEADER data. If no #CODE blocks were found,
- this will be a list of one element.
+ list: A list of all `#CODE` blocks, each with
+ prepended `#HEADER` block data. If no `#CODE`
+ blocks were found, this will be a list of one element
+ containing all code in the file (so a normal Python file). Notes:
+ Parsing is done according to the following rules:
+
1. Code before a #CODE/HEADER block are considered part of
- the first code/header block or is the ONLY block if no
- #CODE/HEADER blocks are defined.
+ the first code/header block or is the ONLY block if no
+ `#CODE/HEADER` blocks are defined. 2. Lines starting with #HEADER starts a header block (ends other blocks) 3. Lines starting with #CODE begins a code block (ends other blocks) 4. Lines starting with #INSERT are on form #INSERT filename. Code from
@@ -377,6 +388,7 @@
5. Code after the last block is considered part of the last header/code block
+
"""text="".join(read_batchfile(pythonpath,file_ending=".py"))
@@ -503,7 +515,6 @@
[docs]classContainer:""" Base container class. A container is simply a storage object whose properties can be acquired as a property on it. This is generally
@@ -196,12 +196,11 @@
new_script.start()returnnew_script
- if(
- (found.interval!=interval)
- or(found.start_delay!=start_delay)
- or(found.repeats!=repeats)
- ):
- found.restart(interval=interval,start_delay=start_delay,repeats=repeats)
+ if((found.interval!=interval)
+ or(found.start_delay!=start_delay)
+ or(found.repeats!=repeats)):
+ # the setup changed
+ found.start(interval=interval,start_delay=start_delay,repeats=repeats)iffound.desc!=desc:found.desc=descreturnfound
@@ -318,7 +317,6 @@
"""
-This module gathers all the essential database-creation
-functions for the game engine's various object types.
+This module gathers all the essential database-creation functions for the game
+engine's various object types.
-Only objects created 'stand-alone' are in here, e.g. object Attributes
-are always created directly through their respective objects.
+Only objects created 'stand-alone' are in here. E.g. object Attributes are
+always created through their respective objects handlers.
-Each creation_* function also has an alias named for the entity being
-created, such as create_object() and object(). This is for
-consistency with the utils.search module and allows you to do the
-shorter "create.object()".
+Each `creation_*` function also has an alias named for the entity being created,
+such as create_object() and object(). This is for consistency with the
+utils.search module and allows you to do the shorter `create.object()`.
-The respective object managers hold more methods for manipulating and
-searching objects already existing in the database.
+The respective object managers hold more methods for manipulating and searching
+objects already existing in the database.
-Models covered:
- Objects
- Scripts
- Help
- Message
- Channel
- Accounts"""fromdjango.confimportsettingsfromdjango.dbimportIntegrityError
@@ -81,7 +73,6 @@
_AccountDB=None_to_object=None_ChannelDB=None
-_channelhandler=None# limit symbol import from API
@@ -122,31 +113,30 @@
Keyword Args: typeclass (class or str): Class or python path to a typeclass. key (str): Name of the new object. If not set, a name of
- #dbref will be set.
+ `#dbref` will be set. home (Object or str): Obj or #dbref to use as the object's home location. permissions (list): A list of permission strings or tuples (permstring, category). locks (str): one or more lockstrings, separated by semicolons. aliases (list): A list of alternative keys or tuples (aliasstring, category). tags (list): List of tag keys or tuples (tagkey, category) or (tagkey, category, data).
- destination (Object or str): Obj or #dbref to use as an Exit's
- target.
+ destination (Object or str): Obj or #dbref to use as an Exit's target. report_to (Object): The object to return error messages to. nohome (bool): This allows the creation of objects without a default home location; only used when creating the default location itself or during unittests. attributes (list): Tuples on the form (key, value) or (key, value, category),
- (key, value, lockstring) or (key, value, lockstring, default_access).
- to set as Attributes on the new object.
+ (key, value, lockstring) or (key, value, lockstring, default_access).
+ to set as Attributes on the new object. nattributes (list): Non-persistent tuples on the form (key, value). Note that
- adding this rarely makes sense since this data will not survive a reload.
+ adding this rarely makes sense since this data will not survive a reload. Returns: object (Object): A newly created object of the given typeclass. Raises: ObjectDB.DoesNotExist: If trying to create an Object with
- `location` or `home` that can't be found.
+ `location` or `home` that can't be found. """global_ObjectDB
@@ -270,9 +260,8 @@
report_to (Object): The object to return error messages to. desc (str): Optional description of script tags (list): List of tags or tuples (tag, category).
- attributes (list): List of tuples `(key, value)`, `(key, value, category)`,
- `(key, value, category, lockstring)` or
- `(key, value, category, lockstring, default_access)`.
+ attributes (list): List if tuples (key, value) or (key, value, category)
+ (key, value, lockstring) or (key, value, lockstring, default_access). Returns: script (obj): An instance of the script created
@@ -412,33 +401,38 @@
[docs]defcreate_message(
- senderobj,message,channels=None,receivers=None,locks=None,tags=None,header=None
-):
+ senderobj,message,receivers=None,locks=None,tags=None,
+ header=None,**kwargs):""" Create a new communication Msg. Msgs represent a unit of database-persistent communication between entites. Args:
- senderobj (Object or Account): The entity sending the Msg.
+ senderobj (Object, Account, Script, str or list): The entity (or
+ entities) sending the Msg. If a `str`, this is the id-string
+ for an external sender type. message (str): Text with the message. Eventual headers, titles etc should all be included in this text string. Formatting will be retained.
- channels (Channel, key or list): A channel or a list of channels to
- send to. The channels may be actual channel objects or their
- unique key strings.
- receivers (Object, Account, str or list): An Account/Object to send
- to, or a list of them. May be Account objects or accountnames.
+ receivers (Object, Account, Script, str or list): An Account/Object to send
+ to, or a list of them. If a string, it's an identifier for an external
+ receiver. locks (str): Lock definition string. tags (list): A list of tags or tuples `(tag, category)`. header (str): Mime-type or other optional information for the message Notes:
- The Comm system is created very open-ended, so it's fully possible
- to let a message both go to several channels and to several
- receivers at the same time, it's up to the command definitions to
- limit this as desired.
+ The Comm system is created to be very open-ended, so it's fully
+ possible to let a message both go several receivers at the same time,
+ it's up to the command definitions to limit this as desired. """
+ if'channels'inkwargs:
+ raiseDeprecationWarning(
+ "create_message() does not accept 'channel' kwarg anymore "
+ "- channels no longer accept Msg objects."
+ )
+
global_Msgifnot_Msg:fromevennia.comms.modelsimportMsgas_Msg
@@ -450,8 +444,6 @@
forsenderinmake_iter(senderobj):new_message.senders=sendernew_message.header=header
- forchannelinmake_iter(channels):
- new_message.channels=channelforreceiverinmake_iter(receivers):new_message.receivers=receiveriflocks:
@@ -670,7 +662,6 @@
diff --git a/docs/0.9.5/_modules/evennia/utils/dbserialize.html b/docs/0.9.5/_modules/evennia/utils/dbserialize.html
index f178e700fe..c102529543 100644
--- a/docs/0.9.5/_modules/evennia/utils/dbserialize.html
+++ b/docs/0.9.5/_modules/evennia/utils/dbserialize.html
@@ -70,7 +70,7 @@
fromdjango.core.exceptionsimportObjectDoesNotExistfromdjango.contrib.contenttypes.modelsimportContentTypefromdjango.utils.safestringimportSafeString
-fromevennia.utils.utilsimportuses_database,is_iter,to_str,to_bytes
+fromevennia.utils.utilsimportuses_database,is_iter,to_bytesfromevennia.utilsimportlogger__all__=("to_pickle","from_pickle","do_pickle","do_unpickle","dbserialize","dbunserialize")
@@ -657,7 +657,7 @@
that saves assigned data to the database. Skip if not serializing onto a given object. If db_obj is given, this function will convert lists, dicts and sets to their
- `_SaverList`, `_SaverDict` and `_SaverSet` counterparts.
+ _SaverList, _SaverDict and _SaverSet counterparts. Returns: data (any): Unpickled data.
@@ -832,7 +832,6 @@
diff --git a/docs/0.9.5/_modules/evennia/utils/eveditor.html b/docs/0.9.5/_modules/evennia/utils/eveditor.html
index f6c34b5aec..cc30f40668 100644
--- a/docs/0.9.5/_modules/evennia/utils/eveditor.html
+++ b/docs/0.9.5/_modules/evennia/utils/eveditor.html
@@ -42,56 +42,52 @@
"""EvEditor (Evennia Line Editor)
-This implements an advanced line editor for editing longer texts
-in-game. The editor mimics the command mechanisms of the "VI" editor
-(a famous line-by-line editor) as far as reasonable.
+This implements an advanced line editor for editing longer texts in-game. The
+editor mimics the command mechanisms of the "VI" editor (a famous line-by-line
+editor) as far as reasonable.Features of the editor:
- - undo/redo.
- - edit/replace on any line of the buffer.
- - search&replace text anywhere in buffer.
- - formatting of buffer, or selection, to certain width + indentations.
- - allow to echo the input or not, depending on your client.
+- undo/redo.
+- edit/replace on any line of the buffer.
+- search&replace text anywhere in buffer.
+- formatting of buffer, or selection, to certain width + indentations.
+- allow to echo the input or not, depending on your client.
+- in-built help
-To use the editor, just import EvEditor from this module
-and initialize it:
-::
+To use the editor, just import EvEditor from this module and initialize it:
- from evennia.utils.eveditor import EvEditor
- EvEditor(caller, loadfunc=None, savefunc=None, quitfunc=None, key="", persistent=True)
+```python
+from evennia.utils.eveditor import EvEditor
-- `caller` is the user of the editor, the one to see all feedback.
-- `loadfunc(caller)` is called when the editor is first launched; the
- return from this function is loaded as the starting buffer in the
- editor.
-- `safefunc(caller, buffer)` is called with the current buffer when
- saving in the editor. The function should return True/False depending
- on if the saving was successful or not.
-- `quitfunc(caller)` is called when the editor exits. If this is given,
- no automatic quit messages will be given.
-- `key` is an optional identifier for the editing session, to be
- displayed in the editor.
-- `persistent` means the editor state will be saved to the database making it
- survive a server reload. Note that using this mode, the load- save-
- and quit-funcs must all be possible to pickle - notable unusable
- callables are class methods and functions defined inside other
- functions. With persistent=False, no such restriction exists.
-- `code` set to True activates features on the EvEditor to enter Python code.
+# set up an editor to edit the caller's 'desc' Attribute
+def _loadfunc(caller):
+ return caller.db.desc
-In addition, the EvEditor can be used to enter Python source code,
-and offers basic handling of indentation.
+def _savefunc(caller, buffer):
+ caller.db.desc = buffer.strip()
+ return True
-----
+def _quitfunc(caller):
+ caller.msg("Custom quit message")
+
+# start the editor
+EvEditor(caller, loadfunc=None, savefunc=None, quitfunc=None, key="",
+ persistent=True, code=False)
+```
+
+The editor can also be used to format Python code and be made to
+survive a reload. See the `EvEditor` class for more details."""importrefromdjango.confimportsettings
-fromevenniaimportCommand,CmdSet
+fromevenniaimportCmdSetfromevennia.utilsimportis_iter,fill,dedent,logger,justify,to_str,utilsfromevennia.utils.ansiimportrawfromevennia.commandsimportcmdhandler
+fromdjango.utils.translationimportgettextas_# we use cmdhandler instead of evennia.syscmdkeys to# avoid some cases of loading before evennia init'd
@@ -109,7 +105,7 @@
## -------------------------------------------------------------
-_HELP_TEXT="""
+_HELP_TEXT=_(""" <txt> - any non-command is appended to the end of the buffer. : <l> - view buffer or only line(s) <l> :: <l> - raw-view buffer or only line(s) <l>
@@ -145,66 +141,66 @@
:fd <l> - de-indent entire buffer or line <l> :echo - turn echoing of the input on/off (helpful for some clients)
-"""
+""")
-_HELP_LEGEND="""
+_HELP_LEGEND=_(""" Legend: <l> - line number, like '5' or range, like '3:7'. <w> - a single word, or multiple words with quotes around them. <txt> - longer string, usually not needing quotes.
-"""
+""")
-_HELP_CODE="""
+_HELP_CODE=_(""" :! - Execute code buffer without saving :< - Decrease the level of automatic indentation for the next lines :> - Increase the level of automatic indentation for the next lines := - Switch automatic indentation on/off""".lstrip("\n"
-)
+))
-_ERROR_LOADFUNC="""
+_ERROR_LOADFUNC=_("""{error}|rBuffer load function error. Could not load initial data.|n
-"""
+""")
-_ERROR_SAVEFUNC="""
+_ERROR_SAVEFUNC=_("""{error}|rSave function returned an error. Buffer not saved.|n
-"""
+""")
-_ERROR_NO_SAVEFUNC="|rNo save function defined. Buffer cannot be saved.|n"
+_ERROR_NO_SAVEFUNC=_("|rNo save function defined. Buffer cannot be saved.|n")
-_MSG_SAVE_NO_CHANGE="No changes need saving"
-_DEFAULT_NO_QUITFUNC="Exited editor."
+_MSG_SAVE_NO_CHANGE=_("No changes need saving")
+_DEFAULT_NO_QUITFUNC=_("Exited editor.")
-_ERROR_QUITFUNC="""
+_ERROR_QUITFUNC=_("""{error}|rQuit function gave an error. Skipping.|n
-"""
+""")
-_ERROR_PERSISTENT_SAVING="""
+_ERROR_PERSISTENT_SAVING=_("""{error}|rThe editor state could not be saved for persistent mode. Switchingto non-persistent mode (which means the editor session won't survivean eventual server reload - so save often!)|n
-"""
+""")
-_TRACE_PERSISTENT_SAVING=(
+_TRACE_PERSISTENT_SAVING=_("EvEditor persistent-mode error. Commonly, this is because one or ""more of the EvEditor callbacks could not be pickled, for example ""because it's a class method or is defined inside another function.")
-_MSG_NO_UNDO="Nothing to undo."
-_MSG_NO_REDO="Nothing to redo."
-_MSG_UNDO="Undid one step."
-_MSG_REDO="Redid one step."
+_MSG_NO_UNDO=_("Nothing to undo.")
+_MSG_NO_REDO=_("Nothing to redo.")
+_MSG_UNDO=_("Undid one step.")
+_MSG_REDO=_("Redid one step.")# -------------------------------------------------------------#
@@ -226,7 +222,10 @@
help_cateogory="LineEditor"
[docs]deffunc(self):
- """Implement the yes/no choice."""
+ """
+ Implement the yes/no choice.
+
+ """# this is only called from inside the lineeditor# so caller.ndb._lineditor must be set.
@@ -241,7 +240,10 @@
[docs]classSaveYesNoCmdSet(CmdSet):
- """Stores the yesno question"""
+ """
+ Stores the yesno question
+
+ """key="quitsave_yesno"priority=150# override other cmdsets.
@@ -271,17 +273,18 @@
[docs]defparse(self):"""
- Handles pre-parsing
+ Handles pre-parsing. Editor commands are on the form
+
+ ::
- Usage: :cmd [li] [w] [txt] Where all arguments are optional.
- - li - line number (int), starting from 1. This could also
- be a range given as <l>:<l>.
- - w - word(s) (string), could be encased in quotes.
- - txt - extra text (string), could be encased in quotes.
+ - `li` - line number (int), starting from 1. This could also
+ be a range given as <l>:<l>.
+ - `w` - word(s) (string), could be encased in quotes.
+ - `txt` - extra text (string), could be encased in quotes. """
@@ -376,6 +379,7 @@
def_load_editor(caller):""" Load persistent editor from storage.
+
"""saved_options=caller.attributes.get("_eveditor_saved")saved_buffer,saved_undo=caller.attributes.get("_eveditor_buffer_temp",(None,None))
@@ -401,6 +405,7 @@
[docs]classCmdLineInput(CmdEditorBase):""" No command match - Inputs line of text into buffer.
+
"""key=_CMD_NOMATCH
@@ -485,6 +490,7 @@
This command handles all the in-editor :-style commands. Since each command is small and very limited, this makes for a more efficient presentation.
+
"""caller=self.callereditor=caller.ndb._eveditor
@@ -512,7 +518,7 @@
# Insert single colon alone on a lineeditor.update_buffer([":"]iflstart==0elselinebuffer+[":"])ifecho_mode:
- caller.msg("Single ':' added to buffer.")
+ caller.msg(_("Single ':' added to buffer."))elifcmd==":h":# help entryeditor.display_help()
@@ -527,7 +533,7 @@
# quit. If not saved, will askifself.editor._unsaved:caller.cmdset.add(SaveYesNoCmdSet)
- caller.msg("Save before quitting? |lcyes|lt[Y]|le/|lcno|ltN|le")
+ caller.msg(_("Save before quitting?")+" |lcyes|lt[Y]|le/|lcno|ltN|le")else:editor.quit()elifcmd==":q!":
@@ -542,24 +548,26 @@
elifcmd==":UU":# reset buffereditor.update_buffer(editor._pristine_buffer)
- caller.msg("Reverted all changes to the buffer back to original state.")
+ caller.msg(_("Reverted all changes to the buffer back to original state."))elifcmd==":dd":# :dd <l> - delete line <l>buf=linebuffer[:lstart]+linebuffer[lend:]editor.update_buffer(buf)
- caller.msg("Deleted %s."%self.lstr)
+ caller.msg(_("Deleted {string}.").format(string=self.lstr))elifcmd==":dw":# :dw <w> - delete word in entire buffer# :dw <l> <w> delete word only on line(s) <l>ifnotself.arg1:
- caller.msg("You must give a search word to delete.")
+ caller.msg(_("You must give a search word to delete."))else:ifnotself.linerange:lstart=0lend=self.cline+1
- caller.msg("Removed %s for lines %i-%i."%(self.arg1,lstart+1,lend+1))
+ caller.msg(_("Removed {arg1} for lines {l1}-{l2}.").format(
+ arg1=self.arg1,l1=lstart+1,l2=lend+1))else:
- caller.msg("Removed %s for %s."%(self.arg1,self.lstr))
+ caller.msg(_("Removed {arg1} for {line}.").format(
+ arg1=self.arg1,line=self.lstr))sarea="\n".join(linebuffer[lstart:lend])sarea=re.sub(r"%s"%self.arg1.strip("'").strip('"'),"",sarea,re.MULTILINE)buf=linebuffer[:lstart]+sarea.split("\n")+linebuffer[lend:]
@@ -574,49 +582,52 @@
editor._indent=0ifeditor._persistent:caller.attributes.add("_eveditor_indent",0)
- caller.msg("Cleared %i lines from buffer."%self.nlines)
+ caller.msg(_("Cleared {nlines} lines from buffer.").format(nlines=self.nlines))elifcmd==":y":# :y <l> - yank line(s) to copy buffercbuf=linebuffer[lstart:lend]editor._copy_buffer=cbuf
- caller.msg("%s, %s yanked."%(self.lstr.capitalize(),cbuf))
+ caller.msg(_("{line}, {cbuf} yanked.").format(line=self.lstr.capitalize(),cbuf=cbuf))elifcmd==":x":# :x <l> - cut line to copy buffercbuf=linebuffer[lstart:lend]editor._copy_buffer=cbufbuf=linebuffer[:lstart]+linebuffer[lend:]editor.update_buffer(buf)
- caller.msg("%s, %s cut."%(self.lstr.capitalize(),cbuf))
+ caller.msg(_("{line}, {cbuf} cut.").format(line=self.lstr.capitalize(),cbuf=cbuf))elifcmd==":p":# :p <l> paste line(s) from copy bufferifnoteditor._copy_buffer:
- caller.msg("Copy buffer is empty.")
+ caller.msg(_("Copy buffer is empty."))else:buf=linebuffer[:lstart]+editor._copy_buffer+linebuffer[lstart:]editor.update_buffer(buf)
- caller.msg("Pasted buffer %s to %s."%(editor._copy_buffer,self.lstr))
+ caller.msg(_("Pasted buffer {cbuf} to {line}.").format(
+ cbuf=editor._copy_buffer,line=self.lstr))elifcmd==":i":# :i <l> <txt> - insert new linenew_lines=self.args.split("\n")ifnotnew_lines:
- caller.msg("You need to enter a new line and where to insert it.")
+ caller.msg(_("You need to enter a new line and where to insert it."))else:buf=linebuffer[:lstart]+new_lines+linebuffer[lstart:]editor.update_buffer(buf)
- caller.msg("Inserted %i new line(s) at %s."%(len(new_lines),self.lstr))
+ caller.msg(_("Inserted {num} new line(s) at {line}.").format(
+ num=len(new_lines),line=self.lstr))elifcmd==":r":# :r <l> <txt> - replace linesnew_lines=self.args.split("\n")ifnotnew_lines:
- caller.msg("You need to enter a replacement string.")
+ caller.msg(_("You need to enter a replacement string."))else:buf=linebuffer[:lstart]+new_lines+linebuffer[lend:]editor.update_buffer(buf)
- caller.msg("Replaced %i line(s) at %s."%(len(new_lines),self.lstr))
+ caller.msg(_("Replaced {num} line(s) at {line}.").format(
+ num=len(new_lines),line=self.lstr))elifcmd==":I":# :I <l> <txt> - insert text at beginning of line(s) <l>ifnotself.raw_stringandnoteditor._codefunc:
- caller.msg("You need to enter text to insert.")
+ caller.msg(_("You need to enter text to insert."))else:buf=(linebuffer[:lstart]
@@ -624,11 +635,11 @@
+linebuffer[lend:])editor.update_buffer(buf)
- caller.msg("Inserted text at beginning of %s."%self.lstr)
+ caller.msg(_("Inserted text at beginning of {line}.").format(line=self.lstr))elifcmd==":A":# :A <l> <txt> - append text after end of line(s)ifnotself.args:
- caller.msg("You need to enter text to append.")
+ caller.msg(_("You need to enter text to append."))else:buf=(linebuffer[:lstart]
@@ -636,23 +647,24 @@
+linebuffer[lend:])editor.update_buffer(buf)
- caller.msg("Appended text to end of %s."%self.lstr)
+ caller.msg(_("Appended text to end of {line}.").format(line=self.lstr))elifcmd==":s":# :s <li> <w> <txt> - search and replace words# in entire buffer or on certain linesifnotself.arg1ornotself.arg2:
- caller.msg("You must give a search word and something to replace it with.")
+ caller.msg(_("You must give a search word and something to replace it with."))else:ifnotself.linerange:lstart=0lend=self.cline+1caller.msg(
- "Search-replaced %s -> %s for lines %i-%i."
- %(self.arg1,self.arg2,lstart+1,lend)
+ _("Search-replaced {arg1} -> {arg2} for lines {l1}-{l2}.").format(
+ arg1=self.arg1,arg2=self.arg2,l1=lstart+1,l2=lend))else:caller.msg(
- "Search-replaced %s -> %s for %s."%(self.arg1,self.arg2,self.lstr)
+ _("Search-replaced {arg1} -> {arg2} for {line}.").format(
+ arg1=self.arg1,arg2=self.arg2,line=self.lstr))sarea="\n".join(linebuffer[lstart:lend])
@@ -674,9 +686,10 @@
ifnotself.linerange:lstart=0lend=self.cline+1
- caller.msg("Flood filled lines %i-%i."%(lstart+1,lend))
+ caller.msg(_("Flood filled lines {l1}-{l2}.").format(
+ l1=lstart+1,l2=lend))else:
- caller.msg("Flood filled %s."%self.lstr)
+ caller.msg(_("Flood filled {line}.").format(line=self.lstr))fbuf="\n".join(linebuffer[lstart:lend])fbuf=fill(fbuf,width=width)buf=linebuffer[:lstart]+fbuf.split("\n")+linebuffer[lend:]
@@ -698,16 +711,19 @@
width=_DEFAULT_WIDTHifself.arg1andself.arg1.lower()notinalign_map:self.caller.msg(
- "Valid justifications are [f]ull (default), [c]enter, [r]right or [l]eft"
+ _("Valid justifications are")
+ +" [f]ull (default), [c]enter, [r]right or [l]eft")returnalign=align_map[self.arg1.lower()]ifself.arg1else"f"ifnotself.linerange:lstart=0lend=self.cline+1
- self.caller.msg("%s-justified lines %i-%i."%(align_name[align],lstart+1,lend))
+ self.caller.msg(_("{align}-justified lines {l1}-{l2}.").format(
+ align=align_name[align],l1=lstart+1,l2=lend))else:
- self.caller.msg("%s-justified %s."%(align_name[align],self.lstr))
+ self.caller.msg(_("{align}-justified {line}.").format(
+ align=align_name[align],line=self.lstr))jbuf="\n".join(linebuffer[lstart:lend])jbuf=justify(jbuf,width=width,align=align)buf=linebuffer[:lstart]+jbuf.split("\n")+linebuffer[lend:]
@@ -718,9 +734,9 @@
ifnotself.linerange:lstart=0lend=self.cline+1
- caller.msg("Indented lines %i-%i."%(lstart+1,lend))
+ caller.msg(_("Indented lines {l1}-{l2}.").format(l1=lstart+1,l2=lend))else:
- caller.msg("Indented %s."%self.lstr)
+ caller.msg(_("Indented {line}.").format(line=self.lstr))fbuf=[indent+lineforlineinlinebuffer[lstart:lend]]buf=linebuffer[:lstart]+fbuf+linebuffer[lend:]editor.update_buffer(buf)
@@ -729,9 +745,10 @@
ifnotself.linerange:lstart=0lend=self.cline+1
- caller.msg("Removed left margin (dedented) lines %i-%i."%(lstart+1,lend))
+ caller.msg(_("Removed left margin (dedented) lines {l1}-{l2}.").format(
+ l1=lstart+1,l2=lend))else:
- caller.msg("Removed left margin (dedented) %s."%self.lstr)
+ caller.msg(_("Removed left margin (dedented) {line}.").format(line=self.lstr))fbuf="\n".join(linebuffer[lstart:lend])fbuf=dedent(fbuf)buf=linebuffer[:lstart]+fbuf.split("\n")+linebuffer[lend:]
@@ -739,45 +756,49 @@
elifcmd==":echo":# set echoing on/offeditor._echo_mode=noteditor._echo_mode
- caller.msg("Echo mode set to %s"%editor._echo_mode)
+ caller.msg(_("Echo mode set to {mode}").format(mode=editor._echo_mode))elifcmd==":!":ifeditor._codefunc:editor._codefunc(caller,editor._buffer)else:
- caller.msg("This command is only available in code editor mode.")
+ caller.msg(_("This command is only available in code editor mode."))elifcmd==":<":# :<ifeditor._codefunc:editor.decrease_indent()indent=editor._indentifindent>=0:
- caller.msg("Decreased indentation: new indentation is {}.".format(indent))
+ caller.msg(_(
+ "Decreased indentation: new indentation is {indent}.").format(
+ indent=indent))else:
- caller.msg("|rManual indentation is OFF.|n Use := to turn it on.")
+ caller.msg(_("|rManual indentation is OFF.|n Use := to turn it on."))else:
- caller.msg("This command is only available in code editor mode.")
+ caller.msg(_("This command is only available in code editor mode."))elifcmd==":>":# :>ifeditor._codefunc:editor.increase_indent()indent=editor._indentifindent>=0:
- caller.msg("Increased indentation: new indentation is {}.".format(indent))
+ caller.msg(_(
+ "Increased indentation: new indentation is {indent}.").format(
+ indent=indent))else:
- caller.msg("|rManual indentation is OFF.|n Use := to turn it on.")
+ caller.msg(_("|rManual indentation is OFF.|n Use := to turn it on."))else:
- caller.msg("This command is only available in code editor mode.")
+ caller.msg(_("This command is only available in code editor mode."))elifcmd==":=":# :=ifeditor._codefunc:editor.swap_autoindent()indent=editor._indentifindent>=0:
- caller.msg("Auto-indentation turned on.")
+ caller.msg(_("Auto-indentation turned on."))else:
- caller.msg("Auto-indentation turned off.")
+ caller.msg(_("Auto-indentation turned off."))else:
- caller.msg("This command is only available in code editor mode.")
+ caller.msg(_("This command is only available in code editor mode."))
[docs]classEvEditor:""" This defines a line editor object. It creates all relevant commands and tracks the current state of the buffer. It also cleans up after
@@ -924,12 +945,13 @@
[docs]defload_buffer(self):""" Load the buffer using the load function hook.
+
"""try:self._buffer=self._loadfunc(self._caller)ifnotisinstance(self._buffer,str):self._buffer=to_str(self._buffer)
- self._caller.msg("|rNote: input buffer was converted to a string.|n")
+ self._caller.msg(_("|rNote: input buffer was converted to a string.|n"))exceptExceptionase:fromevennia.utilsimportlogger
@@ -1066,7 +1088,7 @@
header=("|n"+sep*10
- +"Line Editor [%s]"%self._key
+ +_("Line Editor [{name}]").format(name=self._key)+sep*(_DEFAULT_WIDTH-24-len(self._key)))footer=(
@@ -1074,7 +1096,7 @@
+sep*10+"[l:%02i w:%03i c:%04i]"%(nlines,nwords,nchars)+sep*12
- +"(:h for help)"
+ +_("(:h for help)")+sep*(_DEFAULT_WIDTH-54))iflinenums:
@@ -1195,7 +1217,6 @@
diff --git a/docs/0.9.5/_modules/evennia/utils/evform.html b/docs/0.9.5/_modules/evennia/utils/evform.html
index 340425da19..dfc2d7d2b6 100644
--- a/docs/0.9.5/_modules/evennia/utils/evform.html
+++ b/docs/0.9.5/_modules/evennia/utils/evform.html
@@ -54,32 +54,34 @@
object when displaying the form.Example of input file `testform.py`:
-::
- FORMCHAR = "x"
- TABLECHAR = "c"
+```python
+FORMCHAR = "x"
+TABLECHAR = "c"
- FORM = '''
- .------------------------------------------------.
- | |
- | Name: xxxxx1xxxxx Player: xxxxxxx2xxxxxxx |
- | xxxxxxxxxxx |
- | |
- >----------------------------------------------<
- | |
- | Desc: xxxxxxxxxxx STR: x4x DEX: x5x |
- | xxxxx3xxxxx INT: x6x STA: x7x |
- | xxxxxxxxxxx LUC: x8x MAG: x9x |
- | |
- >----------------------------------------------<
- | | |
- | cccccccc | ccccccccccccccccccccccccccccccccccc |
- | cccccccc | ccccccccccccccccccccccccccccccccccc |
- | cccAcccc | ccccccccccccccccccccccccccccccccccc |
- | cccccccc | ccccccccccccccccccccccccccccccccccc |
- | cccccccc | cccccccccccccccccBccccccccccccccccc |
- | | |
- -------------------------------------------------
+FORM = '''
+.------------------------------------------------.
+| |
+| Name: xxxxx1xxxxx Player: xxxxxxx2xxxxxxx |
+| xxxxxxxxxxx |
+| |
+ >----------------------------------------------<
+| |
+| Desc: xxxxxxxxxxx STR: x4x DEX: x5x |
+| xxxxx3xxxxx INT: x6x STA: x7x |
+| xxxxxxxxxxx LUC: x8x MAG: x9x |
+| |
+ >----------------------------------------------<
+| | |
+| cccccccc | ccccccccccccccccccccccccccccccccccc |
+| cccccccc | ccccccccccccccccccccccccccccccccccc |
+| cccAcccc | ccccccccccccccccccccccccccccccccccc |
+| cccccccc | ccccccccccccccccccccccccccccccccccc |
+| cccccccc | cccccccccccccccccBccccccccccccccccc |
+| | |
+-------------------------------------------------
+'''
+```The first line of the `FORM` string is ignored. The forms and tablemarkers must mark out complete, unbroken rectangles, each containing
@@ -93,8 +95,8 @@
Use as follows:
-::
+```python from evennia import EvForm, EvTable # create a new form from the template
@@ -126,9 +128,10 @@
"B": tableB}) print(form)
-
+```This produces the following result:
+
:: .------------------------------------------------.
@@ -152,7 +155,6 @@
| | | ------------------------------------------------
-
The marked forms have been replaced with EvCells of text and withEvTables. The form can be updated by simply re-applying `form.map()`with the updated data.
@@ -230,15 +232,16 @@
[docs]def__init__(self,filename=None,cells=None,tables=None,form=None,**kwargs):"""
- Initiate the form.
+ Initiate the form Keyword Args: filename (str): Path to template file.
- cells (dict): A dictionary mapping of `{id:text}`.
- tables (dict): A dictionary mapping of `{id:EvTable}`.
- form (dict): A dictionary of
- `{"FORMCHAR":char, "TABLECHAR":char, "FORM":templatestring}`.
- if this is given, filename is not read.
+ cells (dict): A dictionary mapping `{id: text}`
+ tables (dict): A dictionary mapping `{id: EvTable}`.
+ form (dict): A dictionary
+ `{"FORMCHAR":char, "TABLECHAR":char, "FORM":templatestring}`.
+ If this is given, filename is not read.
+
Notes: Other kwargs are fed as options to the EvCells and EvTables (see `evtable.EvCell` and `evtable.EvTable` for more info).
@@ -541,7 +544,6 @@
"""
-The EvMenu is a full in-game menu system for Evennia.
+EvMenu
+
+This implements a full menu system for Evennia.To start the menu, just import the EvMenu class from this module.
-
Example usage:
-::
+
+```python from evennia.utils.evmenu import EvMenu
@@ -53,10 +55,11 @@
startnode="node1", cmdset_mergetype="Replace", cmdset_priority=1, auto_quit=True, cmd_on_exit="look", persistent=True)
+```Where `caller` is the Object to use the menu on - it will get a new
-cmdset while using the Menu. The `menu_module_path` is the python path
-to a python module containing function definitions. By adjusting the
+cmdset while using the Menu. The menu_module_path is the python path
+to a python module containing function definitions. By adjusting thekeyword options of the Menu() initialization call you can start themenu at different places in the menu definition file, adjust if themenu command should overload the normal commands or not, etc.
@@ -70,7 +73,8 @@
The menu is defined in a module (this can be the same module as thecommand definition too) with function definitions:
-::
+
+```python def node1(caller): # (this is the start node if called like above)
@@ -84,8 +88,9 @@
def another_node(caller, input_string, **kwargs): # code return text, options
+```
-Where `caller` is the object using the menu and input_string is the
+Where caller is the object using the menu and input_string is thecommand entered by the user on the *previous* node (the commandentered to get to this node). The node function code will only beexecuted once per node-visit and the system will accept nodes with
@@ -102,42 +107,42 @@
menu is immediately exited and the default "look" command is called.- `text` (str, tuple or None): Text shown at this node. If a tuple, the
- second element in the tuple is a help text to display at this
- node when the user enters the menu help command there.
+ second element in the tuple is a help text to display at this
+ node when the user enters the menu help command there.- `options` (tuple, dict or None): If `None`, this exits the menu. If a single dict, this is a single-option node. If a tuple,
- it should be a tuple of option dictionaries. Option dicts have
- the following keys:
+ it should be a tuple of option dictionaries. Option dicts have the following keys: - `key` (str or tuple, optional): What to enter to choose this option.
- If a tuple, it must be a tuple of strings, where the first string is the
- key which will be shown to the user and the others are aliases.
- If unset, the options' number will be used. The special key `_default`
- marks this option as the default fallback when no other option matches
- the user input. There can only be one `_default` option per node. It
- will not be displayed in the list.
+ If a tuple, it must be a tuple of strings, where the first string is the
+ key which will be shown to the user and the others are aliases.
+ If unset, the options' number will be used. The special key `_default`
+ marks this option as the default fallback when no other option matches
+ the user input. There can only be one `_default` option per node. It
+ will not be displayed in the list. - `desc` (str, optional): This describes what choosing the option will do. - `goto` (str, tuple or callable): If string, should be the name of node to go to
- when this option is selected. If a callable, it has the signature
- `callable(caller[,raw_input][,**kwargs])`. If a tuple, the first element
- is the callable and the second is a dict with the kwargs to pass to
- the callable. Those kwargs will also be passed into the next node if possible.
- Such a callable should return either a str or a (str, dict), where the
- string is the name of the next node to go to and the dict is the new,
- (possibly modified) kwarg to pass into the next node. If the callable returns
- None or the empty string, the current node will be revisited.
+ when this option is selected. If a callable, it has the signature
+ `callable(caller[,raw_input][,**kwargs])`. If a tuple, the first element
+ is the callable and the second is a dict with the `**kwargs` to pass to
+ the callable. Those kwargs will also be passed into the next node if possible.
+ Such a callable should return either a str or a (str, dict), where the
+ string is the name of the next node to go to and the dict is the new,
+ (possibly modified) kwarg to pass into the next node. If the callable returns
+ None or the empty string, the current node will be revisited. - `exec` (str, callable or tuple, optional): This takes the same input as `goto` above
- and runs before it. If given a node name, the node will be executed but will not
- be considered the next node. If node/callback returns str or (str, dict), these will
- replace the `goto` step (`goto` callbacks will not fire), with the string being the
- next node name and the optional dict acting as the kwargs-input for the next node.
- If an exec callable returns `None`, the current node is re-run.
+ and runs before it. If given a node name, the node will be executed but will not
+ be considered the next node. If node/callback returns str or (str, dict), these will
+ replace the `goto` step (`goto` callbacks will not fire), with the string being the
+ next node name and the optional dict acting as the kwargs-input for the next node.
+ If an exec callable returns the empty string (only), the current node is re-run.
-If key is not given, the option will automatically be identified by
+If `key` is not given, the option will automatically be identified byits number 1..N.Example:
-::
+
+```python # in menu_module.py
@@ -173,8 +178,11 @@
text = "This ends the menu since there are no options." return text, None
+```
+
When starting this menu with `Menu(caller, "path.to.menu_module")`,the first node will look something like this:
+
:: This is a node text
@@ -193,9 +201,8 @@
reaching a node without any options.
-For a menu demo, import CmdTestMenu from this module and add it to
-your default cmdset. Run it with this module, like `testmenu
-evennia.utils.evmenu`.
+For a menu demo, import `CmdTestMenu` from this module and add it to
+your default cmdset. Run it with this module, like `testmenu evennia.utils.evmenu`.## Menu generation from template string
@@ -211,10 +218,13 @@
For maximum flexibility you can inject normally-created nodes in the menu treebefore passing it to EvMenu. If that's not needed, you can also create a menuin one step with:
-::
+
+```python evmenu.template2menu(caller, menu_template, goto_callables)
+```
+
The `goto_callables` is a mapping `{"funcname": callable, ...}`, where eachcallable must be a module-global function on the form`funcname(caller, raw_string, **kwargs)` (like any goto-callable). The
@@ -292,9 +302,9 @@
key:values will be converted to strings/numbers with literal_eval before passedinto the callable.
-The "> " option takes a glob or regex to perform different actions depending on user
-input. Make sure to sort these in increasing order of generality since they
-will be tested in sequence.
+The \\> option takes a glob or regex to perform different actions depending
+on user input. Make sure to sort these in increasing order of generality since
+they will be tested in sequence.----
@@ -446,7 +456,8 @@
)# don't give the session as a kwarg here, direct to originalraiseEvMenuError(err)# we must do this after the caller with the menu has been correctly identified since it
- # can be either Account, Object or Session (in the latter case this info will be superfluous).
+ # can be either Account, Object or Session (in the latter case this info will be
+ # superfluous).caller.ndb._evmenu._session=self.session# we have a menu, use it.menu.parse_input(self.raw_string)
@@ -573,9 +584,7 @@
by default in all nodes of the menu. This will print out the current state of the menu. Deactivate for production use! When the debug flag is active, the `persistent` flag is deactivated.
-
- Keyword Args:
- any (any): All kwargs will become initialization variables on `caller.ndb._evmenu`,
+ **kwargs: All kwargs will become initialization variables on `caller.ndb._menutree`, to be available at run. Raises:
@@ -652,7 +661,8 @@
).intersection(set(kwargs.keys()))ifreserved_clash:raiseRuntimeError(
- f"One or more of the EvMenu `**kwargs` ({list(reserved_clash)}) is reserved by EvMenu for internal use."
+ f"One or more of the EvMenu `**kwargs` ({list(reserved_clash)}) "
+ "is reserved by EvMenu for internal use.")forkey,valinkwargs.items():setattr(self,key,val)
@@ -775,30 +785,6 @@
Call a node-like callable, with a variable number of raw_string, *args, **kwargs, all of which should work also if not present (only `caller` is always required). Return its result.
- Viable node-like callable forms:
- ::
-
- _callname(caller)
- _callname(caller, raw_string)
- _callname(caller, **kwargs)
- _callname(caller, raw_string, **kwargs)
-
- If this is a node:
-
- - `caller` is the one using the menu.
- - `raw_string` is the users exact input on the *previous* node.
- - `**kwargs` is either passed through the previous node or returned
- along with the node name from the goto-callable leading to this node.
-
- If this is a goto-callable:
-
- - `caller` is the one using the menu.
- - `raw_string` is the user's exact input when chosing the option that triggered
- this goto-callable.
- - `**kwargs` is any extra dict passed to the callable in the option
- definition, or (if no explit kwarg was given to the callable) the
- previous node's kwarg, if any.
-
"""try:try:
@@ -1003,8 +989,7 @@
raw_string (str): The raw default string entered on the previous node (only used if the node accepts it as an argument)
- Keyword Args:
- any: Extra arguments to goto callables.
+ **kwargs: Extra arguments to goto callables. """
@@ -1320,7 +1305,7 @@
table.extend([" "foriinrange(nrows-nlastcol)])# build the actual table grid
- table=[table[icol*nrows:(icol*nrows)+nrows]foricolinrange(0,ncols)]
+ table=[table[icol*nrows:(icol*nrows)+nrows]foricolinrange(0,ncols)]# adjust the width of each columnforicolinrange(len(table)):
@@ -1377,33 +1362,50 @@
option_generator (callable or list): A list of strings indicating the options, or a callable that is called as option_generator(caller) to produce such a list. select (callable or str, optional): Node to redirect a selection to. Its `**kwargs` will
- contain the `available_choices` list and `selection` will hold one
- of the elements in that list. If a callable, it will be called as
- `select(caller, menuchoice, **kwargs)` where menuchoice is the
- chosen option as a string and `available_choices` is the list of available
- options offered by the option_generator. The callable whould return
- the name of the target node to goto after this selection (or None to repeat the
- list-node). Note that if this is not given, the decorated node
- must itself provide a way to continue from the node!
+ contain the `available_choices` list and `selection` will hold one of the elements in
+ that list. If a callable, it will be called as
+ `select(caller, menuchoice, **kwargs)` where menuchoice is the chosen option as a
+ string and `available_choices` is a kwarg mapping the option keys to the choices
+ offered by the option_generator. The callable whould return the name of the target node
+ to goto after this selection (or None to repeat the list-node). Note that if this is not
+ given, the decorated node must itself provide a way to continue from the node! pagesize (int): How many options to show per page. Example:
- ::
- def _selectfunc(caller, menuchoice, **kwargs):
- # menuchoice would be either 'foo' or 'bar' here
- # kwargs['available_choices'] would be the list ['foo', 'bar']
- return "the_next_node_to_go_to"
+ ```python
+ def select(caller, selection, available_choices=None, **kwargs):
+ '''
+ Args:
+ caller (Object or Account): User of the menu.
+ selection (str): What caller chose in the menu
+ available_choices (list): The keys of elements available on the *current listing
+ page*.
+ **kwargs: Kwargs passed on from the node.
+ Returns:
+ tuple, str or None: A tuple (nextnodename, **kwargs) or just nextnodename. Return
+ `None` to go back to the listnode.
- @list_node(['foo', 'bar'], _selectfunc)
- def node_index(caller):
- text = "describing the list"
- return text, []
+ # (do something with `selection` here)
+
+ return "nextnode", **kwargs
+
+ @list_node(['foo', 'bar'], select)
+ def node_index(caller):
+ text = "describing the list"
+
+ # optional extra options in addition to the list-options
+ extra_options = []
+
+ return text, extra_options
+
+ ``` Notes:
- All normal `goto` or `exec` callables returned from the decorated nodes will, if they accept
- `**kwargs`, get a new kwarg `available_choices` injected. This is the ordered list of named
- options (descs) visible on the current node page.
+ All normal `goto` or `exec` callables returned from the decorated nodes
+ will, if they accept `**kwargs`, get a new kwarg 'available_choices'
+ injected. These are the ordered list of named options (descs) visible
+ on the current node page. """
@@ -1411,6 +1413,7 @@
def_select_parser(caller,raw_string,**kwargs):""" Parse the select action
+
"""available_choices=kwargs.get("available_choices",[])
@@ -1418,14 +1421,15 @@
index=int(raw_string.strip())-1selection=available_choices[index]exceptException:
- caller.msg("|rInvalid choice.|n")
+ caller.msg(_("|rInvalid choice.|n"))else:ifcallable(select):try:ifbool(getargspec(select).keywords):
- returnselect(caller,selection,available_choices=available_choices)
+ returnselect(
+ caller,selection,available_choices=available_choices,**kwargs)else:
- returnselect(caller,selection)
+ returnselect(caller,selection,**kwargs)exceptException:logger.log_trace()elifselect:
@@ -1450,7 +1454,7 @@
ifoption_list:nall_options=len(option_list)pages=[
- option_list[ind:ind+pagesize]forindinrange(0,nall_options,pagesize)
+ option_list[ind:ind+pagesize]forindinrange(0,nall_options,pagesize)]npages=len(pages)
@@ -1464,7 +1468,7 @@
# callback being called with a result from the available choicesoptions.extend([
- {"desc":opt,"goto":(_select_parser,{"available_choices":page})}
+ {"desc":opt,"goto":(_select_parser,{"available_choices":page,**kwargs})}foroptinpage])
@@ -1475,7 +1479,7 @@
# allows us to call ourselves over and over, using different kwargs.options.append({
- "key":("|Wcurrent|n","c"),
+ "key":(_("|Wcurrent|n"),"c"),"desc":"|W({}/{})|n".format(page_index+1,npages),"goto":(lambdacaller:None,{"optionpage_index":page_index}),}
@@ -1483,14 +1487,14 @@
ifpage_index>0:options.append({
- "key":("|wp|Wrevious page|n","p"),
+ "key":(_("|wp|Wrevious page|n"),"p"),"goto":(lambdacaller:None,{"optionpage_index":page_index-1}),})ifpage_index<npages-1:options.append({
- "key":("|wn|Wext page|n","n"),
+ "key":(_("|wn|Wext page|n"),"n"),"goto":(lambdacaller:None,{"optionpage_index":page_index+1}),})
@@ -1616,7 +1620,7 @@
self.add(CmdGetInput())
-class_Prompt(object):
+class_Prompt:"""Dummy holder"""pass
@@ -1624,60 +1628,51 @@
[docs]defget_input(caller,prompt,callback,session=None,*args,**kwargs):"""
- This is a helper function for easily request input from
- the caller.
+ This is a helper function for easily request input from the caller. Args:
- caller (Account or Object): The entity being asked
- the question. This should usually be an object
- controlled by a user.
- prompt (str): This text will be shown to the user,
- in order to let them know their input is needed.
+ caller (Account or Object): The entity being asked the question. This
+ should usually be an object controlled by a user.
+ prompt (str): This text will be shown to the user, in order to let them
+ know their input is needed. callback (callable): A function that will be called
- when the user enters a reply. It must take three
- arguments: the `caller`, the `prompt` text and the
- `result` of the input given by the user. If the
- callback doesn't return anything or return False,
- the input prompt will be cleaned up and exited. If
- returning True, the prompt will remain and continue to
- accept input.
+ when the user enters a reply. It must take three arguments: the
+ `caller`, the `prompt` text and the `result` of the input given by
+ the user. If the callback doesn't return anything or return False,
+ the input prompt will be cleaned up and exited. If returning True,
+ the prompt will remain and continue to accept input. session (Session, optional): This allows to specify the
- session to send the prompt to. It's usually only
- needed if `caller` is an Account in multisession modes
- greater than 2. The session is then updated by the
- command and is available (for example in callbacks)
- through `caller.ndb.getinput._session`.
- args, kwargs (optional): Extra arguments will be
- passed to the fall back function as a list 'args'
- and all keyword arguments as a dictionary 'kwargs'.
- To utilise `*args` and `**kwargs`, a value for the
- session argument must be provided (None by default)
- and the callback function must take `*args` and
- `**kwargs` as arguments.
+ session to send the prompt to. It's usually only needed if `caller`
+ is an Account in multisession modes greater than 2. The session is
+ then updated by the command and is available (for example in
+ callbacks) through `caller.ndb.getinput._session`.
+ *args (any): Extra arguments to pass to `callback`. To utilise `*args`
+ (and `**kwargs`), a value for the `session` argument must also be
+ provided.
+ **kwargs (any): Extra kwargs to pass to `callback`. Raises: RuntimeError: If the given callback is not callable. Notes:
- The result value sent to the callback is raw and not
- processed in any way. This means that you will get
- the ending line return character from most types of
- client inputs. So make sure to strip that before
- doing a comparison.
+ The result value sent to the callback is raw and not processed in any
+ way. This means that you will get the ending line return character from
+ most types of client inputs. So make sure to strip that before doing a
+ comparison.
- When the prompt is running, a temporary object
- `caller.ndb._getinput` is stored; this will be removed
- when the prompt finishes.
- If you need the specific Session of the caller (which
- may not be easy to get if caller is an account in higher
- multisession modes), then it is available in the
- callback through `caller.ndb._getinput._session`.
+ When the prompt is running, a temporary object `caller.ndb._getinput`
+ is stored; this will be removed when the prompt finishes.
- Chaining get_input functions will result in the caller
- stacking ever more instances of InputCmdSets. Whilst
- they will all be cleared on concluding the get_input
- chain, EvMenu should be considered for anything beyond
- a single question.
+ If you need the specific Session of the caller (which may not be easy
+ to get if caller is an account in higher multisession modes), then it
+ is available in the callback through `caller.ndb._getinput._session`.
+ This is why the `session` is required as input.
+
+ It's not recommended to 'chain' `get_input` into a sequence of
+ questions. This will result in the caller stacking ever more instances
+ of InputCmdSets. While they will all be cleared on concluding the
+ get_input chain, EvMenu should be considered for anything beyond a
+ single question. """ifnotcallable(callback):
@@ -1692,6 +1687,186 @@
caller.msg(prompt,session=session)
+
[docs]classCmdYesNoQuestion(Command):
+ """
+ Handle a prompt for yes or no. Press [return] for the default choice.
+
+ """
+
+ key=_CMD_NOINPUT
+ aliases=[_CMD_NOMATCH,"yes","no",'y','n','a','abort']
+ arg_regex=r"^$"
+
+ def_clean(self,caller):
+ delcaller.ndb._yes_no_question
+ ifnotcaller.cmdset.has(YesNoQuestionCmdSet)andhasattr(caller,"account"):
+ caller.account.cmdset.remove(YesNoQuestionCmdSet)
+ else:
+ caller.cmdset.remove(YesNoQuestionCmdSet)
+
+
[docs]deffunc(self):
+ """This is called when user enters anything."""
+ caller=self.caller
+ try:
+ yes_no_question=caller.ndb._yes_no_question
+ ifnotyes_no_questionandhasattr(caller,"account"):
+ yes_no_question=caller.account.ndb._yes_no_question
+ caller=caller.account
+
+ ifnotyes_no_question:
+ self._clean(caller)
+ return
+
+ inp=self.cmdname
+
+ ifinp==_CMD_NOINPUT:
+ raw=self.raw_cmdname.strip()
+ ifnotraw:
+ # use default
+ inp=yes_no_question.default
+ else:
+ inp=raw
+
+ ifinpin('a','abort')andyes_no_question.allow_abort:
+ caller.msg(_("Aborted."))
+ self._clean(caller)
+ return
+
+ caller.ndb._yes_no_question.session=self.session
+
+ args=yes_no_question.args
+ kwargs=yes_no_question.kwargs
+ kwargs['caller_session']=self.session
+
+ ifinpin('yes','y'):
+ yes_no_question.yes_callable(caller,*args,**kwargs)
+ elifinpin('no','n'):
+ yes_no_question.no_callable(caller,*args,**kwargs)
+ else:
+ # invalid input. Resend prompt without cleaning
+ caller.msg(yes_no_question.prompt,session=self.session)
+ return
+
+ # cleanup
+ self._clean(caller)
+ exceptException:
+ # make sure to clean up cmdset if something goes wrong
+ caller.msg(_("|rError in ask_yes_no. Choice not confirmed (report to admin)|n"))
+ logger.log_trace("Error in ask_yes_no")
+ self._clean(caller)
+ raise
[docs]defat_cmdset_creation(self):
+ """called once at creation"""
+ self.add(CmdYesNoQuestion())
+
+
+
[docs]defask_yes_no(caller,prompt="Yes or No {options}?",yes_action="Yes",no_action="No",
+ default=None,allow_abort=False,session=None,*args,**kwargs):
+ """
+ A helper question for asking a simple yes/no question. This will cause
+ the system to pause and wait for input from the player.
+
+ Args:
+ prompt (str): The yes/no question to ask. This takes an optional formatting
+ marker `{options}` which will be filled with 'Y/N', '[Y]/N' or
+ 'Y/[N]' depending on the setting of `default`. If `allow_abort` is set,
+ then the 'A(bort)' option will also be available.
+ yes_action (callable or str): If a callable, this will be called
+ with `(caller, *args, **kwargs)` when the Yes-choice is made.
+ If a string, this string will be echoed back to the caller.
+ no_action (callable or str): If a callable, this will be called
+ with `(caller, *args, **kwargs)` when the No-choice is made.
+ If a string, this string will be echoed back to the caller.
+ default (str optional): This is what the user will get if they just press the
+ return key without giving any input. One of 'N', 'Y', 'A' or `None`
+ for no default (an explicit choice must be given). If 'A' (abort)
+ is given, `allow_abort` kwarg is ignored and assumed set.
+ allow_abort (bool, optional): If set, the 'A(bort)' option is available
+ (a third option meaning neither yes or no but just exits the prompt).
+ session (Session, optional): This allows to specify the
+ session to send the prompt to. It's usually only needed if `caller`
+ is an Account in multisession modes greater than 2. The session is
+ then updated by the command and is available (for example in
+ callbacks) through `caller.ndb._yes_no_question.session`.
+ *args: Additional arguments passed on into callables.
+ **kwargs: Additional keyword args passed on into callables.
+
+ Raises:
+ RuntimeError, FooError: If default and `allow_abort` clashes.
+
+ Example:
+ ::
+
+ # just returning strings
+ ask_yes_no(caller, "Are you happy {options}?",
+ "you answered yes", "you answered no")
+ # trigger callables
+ ask_yes_no(caller, "Are you sad {options}?",
+ _callable_yes, _callable_no, allow_abort=True)
+
+ """
+ def_callable_yes_txt(caller,*args,**kwargs):
+ yes_txt=kwargs['yes_txt']
+ session=kwargs['caller_session']
+ caller.msg(yes_txt,session=session)
+
+ def_callable_no_txt(caller,*args,**kwargs):
+ no_txt=kwargs['no_txt']
+ session=kwargs['caller_session']
+ caller.msg(no_txt,session=session)
+
+ ifnotcallable(yes_action):
+ kwargs['yes_txt']=str(yes_action)
+ yes_action=_callable_yes_txt
+
+ ifnotcallable(no_action):
+ kwargs['no_txt']=str(no_action)
+ no_action=_callable_no_txt
+
+ # prepare the prompt with options
+ options="Y/N"
+ abort_txt="/Abort"ifallow_abortelse""
+ ifdefault:
+ default=default.lower()
+ ifdefault=="y":
+ options="[Y]/N"
+ elifdefault=="n":
+ options="Y/[N]"
+ elifdefault=="a":
+ allow_abort=True
+ abort_txt="/[A]bort"
+ options+=abort_txt
+ prompt=prompt.format(options=options)
+
+ caller.ndb._yes_no_question=_Prompt()
+ caller.ndb._yes_no_question.prompt=prompt
+ caller.ndb._yes_no_question.session=session
+ caller.ndb._yes_no_question.prompt=prompt
+ caller.ndb._yes_no_question.default=default
+ caller.ndb._yes_no_question.allow_abort=allow_abort
+ caller.ndb._yes_no_question.yes_callable=yes_action
+ caller.ndb._yes_no_question.no_callable=no_action
+ caller.ndb._yes_no_question.args=args
+ caller.ndb._yes_no_question.kwargs=kwargs
+
+ caller.cmdset.add(YesNoQuestionCmdSet)
+ caller.msg(prompt,session=session)
+
+
# -------------------------------------------------------------## Menu generation from menu template string
@@ -1701,7 +1876,9 @@
_RE_NODE=re.compile(r"##\s*?NODE\s+?(?P<nodename>\S[\S\s]*?)$",re.I+re.M)_RE_OPTIONS_SEP=re.compile(r"##\s*?OPTIONS\s*?$",re.I+re.M)_RE_CALLABLE=re.compile(r"\S+?\(\)",re.I+re.M)
-_RE_CALLABLE=re.compile(r"(?P<funcname>\S+?)(?:\((?P<kwargs>[\S\s]+?)\)|\(\))",re.I+re.M)
+_RE_CALLABLE=re.compile(
+ r"(?P<funcname>\S+?)(?:\((?P<kwargs>[\S\s]+?)\)|\(\))",re.I+re.M
+)_HELP_NO_OPTION_MATCH=_("Choose an option or try 'help'.")
@@ -1715,8 +1892,8 @@
# Input/option/goto handler functions that allows for dynamically generated# nodes read from the menu template.
-
-def_process_callable(caller,goto,goto_callables,raw_string,current_nodename,kwargs):
+def_process_callable(caller,goto,goto_callables,raw_string,
+ current_nodename,kwargs):""" Central helper for parsing a goto-callable (`funcname(**kwargs)`) out of the right-hand-side of the template options and map this to an actual
@@ -1732,18 +1909,12 @@
forkwargingotokwargs.split(","):ifkwargand"="inkwarg:key,value=[part.strip()forpartinkwarg.split("=",1)]
- ifkeyin(
- "evmenu_goto",
- "evmenu_gotomap",
- "_current_nodename",
- "evmenu_current_nodename",
- "evmenu_goto_callables",
- ):
+ ifkeyin("evmenu_goto","evmenu_gotomap","_current_nodename",
+ "evmenu_current_nodename","evmenu_goto_callables"):raiseRuntimeError(f"EvMenu template error: goto-callable '{goto}' uses a "f"kwarg ({kwarg}) that is reserved for the EvMenu templating "
- "system. Rename the kwarg."
- )
+ "system. Rename the kwarg.")try:key=literal_eval(key)exceptValueError:
@@ -1770,7 +1941,8 @@
goto=kwargs["evmenu_goto"]goto_callables=kwargs["evmenu_goto_callables"]current_nodename=kwargs["evmenu_current_nodename"]
- return_process_callable(caller,goto,goto_callables,raw_string,current_nodename,kwargs)
+ return_process_callable(caller,goto,goto_callables,raw_string,
+ current_nodename,kwargs)def_generated_input_goto_func(caller,raw_string,**kwargs):
@@ -1790,15 +1962,13 @@
# start with glob patternsforpattern,gotoingotomap.items():iffnmatch(raw_string.lower(),pattern):
- return_process_callable(
- caller,goto,goto_callables,raw_string,current_nodename,kwargs
- )
+ return_process_callable(caller,goto,goto_callables,raw_string,
+ current_nodename,kwargs)# no glob pattern match; try regexforpattern,gotoingotomap.items():ifpatternandre.match(pattern,raw_string.lower(),flags=re.I+re.M):
- return_process_callable(
- caller,goto,goto_callables,raw_string,current_nodename,kwargs
- )
+ return_process_callable(caller,goto,goto_callables,raw_string,
+ current_nodename,kwargs)# no match, show errorraiseEvMenuGotoAbortMessage(_HELP_NO_OPTION_MATCH)
@@ -1829,35 +1999,28 @@
dict: A `{"node": nodefunc}` menutree suitable to pass into EvMenu. """
-
def_validate_kwarg(goto,kwarg):""" Validate goto-callable kwarg is on correct form. """
- ifnot"="inkwarg:
+ if"="notinkwarg:raiseRuntimeError(f"EvMenu template error: goto-callable '{goto}' has a "f"non-kwarg argument ({kwarg}). All callables in the ""template must have only keyword-arguments, or no "
- "args at all."
- )
+ "args at all.")key,_=[part.strip()forpartinkwarg.split("=",1)]
- ifkeyin(
- "evmenu_goto",
- "evmenu_gotomap",
- "_current_nodename",
- "evmenu_current_nodename",
- "evmenu_goto_callables",
- ):
+ ifkeyin("evmenu_goto","evmenu_gotomap","_current_nodename",
+ "evmenu_current_nodename","evmenu_goto_callables"):raiseRuntimeError(f"EvMenu template error: goto-callable '{goto}' uses a "f"kwarg ({kwarg}) that is reserved for the EvMenu templating "
- "system. Rename the kwarg."
- )
+ "system. Rename the kwarg.")def_parse_options(nodename,optiontxt,goto_callables):""" Parse option section into option dict.
+
"""options=[]optiontxt=optiontxt[0].strip()ifoptiontxtelse""
@@ -1883,7 +2046,7 @@
ifmatch:kwargs=match.group("kwargs")ifkwargs:
- forkwarginkwargs.split(","):
+ forkwarginkwargs.split(','):_validate_kwarg(goto,kwarg)# parse key [;aliases|pattern]
@@ -1895,7 +2058,7 @@
ifmain_key.startswith(_OPTION_INPUT_MARKER):# if we have a pattern, build the arguments for _default later
- pattern=main_key[len(_OPTION_INPUT_MARKER):].strip()
+ pattern=main_key[len(_OPTION_INPUT_MARKER):].strip()inputparsemap[pattern]=gotoelse:# a regular goto string/callable target
@@ -1935,6 +2098,7 @@
def_parse(caller,menu_template,goto_callables):""" Parse the menu string format into a node tree.
+
"""nodetree={}splits=_RE_NODE.split(menu_template)
@@ -1956,7 +2120,12 @@
[docs]deftemplate2menu(
- caller,menu_template,goto_callables=None,startnode="start",persistent=False,**kwargs,
+ caller,
+ menu_template,
+ goto_callables=None,
+ startnode="start",
+ persistent=False,
+ **kwargs,):""" Helper function to generate and start an EvMenu based on a menu template
@@ -1981,7 +2150,12 @@
"""goto_callables=goto_callablesor{}menu_tree=parse_menu_template(caller,menu_template,goto_callables)
- returnEvMenu(caller,menu_tree,persistent=persistent,**kwargs,)
-
diff --git a/docs/0.9.5/_modules/evennia/utils/evmore.html b/docs/0.9.5/_modules/evennia/utils/evmore.html
index a85dca37e0..d75c7715ae 100644
--- a/docs/0.9.5/_modules/evennia/utils/evmore.html
+++ b/docs/0.9.5/_modules/evennia/utils/evmore.html
@@ -43,29 +43,34 @@
"""EvMore - pager mechanism
-This is a pager for displaying long texts and allows stepping up and
-down in the text (the name comes from the traditional 'more' unix
-command).
+This is a pager for displaying long texts and allows stepping up and down in
+the text (the name comes from the traditional 'more' unix command).To use, simply pass the text through the EvMore object:
-::
+
+
+```python from evennia.utils.evmore import EvMore text = some_long_text_output() EvMore(caller, text, always_page=False, session=None, justify_kwargs=None, **kwargs)
+```
-One can also use the convenience function msg from this module:
-::
+One can also use the convenience function `msg` from this module to avoid
+having to set up the `EvMenu` object manually:
+
+```python from evennia.utils import evmore text = some_long_text_output() evmore.msg(caller, text, always_page=False, session=None, justify_kwargs=None, **kwargs)
+```
-Where always_page decides if the pager is used also if the text is not long
-enough to need to scroll, session is used to determine which session to relay
-to and `justify_kwargs` are kwargs to pass to `utils.utils.justify` in order to
+The `always_page` argument decides if the pager is used also if the text is not long
+enough to need to scroll, `session` is used to determine which session to relay
+to and `justify_kwargs` are kwargs to pass to utils.utils.justify in order tochange the formatting of the text. The remaining `**kwargs` will be passed on tothe `caller.msg()` construct every time the page is updated.
@@ -77,7 +82,9 @@
fromdjango.core.paginatorimportPaginatorfromevenniaimportCommand,CmdSetfromevennia.commandsimportcmdhandler
-fromevennia.utils.utilsimportmake_iter,inherits_from,justify
+fromevennia.utils.ansiimportANSIString
+fromevennia.utils.utilsimportmake_iter,inherits_from,justify,dedent
+fromdjango.utils.translationimportgettextas__CMD_NOMATCH=cmdhandler.CMD_NOMATCH_CMD_NOINPUT=cmdhandler.CMD_NOINPUT
@@ -88,6 +95,8 @@
_EVTABLE=None
+_LBR=ANSIString("\n")
+
# text_DISPLAY="""{text}
@@ -169,10 +178,9 @@
returnqs.count()
-
[docs]classEvMore(object):"""
- The main pager object.
-
+ The main pager object """
[docs]def__init__(
@@ -190,16 +198,15 @@
):"""
- Initialization of the Evmore input handler.
+ Initialization of the EvMore pager. Args: caller (Object or Account): Entity reading the text. inp (str, EvTable, Paginator or iterator): The text or data to put under paging. - If a string, paginage normally. If this text contains
- one or more \\\\f (backslash + f) format symbols, automatic
- pagination and justification are force-disabled and
- page-breaks will only happen after each \\\\f.
+ one or more `\\\\f` format symbol, automatic pagination and justification
+ are force-disabled and page-breaks will only happen after each `\\\\f`. - If `EvTable`, the EvTable will be paginated with the same setting on each page if it is too long. The table decorations will be considered in the size of the page.
@@ -207,8 +214,9 @@
expected to be a line in the final display. Each line will be run through `iter_callable`.
- always_page (bool, optional): If `False`, the pager will only kick
- in if `inp` is too big to fit the screen.
+ always_page (bool, optional): If `False`, the
+ pager will only kick in if `inp` is too big
+ to fit the screen. session (Session, optional): If given, this session will be used to determine the screen width and will receive all output. justify (bool, optional): If set, auto-justify long lines. This must be turned
@@ -224,51 +232,29 @@
the caller when the more page exits. Note that this will be using whatever cmdset the user had *before* the evmore pager was activated (so none of the evmore commands will be available when this is run).
- kwargs (any, any): These will be passed on to the `caller.msg` method.
+ kwargs (any, optional): These will be passed on to the `caller.msg` method. Examples:
- Basic use:
- ::
- super_long_text = " ... "
- EvMore(caller, super_long_text)
-
- Paginated query data - this is an optimization to avoid fetching
- database data until it's actually paged to.
- ::
-
- from django.core.paginator import Paginator
-
- query = ObjectDB.objects.all()
- pages = Paginator(query, 10) # 10 objs per page
- EvMore(caller, pages)
-
- Automatic split EvTable over multiple EvMore pages
- ::
-
- table = EvMore(*header, table=tabledata)
- EvMore(caller, table)
-
- Every page a separate EvTable (optimization for very large data sets)
- ::
-
- from evennia import EvTable, EvMore
-
- class TableEvMore(EvMore):
- def init_pages(self, data):
- pages = # depends on data type
- super().init_pages(pages)
-
- def page_formatter(self, page):
- table = EvTable()
-
- for line in page:
- cols = # split raw line into columns
- table.add_row(*cols)
-
- return str(table)
-
- TableEvMore(caller, pages)
+ ```python
+ super_long_text = " ... "
+ EvMore(caller, super_long_text)
+ ```
+ Paginator
+ ```python
+ from django.core.paginator import Paginator
+ query = ObjectDB.objects.all()
+ pages = Paginator(query, 10) # 10 objs per page
+ EvMore(caller, pages)
+ ```
+ Every page an EvTable
+ ```python
+ from evennia import EvTable
+ def _to_evtable(page):
+ table = ... # convert page to a table
+ return EvTable(*headers, table=table, ...)
+ EvMore(caller, pages, page_formatter=_to_evtable)
+ ``` """self._caller=caller
@@ -287,7 +273,7 @@
self._justify_kwargs=justify_kwargsself.exit_on_lastpage=exit_on_lastpageself.exit_cmd=exit_cmd
- self._exit_msg="Exited |wmore|n pager."
+ self._exit_msg=_("Exited |wmore|n pager.")self._kwargs=kwargsself._data=None
@@ -410,8 +396,9 @@
""" Paginate by slice. This is done with an eye on memory efficiency (usually for querysets); to avoid fetching all objects at the same time.
+
"""
- returnself._data[pageno*self.height:pageno*self.height+self.height]
[docs]definit_f_str(self,text):"""
- The input contains \\\\f (backslash + f) markers. We use \\\\f to indicate
- the user wants to enforce their line breaks on their own. If so, we do
- no automatic line-breaking/justification at all.
+ The input contains `\\f` markers. We use `\\f` to indicate the user wants to
+ enforce their line breaks on their own. If so, we do no automatic
+ line-breaking/justification at all.
+
+ Args:
+ text (str): The string to format with f-markers. """self._data=text.split("\f")
@@ -486,7 +476,7 @@
lines=text.split("\n")self._data=[
- "\n".join(lines[i:i+self.height])foriinrange(0,len(lines),self.height)
+ _LBR.join(lines[i:i+self.height])foriinrange(0,len(lines),self.height)]self._npages=len(self._data)
@@ -504,16 +494,15 @@
Notes: If overridden, this method must perform the following actions:
- - read and re-store `self._data` (the incoming data set) if needed
- for pagination to work.
+ - read and re-store `self._data` (the incoming data set) if needed for pagination to
+ work. - set `self._npages` to the total number of pages. Default is 1. - set `self._paginator` to a callable that will take a page number 1...N and return the data to display on that page (not any decorations or next/prev buttons). If only wanting to change the paginator, override `self.paginator` instead.
- - set `self._page_formatter` to a callable that will receive the
- page from `self._paginator` and format it with one element per
- line. Default is `str`. Or override `self.page_formatter`
- directly instead.
+ - set `self._page_formatter` to a callable that will receive the page from
+ `self._paginator` and format it with one element per line. Default is `str`. Or
+ override `self.page_formatter` directly instead. By default, helper methods are called that perform these actions depending on supported inputs.
@@ -590,40 +579,6 @@
""" EvMore-supported version of msg, mimicking the normal msg method.
- Args:
- caller (Object or Account): Entity reading the text.
- text (str, EvTable or iterator): The text or data to put under paging.
-
- - If a string, paginage normally. If this text contains
- one or more \\\\f (backslash + f) format symbol, automatic pagination is disabled
- and page-breaks will only happen after each \\\\f.
- - If `EvTable`, the EvTable will be paginated with the same
- setting on each page if it is too long. The table
- decorations will be considered in the size of the page.
- - Otherwise `text` is converted to an iterator, where each step is
- is expected to be a line in the final display, and each line
- will be run through repr().
-
- always_page (bool, optional): If `False`, the
- pager will only kick in if `text` is too big
- to fit the screen.
- session (Session, optional): If given, this session will be used
- to determine the screen width and will receive all output.
- justify (bool, optional): If set, justify long lines in output. Disable for
- fixed-format output, like tables.
- justify_kwargs (dict, bool or None, optional): If given, this should
- be valid keyword arguments to the utils.justify() function. If False,
- no justification will be done.
- exit_on_lastpage (bool, optional): Immediately exit pager when reaching the last page.
- use_evtable (bool, optional): If True, each page will be rendered as an
- EvTable. For this to work, `text` must be an iterable, where each element
- is the table (list of list) to render on that page.
- evtable_args (tuple, optional): The args to use for EvTable on each page.
- evtable_kwargs (dict, optional): The kwargs to use for EvTable on each
- page (except `table`, which is supplied by EvMore per-page).
- kwargs (any, optional): These will be passed on
- to the `caller.msg` method.
-
"""EvMore(caller,
@@ -635,6 +590,9 @@
exit_on_lastpage=exit_on_lastpage,**kwargs,)
"""
-This is an advanced ASCII table creator. It was inspired by
-[prettytable](https://code.google.com/p/prettytable/) but shares no code.
+This is an advanced ASCII table creator. It was inspired by Prettytable
+(https://code.google.com/p/prettytable/) but shares no code and is considerably
+more advanced, supporting auto-balancing of incomplete tables and ANSI colors among
+other things.Example usage:
-::
- from evennia.utils import evtable
+```python
+ from evennia.utils import evtable
- table = evtable.EvTable("Heading1", "Heading2",
+ table = evtable.EvTable("Heading1", "Heading2", table=[[1,2,3],[4,5,6],[7,8,9]], border="cells")
- table.add_column("This is long data", "This is even longer data")
- table.add_row("This is a single row")
- print table
+ table.add_column("This is long data", "This is even longer data")
+ table.add_row("This is a single row")
+ print table
+```Result:
+
:: +----------------------+----------+---+--------------------------+
@@ -71,13 +75,15 @@
As seen, the table will automatically expand with empty cells to makethe table symmetric. Tables can be restricted to a given width:
-::
- table.reformat(width=50, align="l")
+```python
+ table.reformat(width=50, align="l")
+```(We could just have added these keywords to the table creation call)This yields the following result:
+
:: +-----------+------------+-----------+-----------+
@@ -98,16 +104,21 @@
| row | | | | +-----------+------------+-----------+-----------+
+
Table-columns can be individually formatted. Note that if anindividual column is set with a specific width, table auto-balancingwill not affect this column (this may lead to the full table being toowide, so be careful mixing fixed-width columns with auto- balancing).Here we change the width and alignment of the column at index 3(Python starts from 0):
-::
- table.reformat_column(3, width=30, align="r")
- print table
+```python
+
+table.reformat_column(3, width=30, align="r")
+print table
+```
+
+:: +-----------+-------+-----+-----------------------------+---------+ | Heading1 | Headi | | | |
@@ -131,15 +142,14 @@
vertically. This will lead to text contents being cropped. Each cellcan only shrink to a minimum width and height of 1.
-`EvTable` is intended to be used with [ANSIString](evennia.utils.ansi#ansistring)
-for supporting ANSI-coloured string types.
+`EvTable` is intended to be used with `ANSIString` for supporting ANSI-coloured
+string types.
-When a cell is auto-wrapped across multiple lines, ANSI-reset
-sequences will be put at the end of each wrapped line. This means that
-the colour of a wrapped cell will not "bleed", but it also means that
-eventual colour outside the table will not transfer "across" a table,
-you need to re-set the color to have it appear on both sides of the
-table string.
+When a cell is auto-wrapped across multiple lines, ANSI-reset sequences will be
+put at the end of each wrapped line. This means that the colour of a wrapped
+cell will not "bleed", but it also means that eventual colour outside the table
+will not transfer "across" a table, you need to re-set the color to have it
+appear on both sides of the table string.----
@@ -270,12 +280,12 @@
delchunks[-1]whilechunks:
- l=d_len(chunks[-1])
+ ln=d_len(chunks[-1])# Can at least squeeze this chunk onto the current line.
- ifcur_len+l<=width:
+ ifcur_len+ln<=width:cur_line.append(chunks.pop())
- cur_len+=l
+ cur_len+=ln# Nope, this line is full.else:
@@ -293,10 +303,10 @@
# Convert current line back to a string and store it in list# of all lines (return value).ifcur_line:
- l=""
+ ln=""forwincur_line:# ANSI fix
- l+=w#
- lines.append(indent+l)
+ ln+=w#
+ lines.append(indent+ln)returnlines
@@ -1130,8 +1140,9 @@
height (int, optional): Fixed height of table. Defaults to being unset. Width is still given precedence. If given, table cells will crop text rather than expand vertically.
- evenwidth (bool, optional): Used with the `width` keyword. Adjusts columns to have as even width as
- possible. This often looks best also for mixed-length tables. Default is `False`.
+ evenwidth (bool, optional): Used with the `width` keyword. Adjusts columns to have as
+ even width as possible. This often looks best also for mixed-length tables. Default
+ is `False`. maxwidth (int, optional): This will set a maximum width of the table while allowing it to be smaller. Only if it grows wider than this size will it be resized by expanding horizontally (or crop `height` is given).
@@ -1378,7 +1389,8 @@
self.ncols=ncolsself.nrows=nrowmax
- # add borders - these add to the width/height, so we must do this before calculating width/height
+ # add borders - these add to the width/height, so we must do this before calculating
+ # width/heightself._borders()# equalize widths within each column
@@ -1465,7 +1477,8 @@
exceptException:raise
- # equalize heights for each row (we must do this here, since it may have changed to fit new widths)
+ # equalize heights for each row (we must do this here, since it may have changed to fit new
+ # widths)cheights=[max(cell.get_height()forcellin(col[iy]forcolinself.worktable))foriyinrange(nrowmax)
@@ -1826,7 +1839,6 @@
-
diff --git a/docs/0.9.5/_modules/evennia/utils/gametime.html b/docs/0.9.5/_modules/evennia/utils/gametime.html
index 1642cf046f..73ad949c3c 100644
--- a/docs/0.9.5/_modules/evennia/utils/gametime.html
+++ b/docs/0.9.5/_modules/evennia/utils/gametime.html
@@ -48,7 +48,6 @@
"""importtime
-fromcalendarimportmonthrangefromdatetimeimportdatetime,timedeltafromdjango.db.utilsimportOperationalError
@@ -68,6 +67,7 @@
try:GAME_TIME_OFFSET=ServerConfig.objects.conf("gametime_offset",default=0)exceptOperationalError:
+ # the db is not initializedprint("Gametime offset could not load - db not set up.")GAME_TIME_OFFSET=0
@@ -349,7 +349,6 @@
-
diff --git a/docs/0.9.5/_modules/evennia/utils/idmapper/models.html b/docs/0.9.5/_modules/evennia/utils/idmapper/models.html
index db2449338c..e8e63d8020 100644
--- a/docs/0.9.5/_modules/evennia/utils/idmapper/models.html
+++ b/docs/0.9.5/_modules/evennia/utils/idmapper/models.html
@@ -104,7 +104,8 @@
returnsuper(SharedMemoryModelBase,cls).__call__(*args,**kwargs)instance_key=cls._get_cache_key(args,kwargs)
- # depending on the arguments, we might not be able to infer the PK, so in that case we create a new instance
+ # depending on the arguments, we might not be able to infer the PK, so in that case we
+ # create a new instanceifinstance_keyisNone:returnnew_instance()cached_instance=cls.get_cached_instance(instance_key)
@@ -195,9 +196,9 @@
ifisinstance(value,(str,int)):value=to_str(value)ifvalue.isdigit()orvalue.startswith("#"):
- # we also allow setting using dbrefs, if so we try to load the matching object.
- # (we assume the object is of the same type as the class holding the field, if
- # not a custom handler must be used for that field)
+ # we also allow setting using dbrefs, if so we try to load the matching
+ # object. (we assume the object is of the same type as the class holding
+ # the field, if not a custom handler must be used for that field)dbid=dbref(value,reqhash=False)ifdbid:model=_GA(cls,"_meta").get_field(fname).model
@@ -307,21 +308,24 @@
pk=cls._meta.pks[0]else:pk=cls._meta.pk
- # get the index of the pk in the class fields. this should be calculated *once*, but isn't atm
+ # get the index of the pk in the class fields. this should be calculated *once*, but isn't
+ # atmpk_position=cls._meta.fields.index(pk)iflen(args)>pk_position:# if it's in the args, we can get it easily by indexresult=args[pk_position]elifpk.attnameinkwargs:
- # retrieve the pk value. Note that we use attname instead of name, to handle the case where the pk is a
- # a ForeignKey.
+ # retrieve the pk value. Note that we use attname instead of name, to handle the case
+ # where the pk is a a ForeignKey.result=kwargs[pk.attname]elifpk.name!=pk.attnameandpk.nameinkwargs:
- # ok we couldn't find the value, but maybe it's a FK and we can find the corresponding object instead
+ # ok we couldn't find the value, but maybe it's a FK and we can find the corresponding
+ # object insteadresult=kwargs[pk.name]ifresultisnotNoneandisinstance(result,Model):
- # if the pk value happens to be a model instance (which can happen wich a FK), we'd rather use its own pk as the key
+ # if the pk value happens to be a model instance (which can happen wich a FK), we'd
+ # rather use its own pk as the keyresult=result._get_pk_val()returnresult
@@ -749,7 +753,6 @@
-"""
-Inline functions (nested form).
-
-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 "(". It 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:
-::
-
- 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 not completely closing all inlinefunc blocks, will lead
-to the entire string remaining unparsed.
-
-----
-
-"""
-
-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, template (regex, str): Regex to match against strings and a
- template with markers `{arg1}`, `{arg2}`, etc for replacement using the
- standard `.format` method.
-
- Raises:
- 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/0.9.5/_modules/evennia/utils/logger.html b/docs/0.9.5/_modules/evennia/utils/logger.html
index 4a02a01b93..8566473a6a 100644
--- a/docs/0.9.5/_modules/evennia/utils/logger.html
+++ b/docs/0.9.5/_modules/evennia/utils/logger.html
@@ -57,7 +57,6 @@
importosimporttime
-importglobfromdatetimeimportdatetimefromtracebackimportformat_excfromtwisted.pythonimportlog,logfile
@@ -88,6 +87,7 @@
Returns: timestring (str): A formatted string of the given time.
+
"""when=whenifwhenelsetime.time()
@@ -167,6 +167,7 @@
server.log.2020_01_29 server.log.2020_01_29__1 server.log.2020_01_29__2
+
"""suffix=""copy_suffix=0
@@ -187,7 +188,10 @@
returnsuffix
[docs]defwrite(self,data):
- "Write data to log file"
+ """
+ Write data to log file
+
+ """logfile.BaseLogFile.write(self,data)self.lastDate=max(self.lastDate,self.toDate())self.size+=len(data)
@@ -196,6 +200,7 @@
[docs]classPortalLogObserver(log.FileLogObserver):""" Reformat logging
+
"""timeFormat=None
@@ -330,6 +335,7 @@
Prints any generic debugging/informative info that should appear in the log. infomsg: (string) The message to be logged.
+
"""try:infomsg=str(infomsg)
@@ -348,6 +354,7 @@
Args: depmsg (str): The deprecation message to log.
+
"""try:depmsg=str(depmsg)
@@ -366,6 +373,7 @@
Args: secmsg (str): The security message to log.
+
"""try:secmsg=str(secmsg)
@@ -387,6 +395,7 @@
the LogFile's rotate method in order to append some of the last lines of the previous log to the start of the new log, in order to preserve a continuous chat history for channel log files.
+
"""# we delay import of settings to keep logger module as free
@@ -398,12 +407,15 @@
_CHANNEL_LOG_NUM_TAIL_LINES=settings.CHANNEL_LOG_NUM_TAIL_LINESnum_lines_to_append=_CHANNEL_LOG_NUM_TAIL_LINES
-
[docs]defrotate(self,num_lines_to_append=None):""" Rotates our log file and appends some number of lines from the previous log to the start of the new one.
+
"""
- append_tail=self.num_lines_to_append>0
+ append_tail=(num_lines_to_append
+ ifnum_lines_to_appendisnotNone
+ elseself.num_lines_to_append)ifnotappend_tail:logfile.LogFile.rotate(self)return
@@ -416,9 +428,11 @@
""" Convenience method for accessing our _file attribute's seek method, which is used in tail_log_function.
+
Args: *args: Same args as file.seek **kwargs: Same kwargs as file.seek
+
"""returnself._file.seek(*args,**kwargs)
@@ -426,12 +440,14 @@
""" Convenience method for accessing our _file attribute's readlines method, which is used in tail_log_function.
+
Args: *args: same args as file.readlines **kwargs: same kwargs as file.readlines Returns: lines (list): lines from our _file attribute.
+
"""return[line.decode("utf-8")forlineinself._file.readlines(*args,**kwargs)]
[docs]deflog_file_exists(filename="game.log"):
+ """
+ Determine if a log-file already exists.
+
+ Args:
+ filename (str): The filename (within the log-dir).
+
+ Returns:
+ bool: If the log file exists or not.
+
+ """
+ global_LOGDIR
+ ifnot_LOGDIR:
+ fromdjango.confimportsettings
+ _LOGDIR=settings.LOG_DIR
+
+ filename=os.path.join(_LOGDIR,filename)
+ returnos.path.exists(filename)
+
+
+
[docs]defrotate_log_file(filename="game.log",num_lines_to_append=None):
+ """
+ Force-rotate a log-file, without
+
+ Args:
+ filename (str): The log file, located in settings.LOG_DIR.
+ num_lines_to_append (int, optional): Include N number of
+ lines from previous file in new one. If `None`, use default.
+ Set to 0 to include no lines.
+
+ """
+ iflog_file_exists(filename):
+ file_handle=_open_log_file(filename)
+ iffile_handle:
+ file_handle.rotate(num_lines_to_append=num_lines_to_append)
+
+
[docs]deftail_log_file(filename,offset,nlines,callback=None):""" Return the tail of the log file.
@@ -552,7 +605,7 @@
lines_found=filehandle.readlines()block_count-=1# return the right number of lines
- lines_found=lines_found[-nlines-offset:-offsetifoffsetelseNone]
+ lines_found=lines_found[-nlines-offset:-offsetifoffsetelseNone]ifcallback:callback(lines_found)returnNone
@@ -610,7 +663,6 @@
-"""
-Option classes store user- or server Options in a generic way
-while also providing validation.
-
-"""
-
-importdatetime
+importdatetimefromevenniaimportloggerfromevennia.utils.ansiimportstrip_ansifromevennia.utils.validatorfuncsimport_TZ_DICT
@@ -157,8 +151,8 @@
[docs]defsave(self,**kwargs):""" Stores the current value using `.handler.save_handler(self.key, value, **kwargs)`
- where kwargs are a combination of those passed into this function and the
- ones specified by the OptionHandler.
+ where `kwargs` are a combination of those passed into this function and
+ the ones specified by the `OptionHandler`. Keyword Args: any (any): Not used by default. These are passed in from self.set
@@ -405,7 +399,6 @@
[docs]classInMemorySaveHandler:""" Fallback SaveHandler, implementing a minimum of the required save mechanism and storing data in memory.
@@ -63,7 +64,7 @@
returnself.storage.get(key,default)
[docs]classOptionHandler:""" This is a generic Option handler. Retrieve options either as properties on this handler or by using the .get method.
@@ -98,6 +99,7 @@
A common one to pass would be AttributeHandler.get. save_kwargs (any): Optional extra kwargs to pass into `savefunc` above. load_kwargs (any): Optional extra kwargs to pass into `loadfunc` above.
+
Notes: Both loadfunc and savefunc must be specified. If only one is given, the other will be ignored and in-memory storage will be used.
@@ -174,7 +176,7 @@
"""ifkeynotinself.options_dict:ifraise_error:
- raiseKeyError("Option not found!")
+ raiseKeyError(_("Option not found!"))returndefault# get the options or load/recache itop_found=self.options.get(key)orself._load_option(key)
@@ -195,12 +197,14 @@
"""ifnotkey:
- raiseValueError("Option field blank!")
+ raiseValueError(_("Option field blank!"))match=string_partial_matching(list(self.options_dict.keys()),key,ret_index=False)ifnotmatch:
- raiseValueError("Option not found!")
+ raiseValueError(_("Option not found!"))iflen(match)>1:
- raiseValueError(f"Multiple matches: {', '.join(match)}. Please be more specific.")
+ raiseValueError(_("Multiple matches:")
+ +f"{', '.join(match)}. "
+ +_("Please be more specific."))match=match[0]op=self.get(match,return_obj=True)op.set(value,**kwargs)
@@ -256,7 +260,6 @@
-
diff --git a/docs/0.9.5/_modules/evennia/utils/test_resources.html b/docs/0.9.5/_modules/evennia/utils/test_resources.html
index 2984f02bfd..cf2e11a20c 100644
--- a/docs/0.9.5/_modules/evennia/utils/test_resources.html
+++ b/docs/0.9.5/_modules/evennia/utils/test_resources.html
@@ -80,18 +80,18 @@
should directly give the module pathname to unload. Example:
- ::
- # (in a test method)
- unload_module(foo)
- with mock.patch("foo.GLOBALTHING", "mockval"):
- import foo
- ... # test code using foo.GLOBALTHING, now set to 'mockval'
+ ```python
+ # (in a test method)
+ unload_module(foo)
+ with mock.patch("foo.GLOBALTHING", "mockval"):
+ import foo
+ ... # test code using foo.GLOBALTHING, now set to 'mockval'
+ ```
- Notes:
- This allows for mocking constants global to the module, since
- otherwise those would not be mocked (since a module is only
- loaded once).
+ This allows for mocking constants global to the module, since
+ otherwise those would not be mocked (since a module is only
+ loaded once). """ifisinstance(module,str):
@@ -251,7 +251,6 @@
[docs]defsub_mxp_urls(self,match):
+ """
+ Helper method to be passed to re.sub,
+ replaces MXP links with HTML code.
+ Args:
+ match (re.Matchobject): Match for substitution.
+ Returns:
+ text (str): Processed text.
+ """
+ url,text=[grp.replace('"',"\\"")forgrpinmatch.groups()]
+ val=(
+ r"""<a id="mxplink" href="{url}" target="_blank">{text}</a>""".format(url=url,text=text)
+ )
+ returnval
+
[docs]defsub_text(self,match):""" Helper method to be passed to re.sub,
@@ -378,6 +394,7 @@
# convert all ansi to htmlresult=re.sub(self.re_string,self.sub_text,text)result=re.sub(self.re_mxplink,self.sub_mxp_links,result)
+ result=re.sub(self.re_mxpurl,self.sub_mxp_urls,result)result=self.re_color(result)result=self.re_bold(result)result=self.re_underline(result)
@@ -444,7 +461,6 @@
[docs]defcrop(text,width=None,suffix="[...]"):""" Crop text to a certain width, throwing away text from too-long lines.
@@ -203,16 +209,18 @@
returnto_str(text)
[docs]defdedent(text,baseline_index=None,indent=None):""" Safely clean all whitespace at the left of a paragraph. Args: text (str): The text to dedent.
- baseline_index (int or None, optional): Which row to use as a 'base'
+ baseline_index (int, optional): Which row to use as a 'base' for the indentation. Lines will be dedented to this level but no further. If None, indent so as to completely deindent the least indented text.
+ indent (int, optional): If given, force all lines to this indent.
+ This bypasses `baseline_index`. Returns: text (str): Dedented string.
@@ -225,18 +233,23 @@
"""ifnottext:return""
- ifbaseline_indexisNone:
+ ifindentisnotNone:
+ lines=text.split("\n")
+ ind=" "*indent
+ indline="\n"+ind
+ returnind+indline.join(line.strip()forlineinlines)
+ elifbaseline_indexisNone:returntextwrap.dedent(text)else:lines=text.split("\n")baseline=lines[baseline_index]spaceremove=len(baseline)-len(baseline.lstrip(" "))return"\n".join(
- line[min(spaceremove,len(line)-len(line.lstrip(" "))):]forlineinlines
+ line[min(spaceremove,len(line)-len(line.lstrip(" "))):]forlineinlines)
[docs]defjustify(text,width=None,align="f",indent=0):""" Fully justify a text so that it fits inside `width`. When using full justification (default) this will be done by padding between
@@ -329,7 +342,7 @@
return"\n".join([indentstring+lineforlineinlines])
[docs]defcolumnize(string,columns=2,spacing=4,align="l",width=None):""" Break a string into a number of columns, using as little vertical space as possible.
@@ -371,7 +384,7 @@
cols=[]istart=0forirowsinnrows:
- cols.append(onecol[istart:istart+irows])
+ cols.append(onecol[istart:istart+irows])istart=istart+irowsforcolincols:iflen(col)<height:
@@ -385,7 +398,7 @@
return"\n".join(rows)
[docs]defiter_to_str(initer,endsep="and",addquote=False):""" This pretty-formats an iterable list as string output, adding an optional alternative separator to the second to last entry. If `addquote`
@@ -401,23 +414,21 @@
values with double quotes. Returns:
- liststr (str): The list represented as a string.
+ str: The list represented as a string. Examples: ```python
- # no endsep:
- [1,2,3] -> '1, 2, 3'
- # with endsep=='and':
- [1,2,3] -> '1, 2 and 3'
- # with addquote and endsep
- [1,2,3] -> '"1", "2" and "3"'
+ >>> list_to_string([1,2,3], endsep='')
+ '1, 2, 3'
+ >>> list_to_string([1,2,3], ensdep='and')
+ '1, 2, and 3'
+ >>> list_to_string([1,2,3], endsep='and', addquote=True)
+ '"1", "2", and "3"' ``` """
- ifnotendsep:
- endsep=","
- else:
+ ifendsep:endsep=" "+endsepifnotiniter:return""
@@ -425,18 +436,23 @@
ifaddquote:iflen(initer)==1:return'"%s"'%initer[0]
- return", ".join('"%s"'%vforvininiter[:-1])+"%s%s"%(endsep,'"%s"'%initer[-1])
+ eliflen(initer)==2:
+ return'"%s"'%('"%s "'%endsep).join(str(v)forvininiter)
+ return", ".join('"%s"'%vforvininiter[:-1])+",%s%s"%(endsep,'"%s"'%initer[-1])else:iflen(initer)==1:returnstr(initer[0])
- return", ".join(str(v)forvininiter[:-1])+"%s%s"%(endsep,initer[-1])
[docs]defwildcard_to_regexp(instring):""" Converts a player-supplied string that may have wildcards in it to regular expressions. This is useful for name matching.
@@ -468,7 +484,7 @@
returnregexp_string
[docs]defpypath_to_realpath(python_path,file_ending=".py",pypath_prefixes=None):""" Converts a dotted Python path to an absolute path under the Evennia library directory or under the current game directory.
@@ -736,7 +752,7 @@
returnlist(set(pforpinpathsifos.path.isfile(p)))
[docs]deflatinify(string,default="?",pure_ascii=False):""" Convert a unicode string to "safe" ascii/latin-1 characters. This is used as a last resort when normal encoding does not work.
@@ -874,7 +890,7 @@
return"".join(converted)
[docs]defto_bytes(text,session=None):""" Try to encode the given text to bytes, using encodings from settings or from Session. Will always return a bytes, even if given something that is not str or bytes.
@@ -891,7 +907,7 @@
the text with "?" in place of problematic characters. If the specified encoding cannot be found, the protocol flag is reset to utf-8. In any case, returns bytes.
- Note:
+ Notes: If `text` is already bytes, return it as is. """
@@ -917,7 +933,7 @@
returntext.encode(default_encoding,errors="replace")
[docs]defto_str(text,session=None):""" Try to decode a bytestream to a python str, using encoding schemas from settings or from Session. Will always return a str(), also if not given a str/bytes.
@@ -931,7 +947,7 @@
Returns: decoded_text (str): The decoded text.
- Note:
+ Notes: If `text` is already str, return it as is. """ifisinstance(text,str):
@@ -956,7 +972,7 @@
returntext.decode(default_encoding,errors="replace")
[docs]defvalidate_email_address(emailaddress):""" Checks if an email address is syntactically correct. Makes use of the django email-validator for consistency.
@@ -979,24 +995,23 @@
returnTrue
[docs]definherits_from(obj,parent):""" Takes an object and tries to determine if it inherits at *any* distance from parent. Args:
- obj (any): Object to analyze. This may be either an instance
- or a class.
- parent (any): Can be either instance, class or python path to class.
+ obj (any): Object to analyze. This may be either an instance or
+ a class.
+ parent (any): Can be either an instance, a class or the python
+ path to the class. Returns: inherits_from (bool): If `parent` is a parent to `obj` or not. Notes:
- What differs this function from e.g. `isinstance()` is that `obj`
- may be both an instance and a class, and parent may be an
- instance, a class, or the python path to a class (counting from
- the evennia root directory).
+ What differentiates this function from Python's `isinstance()` is the
+ flexibility in the types allowed for the object and parent being compared. """
@@ -1017,7 +1032,7 @@
returnany(1forobj_pathinobj_pathsifobj_path==parent_path)
[docs]defserver_services():""" Lists all services active on the Server. Observe that since services are launched in memory, this function will only return
@@ -1038,14 +1053,13 @@
returnserver
[docs]defuses_database(name="sqlite3"):""" Checks if the game is currently using a given database. This is a shortcut to having to use the full backend name. Args:
- name (str): One of 'sqlite3', 'mysql', 'postgresql'
- or 'oracle'.
+ name (str): One of 'sqlite3', 'mysql', 'postgresql' or 'oracle'. Returns: uses (bool): If the given database is used or not.
@@ -1058,10 +1072,7 @@
returnengine=="django.db.backends.%s"%name
[docs]defdelay(timedelay,callback,*args,**kwargs):""" Delay the calling of a callback (function).
@@ -1069,8 +1080,7 @@
timedelay (int or float): The delay in seconds. callback (callable): Will be called as `callback(*args, **kwargs)` after `timedelay` seconds.
- args (any): Will be used as arguments to callback.
-
+ *args: Will be used as arguments to callback Keyword Args: persistent (bool, optional): If True the delay remains after a server restart. persistent is False by default.
@@ -1080,7 +1090,7 @@
task (TaskHandlerTask): An instance of a task. Refer to, evennia.scripts.taskhandler.TaskHandlerTask
- Note:
+ Notes: The task handler (`evennia.scripts.taskhandler.TASK_HANDLER`) will be called for persistent or non-persistent tasks. If persistent is set to True, the callback, its arguments
@@ -1097,18 +1107,88 @@
"""global_TASK_HANDLER
- # Do some imports here to avoid circular import and speed things upif_TASK_HANDLERisNone:fromevennia.scripts.taskhandlerimportTASK_HANDLERas_TASK_HANDLER
+
return_TASK_HANDLER.add(timedelay,callback,*args,**kwargs)
+
[docs]defrepeat(interval,callback,persistent=True,idstring="",stop=False,
+ store_key=None,*args,**kwargs):
+ """
+ Start a repeating task using the TickerHandler.
+
+ Args:
+ interval (int): How often to call callback.
+ callback (callable): This will be called with `*args, **kwargs` every
+ `interval` seconds. This must be possible to pickle regardless
+ of if `persistent` is set or not!
+ persistent (bool, optional): If ticker survives a server reload.
+ idstring (str, optional): Separates multiple tickers. This is useful
+ mainly if wanting to set up multiple repeats for the same
+ interval/callback but with different args/kwargs.
+ stop (bool, optional): If set, use the given parameters to _stop_ a running
+ ticker instead of creating a new one.
+ 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: 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
+ created ticker. Store this and pass into unrepat() in order to to stop
+ this ticker later. Returns `None` if `stop=True`.
+
+ Raises:
+ KeyError: If trying to stop a ticker that was not found.
+
+ """
+ global_TICKER_HANDLER
+ if_TICKER_HANDLERisNone:
+ fromevennia.scripts.tickerhandlerimportTICKER_HANDLERas_TICKER_HANDLER
+
+ ifstop:
+ # we pass all args, but only store_key matters if given
+ _TICKER_HANDLER.remove(interval=interval,
+ callback=callback,
+ idstring=idstring,
+ persistent=persistent,
+ store_key=store_key)
+ else:
+ return_TICKER_HANDLER.add(interval=interval,
+ callback=callback,
+ idstring=idstring,
+ persistent=persistent)
+
+
[docs]defunrepeat(store_key):
+ """
+ This is used to stop a ticker previously started with `repeat`.
+
+ 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`
+ directly.
+
+ Returns:
+ bool: True if a ticker was stopped, False if not (for example because no
+ matching ticker was found or it was already stopped).
+
+ """
+ try:
+ repeat(None,None,stop=True,store_key=store_key)
+ returnTrue
+ exceptKeyError:
+ returnFalse
+
+
_PPOOL=None_PCMD=None_PROC_ERR="A process has ended with a probable error condition: process ended by signal 9."
-
[docs]defrun_async(to_execute,*args,**kwargs):""" Runs a function or executes a code snippet asynchronously.
@@ -1117,17 +1197,16 @@
executed with `*args` and non-reserved `**kwargs` as arguments. The callable will be executed using ProcPool, or in a thread if ProcPool is not available.
-
Keyword Args: at_return (callable): Should point to a callable with one
- argument. It will be called with the return value from
- to_execute.
+ argument. It will be called with the return value from
+ to_execute. at_return_kwargs (dict): This dictionary will be used as
- keyword arguments to the at_return callback.
+ keyword arguments to the at_return callback. at_err (callable): This will be called with a Failure instance
- if there is an error in to_execute.
+ if there is an error in to_execute. at_err_kwargs (dict): This dictionary will be used as keyword
- arguments to the at_err errback.
+ arguments to the at_err errback. Notes: All other `*args` and `**kwargs` will be passed on to
@@ -1167,7 +1246,7 @@
deferred.addErrback(errback,**errback_kwargs)
[docs]defcheck_evennia_dependencies():""" Checks the versions of Evennia's dependencies including making some checks for runtime libraries.
@@ -1200,8 +1279,8 @@
exceptImportError:errstring+=("\n ERROR: IRC is enabled, but twisted.words is not installed. Please install it."
- "\n Linux Debian/Ubuntu users should install package 'python-twisted-words', others"
- "\n can get it from http://twistedmatrix.com/trac/wiki/TwistedWords."
+ "\n Linux Debian/Ubuntu users should install package 'python-twisted-words', "
+ "\n others can get it from http://twistedmatrix.com/trac/wiki/TwistedWords.")not_error=Falseerrstring=errstring.strip()
@@ -1211,9 +1290,9 @@
returnnot_error
[docs]defhas_parent(basepath,obj):"""
- Checks if `basepath` is somewhere in `obj`'s parent tree.
+ Checks if `basepath` is somewhere in obj's parent tree. Args: basepath (str): Python dotpath to compare against obj path.
@@ -1235,7 +1314,7 @@
returnFalse
[docs]defall_from_module(module):""" Return all global-level variables defined in a module.
@@ -1301,23 +1380,24 @@
already imported module object (e.g. `models`) Returns:
- variables (dict): A dict of {variablename: variable} for all
+ dict: A dict of {variablename: variable} for all variables in the given module. Notes:
- Ignores modules and variable names starting with an underscore.
+ Ignores modules and variable names starting with an underscore, as well
+ as variables imported into the module from other modules. """mod=mod_import(module)ifnotmod:return{}# make sure to only return variables actually defined in this
- # module if available (try to avoid not imports)
+ # module if available (try to avoid imports)members=getmembers(mod,predicate=lambdaobj:getmodule(obj)in(mod,None))returndict((key,val)forkey,valinmembersifnotkey.startswith("_"))
[docs]defcallables_from_module(module):""" Return all global-level callables defined in a module.
@@ -1340,7 +1420,7 @@
returndict((key,val)forkey,valinmembersifnotkey.startswith("_"))
[docs]defvariable_from_module(module,variable=None,default=None):""" Retrieve a variable or list of variables from a module. The variable(s) must be defined globally in the module. If no variable
@@ -1387,7 +1467,7 @@
returnresult
[docs]defstring_from_module(module,variable=None,default=None):""" This is a wrapper for `variable_from_module` that requires return value to be a string to pass. It's primarily used by login screen.
@@ -1416,7 +1496,7 @@
returndefault
[docs]defrandom_string_from_module(module):""" Returns a random global string from a module.
@@ -1429,7 +1509,7 @@
returnrandom.choice(string_from_module(module))
[docs]deffuzzy_import_from_module(path,variable,default=None,defaultpaths=None):""" Import a variable based on a fuzzy path. First the literal `path` will be tried, then all given `defaultpaths` will be
@@ -1461,9 +1541,9 @@
returndefault
[docs]defclass_from_module(path,defaultpaths=None,fallback=None):"""
- Return a class from a module, given the module's path. This is
+ Return a class from a module, given the class' full python path. This is primarily used to convert db_typeclass_path:s to classes. Args:
@@ -1541,7 +1621,7 @@
object_from_module=class_from_module
-
[docs]definit_new_account(account):""" Deprecated. """
@@ -1550,7 +1630,7 @@
logger.log_dep("evennia.utils.utils.init_new_account is DEPRECATED and should not be used.")
[docs]defstring_similarity(string1,string2):""" This implements a "cosine-similarity" algorithm as described for example in *Proceedings of the 22nd International Conference on Computation
@@ -1580,7 +1660,7 @@
return0
[docs]defstring_suggestions(string,vocabulary,cutoff=0.6,maxnum=3):""" Given a `string` and a `vocabulary`, return a match or a list of suggestions based on string similarity.
@@ -1594,8 +1674,8 @@
Returns: suggestions (list): Suggestions from `vocabulary` with a
- similarity-rating that higher than or equal to `cutoff`.
- Could be empty if there are no matches.
+ similarity-rating that higher than or equal to `cutoff`.
+ Could be empty if there are no matches. """return[
@@ -1609,7 +1689,7 @@
][:maxnum]
[docs]defstring_partial_matching(alternatives,inp,ret_index=True):""" Partially matches a string based on a list of `alternatives`. Matching is made from the start of each subword in each
@@ -1661,13 +1741,11 @@
return[]
[docs]defformat_table(table,extra_space=1):"""
- Note: `evennia.utils.evtable` is more powerful than this, but this function
- can be useful when the number of columns and rows are unknown and must be
- calculated on the fly.
+ Format a 2D array of strings into a multi-column table.
- Args.
+ Args: table (list): A list of lists to represent columns in the table: `[[val,val,val,...], [val,val,val,...], ...]`, where each val will be placed on a separate row in the
@@ -1677,26 +1755,30 @@
padding (in characters) should be left between columns. Returns:
- table (list): A list of lists representing the rows to print
- out one by one.
+ list: A list of lists representing the rows to print out one by one. Notes: The function formats the columns to be as wide as the widest member of each column.
- Example:
- ::
+ `evennia.utils.evtable` is more powerful than this, but this
+ function can be useful when the number of columns and rows are
+ unknown and must be calculated on the fly.
- ftable = format_table([[...], [...], ...])
- for ir, row in enumarate(ftable):
- if ir == 0:
- # make first row white
- string += "\\\\n|w" + ""join(row) + "|n"
- else:
- string += "\\\\n" + "".join(row)
- print(string)
+ Examples: ::
+
+ ftable = format_table([[1,2,3], [4,5,6]])
+ string = ""
+ for ir, row in enumarate(ftable):
+ if ir == 0:
+ # make first row white
+ string += "\\n|w" + "".join(row) + "|n"
+ else:
+ string += "\\n" + "".join(row)
+ print(string) """
+
ifnottable:return[[]]
@@ -1712,7 +1794,226 @@
returnftable
[docs]defpercent(value,minval,maxval,formatting="{:3.1f}%"):
+ """
+ Get a value in an interval as a percentage of its position
+ in that interval. This also understands negative numbers.
+
+ Args:
+ value (number): This should be a value minval<=value<=maxval.
+ minval (number or None): Smallest value in interval. This could be None
+ for an open interval (then return will always be 100%)
+ maxval (number or None): Biggest value in interval. This could be None
+ for an open interval (then return will always be 100%)
+ formatted (str, optional): This is a string that should
+ accept one formatting tag. This will receive the
+ current value as a percentage. If None, the
+ raw float will be returned instead.
+ Returns:
+ str or float: The formatted value or the raw percentage as a float.
+ Notes:
+ We try to handle a weird interval gracefully.
+
+ - If either maxval or minval is None (open interval), we (aribtrarily) assume 100%.
+ - If minval > maxval, we return 0%.
+ - If minval == maxval == value we are looking at a single value match and return 100%.
+ - If minval == maxval != value we return 0%.
+ - If value not in [minval..maxval], we set value to the closest
+ boundary, so the result will be 0% or 100%, respectively.
+
+ """
+ result=None
+ ifNonein(minval,maxval):
+ # we have no boundaries, percent calculation makes no sense,
+ # we set this to 100% since it
+ result=100.0
+ elifminval>maxval:
+ # interval has no width so we cannot
+ # occupy any position within it.
+ result=0.0
+ elifminval==maxval==value:
+ # this is a single value that we match
+ result=100.0
+ elifminval==maxval!=value:
+ # interval has no width so we cannot be in it.
+ result=0.0
+
+ ifresultisNone:
+ # constrain value to interval
+ value=min(max(minval,value),maxval)
+
+ # these should both be >0
+ dpart=value-minval
+ dfull=maxval-minval
+ result=(dpart/dfull)*100.0
+
+ ifisinstance(formatting,str):
+ returnformatting.format(result)
+ returnresult
+
+
+importfunctools# noqa
+
+
+
[docs]defpercentile(iterable,percent,key=lambdax:x):
+ """
+ Find the percentile of a list of values.
+
+ Args:
+ iterable (iterable): A list of values. Note N MUST BE already sorted.
+ percent (float): A value from 0.0 to 1.0.
+ key (callable, optional). Function to compute value from each element of N.
+
+ Returns:
+ float: The percentile of the values
+
+ """
+ ifnotiterable:
+ returnNone
+ k=(len(iterable)-1)*percent
+ f=math.floor(k)
+ c=math.ceil(k)
+ iff==c:
+ returnkey(iterable[int(k)])
+ d0=key(iterable[int(f)])*(c-k)
+ d1=key(iterable[int(c)])*(k-f)
+ returnd0+d1
+
+
+
[docs]defformat_grid(elements,width=78,sep=" ",verbatim_elements=None):
+ """
+ This helper function makes a 'grid' output, where it distributes the given
+ string-elements as evenly as possible to fill out the given width.
+ will not work well if the variation of length is very big!
+
+ Args:
+ elements (iterable): A 1D list of string elements to put in the grid.
+ width (int, optional): The width of the grid area to fill.
+ sep (str, optional): The extra separator to put between words. If
+ set to the empty string, words may run into each other.
+ verbatim_elements (list, optional): This is a list of indices pointing to
+ specific items in the `elements` list. An element at this index will
+ not be included in the calculation of the slot sizes. It will still
+ be inserted into the grid at the correct position and may be surrounded
+ by padding unless filling the entire line. This is useful for embedding
+ decorations in the grid, such as horizontal bars.
+ ignore_ansi (bool, optional): Ignore ansi markups when calculating white spacing.
+
+ Returns:
+ list: The grid as a list of ready-formatted rows. We return it
+ like this to make it easier to insert decorations between rows, such
+ as horizontal bars.
+ """
+
+ def_minimal_rows(elements):
+ """
+ Minimalistic distribution with minimal spacing, good for single-line
+ grids but will look messy over many lines.
+ """
+ rows=[""]
+ forelementinelements:
+ rowlen=display_len((rows[-1]))
+ elen=display_len((element))
+ ifrowlen+elen<=width:
+ rows[-1]+=element
+ else:
+ rows.append(element)
+ returnrows
+
+ def_weighted_rows(elements):
+ """
+ Dynamic-space, good for making even columns in a multi-line grid but
+ will look strange for a single line.
+ """
+ wls=[display_len((elem))foreleminelements]
+ wls_percentile=[wlforiw,wlinenumerate(wls)ifiwnotinverbatim_elements]
+
+ ifwls_percentile:
+ # get the nth percentile as a good representation of average width
+ averlen=int(percentile(sorted(wls_percentile),0.9))+2# include extra space
+ aver_per_row=width//averlen+1
+ else:
+ # no adjustable rows, just keep all as-is
+ aver_per_row=1
+
+ ifaver_per_row==1:
+ # one line per row, output directly since this is trivial
+ # we use rstrip here to remove extra spaces added by sep
+ return[
+ crop(element.rstrip(),width)+" " \
+ *max(0,width-display_len((element.rstrip())))
+ foriel,elementinenumerate(elements)
+ ]
+
+ indices=[averlen*indforindinrange(aver_per_row-1)]
+
+ rows=[]
+ ic=0
+ row=""
+ forie,elementinenumerate(elements):
+
+ wl=wls[ie]
+ lrow=display_len((row))
+ # debug = row.replace(" ", ".")
+
+ iflrow+wl>width:
+ # this slot extends outside grid, move to next line
+ row+=" "*(width-lrow)
+ rows.append(row)
+ ifwl>=width:
+ # remove sep if this fills the entire line
+ element=element.rstrip()
+ row=crop(element,width)
+ ic=0
+ elific>=aver_per_row-1:
+ # no more slots available on this line
+ row+=" "*max(0,(width-lrow))
+ rows.append(row)
+ row=crop(element,width)
+ ic=0
+ else:
+ try:
+ whilelrow>max(0,indices[ic]):
+ # slot too wide, extend into adjacent slot
+ ic+=1
+ row+=" "*max(0,indices[ic]-lrow)
+ exceptIndexError:
+ # we extended past edge of grid, crop or move to next line
+ ific==0:
+ row=crop(element,width)
+ else:
+ row+=" "*max(0,width-lrow)
+ rows.append(row)
+ ic=0
+ else:
+ # add a new slot
+ row+=element+" "*max(0,averlen-wl)
+ ic+=1
+
+ ifie>=nelements-1:
+ # last element, make sure to store
+ row+=" "*max(0,width-display_len((row)))
+ rows.append(row)
+ returnrows
+
+ ifnotelements:
+ return[]
+ ifnotverbatim_elements:
+ verbatim_elements=[]
+
+ nelements=len(elements)
+ # add sep to all but the very last element
+ elements=[elements[ie]+sepforieinrange(nelements-1)]+[elements[-1]]
+
+ ifsum(display_len((element))forelementinelements)<=width:
+ # grid fits in one line
+ return_minimal_rows(elements)
+ else:
+ # full multi-line grid
+ return_weighted_rows(elements)
+
+
+
[docs]defget_evennia_pids():""" Get the currently valid PIDs (Process IDs) of the Portal and Server by trying to access a PID file.
@@ -1723,13 +2024,13 @@
Examples: This can be used to determine if we are in a subprocess by
- something like: ```python self_pid = os.getpid() server_pid, portal_pid = get_evennia_pids() is_subprocess = self_pid not in (server_pid, portal_pid) ```
+
"""server_pidfile=os.path.join(settings.GAME_DIR,"server.pid")portal_pidfile=os.path.join(settings.GAME_DIR,"portal.pid")
@@ -1745,7 +2046,7 @@
returnNone,None
[docs]defdeepsize(obj,max_depth=4):""" Get not only size of the given object, but also the size of objects referenced by the object, down to `max_depth` distance
@@ -1787,7 +2088,7 @@
_missing=object()
-
[docs]classlazy_property(object):""" Delays loading of property until first access. Credit goes to the Implementation in the werkzeug suite:
@@ -1807,7 +2108,7 @@
"""
-
[docs]defcalledby(callerdepth=1):""" Only to be used for debug purposes. Insert this debug function in another function; it will print which function called it.
@@ -1873,7 +2174,7 @@
return"[called by '%s': %s:%s%s]"%(frame[3],path,frame[2],frame[4])
[docs]defm_len(target):""" Provides length checking for strings with MXP patterns, and falls back to normal len for other objects.
@@ -1894,7 +2195,7 @@
returnlen(target)
[docs]defdisplay_len(target):""" Calculate the 'visible width' of text. This is not necessarily the same as the number of characters in the case of certain asian characters. This will also
@@ -1928,7 +2229,7 @@
# Replace this hook function by changing settings.SEARCH_AT_RESULT.
-
[docs]defat_search_result(matches,caller,query="",quiet=False,**kwargs):""" This is a generic hook for handling all processing of a search result, including error reporting. This is also called by the cmdhandler
@@ -1945,23 +2246,22 @@
query (str, optional): The search query used to produce `matches`. quiet (bool, optional): If `True`, no messages will be echoed to caller on errors.
-
Keyword Args: nofound_string (str): Replacement string to echo on a notfound error. multimatch_string (str): Replacement string to echo on a multimatch error. Returns: processed_result (Object or None): This is always a single result
- or `None`. If `None`, any error reporting/handling should
- already have happened. The returned object is of the type we are
- checking multimatches for (e.g. Objects or Commands)
+ or `None`. If `None`, any error reporting/handling should
+ already have happened. The returned object is of the type we are
+ checking multimatches for (e.g. Objects or Commands) """error=""ifnotmatches:# no results.
- error=kwargs.get("nofound_string")or_("Could not find '%s'."%query)
+ error=kwargs.get("nofound_string")or_("Could not find '{query}'.").format(query=query)matches=Noneeliflen(matches)>1:multimatch_string=kwargs.get("multimatch_string")
@@ -1986,7 +2286,7 @@
name=result.get_display_name(caller)ifhasattr(result,"get_display_name")elsequery,
- aliases=" [%s]"%";".join(aliases)ifaliaseselse"",
+ aliases=" [{alias}]".format(alias=";".join(aliases)ifaliaseselse""),info=result.get_extra_info(caller),)matches=None
@@ -1999,7 +2299,7 @@
returnmatches
[docs]classLimitedSizeOrderedDict(OrderedDict):""" This dictionary subclass is both ordered and limited to a maximum number of elements. Its main use is to hold a cache that can never
@@ -2007,16 +2307,16 @@
"""
-
[docs]def__init__(self,*args,**kwargs):""" Limited-size ordered dict. Keyword Args: size_limit (int): Use this to limit the number of elements
- alloweds to be in this list. By default the overshooting elements
- will be removed in FIFO order.
+ alloweds to be in this list. By default the overshooting elements
+ will be removed in FIFO order. fifo (bool, optional): Defaults to `True`. Remove overshooting elements
- in FIFO order. If `False`, remove in FILO order.
+ in FIFO order. If `False`, remove in FILO order. """super().__init__()
@@ -2049,12 +2349,12 @@
super().__setitem__(key,value)self._check_size()
-
[docs]defget_game_dir_path():""" This is called by settings_default in order to determine the path of the game directory.
@@ -2064,7 +2364,7 @@
"""# current working directory, assumed to be somewhere inside gamedir.
- for_inrange(10):
+ forinuminrange(10):gpath=os.getcwd()if"server"inos.listdir(gpath):ifos.path.isfile(os.path.join("server","conf","settings.py")):
@@ -2074,21 +2374,21 @@
raiseRuntimeError("server/conf/settings.py not found: Must start from inside game dir.")
[docs]defget_all_typeclasses(parent=None):""" List available typeclasses from all available modules. Args:
- parent (str, optional): If given, only return typeclasses inheriting (at any distance)
- from this parent.
+ parent (str, optional): If given, only return typeclasses inheriting
+ (at any distance) from this parent. Returns:
- typeclasses (dict): On the form {"typeclass.path": typeclass, ...}
+ dict: On the form `{"typeclass.path": typeclass, ...}` Notes:
- This will dynamicall retrieve all abstract django models inheriting at any distance
- from the TypedObject base (aka a Typeclass) so it will work fine with any custom
- classes being added.
+ This will dynamically retrieve all abstract django models inheriting at
+ any distance from the TypedObject base (aka a Typeclass) so it will
+ work fine with any custom classes being added. """fromevennia.typeclasses.modelsimportTypedObject
@@ -2107,16 +2407,49 @@
returntypeclasses
[docs]defget_all_cmdsets(parent=None):"""
- Decorator to make a method pausable with yield(seconds) and able to ask for
- user-input with `response=yield(question)`. For the question-asking to
- work, 'caller' must the name of an argument or kwarg to the decorated
- function.
+ List available cmdsets from all available modules.
- Example:
- ::
+ Args:
+ parent (str, optional): If given, only return cmdsets inheriting (at
+ any distance) from this parent.
+ Returns:
+ dict: On the form {"cmdset.path": cmdset, ...}
+
+ Notes:
+ This will dynamically retrieve all abstract django models inheriting at
+ any distance from the CmdSet base so it will work fine with any custom
+ classes being added.
+
+ """
+ fromevennia.commands.cmdsetimportCmdSet
+
+ base_cmdset=class_from_module(parent)ifparentelseCmdSet
+
+ cmdsets={
+ "{}.{}".format(subclass.__module__,subclass.__name__):subclass
+ forsubclassinbase_cmdset.__subclasses__()
+ }
+ returncmdsets
+
+
+
[docs]definteractive(func):
+ """
+ Decorator to make a method pausable with `yield(seconds)`
+ and able to ask for user-input with `response=yield(question)`.
+ For the question-asking to work, one of the args or kwargs to the
+ decorated function must be named 'caller'.
+
+ Raises:
+ ValueError: If asking an interactive question but the decorated
+ function has no arg or kwarg named 'caller'.
+ ValueError: If passing non int/float to yield using for pausing.
+
+ Examples:
+
+ ```python @interactive def myfunc(caller): caller.msg("This is a test")
@@ -2128,9 +2461,10 @@
yield(5) else: # ...
+ ``` Notes:
- This turns the method into a generator!
+ This turns the decorated function or method into a generator. """fromevennia.utils.evmenuimportget_input
@@ -2174,6 +2508,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
diff --git a/docs/0.9.5/_modules/evennia/utils/validatorfuncs.html b/docs/0.9.5/_modules/evennia/utils/validatorfuncs.html
index c75a158a41..04043b8f35 100644
--- a/docs/0.9.5/_modules/evennia/utils/validatorfuncs.html
+++ b/docs/0.9.5/_modules/evennia/utils/validatorfuncs.html
@@ -63,18 +63,20 @@
try:returnstr(entry)exceptExceptionaserr:
- raiseValueError(f"Input could not be converted to text ({err})")
+ raiseValueError(_("Input could not be converted to text ({err})").format(err=err))
[docs]defcolor(entry,option_key="Color",**kwargs):""" The color should be just a color character, so 'r' if red color is desired.
+
"""ifnotentry:
- raiseValueError(f"Nothing entered for a {option_key}!")
+ raiseValueError(_("Nothing entered for a {option_key}!").format(option_key=option_key))test_str=strip_ansi(f"|{entry}|n")iftest_str:
- raiseValueError(f"'{entry}' is not a valid {option_key}.")
+ raiseValueError(_("'{entry}' is not a valid {option_key}.").format(
+ entry=entry,option_key=option_key))returnentry
@@ -89,12 +91,10 @@
account (AccountDB): The Account performing this lookup. Unless `from_tz` is provided, the account's timezone option will be used. from_tz (pytz.timezone): An instance of a pytz timezone object from the
- user. If not provided, tries to use the timezone option of the `account`.
+ user. If not provided, tries to use the timezone option of `account`. If neither one is provided, defaults to UTC.
-
Returns: datetime in UTC.
-
Raises: ValueError: If encountering a malformed timezone, date string or other format error.
@@ -126,13 +126,17 @@
entry=f"{split_time[0]}{split_time[1]}{split_time[2]}{split_time[3]}"else:raiseValueError(
- f"{option_key} must be entered in a 24-hour format such as: {now.strftime('%b %d %H:%M')}"
+ _("{option_key} must be entered in a 24-hour format such as: {timeformat}").format(
+ option_key=option_key,
+ timeformat=now.strftime('%b %d %H:%M')))try:local=_dt.datetime.strptime(entry,"%b %d %H:%M %Y")exceptValueError:raiseValueError(
- f"{option_key} must be entered in a 24-hour format such as: {now.strftime('%b %d %H:%M')}"
+ _("{option_key} must be entered in a 24-hour format such as: {timeformat}").format(
+ option_key=option_key,
+ timeformat=now.strftime('%b %d %H:%M')))local_tz=from_tz.localize(local)returnlocal_tz.astimezone(utc)
@@ -143,8 +147,9 @@
Take a string and derive a datetime timedelta from it. Args:
- entry (string): This is a string from user-input. The intended format is, for example: "5d 2w 90s" for
- 'five days, two weeks, and ninety seconds.' Invalid sections are ignored.
+ entry (string): This is a string from user-input. The intended format is, for example:
+ "5d 2w 90s" for 'five days, two weeks, and ninety seconds.' Invalid sections are
+ ignored. option_key (str): Name to display this query as. Returns:
@@ -172,7 +177,10 @@
elif_re.match(r"^[\d]+y$",interval):days+=int(interval.rstrip("y"))*365else:
- raiseValueError(f"Could not convert section '{interval}' to a {option_key}.")
+ raiseValueError(
+ _("Could not convert section '{interval}' to a {option_key}.").format(
+ interval=interval,option_key=option_key)
+ )return_dt.timedelta(days,seconds,0,0,minutes,hours,weeks)
@@ -180,45 +188,56 @@
[docs]deffuture(entry,option_key="Future Datetime",from_tz=None,**kwargs):time=datetime(entry,option_key,from_tz=from_tz)iftime<_dt.datetime.utcnow().replace(tzinfo=_dt.timezone.utc):
- raiseValueError(f"That {option_key} is in the past! Must give a Future datetime!")
+ raiseValueError(_("That {option_key} is in the past! Must give a Future datetime!").format(
+ option_key=option_key))returntime
[docs]defsigned_integer(entry,option_key="Signed Integer",**kwargs):ifnotentry:
- raiseValueError(f"Must enter a whole number for {option_key}!")
+ raiseValueError(_("Must enter a whole number for {option_key}!").format(
+ option_key=option_key))try:num=int(entry)exceptValueError:
- raiseValueError(f"Could not convert '{entry}' to a whole number for {option_key}!")
+ raiseValueError(_("Could not convert '{entry}' to a whole "
+ "number for {option_key}!").format(
+ entry=entry,option_key=option_key))returnnum
[docs]defpositive_integer(entry,option_key="Positive Integer",**kwargs):num=signed_integer(entry,option_key)ifnotnum>=1:
- raiseValueError(f"Must enter a whole number greater than 0 for {option_key}!")
+ raiseValueError(_("Must enter a whole number greater than 0 for {option_key}!").format(
+ option_key=option_key))returnnum
[docs]defunsigned_integer(entry,option_key="Unsigned Integer",**kwargs):num=signed_integer(entry,option_key)ifnotnum>=0:
- raiseValueError(f"{option_key} must be a whole number greater than or equal to 0!")
+ raiseValueError(_("{option_key} must be a whole number greater than "
+ "or equal to 0!").format(
+ option_key=option_key))returnnum
[docs]defboolean(entry,option_key="True/False",**kwargs):""" Simplest check in computer logic, right? This will take user input to flick the switch on or off
+
Args: entry (str): A value such as True, On, Enabled, Disabled, False, 0, or 1. option_key (str): What kind of Boolean we are setting. What Option is this for? Returns: Boolean
+
"""
- error=f"Must enter 0 (false) or 1 (true) for {option_key}. Also accepts True, False, On, Off, Yes, No, Enabled, and Disabled"
+ error=(_("Must enter a true/false input for {option_key}. Accepts {alternatives}.").format(
+ option_key=option_key,
+ alternatives="0/1, True/False, On/Off, Yes/No, Enabled/Disabled"))ifnotisinstance(entry,str):raiseValueError(error)entry=entry.upper()
@@ -239,41 +258,44 @@
Returns: A PYTZ timezone.
+
"""ifnotentry:
- raiseValueError(f"No {option_key} entered!")
+ raiseValueError(_("No {option_key} entered!").format(option_key=option_key))found=_partial(list(_TZ_DICT.keys()),entry,ret_index=False)iflen(found)>1:raiseValueError(
- f"That matched: {', '.join(str(t)fortinfound)}. Please be more specific!"
- )
+ _("That matched: {matches}. Please be more specific!").format(
+ matches=', '.join(str(t)fortinfound)))iffound:return_TZ_DICT[found[0]]
- raiseValueError(f"Could not find timezone '{entry}' for {option_key}!")
+ raiseValueError(_("Could not find timezone '{entry}' for {option_key}!").format(
+ entry=entry,option_key=option_key))
[docs]defemail(entry,option_key="Email Address",**kwargs):ifnotentry:
- raiseValueError("Email address field empty!")
+ raiseValueError(_("Email address field empty!"))valid=validate_email_address(entry)ifnotvalid:
- raiseValueError(f"That isn't a valid {option_key}!")
+ raiseValueError(_("That isn't a valid {option_key}!").format(option_key=option_key))returnentry
[docs]deflock(entry,option_key="locks",access_options=None,**kwargs):entry=entry.strip()ifnotentry:
- raiseValueError(f"No {option_key} entered to set!")
+ raiseValueError(_("No {option_key} entered to set!").format(option_key=option_key))forlocksettinginentry.split(";"):access_type,lockfunc=locksetting.split(":",1)ifnotaccess_type:
- raiseValueError("Must enter an access type!")
+ raiseValueError(_("Must enter an access type!"))ifaccess_options:ifaccess_typenotinaccess_options:
- raiseValueError(f"Access type must be one of: {', '.join(access_options)}")
+ raiseValueError(_("Access type must be one of: {alternatives}").format(
+ alternatives=', '.join(access_options)))ifnotlockfunc:
- raiseValueError("Lock func not entered.")
+ raiseValueError(_("Lock func not entered."))returnentry
-#
-# This file defines global variables that will always be
-# available in a view context without having to repeatedly
-# include it. For this to work, this file is included in
-# the settings file, in the TEMPLATE_CONTEXT_PROCESSORS
-# tuple.
-#
+"""
+This file defines global variables that will always be available in a view
+context without having to repeatedly include it.
+
+For this to work, this file is included in the settings file, in the
+TEMPLATES["OPTIONS"]["context_processors"] list.
+
+"""
+
importosfromdjango.confimportsettingsfromevennia.utils.utilsimportget_evennia_version
-# Determine the site name and server version
-
[docs]defset_game_name_and_slogan():
- """
- Sets global variables GAME_NAME and GAME_SLOGAN which are used by
- general_context.
-
- Notes:
- This function is used for unit testing the values of the globals.
- """
- globalGAME_NAME,GAME_SLOGAN,SERVER_VERSION
- try:
- GAME_NAME=settings.SERVERNAME.strip()
- exceptAttributeError:
- GAME_NAME="Evennia"
- SERVER_VERSION=get_evennia_version()
- try:
- GAME_SLOGAN=settings.GAME_SLOGAN.strip()
- exceptAttributeError:
- GAME_SLOGAN=SERVER_VERSION
-
-
-set_game_name_and_slogan()
-
# Setup lists of the most relevant apps so# the adminsite becomes more readable.
@@ -82,7 +61,30 @@
GAME_SETUP=["Permissions","Config"]CONNECTIONS=["Irc"]WEBSITE=["Flatpages","News","Sites"]
+REST_API_ENABLED=False
+# Determine the site name and server version
+
[docs]defset_game_name_and_slogan():
+ """
+ Sets global variables GAME_NAME and GAME_SLOGAN which are used by
+ general_context.
+
+ Notes:
+ This function is used for unit testing the values of the globals.
+
+ """
+ globalGAME_NAME,GAME_SLOGAN,SERVER_VERSION,REST_API_ENABLED
+ try:
+ GAME_NAME=settings.SERVERNAME.strip()
+ exceptAttributeError:
+ GAME_NAME="Evennia"
+ SERVER_VERSION=get_evennia_version()
+ try:
+ GAME_SLOGAN=settings.GAME_SLOGAN.strip()
+ exceptAttributeError:
+ GAME_SLOGAN=SERVER_VERSION
+
+ REST_API_ENABLED=settings.REST_API_ENABLED
[docs]defset_webclient_settings():"""
@@ -91,6 +93,7 @@
Notes: Used for unit testing.
+
"""globalWEBCLIENT_ENABLED,WEBSOCKET_CLIENT_ENABLED,WEBSOCKET_PORT,WEBSOCKET_URLWEBCLIENT_ENABLED=settings.WEBCLIENT_ENABLED
@@ -105,13 +108,15 @@
WEBSOCKET_URL=settings.WEBSOCKET_CLIENT_URL
+set_game_name_and_slogan()set_webclient_settings()# The main context processor function
[docs]defgeneral_context(request):"""
- Returns common Evennia-related context stuff, which
- is automatically added to context of all views.
+ Returns common Evennia-related context stuff, which is automatically added
+ to context of all views.
+
"""account=Noneifrequest.user.is_authenticated:
@@ -136,6 +141,7 @@
"websocket_enabled":WEBSOCKET_CLIENT_ENABLED,"websocket_port":WEBSOCKET_PORT,"websocket_url":WEBSOCKET_URL,
+ "rest_api_enabled":REST_API_ENABLED,}
-"""
-This file contains the generic, assorted views that don't fall under one of the other applications.
-Views are django's way of processing e.g. html templates on the fly.
-
-"""
-
-fromcollectionsimportOrderedDict
-
-fromdjango.contrib.admin.sitesimportsite
-fromdjango.confimportsettings
-fromdjango.contribimportmessages
-fromdjango.contrib.auth.mixinsimportLoginRequiredMixin
-fromdjango.contrib.admin.views.decoratorsimportstaff_member_required
-fromdjango.core.exceptionsimportPermissionDenied
-fromdjango.db.models.functionsimportLower
-fromdjango.httpimportHttpResponseBadRequest,HttpResponseRedirect
-fromdjango.shortcutsimportrender
-fromdjango.urlsimportreverse_lazy
-fromdjango.views.genericimportTemplateView,ListView,DetailView
-fromdjango.views.generic.baseimportRedirectView
-fromdjango.views.generic.editimportCreateView,UpdateView,DeleteView
-
-fromevenniaimportSESSION_HANDLER
-fromevennia.help.modelsimportHelpEntry
-fromevennia.objects.modelsimportObjectDB
-fromevennia.accounts.modelsimportAccountDB
-fromevennia.utilsimportclass_from_module
-fromevennia.utils.loggerimporttail_log_file
-fromevennia.web.websiteimportformsaswebsite_forms
-
-fromdjango.utils.textimportslugify
-
-_BASE_CHAR_TYPECLASS=settings.BASE_CHARACTER_TYPECLASS
-
-# typeclass fallbacks
-
-def_gamestats():
- # Some misc. configurable stuff.
- # TODO: Move this to either SQL or settings.py based configuration.
- fpage_account_limit=4
-
- # A QuerySet of the most recently connected accounts.
- recent_users=AccountDB.objects.get_recently_connected_accounts()[:fpage_account_limit]
- nplyrs_conn_recent=len(recent_users)or"none"
- nplyrs=AccountDB.objects.num_total_accounts()or"none"
- nplyrs_reg_recent=len(AccountDB.objects.get_recently_created_accounts())or"none"
- nsess=SESSION_HANDLER.account_count()
- # nsess = len(AccountDB.objects.get_connected_accounts()) or "no one"
-
- nobjs=ObjectDB.objects.count()
- nobjs=nobjsor1# fix zero-div error with empty database
- Character=class_from_module(settings.BASE_CHARACTER_TYPECLASS,
- fallback=settings.FALLBACK_CHARACTER_TYPECLASS)
- nchars=Character.objects.all_family().count()
- Room=class_from_module(settings.BASE_ROOM_TYPECLASS,
- fallback=settings.FALLBACK_ROOM_TYPECLASS)
- nrooms=Room.objects.all_family().count()
- Exit=class_from_module(settings.BASE_EXIT_TYPECLASS,
- fallback=settings.FALLBACK_EXIT_TYPECLASS)
- nexits=Exit.objects.all_family().count()
- nothers=nobjs-nchars-nrooms-nexits
-
- pagevars={
- "page_title":"Front Page",
- "accounts_connected_recent":recent_users,
- "num_accounts_connected":nsessor"no one",
- "num_accounts_registered":nplyrsor"no",
- "num_accounts_connected_recent":nplyrs_conn_recentor"no",
- "num_accounts_registered_recent":nplyrs_reg_recentor"no one",
- "num_rooms":nroomsor"none",
- "num_exits":nexitsor"no",
- "num_objects":nobjsor"none",
- "num_characters":ncharsor"no",
- "num_others":nothersor"no",
- }
- returnpagevars
-
-
-
[docs]defto_be_implemented(request):
- """
- A notice letting the user know that this particular feature hasn't been
- implemented yet.
- """
-
- pagevars={"page_title":"To Be Implemented..."}
-
- returnrender(request,"tbi.html",pagevars)
[docs]defadmin_wrapper(request):
- """
- Wrapper that allows us to properly use the base Django admin site, if needed.
- """
- returnstaff_member_required(site.index)(request)
-
-
-#
-# Class-based views
-#
-
-
-
[docs]classEvenniaIndexView(TemplateView):
- """
- This is a basic example of a Django class-based view, which are functionally
- very similar to Evennia Commands but differ in structure. Commands are used
- to interface with users using a terminal client. Views are used to interface
- with users using a web browser.
-
- To use a class-based view, you need to have written a template in HTML, and
- then you write a view like this to tell Django what values to display on it.
-
- While there are simpler ways of writing views using plain functions (and
- Evennia currently contains a few examples of them), just like Commands,
- writing views as classes provides you with more flexibility-- you can extend
- classes and change things to suit your needs rather than having to copy and
- paste entire code blocks over and over. Django also comes with many default
- views for displaying things, all of them implemented as classes.
-
- This particular example displays the index page.
-
- """
-
- # Tell the view what HTML template to use for the page
- template_name="website/index.html"
-
- # This method tells the view what data should be displayed on the template.
-
[docs]defget_context_data(self,**kwargs):
- """
- This is a common Django method. Think of this as the website
- equivalent of the Evennia Command.func() method.
-
- If you just want to display a static page with no customization, you
- don't need to define this method-- just create a view, define
- template_name and you're done.
-
- The only catch here is that if you extend or overwrite this method,
- you'll always want to make sure you call the parent method to get a
- context object. It's just a dict, but it comes prepopulated with all
- sorts of background data intended for display on the page.
-
- You can do whatever you want to it, but it must be returned at the end
- of this method.
-
- Keyword Args:
- any (any): Passed through.
-
- Returns:
- context (dict): Dictionary of data you want to display on the page.
-
- """
- # Always call the base implementation first to get a context object
- context=super(EvenniaIndexView,self).get_context_data(**kwargs)
-
- # Add game statistics and other pagevars
- context.update(_gamestats())
-
- returncontext
-
-
-
[docs]classTypeclassMixin(object):
- """
- This is a "mixin", a modifier of sorts.
-
- Django views typically work with classes called "models." Evennia objects
- are an enhancement upon these Django models and are called "typeclasses."
- But Django itself has no idea what a "typeclass" is.
-
- For the sake of mitigating confusion, any view class with this in its
- inheritance list will be modified to work with Evennia Typeclass objects or
- Django models interchangeably.
-
- """
-
- @property
- deftypeclass(self):
- returnself.model
-
- @typeclass.setter
- deftypeclass(self,value):
- self.model=value
-
-
-
[docs]classEvenniaCreateView(CreateView,TypeclassMixin):
- """
- This view extends Django's default CreateView.
-
- CreateView is used for creating new objects, be they Accounts, Characters or
- otherwise.
-
- """
-
- @property
- defpage_title(self):
- # Makes sure the page has a sensible title.
- return"Create %s"%self.typeclass._meta.verbose_name.title()
-
-
-
[docs]classEvenniaDetailView(DetailView,TypeclassMixin):
- """
- This view extends Django's default DetailView.
-
- DetailView is used for displaying objects, be they Accounts, Characters or
- otherwise.
-
- """
-
- @property
- defpage_title(self):
- # Makes sure the page has a sensible title.
- return"%s Detail"%self.typeclass._meta.verbose_name.title()
-
-
-
[docs]classEvenniaUpdateView(UpdateView,TypeclassMixin):
- """
- This view extends Django's default UpdateView.
-
- UpdateView is used for updating objects, be they Accounts, Characters or
- otherwise.
-
- """
-
- @property
- defpage_title(self):
- # Makes sure the page has a sensible title.
- return"Update %s"%self.typeclass._meta.verbose_name.title()
-
-
-
[docs]classEvenniaDeleteView(DeleteView,TypeclassMixin):
- """
- This view extends Django's default DeleteView.
-
- DeleteView is used for deleting objects, be they Accounts, Characters or
- otherwise.
-
- """
-
- @property
- defpage_title(self):
- # Makes sure the page has a sensible title.
- return"Delete %s"%self.typeclass._meta.verbose_name.title()
-
-
-#
-# Object views
-#
-
-
-
[docs]classObjectDetailView(EvenniaDetailView):
- """
- This is an important view.
-
- Any view you write that deals with displaying, updating or deleting a
- specific object will want to inherit from this. It provides the mechanisms
- by which to retrieve the object and make sure the user requesting it has
- permissions to actually *do* things to it.
-
- """
-
- # -- Django constructs --
- #
- # Choose what class of object this view will display. Note that this should
- # be an actual Python class (i.e. do `from typeclasses.characters import
- # Character`, then put `Character`), not an Evennia typeclass path
- # (i.e. `typeclasses.characters.Character`).
- #
- # So when you extend it, this line should look simple, like:
- # model = Object
- model=class_from_module(settings.BASE_OBJECT_TYPECLASS,
- fallback=settings.FALLBACK_OBJECT_TYPECLASS)
-
- # What HTML template you wish to use to display this page.
- template_name="website/object_detail.html"
-
- # -- Evennia constructs --
- #
- # What lock type to check for the requesting user, authenticated or not.
- # https://github.com/evennia/evennia/wiki/Locks#valid-access_types
- access_type="view"
-
- # What attributes of the object you wish to display on the page. Model-level
- # attributes will take precedence over identically-named db.attributes!
- # The order you specify here will be followed.
- attributes=["name","desc"]
-
-
[docs]defget_context_data(self,**kwargs):
- """
- Adds an 'attributes' list to the request context consisting of the
- attributes specified at the class level, and in the order provided.
-
- Django views do not provide a way to reference dynamic attributes, so
- we have to grab them all before we render the template.
-
- Returns:
- context (dict): Django context object
-
- """
- # Get the base Django context object
- context=super(ObjectDetailView,self).get_context_data(**kwargs)
-
- # Get the object in question
- obj=self.get_object()
-
- # Create an ordered dictionary to contain the attribute map
- attribute_list=OrderedDict()
-
- forattributeinself.attributes:
- # Check if the attribute is a core fieldname (name, desc)
- ifattributeinself.typeclass._meta._property_names:
- attribute_list[attribute.title()]=getattr(obj,attribute,"")
-
- # Check if the attribute is a db attribute (char1.db.favorite_color)
- else:
- attribute_list[attribute.title()]=getattr(obj.db,attribute,"")
-
- # Add our attribute map to the Django request context, so it gets
- # displayed on the template
- context["attribute_list"]=attribute_list
-
- # Return the comprehensive context object
- returncontext
-
-
[docs]defget_object(self,queryset=None):
- """
- Override of Django hook that provides some important Evennia-specific
- functionality.
-
- Evennia does not natively store slugs, so where a slug is provided,
- calculate the same for the object and make sure it matches.
-
- This also checks to make sure the user has access to view/edit/delete
- this object!
-
- """
- # A queryset can be provided to pre-emptively limit what objects can
- # possibly be returned. For example, you can supply a queryset that
- # only returns objects whose name begins with "a".
- ifnotqueryset:
- queryset=self.get_queryset()
-
- # Get the object, ignoring all checks and filters for now
- obj=self.typeclass.objects.get(pk=self.kwargs.get("pk"))
-
- # Check if this object was requested in a valid manner
- ifslugify(obj.name)!=self.kwargs.get(self.slug_url_kwarg):
- raiseHttpResponseBadRequest(
- "No %(verbose_name)s found matching the query"
- %{"verbose_name":queryset.model._meta.verbose_name}
- )
-
- # Check if the requestor account has permissions to access object
- account=self.request.user
- ifnotobj.access(account,self.access_type):
- raisePermissionDenied("You are not authorized to %s this object."%self.access_type)
-
- # Get the object, if it is in the specified queryset
- obj=super(ObjectDetailView,self).get_object(queryset)
-
- returnobj
-
-
-
[docs]classObjectCreateView(LoginRequiredMixin,EvenniaCreateView):
- """
- This is an important view.
-
- Any view you write that deals with creating a specific object will want to
- inherit from this. It provides the mechanisms by which to make sure the user
- requesting creation of an object is authenticated, and provides a sane
- default title for the page.
-
- """
-
- model=class_from_module(settings.BASE_OBJECT_TYPECLASS,
- fallback=settings.FALLBACK_OBJECT_TYPECLASS)
-
-
-
[docs]classObjectDeleteView(LoginRequiredMixin,ObjectDetailView,EvenniaDeleteView):
- """
- This is an important view for obvious reasons!
-
- Any view you write that deals with deleting a specific object will want to
- inherit from this. It provides the mechanisms by which to make sure the user
- requesting deletion of an object is authenticated, and that they have
- permissions to delete the requested object.
-
- """
-
- # -- Django constructs --
- model=class_from_module(settings.BASE_OBJECT_TYPECLASS,
- fallback=settings.FALLBACK_OBJECT_TYPECLASS)
- template_name="website/object_confirm_delete.html"
-
- # -- Evennia constructs --
- access_type="delete"
-
-
[docs]defdelete(self,request,*args,**kwargs):
- """
- Calls the delete() method on the fetched object and then
- redirects to the success URL.
-
- We extend this so we can capture the name for the sake of confirmation.
-
- """
- # Get the object in question. ObjectDetailView.get_object() will also
- # check to make sure the current user (authenticated or not) has
- # permission to delete it!
- obj=str(self.get_object())
-
- # Perform the actual deletion (the parent class handles this, which will
- # in turn call the delete() method on the object)
- response=super(ObjectDeleteView,self).delete(request,*args,**kwargs)
-
- # Notify the user of the deletion
- messages.success(request,"Successfully deleted '%s'."%obj)
- returnresponse
-
-
-
[docs]classObjectUpdateView(LoginRequiredMixin,ObjectDetailView,EvenniaUpdateView):
- """
- This is an important view.
-
- Any view you write that deals with updating a specific object will want to
- inherit from this. It provides the mechanisms by which to make sure the user
- requesting editing of an object is authenticated, and that they have
- permissions to edit the requested object.
-
- This functions slightly different from default Django UpdateViews in that
- it does not update core model fields, *only* object attributes!
-
- """
-
- # -- Django constructs --
- model=class_from_module(settings.BASE_OBJECT_TYPECLASS,
- fallback=settings.FALLBACK_OBJECT_TYPECLASS)
-
- # -- Evennia constructs --
- access_type="edit"
-
-
[docs]defget_success_url(self):
- """
- Django hook.
-
- Can be overridden to return any URL you want to redirect the user to
- after the object is successfully updated, but by default it goes to the
- object detail page so the user can see their changes reflected.
-
- """
- ifself.success_url:
- returnself.success_url
- returnself.object.web_get_detail_url()
-
-
[docs]defget_initial(self):
- """
- Django hook, modified for Evennia.
-
- Prepopulates the update form field values based on object db attributes.
-
- Returns:
- data (dict): Dictionary of key:value pairs containing initial form
- data.
-
- """
- # Get the object we want to update
- obj=self.get_object()
-
- # Get attributes
- data={k:getattr(obj.db,k,"")forkinself.form_class.base_fields}
-
- # Get model fields
- data.update({k:getattr(obj,k,"")forkinself.form_class.Meta.fields})
-
- returndata
-
-
[docs]defform_valid(self,form):
- """
- Override of Django hook.
-
- Updates object attributes based on values submitted.
-
- This is run when the form is submitted and the data on it is deemed
- valid-- all values are within expected ranges, all strings contain
- valid characters and lengths, etc.
-
- This method is only called if all values for the fields submitted
- passed form validation, so at this point we can assume the data is
- validated and sanitized.
-
- """
- # Get the attributes after they've been cleaned and validated
- data={k:vfork,vinform.cleaned_data.items()ifknotinself.form_class.Meta.fields}
-
- # Update the object attributes
- forkey,valueindata.items():
- self.object.attributes.add(key,value)
- messages.success(self.request,"Successfully updated '%s' for %s."%(key,self.object))
-
- # Do not return super().form_valid; we don't want to update the model
- # instance, just its attributes.
- returnHttpResponseRedirect(self.get_success_url())
-
-
-#
-# Account views
-#
-
-
-
[docs]classAccountMixin(TypeclassMixin):
- """
- This is a "mixin", a modifier of sorts.
-
- Any view class with this in its inheritance list will be modified to work
- with Account objects instead of generic Objects or otherwise.
-
- """
-
- # -- Django constructs --
- model=class_from_module(settings.BASE_ACCOUNT_TYPECLASS,
- fallback=settings.FALLBACK_ACCOUNT_TYPECLASS)
- form_class=website_forms.AccountForm
[docs]defform_valid(self,form):
- """
- Django hook, modified for Evennia.
-
- This hook is called after a valid form is submitted.
-
- When an account creation form is submitted and the data is deemed valid,
- proceeds with creating the Account object.
-
- """
- # Get values provided
- username=form.cleaned_data["username"]
- password=form.cleaned_data["password1"]
- email=form.cleaned_data.get("email","")
-
- # Create account
- account,errs=self.typeclass.create(username=username,password=password,email=email)
-
- # If unsuccessful, display error messages to user
- ifnotaccount:
- [messages.error(self.request,err)forerrinerrs]
-
- # Call the Django "form failure" hook
- returnself.form_invalid(form)
-
- # Inform user of success
- messages.success(
- self.request,
- "Your account '%s' was successfully created! "
- "You may log in using it now."%account.name,
- )
-
- # Redirect the user to the login page
- returnHttpResponseRedirect(self.success_url)
-
-
-#
-# Character views
-#
-
-
-
[docs]classCharacterMixin(TypeclassMixin):
- """
- This is a "mixin", a modifier of sorts.
-
- Any view class with this in its inheritance list will be modified to work
- with Character objects instead of generic Objects or otherwise.
-
- """
-
- # -- Django constructs --
- model=class_from_module(settings.BASE_CHARACTER_TYPECLASS,
- fallback=settings.FALLBACK_CHARACTER_TYPECLASS)
- form_class=website_forms.CharacterForm
- success_url=reverse_lazy("character-manage")
-
-
[docs]defget_queryset(self):
- """
- This method will override the Django get_queryset method to only
- return a list of characters associated with the current authenticated
- user.
-
- Returns:
- queryset (QuerySet): Django queryset for use in the given view.
-
- """
- # Get IDs of characters owned by account
- account=self.request.user
- ids=[getattr(x,"id")forxinaccount.charactersifx]
-
- # Return a queryset consisting of those characters
- returnself.typeclass.objects.filter(id__in=ids).order_by(Lower("db_key"))
-
-
-
[docs]classCharacterListView(LoginRequiredMixin,CharacterMixin,ListView):
- """
- This view provides a mechanism by which a logged-in player can view a list
- of all other characters.
-
- This view requires authentication by default as a nominal effort to prevent
- human stalkers and automated bots/scrapers from harvesting data on your users.
-
- """
-
- # -- Django constructs --
- template_name="website/character_list.html"
- paginate_by=100
-
- # -- Evennia constructs --
- page_title="Character List"
- access_type="view"
-
-
[docs]defget_queryset(self):
- """
- This method will override the Django get_queryset method to return a
- list of all characters (filtered/sorted) instead of just those limited
- to the account.
-
- Returns:
- queryset (QuerySet): Django queryset for use in the given view.
-
- """
- account=self.request.user
-
- # Return a queryset consisting of characters the user is allowed to
- # see.
- ids=[
- obj.idforobjinself.typeclass.objects.all()ifobj.access(account,self.access_type)
- ]
-
- returnself.typeclass.objects.filter(id__in=ids).order_by(Lower("db_key"))
-
-
-
[docs]classCharacterPuppetView(LoginRequiredMixin,CharacterMixin,RedirectView,ObjectDetailView):
- """
- This view provides a mechanism by which a logged-in player can "puppet" one
- of their characters within the context of the website.
-
- It also ensures that any user attempting to puppet something is logged in,
- and that their intended puppet is one that they own.
-
- """
-
-
[docs]defget_redirect_url(self,*args,**kwargs):
- """
- Django hook.
-
- This view returns the URL to which the user should be redirected after
- a passed or failed puppet attempt.
-
- Returns:
- url (str): Path to post-puppet destination.
-
- """
- # Get the requested character, if it belongs to the authenticated user
- char=self.get_object()
-
- # Get the page the user came from
- next_page=self.request.GET.get("next",self.success_url)
-
- ifchar:
- # If the account owns the char, store the ID of the char in the
- # Django request's session (different from Evennia session!).
- # We do this because characters don't serialize well.
- self.request.session["puppet"]=int(char.pk)
- messages.success(self.request,"You become '%s'!"%char)
- else:
- # If the puppeting failed, clear out the cached puppet value
- self.request.session["puppet"]=None
- messages.error(self.request,"You cannot become '%s'."%char)
-
- returnnext_page
-
-
-
[docs]classCharacterManageView(LoginRequiredMixin,CharacterMixin,ListView):
- """
- This view provides a mechanism by which a logged-in player can browse,
- edit, or delete their own characters.
-
- """
-
- # -- Django constructs --
- paginate_by=10
- template_name="website/character_manage_list.html"
-
- # -- Evennia constructs --
- page_title="Manage Characters"
-
-
-
[docs]classCharacterUpdateView(CharacterMixin,ObjectUpdateView):
- """
- This view provides a mechanism by which a logged-in player (enforced by
- ObjectUpdateView) can edit the attributes of a character they own.
-
- """
-
- # -- Django constructs --
- form_class=website_forms.CharacterUpdateForm
- template_name="website/character_form.html"
-
-
-
[docs]classCharacterDetailView(CharacterMixin,ObjectDetailView):
- """
- This view provides a mechanism by which a user can view the attributes of
- a character, owned by them or not.
-
- """
-
- # -- Django constructs --
- template_name="website/object_detail.html"
-
- # -- Evennia constructs --
- # What attributes to display for this object
- attributes=["name","desc"]
- access_type="view"
-
-
[docs]defget_queryset(self):
- """
- This method will override the Django get_queryset method to return a
- list of all characters the user may access.
-
- Returns:
- queryset (QuerySet): Django queryset for use in the given view.
-
- """
- account=self.request.user
-
- # Return a queryset consisting of characters the user is allowed to
- # see.
- ids=[
- obj.idforobjinself.typeclass.objects.all()ifobj.access(account,self.access_type)
- ]
-
- returnself.typeclass.objects.filter(id__in=ids).order_by(Lower("db_key"))
-
-
-
[docs]classCharacterDeleteView(CharacterMixin,ObjectDeleteView):
- """
- This view provides a mechanism by which a logged-in player (enforced by
- ObjectDeleteView) can delete a character they own.
-
- """
-
- pass
-
-
-
[docs]classCharacterCreateView(CharacterMixin,ObjectCreateView):
- """
- This view provides a mechanism by which a logged-in player (enforced by
- ObjectCreateView) can create a new character.
-
- """
-
- # -- Django constructs --
- template_name="website/character_form.html"
-
-
[docs]defform_valid(self,form):
- """
- Django hook, modified for Evennia.
-
- This hook is called after a valid form is submitted.
-
- When an character creation form is submitted and the data is deemed valid,
- proceeds with creating the Character object.
-
- """
- # Get account object creating the character
- account=self.request.user
- character=None
-
- # Get attributes from the form
- self.attributes={k:form.cleaned_data[k]forkinform.cleaned_data.keys()}
- charname=self.attributes.pop("db_key")
- description=self.attributes.pop("desc")
- # Create a character
- character,errors=self.typeclass.create(charname,account,description=description)
-
- iferrors:
- # Echo error messages to the user
- [messages.error(self.request,x)forxinerrors]
-
- ifcharacter:
- # Assign attributes from form
- forkey,valueinself.attributes.items():
- setattr(character.db,key,value)
-
- # Return the user to the character management page, unless overridden
- messages.success(self.request,"Your character '%s' was created!"%character.name)
- returnHttpResponseRedirect(self.success_url)
-
- else:
- # Call the Django "form failed" hook
- messages.error(self.request,"Your character could not be created.")
- returnself.form_invalid(form)
-
-
-#
-# Channel views
-#
-
-
-
[docs]classChannelMixin(TypeclassMixin):
- """
- This is a "mixin", a modifier of sorts.
-
- Any view class with this in its inheritance list will be modified to work
- with HelpEntry objects instead of generic Objects or otherwise.
-
- """
-
- # -- Django constructs --
- model=class_from_module(settings.BASE_CHANNEL_TYPECLASS,
- fallback=settings.FALLBACK_CHANNEL_TYPECLASS)
-
- # -- Evennia constructs --
- page_title="Channels"
-
- # What lock type to check for the requesting user, authenticated or not.
- # https://github.com/evennia/evennia/wiki/Locks#valid-access_types
- access_type="listen"
-
-
[docs]defget_queryset(self):
- """
- Django hook; here we want to return a list of only those Channels
- and other documentation that the current user is allowed to see.
-
- Returns:
- queryset (QuerySet): List of Channels available to the user.
-
- """
- account=self.request.user
-
- # Get list of all Channels
- channels=self.typeclass.objects.all().iterator()
-
- # Now figure out which ones the current user is allowed to see
- bucket=[channel.idforchannelinchannelsifchannel.access(account,"listen")]
-
- # Re-query and set a sorted list
- filtered=self.typeclass.objects.filter(id__in=bucket).order_by(Lower("db_key"))
-
- returnfiltered
-
-
-
[docs]classChannelListView(ChannelMixin,ListView):
- """
- Returns a list of channels that can be viewed by a user, authenticated
- or not.
-
- """
-
- # -- Django constructs --
- paginate_by=100
- template_name="website/channel_list.html"
-
- # -- Evennia constructs --
- page_title="Channel Index"
-
- max_popular=10
-
-
[docs]defget_context_data(self,**kwargs):
- """
- Django hook; we override it to calculate the most popular channels.
-
- Returns:
- context (dict): Django context object
-
- """
- context=super(ChannelListView,self).get_context_data(**kwargs)
-
- # Calculate which channels are most popular
- context["most_popular"]=sorted(
- list(self.get_queryset()),
- key=lambdachannel:len(channel.subscriptions.all()),
- reverse=True,
- )[:self.max_popular]
-
- returncontext
-
-
-
[docs]classChannelDetailView(ChannelMixin,ObjectDetailView):
- """
- Returns the log entries for a given channel.
-
- """
-
- # -- Django constructs --
- template_name="website/channel_detail.html"
-
- # -- Evennia constructs --
- # What attributes of the object you wish to display on the page. Model-level
- # attributes will take precedence over identically-named db.attributes!
- # The order you specify here will be followed.
- attributes=["name"]
-
- # How many log entries to read and display.
- max_num_lines=10000
-
-
[docs]defget_context_data(self,**kwargs):
- """
- Django hook; before we can display the channel logs, we need to recall
- the logfile and read its lines.
-
- Returns:
- context (dict): Django context object
-
- """
- # Get the parent context object, necessary first step
- context=super(ChannelDetailView,self).get_context_data(**kwargs)
-
- # Get the filename this Channel is recording to
- filename=self.object.attributes.get(
- "log_file",default="channel_%s.log"%self.object.key
- )
-
- # Split log entries so we can filter by time
- bucket=[]
- forlogin(x.strip()forxintail_log_file(filename,0,self.max_num_lines)):
- ifnotlog:
- continue
- time,msg=log.split(" [-] ")
- time_key=time.split(":")[0]
-
- bucket.append({"key":time_key,"timestamp":time,"message":msg})
-
- # Add the processed entries to the context
- context["object_list"]=bucket
-
- # Get a list of unique timestamps by hour and sort them
- context["object_filters"]=sorted(set([x["key"]forxinbucket]))
-
- returncontext
-
-
[docs]defget_object(self,queryset=None):
- """
- Override of Django hook that retrieves an object by slugified channel
- name.
-
- Returns:
- channel (Channel): Channel requested in the URL.
-
- """
- # Get the queryset for the help entries the user can access
- ifnotqueryset:
- queryset=self.get_queryset()
-
- # Find the object in the queryset
- channel=slugify(self.kwargs.get("slug",""))
- obj=next((xforxinquerysetifslugify(x.db_key)==channel),None)
-
- # Check if this object was requested in a valid manner
- ifnotobj:
- raiseHttpResponseBadRequest(
- "No %(verbose_name)s found matching the query"
- %{"verbose_name":queryset.model._meta.verbose_name}
- )
-
- returnobj
-
-
-#
-# Help views
-#
-
-
-
[docs]classHelpMixin(TypeclassMixin):
- """
- This is a "mixin", a modifier of sorts.
-
- Any view class with this in its inheritance list will be modified to work
- with HelpEntry objects instead of generic Objects or otherwise.
-
- """
-
- # -- Django constructs --
- model=HelpEntry
-
- # -- Evennia constructs --
- page_title="Help"
-
-
[docs]defget_queryset(self):
- """
- Django hook; here we want to return a list of only those HelpEntries
- and other documentation that the current user is allowed to see.
-
- Returns:
- queryset (QuerySet): List of Help entries available to the user.
-
- """
- account=self.request.user
-
- # Get list of all HelpEntries
- entries=self.typeclass.objects.all().iterator()
-
- # Now figure out which ones the current user is allowed to see
- bucket=[entry.idforentryinentriesifentry.access(account,"view")]
-
- # Re-query and set a sorted list
- filtered=(
- self.typeclass.objects.filter(id__in=bucket)
- .order_by(Lower("db_key"))
- .order_by(Lower("db_help_category"))
- )
-
- returnfiltered
-
-
-
[docs]classHelpListView(HelpMixin,ListView):
- """
- Returns a list of help entries that can be viewed by a user, authenticated
- or not.
-
- """
-
- # -- Django constructs --
- paginate_by=500
- template_name="website/help_list.html"
-
- # -- Evennia constructs --
- page_title="Help Index"
-
-
-
[docs]classHelpDetailView(HelpMixin,EvenniaDetailView):
- """
- Returns the detail page for a given help entry.
-
- """
-
- # -- Django constructs --
- template_name="website/help_detail.html"
-
-
[docs]defget_context_data(self,**kwargs):
- """
- Adds navigational data to the template to let browsers go to the next
- or previous entry in the help list.
-
- Returns:
- context (dict): Django context object
-
- """
- context=super(HelpDetailView,self).get_context_data(**kwargs)
-
- # Get the object in question
- obj=self.get_object()
-
- # Get queryset and filter out non-related categories
- queryset=(
- self.get_queryset()
- .filter(db_help_category=obj.db_help_category)
- .order_by(Lower("db_key"))
- )
- context["topic_list"]=queryset
-
- # Find the index position of the given obj in the queryset
- objs=list(queryset)
- fori,xinenumerate(objs):
- ifobjisx:
- break
-
- # Find the previous and next topics, if either exist
- try:
- asserti+1<=len(objs)andobjs[i+1]isnotobj
- context["topic_next"]=objs[i+1]
- except:
- context["topic_next"]=None
-
- try:
- asserti-1>=0andobjs[i-1]isnotobj
- context["topic_previous"]=objs[i-1]
- except:
- context["topic_previous"]=None
-
- # Format the help entry using HTML instead of newlines
- text=obj.db_entrytext
- text=text.replace("\r\n\r\n","\n\n")
- text=text.replace("\r\n","\n")
- text=text.replace("\n","<br />")
- context["entry_text"]=text
-
- returncontext
-
-
[docs]defget_object(self,queryset=None):
- """
- Override of Django hook that retrieves an object by category and topic
- instead of pk and slug.
-
- Returns:
- entry (HelpEntry): HelpEntry requested in the URL.
-
- """
- # Get the queryset for the help entries the user can access
- ifnotqueryset:
- queryset=self.get_queryset()
-
- # Find the object in the queryset
- category=slugify(self.kwargs.get("category",""))
- topic=slugify(self.kwargs.get("topic",""))
- obj=next(
- (
- x
- forxinqueryset
- ifslugify(x.db_help_category)==categoryandslugify(x.db_key)==topic
- ),
- None,
- )
-
- # Check if this object was requested in a valid manner
- ifnotobj:
- returnHttpResponseBadRequest(
- "No %(verbose_name)s found matching the query"
- %{"verbose_name":queryset.model._meta.verbose_name}
- )
-
- returnobj
-
-
-
\ No newline at end of file
diff --git a/docs/0.9.5/_modules/functools.html b/docs/0.9.5/_modules/functools.html
index 08040b3d3d..c72c574f82 100644
--- a/docs/0.9.5/_modules/functools.html
+++ b/docs/0.9.5/_modules/functools.html
@@ -1055,7 +1055,6 @@
Note that this object is primarily intended to
store OOC information, not game info! This
object represents the actual user (not their
@@ -180,6 +180,24 @@ at_account_creation()
This is called by Django also when logging in; it should not be mixed up with validation, since that
-would mean old passwords in the database (pre validation checks) could get invalidated.
+
This is called by Django also when logging in; it should not be mixed up with
+validation, since that would mean old passwords in the database (pre validation checks)
+could get invalidated.
Called by the Channel just before passing a message into channel_msg.
+This allows for tweak messages per-user and also to abort the
+receive on the receiver-level.
+
+
Parameters
+
+
message (str) – The message sent to the channel.
+
channel (Channel) – The sending channel.
+
senders (list, optional) – Accounts or Objects acting as senders.
+For most normal messages, there is only a single sender. If
+there are no senders, this may be a broadcasting message.
+
**kwargs – These are additional keywords passed into channel_msg.
+If no_prefix=True or emit=True are passed, the channel
+prefix will not be added (**[channelname]: ** by default)
+
+
+
Returns
+
str or None –
+
+
Allows for customizing the message for this recipient.
If returning None (or False) message-receiving is aborted.
+The returning string will be passed into self.channel_msg.
+
+
+
+
+
+
Notes
+
This support posing/emotes by starting channel-send with : or ;.
This performs the actions of receiving a message to an un-muted
+channel.
+
+
Parameters
+
+
message (str) – The message sent to the channel.
+
channel (Channel) – The sending channel.
+
senders (list, optional) – Accounts or Objects acting as senders.
+For most normal messages, there is only a single sender. If
+there are no senders, this may be a broadcasting message or
+similar.
+
**kwargs – These are additional keywords originally passed into
+Channel.msg.
+
+
+
+
Notes
+
Before this, Channel.at_pre_channel_msg will fire, which offers a way
+to customize the message for the receiver on the channel-level.
Called by self.channel_msg after message was received.
+
+
Parameters
+
+
message (str) – The message sent to the channel.
+
channel (Channel) – The sending channel.
+
senders (list, optional) – Accounts or Objects acting as senders.
+For most normal messages, there is only a single sender. If
+there are no senders, this may be a broadcasting message.
+
**kwargs – These are additional keywords passed into channel_msg.
-fieldsets = (('In-game Permissions and Locks', {'fields': ('db_lock_storage',), 'description': '<i>These are permissions/locks for in-game use. They are unrelated to website access rights.</i>'}), ('In-game Account data', {'fields': ('db_typeclass_path', 'db_cmdset_storage'), 'description': '<i>These fields define in-game-specific properties for the Account object in-game.</i>'}))¶
-fieldsets = ((None, {'fields': ('username', 'password', 'email')}), ('Website profile', {'fields': ('first_name', 'last_name'), 'description': '<i>These are not used in the default system.</i>'}), ('Website dates', {'fields': ('last_login', 'date_joined'), 'description': '<i>Relevant only to the website.</i>'}), ('Website Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', 'user_permissions', 'groups'), 'description': '<i>These are permissions/permission groups for accessing the admin site. They are unrelated to in-game access rights.</i>'}), ('Game Options', {'fields': ('db_typeclass_path', 'db_cmdset_storage', 'db_lock_storage'), 'description': '<i>These are attributes that are more relevant to gameplay.</i>'}))¶
-
-
-
-
-add_fieldsets = ((None, {'fields': ('username', 'password1', 'password2', 'email'), 'description': '<i>These account details are shared by the admin system and the game.</i>'}),)¶
Determine the HttpResponse for the add_view stage. It mostly defers to
-its superclass implementation but is customized because the User model
-has a slightly different workflow.
diff --git a/docs/0.9.5/api/evennia.accounts.html b/docs/0.9.5/api/evennia.accounts.html
index 66a22a8a17..627d394066 100644
--- a/docs/0.9.5/api/evennia.accounts.html
+++ b/docs/0.9.5/api/evennia.accounts.html
@@ -96,7 +96,6 @@ more Objects depending on settings. An Account has no in-game existence.
diff --git a/docs/0.9.5/api/evennia.accounts.models.html b/docs/0.9.5/api/evennia.accounts.models.html
index 23c32c62af..8bb4da2a1b 100644
--- a/docs/0.9.5/api/evennia.accounts.models.html
+++ b/docs/0.9.5/api/evennia.accounts.models.html
@@ -412,7 +412,6 @@ class built by **create_forward_many_to_many_manager()** define
diff --git a/docs/0.9.5/api/evennia.commands.cmdhandler.html b/docs/0.9.5/api/evennia.commands.cmdhandler.html
index f9a8f1c6aa..417cc727fe 100644
--- a/docs/0.9.5/api/evennia.commands.cmdhandler.html
+++ b/docs/0.9.5/api/evennia.commands.cmdhandler.html
@@ -45,15 +45,11 @@ command line. The processing of a command works as follows:
The calling object (caller) is analyzed based on its callertype.
Cmdsets are gathered from different sources:
-- channels: all available channel names are auto-created into a cmdset, to allow
+- object cmdsets: all objects at caller’s location are scanned for non-empty
-
for giving the channel name and have the following immediately
-sent to the channel. The sending is performed by the CMD_CHANNEL
-system command.
+
cmdsets. This includes cmdsets on exits.
-
object cmdsets: all objects at caller’s location are scanned for non-empty
-cmdsets. This includes cmdsets on exits.
caller: the caller is searched for its own currently active cmdset.
account: lastly the cmdsets defined on caller.account are added.
@@ -67,19 +63,10 @@ input string for possible command matches.
cmdset, or fallback to error message. Exit.
If no match was found -> check for CMD_NOMATCH in current cmdset or
fallback to error message. Exit.
-
A single match was found. If this is a channel-command (i.e. the
-ommand name is that of a channel), –> check for CMD_CHANNEL in
-current cmdset or use channelhandler default. Exit.
At this point we have found a normal command. We assign useful variables to it that
will be available to the command coder at run-time.
-
-
-
We have a unique cmdobject, primed for use. Call all hooks:
-
-
-
at_pre_cmd(), cmdobj.parse(), cmdobj.func() and finally at_post_cmd().
-
-
+
We have a unique cmdobject, primed for use. Call all hooks:
+at_pre_cmd(), cmdobj.parse(), cmdobj.func() and finally at_post_cmd().
Return deferred that will fire with the return from cmdobj.func() (unused by default).
diff --git a/docs/0.9.5/api/evennia.commands.cmdparser.html b/docs/0.9.5/api/evennia.commands.cmdparser.html
index 88f4c21de9..392e2412e1 100644
--- a/docs/0.9.5/api/evennia.commands.cmdparser.html
+++ b/docs/0.9.5/api/evennia.commands.cmdparser.html
@@ -96,8 +96,8 @@ in the match, otherwise strip them before matching.
Test if user tried to separate multi-matches with a number separator
(default 1-name, 2-name etc). This is usually called last, if no other
match was found.
@@ -206,7 +206,6 @@ the remaining arguments, and the matched cmdobject from the cmdset.
diff --git a/docs/0.9.5/api/evennia.commands.cmdsethandler.html b/docs/0.9.5/api/evennia.commands.cmdsethandler.html
index e132724ed5..92de8f282c 100644
--- a/docs/0.9.5/api/evennia.commands.cmdsethandler.html
+++ b/docs/0.9.5/api/evennia.commands.cmdsethandler.html
@@ -176,7 +176,7 @@ to the central cmdhandler.get_and_merge_cmdsets()!
Add a cmdset to the handler, on top of the old ones, unless it
is set as the default one (it will then end up at the bottom of the stack)
@@ -185,7 +185,7 @@ is set as the default one (it will then end up at the bottom of the stack)
cmdset (CmdSet or str) – Can be a cmdset object or the python path
to such an object.
emit_to_obj (Object, optional) – An object to receive error messages.
-
permanent (bool, optional) – This cmdset will remain across a server reboot.
+
persistent (bool, optional) – Let cmdset remain across server reload.
default_cmdset (Cmdset, optional) – Insert this to replace the
default cmdset position (there is only one such position,
always at the bottom of the stack).
@@ -206,14 +206,14 @@ it’s a ‘quirk’ that has to be documented.
@@ -438,13 +434,11 @@ detailing the contents of the table.
save_for_next = False¶
-
+
+
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'command', 'tags': '', 'text': '\n ## Base command\n\n (you may see this if a child command had no help text defined)\n\n Usage:\n command [args]\n\n This is the base command class. Inherit from this\n to create new commands.\n\n The cmdhandler makes the following variables available to the\n command methods (so you can always assume them to be there):\n self.caller - the game object calling the command\n self.cmdstring - the command name used to trigger this command (allows\n you to know which alias was used, for example)\n cmd.args - everything supplied to the command following the cmdstring\n (this is usually what is parsed in self.parse())\n cmd.cmdset - the cmdset from which this command was matched (useful only\n seldomly, notably for help-type commands, to create dynamic\n help entries and lists)\n cmd.obj - the object on which this command is defined. If a default command,\n this is usually the same as caller.\n cmd.rawstring - the full raw string input, including any args and no parsing.\n\n The following class properties can/should be defined on your child class:\n\n key - identifier for command (e.g. "look")\n aliases - (optional) list of aliases (e.g. ["l", "loo"])\n locks - lock string (default is "cmd:all()")\n help_category - how to organize this help entry in help system\n (default is "General")\n auto_help - defaults to True. Allows for turning off auto-help generation\n arg_regex - (optional) raw string regex defining how the argument part of\n the command should look in order to match for this command\n (e.g. must it be a space between cmdname and arg?)\n auto_help_display_key - (optional) if given, this replaces the string shown\n in the auto-help listing. This is particularly useful for system-commands\n whose actual key is not really meaningful.\n\n (Note that if auto_help is on, this initial string is also used by the\n system to create the help entry for the command, so it\'s a good idea to\n format it similar to this one). This behavior can be changed by\n overriding the method \'get_help\' of a command: by default, this\n method returns cmd.__doc__ (that is, this very docstring, or\n the docstring of your command). You can, however, extend or\n replace this without disabling auto_help.\n '}¶
diff --git a/docs/0.9.5/api/evennia.commands.default.account.html b/docs/0.9.5/api/evennia.commands.default.account.html
index ef9deac1ae..78b2b2919a 100644
--- a/docs/0.9.5/api/evennia.commands.default.account.html
+++ b/docs/0.9.5/api/evennia.commands.default.account.html
@@ -70,7 +70,7 @@ method. Otherwise all text will be returned to all connected sessions.
@@ -99,6 +99,11 @@ method. Otherwise all text will be returned to all connected sessions.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': 'ls l', 'category': 'general', 'key': 'look', 'tags': '', 'text': '\n look while out-of-character\n\n Usage:\n look\n\n Look in the ooc state.\n '}¶
+
+
@@ -153,6 +158,11 @@ as you the account have access right to puppet it.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': 'puppet', 'category': 'general', 'key': 'ic', 'tags': '', 'text': '\n control an object you have permission to puppet\n\n Usage:\n ic <character>\n\n Go in-character (IC) as a given Character.\n\n This will attempt to "become" a different object assuming you have\n the right to do so. Note that it\'s the ACCOUNT character that puppets\n characters/objects and which needs to have the correct permission!\n\n You cannot become an object that is already controlled by another\n account. In principle <character> can be any in-game object as long\n as you the account have access right to puppet it.\n '}¶
+
+
@@ -202,6 +212,11 @@ as you the account have access right to puppet it.
lock_storage = 'cmd:pperm(Player)'¶
+
+
+search_index_entry = {'aliases': 'unpuppet', 'category': 'general', 'key': 'ooc', 'tags': '', 'text': '\n stop puppeting and go ooc\n\n Usage:\n ooc\n\n Go out-of-character (OOC).\n\n This will leave your current character and put you in a incorporeal OOC state.\n '}¶
+
+
@@ -250,6 +265,11 @@ as you the account have access right to puppet it.
lock_storage = 'cmd:pperm(Player)'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'password', 'tags': '', 'text': '\n change your password\n\n Usage:\n password <old password> = <new password>\n\n Changes your password. Make sure to pick a safe one.\n '}¶
+
+
@@ -306,6 +326,11 @@ game. Use the /all switch to disconnect from all sessions.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'quit', 'tags': '', 'text': '\n quit the game\n\n Usage:\n quit\n\n Switch:\n all - disconnect all connected sessions\n\n Gracefully disconnect your current session from the\n game. Use the /all switch to disconnect from all sessions.\n '}¶
+
+
@@ -357,6 +382,11 @@ if you want.
lock_storage = 'cmd:pperm(Player)'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'charcreate', 'tags': '', 'text': '\n create a new character\n\n Usage:\n charcreate <charname> [= desc]\n\n Create a new character, optionally giving it a description. You\n may use upper-case letters in the name - you will nevertheless\n always be able to access your character using lower-case letters\n if you want.\n '}¶
+
+
@@ -415,6 +445,11 @@ later connecting with a client with different capabilities.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': 'options', 'category': 'general', 'key': 'option', 'tags': '', 'text': '\n Set an account option\n\n Usage:\n option[/save] [name = value]\n\n Switches:\n save - Save the current option settings for future logins.\n clear - Clear the saved options.\n\n This command allows for viewing and setting client interface\n settings. Note that saved options may not be able to be used if\n later connecting with a client with different capabilities.\n\n\n '}¶
+
+
@@ -463,6 +498,11 @@ later connecting with a client with different capabilities.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'sessions', 'tags': '', 'text': '\n check your connected session(s)\n\n Usage:\n sessions\n\n Lists the sessions currently connected to your account.\n\n '}¶
+
+
@@ -513,6 +553,11 @@ also for those with all permissions.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': 'doing', 'category': 'general', 'key': 'who', 'tags': '', 'text': '\n list who is currently online\n\n Usage:\n who\n doing\n\n Shows who is currently online. Doing is an alias that limits info\n also for those with all permissions.\n '}¶
+
+
@@ -521,7 +566,7 @@ also for those with all permissions.
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'color', 'tags': '', 'text': '\n testing which colors your client support\n\n Usage:\n color ansi | xterm256\n\n Prints a color map along with in-mud color codes to use to produce\n them. It also tests what is supported in your client. Choices are\n 16-color ansi (supported in most muds) or the 256-color xterm256\n standard. No checking is done to determine your client supports\n color - if not you will see rubbish appear.\n '}¶
+
+
@@ -647,6 +697,11 @@ Use the unquell command to revert back to normal operation.
lock_storage = 'cmd:pperm(Player)'¶
+
+
+search_index_entry = {'aliases': 'unquell', 'category': 'general', 'key': 'quell', 'tags': '', 'text': "\n use character's permissions instead of account's\n\n Usage:\n quell\n unquell\n\n Normally the permission level of the Account is used when puppeting a\n Character/Object to determine access. This command will switch the lock\n system to make use of the puppeted Object's permissions instead. This is\n useful mainly for testing.\n Hierarchical permission quelling only work downwards, thus an Account cannot\n use a higher-permission Character to escalate their permission level.\n Use the unquell command to revert back to normal operation.\n "}¶
+
+
@@ -690,6 +745,11 @@ Use the unquell command to revert back to normal operation.
lock_storage = 'cmd:pperm(Player)'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'chardelete', 'tags': '', 'text': '\n delete a character - this cannot be undone!\n\n Usage:\n chardelete <charname>\n\n Permanently deletes one of your characters.\n '}¶
+
+
@@ -747,6 +807,11 @@ to all the variables defined therein.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'style', 'tags': '', 'text': '\n In-game style options\n\n Usage:\n style\n style <option> = <value>\n\n Configure stylings for in-game display elements like table borders, help\n entriest etc. Use without arguments to see all available options.\n\n '}¶
+
+
@@ -794,7 +859,6 @@ to all the variables defined therein.
diff --git a/docs/0.9.5/api/evennia.commands.default.admin.html b/docs/0.9.5/api/evennia.commands.default.admin.html
index 3cac099b32..6dc6896860 100644
--- a/docs/0.9.5/api/evennia.commands.default.admin.html
+++ b/docs/0.9.5/api/evennia.commands.default.admin.html
@@ -90,6 +90,11 @@ supplied it will be echoed to the user unless /quiet is set.
lock_storage = 'cmd:perm(boot) or perm(Admin)'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'admin', 'key': 'boot', 'tags': '', 'text': '\n kick an account from the server.\n\n Usage\n boot[/switches] <account obj> [: reason]\n\n Switches:\n quiet - Silently boot without informing account\n sid - boot by session id instead of name or dbref\n\n Boot an account object from the server. If a reason is\n supplied it will be echoed to the user unless /quiet is set.\n '}¶
+
+
@@ -164,6 +169,11 @@ values in each tuple is set to the empty string.
lock_storage = 'cmd:perm(ban) or perm(Developer)'¶
+
+
+search_index_entry = {'aliases': 'bans', 'category': 'admin', 'key': 'ban', 'tags': '', 'text': "\n ban an account from the server\n\n Usage:\n ban [<name or ip> [: reason]]\n\n Without any arguments, shows numbered list of active bans.\n\n This command bans a user from accessing the game. Supply an optional\n reason to be able to later remember why the ban was put in place.\n\n It is often preferable to ban an account from the server than to\n delete an account with accounts/delete. If banned by name, that account\n account can no longer be logged into.\n\n IP (Internet Protocol) address banning allows blocking all access\n from a specific address or subnet. Use an asterisk (*) as a\n wildcard.\n\n Examples:\n ban thomas - ban account 'thomas'\n ban/ip 134.233.2.111 - ban specific ip address\n ban/ip 134.233.2.* - ban all in a subnet\n ban/ip 134.233.*.* - even wider ban\n\n A single IP filter can be easy to circumvent by changing computers\n or requesting a new IP address. Setting a wide IP block filter with\n wildcards might be tempting, but remember that it may also\n accidentally block innocent users connecting from the same country\n or region.\n\n "}¶
+
+
@@ -210,6 +220,11 @@ unban.
lock_storage = 'cmd:perm(unban) or perm(Developer)'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'admin', 'key': 'unban', 'tags': '', 'text': '\n remove a ban from an account\n\n Usage:\n unban <banid>\n\n This will clear an account name/ip ban previously set with the ban\n command. Use this command without an argument to view a numbered\n list of bans. Use the numbers in this list to select which one to\n unban.\n\n '}¶
+
+
@@ -268,6 +283,11 @@ to accounts respectively.
lock_storage = 'cmd:perm(emit) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': 'remit pemit', 'category': 'admin', 'key': 'emit', 'tags': '', 'text': '\n admin command for emitting message to multiple objects\n\n Usage:\n emit[/switches] [<obj>, <obj>, ... =] <message>\n remit [<obj>, <obj>, ... =] <message>\n pemit [<obj>, <obj>, ... =] <message>\n\n Switches:\n room - limit emits to rooms only (default)\n accounts - limit emits to accounts only\n contents - send to the contents of matched objects too\n\n Emits a message to the selected objects or to\n your immediate surroundings. If the object is a room,\n send to its contents. remit and pemit are just\n limited forms of emit, for sending to rooms and\n to accounts respectively.\n '}¶
+
+
@@ -311,6 +331,11 @@ to accounts respectively.
lock_storage = 'cmd:perm(newpassword) or perm(Admin)'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'admin', 'key': 'userpassword', 'tags': '', 'text': "\n change the password of an account\n\n Usage:\n userpassword <user obj> = <new password>\n\n Set an account's password.\n "}¶
+
+
@@ -364,6 +389,11 @@ or account. If no permission is given, list all permissions on <object>.
lock_storage = 'cmd:perm(perm) or perm(Developer)'¶
+
+
+search_index_entry = {'aliases': 'setperm', 'category': 'admin', 'key': 'perm', 'tags': '', 'text': '\n set the permissions of an account/object\n\n Usage:\n perm[/switch] <object> [= <permission>[,<permission>,...]]\n perm[/switch] *<account> [= <permission>[,<permission>,...]]\n\n Switches:\n del - delete the given permission from <object> or <account>.\n account - set permission on an account (same as adding * to name)\n\n This command sets/clears individual permission strings on an object\n or account. If no permission is given, list all permissions on <object>.\n '}¶
+
+
@@ -408,6 +438,11 @@ including all currently unlogged in.
lock_storage = 'cmd:perm(wall) or perm(Admin)'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'admin', 'key': 'wall', 'tags': '', 'text': '\n make an announcement to all\n\n Usage:\n wall <message>\n\n Announces a message to all connected sessions\n including all currently unlogged in.\n '}¶
+
+
@@ -457,6 +492,11 @@ including all currently unlogged in.
lock_storage = 'cmd:perm(spawn) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'building', 'key': 'force', 'tags': '', 'text': '\n forces an object to execute a command\n\n Usage:\n force <object>=<command string>\n\n Example:\n force bob=get stick\n '}¶
+
+
@@ -504,7 +544,6 @@ including all currently unlogged in.
@@ -104,6 +104,11 @@ skipping, reloading etc.
lock_storage = 'cmd:perm(batchcommands) or perm(Developer)'¶
+
+
+search_index_entry = {'aliases': 'batchcmd batchcommand', 'category': 'building', 'key': 'batchcommands', 'tags': '', 'text': '\n build from batch-command file\n\n Usage:\n batchcommands[/interactive] <python.path.to.file>\n\n Switch:\n interactive - this mode will offer more control when\n executing the batch file, like stepping,\n skipping, reloading etc.\n\n Runs batches of commands from a batch-cmd text file (*.ev).\n\n '}¶
+
+
@@ -162,6 +167,11 @@ object copies behind when testing out the script.
lock_storage = 'cmd:superuser()'¶
+
+
+search_index_entry = {'aliases': 'batchcodes', 'category': 'building', 'key': 'batchcode', 'tags': '', 'text': '\n build from batch-code file\n\n Usage:\n batchcode[/interactive] <python path to file>\n\n Switch:\n interactive - this mode will offer more control when\n executing the batch file, like stepping,\n skipping, reloading etc.\n debug - auto-delete all objects that has been marked as\n deletable in the script file (see example files for\n syntax). This is useful so as to to not leave multiple\n object copies behind when testing out the script.\n\n Runs batches of commands from a batch-code text file (*.py).\n\n '}¶
+
+
@@ -209,7 +219,6 @@ object copies behind when testing out the script.
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 38fbf504e9..91f1b679b1 100644
--- a/docs/0.9.5/api/evennia.commands.default.building.html
+++ b/docs/0.9.5/api/evennia.commands.default.building.html
@@ -86,6 +86,11 @@ the cases, see the module doc.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'command', 'tags': '', 'text': "\n This is a parent class for some of the defining objmanip commands\n since they tend to have some more variables to define new objects.\n\n Each object definition can have several components. First is\n always a name, followed by an optional alias list and finally an\n some optional data, such as a typeclass or a location. A comma ','\n separates different objects. Like this:\n\n name1;alias;alias;alias:option, name2;alias;alias ...\n\n Spaces between all components are stripped.\n\n A second situation is attribute manipulation. Such commands\n are simpler and offer combinations\n\n objname/attr/attr/attr, objname/attr, ...\n\n "}¶
+
+
@@ -147,6 +152,11 @@ by everyone.
lock_storage = 'cmd:perm(setobjalias) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': 'setobjalias', 'category': 'building', 'key': 'alias', 'tags': '', 'text': "\n adding permanent aliases for object\n\n Usage:\n alias <obj> [= [alias[,alias,alias,...]]]\n alias <obj> =\n alias/category <obj> = [alias[,alias,...]:<category>\n\n Switches:\n category - requires ending input with :category, to store the\n given aliases with the given category.\n\n Assigns aliases to an object so it can be referenced by more\n than one name. Assign empty to remove all aliases from object. If\n assigning a category, all aliases given will be using this category.\n\n Observe that this is not the same thing as personal aliases\n created with the 'nick' command! Aliases set with alias are\n changing the object in question, making those aliases usable\n by everyone.\n "}¶
+search_index_entry = {'aliases': '', 'category': 'building', 'key': 'copy', 'tags': '', 'text': "\n copy an object and its properties\n\n Usage:\n copy <original obj> [= <new_name>][;alias;alias..]\n [:<new_location>] [,<new_name2> ...]\n\n Create one or more copies of an object. If you don't supply any targets,\n one exact copy of the original object will be created with the name *_copy.\n "}¶
+
+
@@ -284,6 +299,11 @@ required and get the attribute from the object.
lock_storage = 'cmd:perm(cpattr) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'building', 'key': 'cpattr', 'tags': '', 'text': "\n copy attributes between objects\n\n Usage:\n cpattr[/switch] <obj>/<attr> = <obj1>/<attr1> [,<obj2>/<attr2>,<obj3>/<attr3>,...]\n cpattr[/switch] <obj>/<attr> = <obj1> [,<obj2>,<obj3>,...]\n cpattr[/switch] <attr> = <obj1>/<attr1> [,<obj2>/<attr2>,<obj3>/<attr3>,...]\n cpattr[/switch] <attr> = <obj1>[,<obj2>,<obj3>,...]\n\n Switches:\n move - delete the attribute from the source object after copying.\n\n Example:\n cpattr coolness = Anna/chillout, Anna/nicety, Tom/nicety\n ->\n copies the coolness attribute (defined on yourself), to attributes\n on Anna and Tom.\n\n Copy the attribute one object to one or more attributes on another object.\n If you don't supply a source object, yourself is used.\n "}¶
+
+
@@ -338,6 +358,11 @@ object. If you don’t supply a source object, yourself is used.
lock_storage = 'cmd:perm(mvattr) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'building', 'key': 'mvattr', 'tags': '', 'text': "\n move attributes between objects\n\n Usage:\n mvattr[/switch] <obj>/<attr> = <obj1>/<attr1> [,<obj2>/<attr2>,<obj3>/<attr3>,...]\n mvattr[/switch] <obj>/<attr> = <obj1> [,<obj2>,<obj3>,...]\n mvattr[/switch] <attr> = <obj1>/<attr1> [,<obj2>/<attr2>,<obj3>/<attr3>,...]\n mvattr[/switch] <attr> = <obj1>[,<obj2>,<obj3>,...]\n\n Switches:\n copy - Don't delete the original after moving.\n\n Move an attribute from one object to one or more attributes on another\n object. If you don't supply a source object, yourself is used.\n "}¶
+
+
@@ -406,6 +431,11 @@ object of this type like this:
lock_storage = 'cmd:perm(create) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'building', 'key': 'create', 'tags': '', 'text': "\n create new objects\n\n Usage:\n create[/drop] <objname>[;alias;alias...][:typeclass], <objname>...\n\n switch:\n drop - automatically drop the new object into your current\n location (this is not echoed). This also sets the new\n object's home to the current location rather than to you.\n\n Creates one or more new objects. If typeclass is given, the object\n is created as a child of this typeclass. The typeclass script is\n assumed to be located under types/ and any further\n directory structure is given in Python notation. So if you have a\n correct typeclass 'RedButton' defined in\n types/examples/red_button.py, you could create a new\n object of this type like this:\n\n create/drop button;red : examples.red_button.RedButton\n\n "}¶
+
+
@@ -462,6 +492,11 @@ describe the current room.
lock_storage = 'cmd:perm(desc) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': 'describe', 'category': 'building', 'key': 'desc', 'tags': '', 'text': '\n describe an object or the current room.\n\n Usage:\n desc [<obj> =] <description>\n\n Switches:\n edit - Open up a line editor for more advanced editing.\n\n Sets the "desc" attribute on an object. If an object is not given,\n describe the current room.\n '}¶
+
+
@@ -533,6 +568,11 @@ You can specify the /force switch to bypass this confirmation.
lock_storage = 'cmd:perm(destroy) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': 'del delete', 'category': 'building', 'key': 'destroy', 'tags': '', 'text': '\n permanently delete objects\n\n Usage:\n destroy[/switches] [obj, obj2, obj3, [dbref-dbref], ...]\n\n Switches:\n override - The destroy command will usually avoid accidentally\n destroying account objects. This switch overrides this safety.\n force - destroy without confirmation.\n Examples:\n destroy house, roof, door, 44-78\n destroy 5-10, flower, 45\n destroy/force north\n\n Destroys one or many objects. If dbrefs are used, a range to delete can be\n given, e.g. 4-10. Also the end points will be deleted. This command\n displays a confirmation before destroying, to make sure of your choice.\n You can specify the /force switch to bypass this confirmation.\n '}¶
+
+
@@ -602,6 +642,11 @@ would be ‘north;no;n’.
lock_storage = 'cmd:perm(dig) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'building', 'key': 'dig', 'tags': '', 'text': "\n build new rooms and connect them to the current location\n\n Usage:\n dig[/switches] <roomname>[;alias;alias...][:typeclass]\n [= <exit_to_there>[;alias][:typeclass]]\n [, <exit_to_here>[;alias][:typeclass]]\n\n Switches:\n tel or teleport - move yourself to the new room\n\n Examples:\n dig kitchen = north;n, south;s\n dig house:myrooms.MyHouseTypeclass\n dig sheer cliff;cliff;sheer = climb up, climb down\n\n This command is a convenient way to build rooms quickly; it creates the\n new room and you can optionally set up exits back and forth between your\n current room and the new one. You can add as many aliases as you\n like to the name of the room and the exits in question; an example\n would be 'north;no;n'.\n "}¶
+
+
@@ -672,6 +717,11 @@ For more flexibility and power in creating rooms, use dig.
lock_storage = 'cmd: perm(tunnel) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': 'tun', 'category': 'building', 'key': 'tunnel', 'tags': '', 'text': '\n create new rooms in cardinal directions only\n\n Usage:\n tunnel[/switch] <direction>[:typeclass] [= <roomname>[;alias;alias;...][:typeclass]]\n\n Switches:\n oneway - do not create an exit back to the current location\n tel - teleport to the newly created room\n\n Example:\n tunnel n\n tunnel n = house;mike\'s place;green building\n\n This is a simple way to build using pre-defined directions:\n |wn,ne,e,se,s,sw,w,nw|n (north, northeast etc)\n |wu,d|n (up and down)\n |wi,o|n (in and out)\n The full names (north, in, southwest, etc) will always be put as\n main name for the exit, using the abbreviation as an alias (so an\n exit will always be able to be used with both "north" as well as\n "n" for example). Opposite directions will automatically be\n created back from the new room unless the /oneway switch is given.\n For more flexibility and power in creating rooms, use dig.\n '}¶
+
+
@@ -727,6 +777,11 @@ currently set destination.
lock_storage = 'cmd:perm(link) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'building', 'key': 'link', 'tags': '', 'text': '\n link existing rooms together with exits\n\n Usage:\n link[/switches] <object> = <target>\n link[/switches] <object> =\n link[/switches] <object>\n\n Switch:\n twoway - connect two exits. For this to work, BOTH <object>\n and <target> must be exit objects.\n\n If <object> is an exit, set its destination to <target>. Two-way operation\n instead sets the destination to the *locations* of the respective given\n arguments.\n The second form (a lone =) sets the destination to None (same as\n the unlink command) and the third form (without =) just shows the\n currently set destination.\n '}¶
+
+
@@ -777,6 +832,11 @@ and call func in CmdLink
lock_storage = 'cmd:perm(unlink) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'building', 'key': 'unlink', 'tags': '', 'text': '\n remove exit-connections between rooms\n\n Usage:\n unlink <Object>\n\n Unlinks an object, for example an exit, disconnecting\n it from whatever it was connected to.\n '}¶
+
+
@@ -825,6 +885,11 @@ It is also a convenient target of the “home” command.
lock_storage = 'cmd:perm(sethome) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'building', 'key': 'sethome', 'tags': '', 'text': '\n set an object\'s home location\n\n Usage:\n sethome <obj> [= <home_location>]\n sethom <obj>\n\n The "home" location is a "safety" location for objects; they\n will be moved there if their current location ceases to exist. All\n objects should always have a home location for this reason.\n It is also a convenient target of the "home" command.\n\n If no location is given, just view the object\'s home location.\n '}¶
+
+
@@ -869,6 +934,11 @@ to a user. Defaults to yourself.
lock_storage = 'cmd:perm(listcmdsets) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': 'listcmsets', 'category': 'building', 'key': 'cmdsets', 'tags': '', 'text': '\n list command sets defined on an object\n\n Usage:\n cmdsets <obj>\n\n This displays all cmdsets assigned\n to a user. Defaults to yourself.\n '}¶
+
+
@@ -913,6 +983,11 @@ rename an account.
lock_storage = 'cmd:perm(rename) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': 'rename', 'category': 'building', 'key': 'name', 'tags': '', 'text': '\n change the name and/or aliases of an object\n\n Usage:\n name <obj> = <newname>;alias1;alias2\n\n Rename an object to something new. Use *obj to\n rename an account.\n\n '}¶
+
+
@@ -974,6 +1049,11 @@ as well as the self.create_exit() method.
lock_storage = 'cmd:perm(open) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'building', 'key': 'open', 'tags': '', 'text': '\n open a new exit from the current room\n\n Usage:\n open <new exit>[;alias;alias..][:typeclass] [,<return exit>[;alias;..][:typeclass]]] = <destination>\n\n Handles the creation of exits. If a destination is given, the exit\n will point there. The <return exit> argument sets up an exit at the\n destination leading back to the current room. Destination name\n can be given both as a #dbref and a name, if that name is globally\n unique.\n\n '}¶
+
+
@@ -1129,6 +1209,11 @@ with older attrs that might have been named with []’s.
lock_storage = 'cmd:perm(set) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'building', 'key': 'set', 'tags': '', 'text': '\n set attribute on an object or account\n\n Usage:\n set <obj>/<attr> = <value>\n set <obj>/<attr> =\n set <obj>/<attr>\n set *<account>/<attr> = <value>\n\n Switch:\n edit: Open the line editor (string values only)\n script: If we\'re trying to set an attribute on a script\n channel: If we\'re trying to set an attribute on a channel\n account: If we\'re trying to set an attribute on an account\n room: Setting an attribute on a room (global search)\n exit: Setting an attribute on an exit (global search)\n char: Setting an attribute on a character (global search)\n character: Alias for char, as above.\n\n Sets attributes on objects. The second example form above clears a\n previously set attribute while the third form inspects the current value of\n the attribute (if any). The last one (with the star) is a shortcut for\n operating on a player Account rather than an Object.\n\n The most common data to save with this command are strings and\n numbers. You can however also set Python primitives such as lists,\n dictionaries and tuples on objects (this might be important for\n the functionality of certain custom objects). This is indicated\n by you starting your value with one of |c\'|n, |c"|n, |c(|n, |c[|n\n or |c{ |n.\n\n Once you have stored a Python primitive as noted above, you can include\n |c[<key>]|n in <attr> to reference nested values in e.g. a list or dict.\n\n Remember that if you use Python primitives like this, you must\n write proper Python syntax too - notably you must include quotes\n around your strings or you will get an error.\n\n '}¶
@@ -1212,6 +1297,11 @@ server settings.
lock_storage = 'cmd:perm(typeclass) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': 'parent type update swap', 'category': 'building', 'key': 'typeclass', 'tags': '', 'text': "\n set or change an object's typeclass\n\n Usage:\n typeclass[/switch] <object> [= typeclass.path]\n typeclass/prototype <object> = prototype_key\n\n typeclass/list/show [typeclass.path]\n swap - this is a shorthand for using /force/reset flags.\n update - this is a shorthand for using the /force/reload flag.\n\n Switch:\n show, examine - display the current typeclass of object (default) or, if\n given a typeclass path, show the docstring of that typeclass.\n update - *only* re-run at_object_creation on this object\n meaning locks or other properties set later may remain.\n reset - clean out *all* the attributes and properties on the\n object - basically making this a new clean object.\n force - change to the typeclass also if the object\n already has a typeclass of the same name.\n list - show available typeclasses. Only typeclasses in modules actually\n imported or used from somewhere in the code will show up here\n (those typeclasses are still available if you know the path)\n prototype - clean and overwrite the object with the specified\n prototype key - effectively making a whole new object.\n\n Example:\n type button = examples.red_button.RedButton\n type/prototype button=a red button\n\n If the typeclass_path is not given, the current object's typeclass is\n assumed.\n\n View or set an object's typeclass. If setting, the creation hooks of the\n new typeclass will be run on the object. If you have clashing properties on\n the old class, use /reset. By default you are protected from changing to a\n typeclass of the same name as the one you already have - use /force to\n override this protection.\n\n The given typeclass must be identified by its location using python\n dot-notation pointing to the correct module and class. If no typeclass is\n given (or a wrong typeclass is given). Errors in the path or new typeclass\n will lead to the old typeclass being kept. The location of the typeclass\n module is searched from the default typeclass directory, as defined in the\n server settings.\n\n "}¶
+
+
@@ -1259,6 +1349,11 @@ matching the given attribute-wildcard search string.
lock_storage = 'cmd:perm(wipe) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'building', 'key': 'wipe', 'tags': '', 'text': "\n clear all attributes from an object\n\n Usage:\n wipe <object>[/<attr>[/<attr>...]]\n\n Example:\n wipe box\n wipe box/colour\n\n Wipes all of an object's attributes, or optionally only those\n matching the given attribute-wildcard search string.\n "}¶
+
+
@@ -1326,6 +1421,11 @@ them by ‘;’, i.e:
lock_storage = 'cmd: perm(locks) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': 'locks', 'category': 'building', 'key': 'lock', 'tags': '', 'text': "\n assign a lock definition to an object\n\n Usage:\n lock <object or *account>[ = <lockstring>]\n or\n lock[/switch] <object or *account>/<access_type>\n\n Switch:\n del - delete given access type\n view - view lock associated with given access type (default)\n\n If no lockstring is given, shows all locks on\n object.\n\n Lockstring is of the form\n access_type:[NOT] func1(args)[ AND|OR][ NOT] func2(args) ...]\n Where func1, func2 ... valid lockfuncs with or without arguments.\n Separator expressions need not be capitalized.\n\n For example:\n 'get: id(25) or perm(Admin)'\n The 'get' lock access_type is checked e.g. by the 'get' command.\n An object locked with this example lock will only be possible to pick up\n by Admins or by an object with id=25.\n\n You can add several access_types after one another by separating\n them by ';', i.e:\n 'get:id(25); delete:perm(Builder)'\n "}¶
+
+
@@ -1447,6 +1547,11 @@ non-persistent data stored on object
lock_storage = 'cmd:perm(examine) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': 'exam ex', 'category': 'building', 'key': 'examine', 'tags': '', 'text': '\n get detailed information about an object\n\n Usage:\n examine [<object>[/attrname]]\n examine [*<account>[/attrname]]\n\n Switch:\n account - examine an Account (same as adding *)\n object - examine an Object (useful when OOC)\n\n The examine command shows detailed game info about an\n object and optionally a specific attribute on it.\n If object is not specified, the current location is examined.\n\n Append a * before the search string to examine an account.\n\n '}¶
@@ -1507,6 +1612,11 @@ one is given.
lock_storage = 'cmd:perm(find) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': 'locate search', 'category': 'building', 'key': 'find', 'tags': '', 'text': '\n search the database for objects\n\n Usage:\n find[/switches] <name or dbref or *account> [= dbrefmin[-dbrefmax]]\n locate - this is a shorthand for using the /loc switch.\n\n Switches:\n room - only look for rooms (location=None)\n exit - only look for exits (destination!=None)\n char - only look for characters (BASE_CHARACTER_TYPECLASS)\n exact - only exact matches are returned.\n loc - display object location if exists and match has one result\n startswith - search for names starting with the string, rather than containing\n\n Searches the database for an object of a particular name or exact #dbref.\n Use *accountname to search for an account. The switches allows for\n limiting object matches to certain game entities. Dbrefmin and dbrefmax\n limits matches to within the given dbrefs range, or above/below if only\n one is given.\n '}¶
+
+
@@ -1580,6 +1690,11 @@ teleported to the target location.
lock_storage = 'cmd:perm(teleport) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': 'teleport', 'category': 'building', 'key': 'tel', 'tags': '', 'text': "\n teleport object to another location\n\n Usage:\n tel/switch [<object> to||=] <target location>\n\n Examples:\n tel Limbo\n tel/quiet box = Limbo\n tel/tonone box\n\n Switches:\n quiet - don't echo leave/arrive messages to the source/target\n locations for the move.\n intoexit - if target is an exit, teleport INTO\n the exit object instead of to its destination\n tonone - if set, teleport the object to a None-location. If this\n switch is set, <target location> is ignored.\n Note that the only way to retrieve\n an object from a None location is by direct #dbref\n reference. A puppeted object cannot be moved to None.\n loc - teleport object to the target's location instead of its contents\n\n Teleports an object somewhere. If no object is given, you yourself are\n teleported to the target location.\n "}¶
+
+
@@ -1588,7 +1703,7 @@ teleported to the target location.
@@ -1637,6 +1752,11 @@ the object.
lock_storage = 'cmd:perm(script) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': 'attachscript', 'category': 'building', 'key': 'addscript', 'tags': '', 'text': '\n attach a script to an object\n\n Usage:\n addscript[/switch] <obj> [= script_path or <scriptkey>]\n\n Switches:\n start - start all non-running scripts on object, or a given script only\n stop - stop all scripts on objects, or a given script only\n\n If no script path/key is given, lists all scripts active on the given\n object.\n Script path can be given from the base location for scripts as given in\n settings. If adding a new script, it will be started automatically\n (no /start switch is needed). Using the /start or /stop switches on an\n object without specifying a script key/path will start/stop ALL scripts on\n the object.\n '}¶
+
+
@@ -1703,6 +1823,11 @@ enough to for most grouping schemes.
lock_storage = 'cmd:perm(tag) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': 'tags', 'category': 'building', 'key': 'tag', 'tags': '', 'text': '\n handles the tags of an object\n\n Usage:\n tag[/del] <obj> [= <tag>[:<category>]]\n tag/search <tag>[:<category]\n\n Switches:\n search - return all objects with a given Tag\n del - remove the given tag. If no tag is specified,\n clear all tags on object.\n\n Manipulates and lists tags on objects. Tags allow for quick\n grouping of and searching for objects. If only <obj> is given,\n list all tags on the object. If /search is used, list objects\n with the given tag.\n The category can be used for grouping tags themselves, but it\n should be used with restrain - tags on their own are usually\n enough to for most grouping schemes.\n '}¶
+
+
@@ -1809,6 +1934,11 @@ displays a list of available prototypes.
lock_storage = 'cmd:perm(spawn) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': 'olc', 'category': 'building', 'key': 'spawn', 'tags': '', 'text': '\n spawn objects from prototype\n\n Usage:\n spawn[/noloc] <prototype_key>\n spawn[/noloc] <prototype_dict>\n\n spawn/search [prototype_keykey][;tag[,tag]]\n spawn/list [tag, tag, ...]\n spawn/list modules - list only module-based prototypes\n spawn/show [<prototype_key>]\n spawn/update <prototype_key>\n\n spawn/save <prototype_dict>\n spawn/edit [<prototype_key>]\n olc - equivalent to spawn/edit\n\n Switches:\n noloc - allow location to be None if not specified explicitly. Otherwise,\n location will default to caller\'s current location.\n search - search prototype by name or tags.\n list - list available prototypes, optionally limit by tags.\n show, examine - inspect prototype by key. If not given, acts like list.\n raw - show the raw dict of the prototype as a one-line string for manual editing.\n save - save a prototype to the database. It will be listable by /list.\n delete - remove a prototype from database, if allowed to.\n update - find existing objects with the same prototype_key and update\n them with latest version of given prototype. If given with /save,\n will auto-update all objects with the old version of the prototype\n without asking first.\n edit, menu, olc - create/manipulate prototype in a menu interface.\n\n Example:\n spawn GOBLIN\n spawn {"key":"goblin", "typeclass":"monster.Monster", "location":"#2"}\n spawn/save {"key": "grunt", prototype: "goblin"};;mobs;edit:all()\n \x0c\n Dictionary keys:\n |wprototype_parent |n - name of parent prototype to use. Required if typeclass is\n not set. Can be a path or a list for multiple inheritance (inherits\n left to right). If set one of the parents must have a typeclass.\n |wtypeclass |n - string. Required if prototype_parent is not set.\n |wkey |n - string, the main object identifier\n |wlocation |n - this should be a valid object or #dbref\n |whome |n - valid object or #dbref\n |wdestination|n - only valid for exits (object or dbref)\n |wpermissions|n - string or list of permission strings\n |wlocks |n - a lock-string\n |waliases |n - string or list of strings.\n |wndb_|n<name> - value of a nattribute (ndb_ is stripped)\n\n |wprototype_key|n - name of this prototype. Unique. Used to store/retrieve from db\n and update existing prototyped objects if desired.\n |wprototype_desc|n - desc of this prototype. Used in listings\n |wprototype_locks|n - locks of this prototype. Limits who may use prototype\n |wprototype_tags|n - tags of this prototype. Used to find prototype\n\n any other keywords are interpreted as Attributes and their values.\n\n The available prototypes are defined globally in modules set in\n settings.PROTOTYPE_MODULES. If spawn is used without arguments it\n displays a list of available prototypes.\n\n '}¶
+
+
@@ -1856,7 +1986,6 @@ displays a list of available prototypes.
diff --git a/docs/0.9.5/api/evennia.commands.default.cmdset_unloggedin.html b/docs/0.9.5/api/evennia.commands.default.cmdset_unloggedin.html
index ac9d3268de..f3f460c80d 100644
--- a/docs/0.9.5/api/evennia.commands.default.cmdset_unloggedin.html
+++ b/docs/0.9.5/api/evennia.commands.default.cmdset_unloggedin.html
@@ -115,7 +115,6 @@ of the state instance in this module.
Comm commands are OOC commands and intended to be made available to
-the Account at all times (they go into the AccountCmdSet). So we
-make sure to homogenize self.caller to always be the account object
-for easy handling.
Note that this will not work if the alias has a space in it. So the
+‘warrior guild’ alias must be used with the channel command:
+
+
channel warrior guild = Hello
+
+
Channel-aliases can be removed one at a time, using the ‘/unalias’ switch.
+
Usage: channel/who channelname
+
List the channel’s subscribers. Shows who are currently offline or are
+muting the channel. Subscribers who are ‘muting’ will not see messages sent
+to the channel (use channel/mute to mute a channel).
+
Usage: channel/history channel [= index]
+
This will display the last |c20|n lines of channel history. By supplying an
+index number, you will step that many lines back before viewing those 20 lines.
+
For example:
+
+
channel/history public = 35
+
+
will go back 35 lines and show the previous 20 lines from that point (so
+lines -35 to -55).
+
+
Usage: channel/sub channel [=alias[;alias;…]]
channel/unsub channel
+
+
+
This subscribes you to a channel and optionally assigns personal shortcuts
+for you to use to send to that channel (see aliases). When you unsub, all
+your personal aliases will also be removed.
+
+
Usage: channel/mute channelname
channel/unmute channelname
+
+
+
Muting silences all output from the channel without actually
+un-subscribing. Other channel members will see that you are muted in the /who
+list. Sending a message to the channel will automatically unmute you.
Creates a new channel (or destroys one you control). You will automatically
+join the channel you create and everyone will be kicked and loose all aliases
+to a destroyed channel.
+
+
Usage: channel/lock channelname = lockstring
channel/unlock channelname = lockstring
+
+
+
Note: this is an admin command.
+
A lockstring is on the form locktype:lockfunc(). Channels understand three
+locktypes:
+
+
listen - who may listen or join the channel.
+send - who may send messages to the channel
+control - who controls the channel. This is usually the one creating
+
+
the channel.
+
+
+
Common lockfuncs are all() and perm(). To make a channel everyone can
+listen to but only builders can talk on, use this:
Booting will kick a named subscriber from channel(s) temporarily. The
+‘reason’ will be passed to the booted user. Unless the /quiet switch is
+used, the channel will also be informed of the action. A booted user is
+still able to re-connect, but they’ll have to set up their aliases again.
+
Banning will blacklist a user from (re)joining the provided channels. It
+will then proceed to boot them from those channels if they were connected.
+The ‘reason’ and /quiet works the same as for booting.
+
Example
+
boot mychannel1 = EvilUser : Kicking you to cool down a bit.
+ban mychannel1,mychannel2= EvilUser : Was banned for spamming.
Helper function for searching for a single channel with some error
+handling.
+
+
Parameters
+
+
channelname (str) – Name, alias #dbref or partial name/alias to search
+for.
+
exact (bool, optional) – If an exact or fuzzy-match of the name should be done.
+Note that even for a fuzzy match, an exactly given, unique channel name
+will always be returned.
+
handle_errors (bool) – If true, use self.msg to report errors if
+there are non/multiple matches. If so, the return will always be
+a single match or None.
+
+
+
Returns
+
object, list or None –
+
+
If handle_errors is True, this is either a found Channel
or None. Otherwise it’s a list of zero, one or more channels found.
+
+
+
+
+
+
Notes
+
The ‘listen’ and ‘control’ accesses are checked before returning.
Add a new alias (nick) for the user to use with this channel.
+
+
Parameters
+
+
channel (Channel) – The channel to alias.
+
alias (str) – The personal alias to use for this channel.
+
**kwargs – If given, passed into nicks.add.
+
+
+
+
+
Note
+
We add two nicks - one is a plain alias -> channel.key that
+we need to be able to reference this channel easily. The other
+is a templated nick to easily be able to send messages to the
+channel without needing to give the full channel command. The
+structure of this nick is given by self.channel_msg_pattern
+and self.channel_msg_nick_replacement. By default it maps
+alias <msg> -> channel <channelname> = <msg>, so that you can
+for example just write pub Hello to send a message.
+
The alias created is alias $1 -> channel channel = $1, to allow
+for sending to channel using the main channel command.
Create a new channel. Its name must not previously exist
+(users can alias as needed). Will also connect to the
+new channel.
+
+
Parameters
+
+
name (str) – The new channel name/key.
+
description (str) – This is used in listings.
+
aliases (list) – A list of strings - alternative aliases for the channel
+(not to be confused with per-user aliases; these are available for
+everyone).
Destroy an existing channel. Access should be checked before
+calling this function.
+
+
Parameters
+
+
channel (Channel) – The channel to alias.
+
message (str, optional) – Final message to send onto the channel
+before destroying it. If not given, a default message is
+used. Set to the empty string for no message.
Un-Ban a user from a channel. This will not reconnect them
+to the channel, just allow them to connect again (assuming
+they have the suitable ‘listen’ lock like everyone else).
Show a list of online people is subscribing to a channel. This will check
+the ‘control’ permission of caller to determine if only online users
+should be returned or everyone.
+
+
Parameters
+
channel (Channel) – The channel to operate on.
+
+
Returns
+
list –
+
+
A list of prepared strings, with name + markers for if they are
+search_index_entry = {'aliases': 'channels chan', 'category': 'comms', 'key': 'channel', 'tags': '', 'text': "\n Use and manage in-game channels.\n\n Usage:\n channel channelname <msg>\n channel channel name = <msg>\n channel (show all subscription)\n channel/all (show available channels)\n channel/alias channelname = alias[;alias...]\n channel/unalias alias\n channel/who channelname\n channel/history channelname [= index]\n channel/sub channelname [= alias[;alias...]]\n channel/unsub channelname[,channelname, ...]\n channel/mute channelname[,channelname,...]\n channel/unmute channelname[,channelname,...]\n\n channel/create channelname[;alias;alias[:typeclass]] [= description]\n channel/destroy channelname [= reason]\n channel/desc channelname = description\n channel/lock channelname = lockstring\n channel/unlock channelname = lockstring\n channel/ban channelname (list bans)\n channel/ban[/quiet] channelname[, channelname, ...] = subscribername [: reason]\n channel/unban[/quiet] channelname[, channelname, ...] = subscribername\n channel/boot[/quiet] channelname[,channelname,...] = subscribername [: reason]\n\n # subtopics\n\n ## sending\n\n Usage: channel channelname msg\n channel channel name = msg (with space in channel name)\n\n This sends a message to the channel. Note that you will rarely use this\n command like this; instead you can use the alias\n\n channelname <msg>\n channelalias <msg>\n\n For example\n\n public Hello World\n pub Hello World\n\n (this shortcut doesn't work for aliases containing spaces)\n\n See channel/alias for help on setting channel aliases.\n\n ## alias and unalias\n\n Usage: channel/alias channel = alias[;alias[;alias...]]\n channel/unalias alias\n channel - this will list your subs and aliases to each channel\n\n Set one or more personal aliases for referencing a channel. For example:\n\n channel/alias warrior's guild = warrior;wguild;warchannel;warrior guild\n\n You can now send to the channel using all of these:\n\n warrior's guild Hello\n warrior Hello\n wguild Hello\n warchannel Hello\n\n Note that this will not work if the alias has a space in it. So the\n 'warrior guild' alias must be used with the `channel` command:\n\n channel warrior guild = Hello\n\n Channel-aliases can be removed one at a time, using the '/unalias' switch.\n\n ## who\n\n Usage: channel/who channelname\n\n List the channel's subscribers. Shows who are currently offline or are\n muting the channel. Subscribers who are 'muting' will not see messages sent\n to the channel (use channel/mute to mute a channel).\n\n ## history\n\n Usage: channel/history channel [= index]\n\n This will display the last |c20|n lines of channel history. By supplying an\n index number, you will step that many lines back before viewing those 20 lines.\n\n For example:\n\n channel/history public = 35\n\n will go back 35 lines and show the previous 20 lines from that point (so\n lines -35 to -55).\n\n ## sub and unsub\n\n Usage: channel/sub channel [=alias[;alias;...]]\n channel/unsub channel\n\n This subscribes you to a channel and optionally assigns personal shortcuts\n for you to use to send to that channel (see aliases). When you unsub, all\n your personal aliases will also be removed.\n\n ## mute and unmute\n\n Usage: channel/mute channelname\n channel/unmute channelname\n\n Muting silences all output from the channel without actually\n un-subscribing. Other channel members will see that you are muted in the /who\n list. Sending a message to the channel will automatically unmute you.\n\n ## create and destroy\n\n Usage: channel/create channelname[;alias;alias[:typeclass]] [= description]\n channel/destroy channelname [= reason]\n\n Creates a new channel (or destroys one you control). You will automatically\n join the channel you create and everyone will be kicked and loose all aliases\n to a destroyed channel.\n\n ## lock and unlock\n\n Usage: channel/lock channelname = lockstring\n channel/unlock channelname = lockstring\n\n Note: this is an admin command.\n\n A lockstring is on the form locktype:lockfunc(). Channels understand three\n locktypes:\n listen - who may listen or join the channel.\n send - who may send messages to the channel\n control - who controls the channel. This is usually the one creating\n the channel.\n\n Common lockfuncs are all() and perm(). To make a channel everyone can\n listen to but only builders can talk on, use this:\n\n listen:all()\n send: perm(Builders)\n\n ## boot and ban\n\n Usage:\n channel/boot[/quiet] channelname[,channelname,...] = subscribername [: reason]\n channel/ban channelname[, channelname, ...] = subscribername [: reason]\n channel/unban channelname[, channelname, ...] = subscribername\n channel/unban channelname\n channel/ban channelname (list bans)\n\n Booting will kick a named subscriber from channel(s) temporarily. The\n 'reason' will be passed to the booted user. Unless the /quiet switch is\n used, the channel will also be informed of the action. A booted user is\n still able to re-connect, but they'll have to set up their aliases again.\n\n Banning will blacklist a user from (re)joining the provided channels. It\n will then proceed to boot them from those channels if they were connected.\n The 'reason' and `/quiet` works the same as for booting.\n\n Example:\n boot mychannel1 = EvilUser : Kicking you to cool down a bit.\n ban mychannel1,mychannel2= EvilUser : Was banned for spamming.\n\n "}¶
+
+
+
+
class evennia.commands.default.comms.CmdAddCom(**kwargs)[source]¶
@@ -93,12 +773,17 @@ aliases to an already joined channel.
lock_storage = 'cmd:not pperm(channel_banned)'¶
+
+
+search_index_entry = {'aliases': 'aliaschan chanalias', 'category': 'comms', 'key': 'addcom', 'tags': '', 'text': '\n Add a channel alias and/or subscribe to a channel\n\n Usage:\n addcom [alias=] <channel>\n\n Joins a given channel. If alias is given, this will allow you to\n refer to the channel by this alias rather than the full channel\n name. Subsequent calls of this command can be used to add multiple\n aliases to an already joined channel.\n '}¶
+
+
class evennia.commands.default.comms.CmdDelCom(**kwargs)[source]¶
@@ -145,12 +830,17 @@ for that channel.
lock_storage = 'cmd:not perm(channel_banned)'¶
+
+
+search_index_entry = {'aliases': 'delaliaschan delchanalias', 'category': 'comms', 'key': 'delcom', 'tags': '', 'text': "\n remove a channel alias and/or unsubscribe from channel\n\n Usage:\n delcom <alias or channel>\n delcom/all <channel>\n\n If the full channel name is given, unsubscribe from the\n channel. If an alias is given, remove the alias but don't\n unsubscribe. If the 'all' switch is used, remove all aliases\n for that channel.\n "}¶
+
+
class evennia.commands.default.comms.CmdAllCom(**kwargs)[source]¶
Lists all channels available to you, whether you listen to them or not.
-Use ‘comlist’ to only view your current channel subscriptions.
-Use addcom/delcom to join and leave channels
+search_index_entry = {'aliases': '', 'category': 'comms', 'key': 'allcom', 'tags': '', 'text': "\n perform admin operations on all channels\n\n Usage:\n allcom [on | off | who | destroy]\n\n Allows the user to universally turn off or on all channels they are on, as\n well as perform a 'who' for all channels they are on. Destroy deletes all\n channels that you control.\n\n Without argument, works like comlist.\n "}¶
@@ -253,7 +896,7 @@ Use addcom/delcom to join and leave channels
class evennia.commands.default.comms.CmdCdestroy(**kwargs)[source]¶
sendername - attach the sender’s name before the message
-quiet - don’t echo the message back to sender
-
-
-
Allows the user to broadcast a message over a channel as long as
-they control it. It does not show the user’s name unless they
-provide the /sendername switch.
-lock_storage = 'cmd: not pperm(channel_banned) and pperm(Player)'¶
+
+search_index_entry = {'aliases': '', 'category': 'comms', 'key': 'cboot', 'tags': '', 'text': "\n kick an account from a channel you control\n\n Usage:\n cboot[/quiet] <channel> = <account> [:reason]\n\n Switch:\n quiet - don't notify the channel\n\n Kicks an account or object from a channel you control.\n\n "}¶
@@ -414,7 +1009,7 @@ provide the /sendername switch.
class evennia.commands.default.comms.CmdCWho(**kwargs)[source]¶
+search_index_entry = {'aliases': '', 'category': 'comms', 'key': 'cwho', 'tags': '', 'text': '\n show who is listening to a channel\n\n Usage:\n cwho <channel>\n\n List who is connected to a given channel you have access to.\n '}¶
@@ -462,7 +1062,7 @@ provide the /sendername switch.
class evennia.commands.default.comms.CmdChannelCreate(**kwargs)[source]¶
+lock_storage = 'cmd:not pperm(channel_banned) and perm(Admin)'¶
+
+
+
+
+search_index_entry = {'aliases': '', 'category': 'comms', 'key': 'clock', 'tags': '', 'text': '\n change channel locks of a channel you control\n\n Usage:\n clock <channel> [= <lockstring>]\n\n Changes the lock access restrictions of a channel. If no\n lockstring was given, view the current lock definitions.\n '}¶
@@ -559,7 +1169,7 @@ lockstring was given, view the current lock definitions.
class evennia.commands.default.comms.CmdCdesc(**kwargs)[source]¶
last - shows who you last messaged
list - show your last <number> of tells/pages (default)
-
Send a message to target user (if online). If no
-argument is given, you will get a list of your latest messages.
+
Send a message to target user (if online). If no argument is given, you
+will get a list of your latest messages. The equal sign is needed for
+multiple targets or if sending to target with space in the name.
@@ -662,6 +1279,11 @@ argument is given, you will get a list of your latest messages.
lock_storage = 'cmd:not pperm(page_banned)'¶
+
+
+search_index_entry = {'aliases': 'tell', 'category': 'comms', 'key': 'page', 'tags': '', 'text': "\n send a private message to another account\n\n Usage:\n page <account> <message>\n page[/switches] [<account>,<account>,... = <message>]\n tell ''\n page <number>\n\n Switches:\n last - shows who you last messaged\n list - show your last <number> of tells/pages (default)\n\n Send a message to target user (if online). If no argument is given, you\n will get a list of your latest messages. The equal sign is needed for\n multiple targets or if sending to target with space in the name.\n\n "}¶
+
+
@@ -701,7 +1323,7 @@ The bot will relay everything said in the evennia channel to the
IRC channel and vice versa. The bot will automatically connect at
server start, so this command need only be given once. The
/disconnect switch will permanently delete the bot. To only
-temporarily deactivate it, use the |wservices|n command instead.
+temporarily deactivate it, use the |wservices|n command instead.
Provide an optional bot class path to use a custom bot.
@@ -739,6 +1361,11 @@ Provide an optional bot class path to use a custom bot.
lock_storage = 'cmd:serversetting(IRC_ENABLED) and pperm(Developer)'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'comms', 'key': 'irc2chan', 'tags': '', 'text': '\n Link an evennia channel to an external IRC channel\n\n Usage:\n irc2chan[/switches] <evennia_channel> = <ircnetwork> <port> <#irchannel> <botname>[:typeclass]\n irc2chan/delete botname|#dbid\n\n Switches:\n /delete - this will delete the bot and remove the irc connection\n to the channel. Requires the botname or #dbid as input.\n /remove - alias to /delete\n /disconnect - alias to /delete\n /list - show all irc<->evennia mappings\n /ssl - use an SSL-encrypted connection\n\n Example:\n irc2chan myircchan = irc.dalnet.net 6667 #mychannel evennia-bot\n irc2chan public = irc.freenode.net 6667 #evgaming #evbot:accounts.mybot.MyBot\n\n This creates an IRC bot that connects to a given IRC network and\n channel. If a custom typeclass path is given, this will be used\n instead of the default bot class.\n The bot will relay everything said in the evennia channel to the\n IRC channel and vice versa. The bot will automatically connect at\n server start, so this command need only be given once. The\n /disconnect switch will permanently delete the bot. To only\n temporarily deactivate it, use the |wservices|n command instead.\n Provide an optional bot class path to use a custom bot.\n '}¶
+
+
@@ -747,7 +1374,7 @@ Provide an optional bot class path to use a custom bot.
If not given arguments, will return a list of all bots (like
@@ -790,6 +1417,11 @@ messages sent to either channel will be lost.
lock_storage = 'cmd:serversetting(IRC_ENABLED) and perm(ircstatus) or perm(Builder))'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'comms', 'key': 'ircstatus', 'tags': '', 'text': "\n Check and reboot IRC bot.\n\n Usage:\n ircstatus [#dbref ping | nicklist | reconnect]\n\n If not given arguments, will return a list of all bots (like\n irc2chan/list). The 'ping' argument will ping the IRC network to\n see if the connection is still responsive. The 'nicklist' argument\n (aliases are 'who' and 'users') will return a list of users on the\n remote IRC channel. Finally, 'reconnect' will force the client to\n disconnect and reconnect again. This may be a last resort if the\n client has silently lost connection (this may happen if the remote\n network experience network issues). During the reconnection\n messages sent to either channel will be lost.\n\n "}¶
+
+
@@ -862,6 +1494,11 @@ to identify the connection uniquely.
lock_storage = 'cmd:serversetting(RSS_ENABLED) and pperm(Developer)'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'comms', 'key': 'rss2chan', 'tags': '', 'text': '\n link an evennia channel to an external RSS feed\n\n Usage:\n rss2chan[/switches] <evennia_channel> = <rss_url>\n\n Switches:\n /disconnect - this will stop the feed and remove the connection to the\n channel.\n /remove - "\n /list - show all rss->evennia mappings\n\n Example:\n rss2chan rsschan = http://code.google.com/feeds/p/evennia/updates/basic\n\n This creates an RSS reader that connects to a given RSS feed url. Updates\n will be echoed as a title and news link to the given channel. The rate of\n updating is set with the RSS_UPDATE_INTERVAL variable in settings (default\n is every 10 minutes).\n\n When disconnecting you need to supply both the channel and url again so as\n to identify the connection uniquely.\n '}¶
+
+
@@ -935,6 +1572,11 @@ must be added to game settings.
lock_storage = 'cmd:serversetting(GRAPEVINE_ENABLED) and pperm(Developer)'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'comms', 'key': 'grapevine2chan', 'tags': '', 'text': '\n Link an Evennia channel to an exteral Grapevine channel\n\n Usage:\n grapevine2chan[/switches] <evennia_channel> = <grapevine_channel>\n grapevine2chan/disconnect <connection #id>\n\n Switches:\n /list - (or no switch): show existing grapevine <-> Evennia\n mappings and available grapevine chans\n /remove - alias to disconnect\n /delete - alias to disconnect\n\n Example:\n grapevine2chan mygrapevine = gossip\n\n This creates a link between an in-game Evennia channel and an external\n Grapevine channel. The game must be registered with the Grapevine network\n (register at https://grapevine.haus) and the GRAPEVINE_* auth information\n must be added to game settings.\n '}¶
+
+
@@ -982,7 +1624,6 @@ must be added to game settings.
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'home', 'tags': '', 'text': "\n move to your character's home location\n\n Usage:\n home\n\n Teleports you to your home location.\n "}¶
+search_index_entry = {'aliases': 'ls l', 'category': 'general', 'key': 'look', 'tags': '', 'text': '\n look at location or object\n\n Usage:\n look\n look <obj>\n look *<account>\n\n Observes your location or objects in your vicinity.\n '}¶
+
+
@@ -225,6 +235,11 @@ for everyone to use, you need build privileges and the alias command.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': 'nicks nickname', 'category': 'general', 'key': 'nick', 'tags': '', 'text': '\n define a personal alias/nick by defining a string to\n match and replace it with another on the fly\n\n Usage:\n nick[/switches] <string> [= [replacement_string]]\n nick[/switches] <template> = <replacement_template>\n nick/delete <string> or number\n nicks\n\n Switches:\n inputline - replace on the inputline (default)\n object - replace on object-lookup\n account - replace on account-lookup\n list - show all defined aliases (also "nicks" works)\n delete - remove nick by index in /list\n clearall - clear all nicks\n\n Examples:\n nick hi = say Hello, I\'m Sarah!\n nick/object tom = the tall man\n nick build $1 $2 = create/drop $1;$2\n nick tell $1 $2=page $1=$2\n nick tm?$1=page tallman=$1\n nick tm\\=$1=page tallman=$1\n\n A \'nick\' is a personal string replacement. Use $1, $2, ... to catch arguments.\n Put the last $-marker without an ending space to catch all remaining text. You\n can also use unix-glob matching for the left-hand side <string>:\n\n * - matches everything\n ? - matches 0 or 1 single characters\n [abcd] - matches these chars in any order\n [!abcd] - matches everything not among these chars\n \\= - escape literal \'=\' you want in your <string>\n\n Note that no objects are actually renamed or changed by this command - your nicks\n are only available to you. If you want to permanently add keywords to an object\n for everyone to use, you need build privileges and the alias command.\n\n '}¶
@@ -324,6 +344,11 @@ look at you.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'setdesc', 'tags': '', 'text': '\n describe yourself\n\n Usage:\n setdesc <description>\n\n Add a description to yourself. This\n will be visible to people when they\n look at you.\n '}¶
+search_index_entry = {'aliases': 'grab', 'category': 'general', 'key': 'get', 'tags': '', 'text': '\n pick up something\n\n Usage:\n get <obj>\n\n Picks up an object from your location and puts it in\n your inventory.\n '}¶
@@ -422,6 +452,11 @@ location you are currently in.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'drop', 'tags': '', 'text': '\n drop something\n\n Usage:\n drop <obj>\n\n Lets you drop an object from your inventory into the\n location you are currently in.\n '}¶
+
+
@@ -476,6 +511,11 @@ placing it in their inventory.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'give', 'tags': '', 'text': '\n give away something to someone\n\n Usage:\n give <inventory obj> <to||=> <target>\n\n Gives an items from your inventory to another character,\n placing it in their inventory.\n '}¶
+
+
@@ -495,7 +535,7 @@ placing it in their inventory.
@@ -519,6 +559,11 @@ placing it in their inventory.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': '\' "', 'category': 'general', 'key': 'say', 'tags': '', 'text': '\n speak as your character\n\n Usage:\n say <message>\n\n Talk to those in your current location.\n '}¶
+
+
@@ -564,6 +609,11 @@ others in the room being informed.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'whisper', 'tags': '', 'text': '\n Speak privately as your character to another\n\n Usage:\n whisper <character> = <message>\n whisper <char1>, <char2> = <message>\n\n Talk privately to one or more characters in your current location, without\n others in the room being informed.\n '}¶
+search_index_entry = {'aliases': 'emote :', 'category': 'general', 'key': 'pose', 'tags': '', 'text': "\n strike a pose\n\n Usage:\n pose <pose text>\n pose's <pose text>\n\n Example:\n pose is standing by the wall, smiling.\n -> others will see:\n Tom is standing by the wall, smiling.\n\n Describe an action being taken. The pose text will\n automatically begin with your name.\n "}¶
+
+
@@ -674,6 +729,11 @@ which permission groups you are a member of.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': 'hierarchy groups', 'category': 'general', 'key': 'access', 'tags': '', 'text': '\n show your current game access\n\n Usage:\n access\n\n This command shows you the permission hierarchy and\n which permission groups you are a member of.\n '}¶
+
+
@@ -721,7 +781,6 @@ which permission groups you are a member of.
The help command. The basic idea is that help texts for commands
-are best written by those that write the commands - the admins. So
-command-help is all auto-loaded and searched from the current command
-set. The normal, database-tied help system is used for collaborative
-creation of other help topics such as RP help or game-world aides.
+
The help command. The basic idea is that help texts for commands are best
+written by those that write the commands - the developers. So command-help is
+all auto-loaded and searched from the current command set. The normal,
+database-tied help system is used for collaborative creation of other help
+topics such as RP help or game-world aides. Help entries can also be created
+outside the game in modules given by **settings.FILE_HELP_ENTRY_MODULES**.
class evennia.commands.default.help.CmdHelp(**kwargs)[source]¶
Output a category-ordered list. The input are the
-pre-loaded help files for commands and database-helpfiles
-respectively. You can override this method to return a
-custom display of the list of commands and topics.
Helper method. If this return True, the given cmd
-auto-help will be viewable in the help listing.
-Override this to easily select what is shown to
-the account. Note that only commands available
-in the caller’s merged cmdset are available.
-
-
Parameters
-
-
cmd (Command) – Command class from the merged cmdset
-
caller (Character, Account or Session) – The current caller
-executing the help command.
Should the specified command appear in the help table?
-
This method only checks whether a specified command should
-appear in the table of topics/commands. The command can be
-used by the caller (see the ‘check_show_help’ method) and
-the command will still be available, for instance, if a
-character type ‘help name of the command’. However, if
-you return False, the specified command will not appear in
-the table. This is sometimes useful to “hide” commands in
-the table, but still access them through the help system.
-
-
Parameters
-
-
cmd – the command to be tested.
-
caller – the caller of the help system.
+
title (str, optional) – The title of the help entry.
+
help_text (str, optional) – Text of the help entry.
+
aliases (list, optional) – List of help-aliases (displayed in header).
+
suggested (list, optional) – Strings suggested reading (based on title).
+
subtopics (list, optional) – A list of strings - the subcategories available
+for this entry.
+
click_topics (bool, optional) – Should help topics be clickable. Default is True.
Returns
-
True – the command should appear in the table.
-False: the command shouldn’t appear in the table.
+
help_message (str) – Help entry formated for console.
Output a category-ordered g for displaying the main help, grouped by
+category.
+
+
Parameters
+
+
cmd_help_dict (dict) – A dict {“category”: [topic, topic, …]} for
+command-based help.
+
db_help_dict (dict) – A dict {“category”: [topic, topic], …]} for
+database-based help.
+
title_lone_category (bool, optional) – If a lone category should
+be titled with the category name or not. While pointless in a
+general index, the title should probably show when explicitly
+listing the category itself.
+
click_topics (bool, optional) – If help-topics are clickable or not
+(for webclient or telnet clients with MXP support).
+
+
+
Returns
+
str – The help index organized into a grid.
+
+
+
Notes
+
The input are the pre-loaded help files for commands and database-helpfiles
+respectively. You can override this method to return a custom display of the list of
+commands and topics.
Helper method. If this return True, the given help topic
+be viewable in the help listing. Note that even if this returns False,
+the entry will still be visible in the help index unless should_list_topic
+is also returning False.
+
+
Parameters
+
+
cmd_or_topic (Command, HelpEntry or FileHelpEntry) – The topic/command to test.
+
caller – the caller checking for access.
+
+
+
Returns
+
bool – If command can be viewed or not.
+
+
+
Notes
+
This uses the ‘read’ lock. If no ‘read’ lock is defined, the topic is assumed readable
+by all.
Should the specified command appear in the help table?
+
This method only checks whether a specified command should appear in the table of
+topics/commands. The command can be used by the caller (see the ‘should_show_help’ method)
+and the command will still be available, for instance, if a character type ‘help name of the
+command’. However, if you return False, the specified command will not appear in the table.
+This is sometimes useful to “hide” commands in the table, but still access them through the
+help system.
+
+
Parameters
+
+
cmd_or_topic (Command, HelpEntry or FileHelpEntry) – The topic/command to test.
+
caller – the caller checking for access.
+
+
+
Returns
+
bool – If command should be listed or not.
+
+
+
Notes
+
By default, the ‘view’ lock will be checked, and if no such lock is defined, the ‘read’
+lock will be used. If neither lock is defined, the help entry is assumed to be
+accessible to all.
Collect help topics from all sources (cmd/db/file).
+
+
Parameters
+
+
caller (Object or Account) – The user of the Command.
+
mode (str) – One of ‘list’ or ‘query’, where the first means we are collecting to view
+the help index and the second because of wanting to search for a specific help
+entry/cmd to read. This determines which access should be checked.
+
+
+
Returns
+
tuple – A tuple of three dicts containing the different types of help entries
+in the order cmd-help, db-help, file-help:
input is a string containing the command or topic to match.
+
The allowed syntax is
+
help<topic>[/<subtopic>[/<subtopic>[/...]]]
+
+
+
The database/command query is always for <topic>, and any subtopics
+is then parsed from there. If a <topic> has spaces in it, it is
+always matched before assuming the space begins a subtopic.
@@ -200,12 +308,17 @@ False: the command shouldn’t appear in the table.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': '?', 'category': 'general', 'key': 'help', 'tags': '', 'text': "\n Get help.\n\n Usage:\n help\n help <topic, command or category>\n help <topic>/<subtopic>\n help <topic>/<subtopic>/<subsubtopic> ...\n\n Use the 'help' command alone to see an index of all help topics, organized\n by category.eSome big topics may offer additional sub-topics.\n\n "}¶
+
+
class evennia.commands.default.help.CmdSetHelp(**kwargs)[source]¶
sethelp lore = In the beginning was …
sethelp/append pickpocketing,Thievery = This steals …
sethelp/replace pickpocketing, ,attr(is_thief) = This steals …
sethelp/edit thievery
-
This command manipulates the help database. A help entry can be created,
-appended/merged to and deleted. If you don’t assign a category, the
-“General” category will be used. If no lockstring is specified, default
-is to let everyone read the help file.
+
If not assigning a category, the settings.DEFAULT_HELP_CATEGORY category
+will be used. If no lockstring is specified, everyone will be able to read
+the help entry. Sub-topics are embedded in the help text.
+
Note that this cannot modify command-help entries - these are modified
+in-code, outside the game.
+
Subtopics helps to break up a long help entry into sub-sections. Users can
+access subtopics with |whelp topic/subtopic/…|n Subtopics are created and
+stored together with the main topic.
+
To start adding subtopics, add the text ‘# SUBTOPICS’ on a new line at the
+end of your help text. After this you can now add any number of subtopics,
+each starting with ‘## <subtopic-name>’ on a line, followed by the
+help-text of that subtopic.
+Use ‘### <subsub-name>’ to add a sub-subtopic and so on. Max depth is 5. A
+subtopic’s title is case-insensitive and can consist of multiple words -
+the user will be able to enter a partial match to access it.
+
For example:
+
+
Main help text for <topic>
+
+
# SUBTOPICS
+
+
## about
+
+
Text for the ‘<topic>/about’ subtopic’
+
+
### more about-info
+
+
Text for the ‘<topic>/about/more about-info sub-subtopic
+search_index_entry = {'aliases': '', 'category': 'building', 'key': 'sethelp', 'tags': '', 'text': "\n Edit the help database.\n\n Usage:\n help[/switches] <topic>[[;alias;alias][,category[,locks]] [= <text>]\n\n Switches:\n edit - open a line editor to edit the topic's help text.\n replace - overwrite existing help topic.\n append - add text to the end of existing topic with a newline between.\n extend - as append, but don't add a newline.\n delete - remove help topic.\n\n Examples:\n sethelp lore = In the beginning was ...\n sethelp/append pickpocketing,Thievery = This steals ...\n sethelp/replace pickpocketing, ,attr(is_thief) = This steals ...\n sethelp/edit thievery\n\n If not assigning a category, the `settings.DEFAULT_HELP_CATEGORY` category\n will be used. If no lockstring is specified, everyone will be able to read\n the help entry. Sub-topics are embedded in the help text.\n\n Note that this cannot modify command-help entries - these are modified\n in-code, outside the game.\n\n # SUBTOPICS\n\n ## Adding subtopics\n\n Subtopics helps to break up a long help entry into sub-sections. Users can\n access subtopics with |whelp topic/subtopic/...|n Subtopics are created and\n stored together with the main topic.\n\n To start adding subtopics, add the text '# SUBTOPICS' on a new line at the\n end of your help text. After this you can now add any number of subtopics,\n each starting with '## <subtopic-name>' on a line, followed by the\n help-text of that subtopic.\n Use '### <subsub-name>' to add a sub-subtopic and so on. Max depth is 5. A\n subtopic's title is case-insensitive and can consist of multiple words -\n the user will be able to enter a partial match to access it.\n\n For example:\n\n | Main help text for <topic>\n |\n | # SUBTOPICS\n |\n | ## about\n |\n | Text for the '<topic>/about' subtopic'\n |\n | ### more about-info\n |\n | Text for the '<topic>/about/more about-info sub-subtopic\n |\n | ## extra\n |\n | Text for the '<topic>/extra' subtopic\n\n "}¶
@@ -309,7 +467,6 @@ is to let everyone read the help file.
-
diff --git a/docs/0.9.5/api/evennia.commands.default.muxcommand.html b/docs/0.9.5/api/evennia.commands.default.muxcommand.html
index 8fe210ea15..72d3c70322 100644
--- a/docs/0.9.5/api/evennia.commands.default.muxcommand.html
+++ b/docs/0.9.5/api/evennia.commands.default.muxcommand.html
@@ -184,6 +184,11 @@ to all the variables defined therein.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'command', 'tags': '', 'text': "\n This sets up the basis for a MUX command. The idea\n is that most other Mux-related commands should just\n inherit from this and don't have to implement much\n parsing of their own unless they do something particularly\n advanced.\n\n Note that the class's __doc__ string (this text) is\n used by Evennia to create the automatic help entry for\n the command, so make sure to document consistently here.\n "}¶
+
+
@@ -224,6 +229,11 @@ character is actually attached to this Account and Session.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'command', 'tags': '', 'text': '\n This is an on-Account version of the MuxCommand. Since these commands sit\n on Accounts rather than on Characters/Objects, we need to check\n this in the parser.\n\n Account commands are available also when puppeting a Character, it\'s\n just that they are applied with a lower priority and are always\n available, also when disconnected from a character (i.e. "ooc").\n\n This class makes sure that caller is always an Account object, while\n creating a new property "character" that is set only if a\n character is actually attached to this Account and Session.\n '}¶
+
+
@@ -271,7 +281,6 @@ character is actually attached to this Account and Session.
-
diff --git a/docs/0.9.5/api/evennia.commands.default.syscommands.html b/docs/0.9.5/api/evennia.commands.default.syscommands.html
index 593891dbf0..b770197e48 100644
--- a/docs/0.9.5/api/evennia.commands.default.syscommands.html
+++ b/docs/0.9.5/api/evennia.commands.default.syscommands.html
@@ -89,6 +89,11 @@ the line is just added to the editor buffer).
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'general', 'key': '__noinput_command', 'tags': '', 'text': '\n This is called when there is no input given\n '}¶
+
+
@@ -127,6 +132,11 @@ the line is just added to the editor buffer).
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'general', 'key': '__nomatch_command', 'tags': '', 'text': '\n No command was found matching the given input.\n '}¶
+
+
@@ -175,118 +185,9 @@ the raw_cmdname is the cmdname unmodified by eventual prefix-st
lock_storage = 'cmd:all()'¶
This method is called by the cmdhandler once the command name
-has been identified. It creates a new set of member variables
-that can be later accessed from self.func() (see below)
-
The following variables are available for our use when entering this
-method (from the command definition, and assigned on the fly by the
-cmdhandler):
-
-
self.key - the name of this command (‘look’)
-self.aliases - the aliases of this cmd (‘l’)
-self.permissions - permission string for this command
-self.help_category - overall category of command
-
self.caller - the object calling this command
-self.cmdstring - the actual command name used to call this
-
-
-
(this allows you to know which alias was used,
for example)
-
-
-
-
self.args - the raw input; everything following self.cmdstring.
-self.cmdset - the cmdset from which this command was picked. Not
-
-
often used (useful for commands like ‘help’ or to
-list all available commands etc)
-
-
-
self.obj - the object on which this command was defined. It is often
the same as self.caller.
-
-
-
-
A MUX command has the following possible syntax:
-
-
name[ with several words][/switch[/switch..]] arg1[,arg2,…] [[=|,] arg[,..]]
-
-
The ‘name[ with several words]’ part is already dealt with by the
-cmdhandler at this point, and stored in self.cmdname (we don’t use
-it here). The rest of the command is stored in self.args, which can
-start with the switch indicator /.
-
-
Optional variables to aid in parsing, if set:
-
self.switch_options - (tuple of valid /switches expected by this
command (without the /))
-
-
self.rhs_split - Alternate string delimiter or tuple of strings
to separate left/right hand sides. tuple form
-gives priority split to first string delimiter.
-
-
-
-
-
This parser breaks self.args into its constituents and stores them in the
-following variables:
-
-
self.switches = [list of /switches (without the /)]
-self.raw = This is the raw argument input, including switches
-self.args = This is re-defined to be everything except the switches
-self.lhs = Everything to the left of = (lhs:’left-hand side’). If
-
-
no = is found, this is identical to self.args.
-
-
-
self.rhs: Everything to the right of = (rhs:’right-hand side’).
If no ‘=’ is found, this is None.
-
-
-
self.lhslist - [self.lhs split into a list by comma]
-self.rhslist - [list of self.rhs split into a list by comma]
-self.arglist = [list of space-separated args (stripped, including ‘=’ if it exists)]
-
All args and list members are stripped of excess whitespace around the
-strings, but case is preserved.
+search_index_entry = {'aliases': '', 'category': 'general', 'key': '__multimatch_command', 'tags': '', 'text': "\n Multiple command matches.\n\n The cmdhandler adds a special attribute 'matches' to this\n system command.\n\n matches = [(cmdname, args, cmdobj, cmdlen, mratio, raw_cmdname) , (cmdname, ...), ...]\n\n Here, `cmdname` is the command's name and `args` the rest of the incoming string,\n without said command name. `cmdobj` is the Command instance, the cmdlen is\n the same as len(cmdname) and mratio is a measure of how big a part of the\n full input string the cmdname takes up - an exact match would be 1.0. Finally,\n the `raw_cmdname` is the cmdname unmodified by eventual prefix-stripping.\n\n "}¶
-
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 c6d136efc0..aecb5d7352 100644
--- a/docs/0.9.5/api/evennia.commands.default.system.html
+++ b/docs/0.9.5/api/evennia.commands.default.system.html
@@ -83,6 +83,11 @@ reset to purge) and at_reload() hooks will be called.
lock_storage = 'cmd:perm(reload) or perm(Developer)'¶
+
+
+search_index_entry = {'aliases': 'restart', 'category': 'system', 'key': 'reload', 'tags': '', 'text': '\n reload the server\n\n Usage:\n reload [reason]\n\n This restarts the server. The Portal is not\n affected. Non-persistent scripts will survive a reload (use\n reset to purge) and at_reload() hooks will be called.\n '}¶
+
+
@@ -135,6 +140,11 @@ cmdsets etc will be wiped.
lock_storage = 'cmd:perm(reload) or perm(Developer)'¶
+
+
+search_index_entry = {'aliases': 'reboot', 'category': 'system', 'key': 'reset', 'tags': '', 'text': '\n reset and reboot the server\n\n Usage:\n reset\n\n Notes:\n For normal updating you are recommended to use reload rather\n than this command. Use shutdown for a complete stop of\n everything.\n\n This emulates a cold reboot of the Server component of Evennia.\n The difference to shutdown is that the Server will auto-reboot\n and that it does not affect the Portal, so no users will be\n disconnected. Contrary to reload however, all shutdown hooks will\n be called and any non-database saved scripts, ndb-attributes,\n cmdsets etc will be wiped.\n\n '}¶
+
+
@@ -178,6 +188,11 @@ cmdsets etc will be wiped.
lock_storage = 'cmd:perm(shutdown) or perm(Developer)'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'system', 'key': 'shutdown', 'tags': '', 'text': '\n stop the server completely\n\n Usage:\n shutdown [announcement]\n\n Gracefully shut down both Server and Portal.\n '}¶
+
+
@@ -265,20 +280,30 @@ should only be accessible by trusted server admins/superusers.|n
lock_storage = 'cmd:perm(py) or perm(Developer)'¶
+
+
+search_index_entry = {'aliases': '!', 'category': 'system', 'key': 'py', 'tags': '', 'text': "\n execute a snippet of python code\n\n Usage:\n py [cmd]\n py/edit\n py/time <cmd>\n py/clientraw <cmd>\n py/noecho\n\n Switches:\n time - output an approximate execution time for <cmd>\n edit - open a code editor for multi-line code experimentation\n clientraw - turn off all client-specific escaping. Note that this may\n lead to different output depending on prototocol (such as angular brackets\n being parsed as HTML in the webclient but not in telnet clients)\n noecho - in Python console mode, turn off the input echo (e.g. if your client\n does this for you already)\n\n Without argument, open a Python console in-game. This is a full console,\n accepting multi-line Python code for testing and debugging. Type `exit()` to\n return to the game. If Evennia is reloaded, the console will be closed.\n\n Enter a line of instruction after the 'py' command to execute it\n immediately. Separate multiple commands by ';' or open the code editor\n using the /edit switch (all lines added in editor will be executed\n immediately when closing or using the execute command in the editor).\n\n A few variables are made available for convenience in order to offer access\n to the system (you can import more at execution time).\n\n Available variables in py environment:\n self, me : caller\n here : caller.location\n evennia : the evennia API\n inherits_from(obj, parent) : check object inheritance\n\n You can explore The evennia API from inside the game by calling\n the `__doc__` property on entities:\n py evennia.__doc__\n py evennia.managers.__doc__\n\n |rNote: In the wrong hands this command is a severe security risk. It\n should only be accessible by trusted server admins/superusers.|n\n\n "}¶
+
+
class evennia.commands.default.system.CmdScripts(**kwargs)[source]¶
scripts[/switches] [#dbref, key, script.path or <obj>]
+
List and manage all running scripts. Allows for creating new global
+scripts.
+
+
Usage:
script[/switches] [#dbref, key, script.path or <obj>]
-
Switches:
start - start a script (must supply a script path)
-stop - stops an existing script
-kill - kills a script - without running its cleanup hooks
-validate - run a validation on the script(s)
+
Switches:
+
create - create a new global script of given typeclass path. This will
auto-start the script’s timer if it has one.
+
+
+
start - start/unpause an existing script’s timer.
+stop - stops an existing script’s timer
+pause - pause a script’s timer
+delete - deletes script. This will also stop the timer as needed
If no switches are given, this command just views all active
@@ -286,7 +311,8 @@ scripts. The argument can be either an object, at which point it
will be searched for all scripts defined on it, or a script name
or #dbref. For using the /stop switch, a unique script #dbref is
required since whole classes of scripts often have the same name.
-
Use script for managing commands on objects.
+
Use the script build-level command for managing scripts attached to
+objects.
@@ -317,6 +343,11 @@ required since whole classes of scripts often have the same name.
excluded_typeclass_paths = ['evennia.prototypes.prototypes.DbPrototype']¶
@@ -328,6 +359,11 @@ required since whole classes of scripts often have the same name.
lock_storage = 'cmd:perm(listscripts) or perm(Admin)'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'system', 'key': 'scripts', 'tags': '', 'text': "\n List and manage all running scripts. Allows for creating new global\n scripts.\n\n Usage:\n script[/switches] [#dbref, key, script.path or <obj>]\n\n Switches:\n create - create a new global script of given typeclass path. This will\n auto-start the script's timer if it has one.\n start - start/unpause an existing script's timer.\n stop - stops an existing script's timer\n pause - pause a script's timer\n delete - deletes script. This will also stop the timer as needed\n\n If no switches are given, this command just views all active\n scripts. The argument can be either an object, at which point it\n will be searched for all scripts defined on it, or a script name\n or #dbref. For using the /stop switch, a unique script #dbref is\n required since whole classes of scripts often have the same name.\n\n Use the `script` build-level command for managing scripts attached to\n objects.\n\n "}¶
@@ -373,6 +409,11 @@ given, <nr> defaults to 10.
lock_storage = 'cmd:perm(listobjects) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': 'stats listobjects listobjs db', 'category': 'system', 'key': 'objects', 'tags': '', 'text': '\n statistics on objects in the database\n\n Usage:\n objects [<nr>]\n\n Gives statictics on objects in database as well as\n a list of <nr> latest objects in database. If not\n given, <nr> defaults to 10.\n '}¶
+
+
@@ -430,6 +471,11 @@ in the list.
lock_storage = 'cmd:perm(service) or perm(Developer)'¶
+
+
+search_index_entry = {'aliases': 'services', 'category': 'system', 'key': 'service', 'tags': '', 'text': '\n manage system services\n\n Usage:\n service[/switch] <service>\n\n Switches:\n list - shows all available services (default)\n start - activates or reactivate a service\n stop - stops/inactivate a service (can often be restarted)\n delete - tries to permanently remove a service\n\n Service management system. Allows for the listing,\n starting, and stopping of services. If no switches\n are given, services will be listed. Note that to operate on the\n service you have to supply the full (green or red) name as given\n in the list.\n '}¶
+
+
@@ -473,6 +519,11 @@ in the list.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': 'version', 'category': 'system', 'key': 'about', 'tags': '', 'text': '\n show Evennia info\n\n Usage:\n about\n\n Display info about the game engine.\n '}¶
+
+
@@ -517,6 +568,11 @@ and the current time stamp.
lock_storage = 'cmd:perm(time) or perm(Player)'¶
+
+
+search_index_entry = {'aliases': 'uptime', 'category': 'system', 'key': 'time', 'tags': '', 'text': '\n show server time statistics\n\n Usage:\n time\n\n List Server time statistics such as uptime\n and the current time stamp.\n '}¶
+
+
@@ -556,7 +612,7 @@ the released memory will instead be re-used by the program.
@@ -585,6 +641,100 @@ the released memory will instead be re-used by the program.
lock_storage = 'cmd:perm(list) or perm(Developer)'¶
+
+
+search_index_entry = {'aliases': 'serverload serverprocess', 'category': 'system', 'key': 'server', 'tags': '', 'text': "\n show server load and memory statistics\n\n Usage:\n server[/mem]\n\n Switches:\n mem - return only a string of the current memory usage\n flushmem - flush the idmapper cache\n\n This command shows server load statistics and dynamic memory\n usage. It also allows to flush the cache of accessed database\n objects.\n\n Some Important statistics in the table:\n\n |wServer load|n is an average of processor usage. It's usually\n between 0 (no usage) and 1 (100% usage), but may also be\n temporarily higher if your computer has multiple CPU cores.\n\n The |wResident/Virtual memory|n displays the total memory used by\n the server process.\n\n Evennia |wcaches|n all retrieved database entities when they are\n loaded by use of the idmapper functionality. This allows Evennia\n to maintain the same instances of an entity and allowing\n non-persistent storage schemes. The total amount of cached objects\n are displayed plus a breakdown of database object types.\n\n The |wflushmem|n switch allows to flush the object cache. Please\n note that due to how Python's memory management works, releasing\n caches may not show you a lower Residual/Virtual memory footprint,\n the released memory will instead be re-used by the program.\n\n "}¶
pause - Pause the callback of a task.
+unpause - Process all callbacks made since pause() was called.
+do_task - Execute the task (call its callback).
+call - Call the callback of this task.
+remove - Remove a task without executing it.
+cancel - Stop a task from automatically executing.
+
+
+
Notes
+
A task is a single use method of delaying the call of a function. Calls are created
+in code, using evennia.utils.delay.
+See |luhttps://www.evennia.com/docs/latest/Command-Duration.html|ltthe docs|le for help.
+
By default, tasks that are canceled and never called are cleaned up after one minute.
+
Examples
+
+
+
tasks/cancel move_callback - Cancels all movement delays from the slow_exit contrib.
In this example slow exits creates it’s tasks with
+utils.delay(move_delay, move_callback)
This is the hook function that actually does all the work. It is called
+by the cmdhandler right after self.parser() finishes, and so has access
+to all the variables defined therein.
+search_index_entry = {'aliases': 'delays task', 'category': 'system', 'key': 'tasks', 'tags': '', 'text': "\n Display or terminate active tasks (delays).\n\n Usage:\n tasks[/switch] [task_id or function_name]\n\n Switches:\n pause - Pause the callback of a task.\n unpause - Process all callbacks made since pause() was called.\n do_task - Execute the task (call its callback).\n call - Call the callback of this task.\n remove - Remove a task without executing it.\n cancel - Stop a task from automatically executing.\n\n Notes:\n A task is a single use method of delaying the call of a function. Calls are created\n in code, using `evennia.utils.delay`.\n See |luhttps://www.evennia.com/docs/latest/Command-Duration.html|ltthe docs|le for help.\n\n By default, tasks that are canceled and never called are cleaned up after one minute.\n\n Examples:\n - `tasks/cancel move_callback` - Cancels all movement delays from the slow_exit contrib.\n In this example slow exits creates it's tasks with\n `utils.delay(move_delay, move_callback)`\n - `tasks/cancel 2` - Cancel task id 2.\n\n "}¶
+
+
@@ -632,7 +782,6 @@ the released memory will instead be re-used by the program.
-
diff --git a/docs/0.9.5/api/evennia.commands.default.tests.html b/docs/0.9.5/api/evennia.commands.default.tests.html
index 3579914496..11c05854f8 100644
--- a/docs/0.9.5/api/evennia.commands.default.tests.html
+++ b/docs/0.9.5/api/evennia.commands.default.tests.html
@@ -51,25 +51,107 @@ main test suite started with
class evennia.commands.default.tests.CommandTest(methodName='runTest')[source]¶
Tests a Command by running it and comparing what messages it sends with
+expected values. This tests without actually spinning up the cmdhandler
+for every test, which is more controlled.
+
Example:
+
fromcommands.echoimportCmdEcho
+
+classMyCommandTest(CommandTest):
+
+ deftest_echo(self):
+ '''
+ Test that the echo command really returns
+ what you pass into it.
+ '''
+ self.call(MyCommand(),"hello world!",
+ "You hear your echo: 'Hello world!'")
+
Test a command by assigning all the needed properties to a cmdobj and
+running the sequence. The resulting .msg calls will be mocked and
+the text= calls to them compared to a expected output.
-
Returns
-
msg (str) – The received message that was sent to the caller.
input_args (str) – This should be the full input the Command should
+see, such as ‘look here’. This will become .args for the Command
+instance to parse.
+
msg (str or dict, optional) – This is the expected return value(s)
+returned through caller.msg(text=…) calls in the command. If a string, the
+receiver is controlled with the receiver kwarg (defaults to caller).
+If this is a dict, it is a mapping
+{receiver1: “expected1”, receiver2: “expected2”,…} and receiver is
+ignored. The message(s) are compared with the actual messages returned
+to the receiver(s) as the Command runs. Each check uses .startswith,
+so you can choose to only include the first part of the
+returned message if that’s enough to verify a correct result. EvMenu
+decorations (like borders) are stripped and should not be included. This
+should also not include color tags unless noansi=False.
+If the command returns texts in multiple separate .msg-
+calls to a receiver, separate these with | if noansi=True
+(default) and || if noansi=False. If no msg is given (None),
+then no automatic comparison will be done.
+
cmdset (str, optional) – If given, make .cmdset available on the Command
+instance as it runs. While .cmdset is normally available on the
+Command instance by default, this is usually only used by
+commands that explicitly operates/displays cmdsets, like
+examine.
+
noansi (str, optional) – By default the color tags of the msg is
+ignored, this makes them significant. If unset, msg must contain
+the same color tags as the actual return message.
+
caller (Object or Account, optional) – By default self.char1 is used as the
+command-caller (the .caller property on the Command). This allows to
+execute with another caller, most commonly an Account.
+
receiver (Object or Account, optional) – This is the object to receive the
+return messages we want to test. By default this is the same as caller
+(which in turn defaults to is self.char1). Note that if msg is
+a dict, this is ignored since the receiver is already specified there.
+
cmdstring (str, optional) – Normally this is the Command’s key.
+This allows for tweaking the .cmdname property of the
+Command**. This isb used for commands with multiple aliases,
+where the command explicitly checs which alias was used to
+determine its functionality.
+
obj (str, optional) – This sets the .obj property of the Command - the
+object on which the Command ‘sits’. By default this is the same as caller.
+This can be used for testing on-object Command interactions.
+
inputs (list, optional) – A list of strings to pass to functions that pause to
+take input from the user (normally using @interactive and
+ret = yield(question) or evmenu.get_input). Each element of the
+list will be passed into the command as if the user wrote that at the prompt.
+
raw_string (str, optional) – Normally the .raw_string property is set as
+a combination of your key/cmdname and input_args. This allows
+direct control of what this is, for example for testing edge cases
+or malformed inputs.
+
+
+
Returns
+
str or dict –
+
+
The message sent to receiver, or a dict of
{receiver: “msg”, …} if multiple are given. This is usually
+only used with msg=None to do the validation externally.
+
+
+
Raises
+
AssertionError – If the returns of .msg calls (tested with .startswith) does not
+match expected_input.
+
+
+
Notes
+
As part of the tests, all methods of the Command will be called in
+the proper order:
+
+
cmdobj.at_pre_cmd()
+
cmdobj.parse()
+
cmdobj.func()
+
cmdobj.at_post_cmd()
+
@@ -139,6 +221,23 @@ output sent to caller.msg in the game
class evennia.commands.default.tests.TestHelp(methodName='runTest')[source]¶
(you may see this if a child command had no help text defined)
Usage:
command [args]
@@ -600,6 +977,11 @@ set in self.parse())
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'interrupt', 'tags': '', 'text': '\n ## Base command\n\n (you may see this if a child command had no help text defined)\n\n Usage:\n command [args]\n\n This is the base command class. Inherit from this\n to create new commands.\n\n The cmdhandler makes the following variables available to the\n command methods (so you can always assume them to be there):\n self.caller - the game object calling the command\n self.cmdstring - the command name used to trigger this command (allows\n you to know which alias was used, for example)\n cmd.args - everything supplied to the command following the cmdstring\n (this is usually what is parsed in self.parse())\n cmd.cmdset - the cmdset from which this command was matched (useful only\n seldomly, notably for help-type commands, to create dynamic\n help entries and lists)\n cmd.obj - the object on which this command is defined. If a default command,\n this is usually the same as caller.\n cmd.rawstring - the full raw string input, including any args and no parsing.\n\n The following class properties can/should be defined on your child class:\n\n key - identifier for command (e.g. "look")\n aliases - (optional) list of aliases (e.g. ["l", "loo"])\n locks - lock string (default is "cmd:all()")\n help_category - how to organize this help entry in help system\n (default is "General")\n auto_help - defaults to True. Allows for turning off auto-help generation\n arg_regex - (optional) raw string regex defining how the argument part of\n the command should look in order to match for this command\n (e.g. must it be a space between cmdname and arg?)\n auto_help_display_key - (optional) if given, this replaces the string shown\n in the auto-help listing. This is particularly useful for system-commands\n whose actual key is not really meaningful.\n\n (Note that if auto_help is on, this initial string is also used by the\n system to create the help entry for the command, so it\'s a good idea to\n format it similar to this one). This behavior can be changed by\n overriding the method \'get_help\' of a command: by default, this\n method returns cmd.__doc__ (that is, this very docstring, or\n the docstring of your command). You can, however, extend or\n replace this without disabling auto_help.\n '}¶
+
+
@@ -638,11 +1020,6 @@ set in self.parse())
test_multimatch()[source]¶
@@ -92,6 +92,11 @@ there is no object yet before the account has logged in)
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': 'co con conn', 'category': 'general', 'key': 'connect', 'tags': '', 'text': '\n connect to the game\n\n Usage (at login screen):\n connect accountname password\n connect "account name" "pass word"\n\n Use the create command to first create an account before logging in.\n\n If you have spaces in your name, enclose it in double quotes.\n '}¶
+search_index_entry = {'aliases': 'cre cr', 'category': 'general', 'key': 'create', 'tags': '', 'text': '\n create a new account account\n\n Usage (at login screen):\n create <accountname> <password>\n create "account name" "pass word"\n\n This creates a new account account.\n\n If you have spaces in your name, enclose it in double quotes.\n '}¶
+
+
@@ -187,6 +197,11 @@ version is a bit more complicated.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': 'qu q', 'category': 'general', 'key': 'quit', 'tags': '', 'text': '\n quit when in unlogged-in state\n\n Usage:\n quit\n\n We maintain a different version of the quit command\n here for unconnected accounts for the sake of simplicity. The logged in\n version is a bit more complicated.\n '}¶
+
+
@@ -232,6 +247,11 @@ All it does is display the connect screen.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': 'l look', 'category': 'general', 'key': '__unloggedin_look_command', 'tags': '', 'text': '\n look when in unlogged-in state\n\n Usage:\n look\n\n This is an unconnected version of the look command for simplicity.\n\n This is called by the server and kicks everything in gear.\n All it does is display the connect screen.\n '}¶
+
+
@@ -252,7 +272,7 @@ for simplicity. It shows a pane of info.
@@ -276,6 +296,11 @@ for simplicity. It shows a pane of info.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': '? h', 'category': 'general', 'key': 'help', 'tags': '', 'text': '\n get help when in unconnected-in state\n\n Usage:\n help\n\n This is an unconnected version of the help command,\n for simplicity. It shows a pane of info.\n '}¶
+
+
@@ -323,7 +348,6 @@ for simplicity. It shows a pane of info.
The channel handler, accessed from this module as CHANNEL_HANDLER is a
-singleton that handles the stored set of channels and how they are
-represented against the cmdhandler.
-
If there is a channel named ‘newbie’, we want to be able to just write
-
-
newbie Hello!
-
-
For this to work, ‘newbie’, the name of the channel, must be
-identified by the cmdhandler as a command name. The channelhandler
-stores all channels as custom ‘commands’ that the cmdhandler can
-import and look through.
-
> Warning - channel names take precedence over command names, so make
-sure to not pick clashing channel names.
-
Unless deleting a channel you normally don’t need to bother about the
-channelhandler at all - the create_channel method handles the update.
-
To delete a channel cleanly, delete the channel object, then call
-update() on the channelhandler. Or use Channel.objects.delete() which
-does this for you.
{lower_channelkey} <message>
-{lower_channelkey}/history [start]
-{lower_channelkey} off - mutes the channel
-{lower_channelkey} on - unmutes the channel
-
-
Switch:
-
history: View 20 previous messages, either from the end or
The ChannelHandler manages all active in-game channels and
-dynamically creates channel commands for users so that they can
-just give the channel’s key or alias to write to it. Whenever a
-new channel is created in the database, the update() method on
-this handler must be called to sync it with the database (this is
-done automatically if creating the channel with
-evennia.create_channel())
Add an individual channel to the handler. This is called
-whenever a new channel is created.
-
-
Parameters
-
channel (Channel) – The channel to add.
-
-
-
Notes
-
To remove a channel, simply delete the channel object and
-run self.update on the handler. This should usually be
-handled automatically by one of the deletion methos of
-the Channel itself.
Add an individual channel to the handler. This is called
-whenever a new channel is created.
-
-
Parameters
-
channel (Channel) – The channel to add.
-
-
-
Notes
-
To remove a channel, simply delete the channel object and
-run self.update on the handler. This should usually be
-handled automatically by one of the deletion methos of
-the Channel itself.
This is the base class for all Channel Comms. Inherit from this to
create different types of communication channels.
+
+
Class-level variables:
+
send_to_online_only (bool, default True) - if set, will only try to
+send to subscribers that are actually active. This is a useful optimization.
+
log_file (str, default “channel_{channelname}.log”). This is the
+log file to which the channel history will be saved. The {channelname} tag
+will be replaced by the key of the Channel. If an Attribute ‘log_file’
+is set, this will be used instead. If this is None and no Attribute is found,
+no history will be saved.
+
channel_prefix_string (str, default “[{channelname} ]”) - this is used
+as a simple template to get the channel prefix with .channel_prefix().
Send the given message to all accounts connected to channel. Note that
-no permission-checking is done here; it is assumed to have been
-done before calling this method. The optional keywords are not used if
-persistent is False.
-
-
Parameters
-
-
msgobj (Msg, TempMsg or str) – If a Msg/TempMsg, the remaining
-keywords will be ignored (since the Msg/TempMsg object already
-has all the data). If a string, this will either be sent as-is
-(if persistent=False) or it will be used together with header
-and senders keywords to create a Msg instance on the fly.
-
header (str, optional) – A header for building the message.
-
senders (Object, Account or list, optional) – Optional if persistent=False, used
-to build senders for the message.
-
sender_strings (list, optional) – Name strings of senders. Used for external
-connections where the sender is not an account or object.
-When this is defined, external will be assumed. The list will be
-filtered so each sender-string only occurs once.
-
keep_log (bool or None, optional) – This allows to temporarily change the logging status of
-this channel message. If None, the Channel’s keep_log Attribute will
-be used. If True or False, that logging status will be used for this
-message only (note that for unlogged channels, a True value here will
-create a new log file only for this message).
-
online (bool, optional) – online. Otherwise, messages all accounts connected. This can
-make things faster, but may not trigger listeners on accounts
-that are offline.
-
emit (bool, optional) – not to be directly associated with a name.
-
external (bool, optional) – Treat this message as being
-agnostic of its sender.
Add a personal user-alias for this channel to a given subscriber.
Parameters
-
senders (list) – Sender object names.
-
**kwargs (dict) – Arbitrary, optional arguments for users
-overriding the call (unused by default).
+
user (Object or Account) – The one to alias this channel.
+
alias (str) – The desired alias.
-
Returns
-
formatted_list (str) – The list of names formatted appropriately.
+
+
+
Note
+
This is tightly coupled to the default channel command. If you
+change that, you need to change this as well.
+
We add two nicks - one is a plain alias -> channel.key that
+users need to be able to reference this channel easily. The other
+is a templated nick to easily be able to send messages to the
+channel without needing to give the full channel command. The
+structure of this nick is given by self.channel_msg_nick_pattern
+and self.channel_msg_nick_replacement. By default it maps
+alias <msg> -> channel <channelname> = <msg>, so that you can
+for example just write pub Hello to send a message.
+
The alias created is alias $1 -> channel channel = $1, to allow
+for sending to channel using the main channel command.
user (Object or Account) – The user to remove an alias from.
+
alias (str) – The alias to remove.
+
**kwargs – Unused by default. Can be used to pass extra variables
+into a custom implementation.
+
Notes
-
This function exists separately so that external sources
-can use it to format source names in the same manner as
-normal object/account names.
+
The channel-alias actually consists of two aliases - one
+channel-based one for searching channels with the alias and one
+inputline one for doing the ‘channelalias msg’ - call.
+
This is a classmethod because it doesn’t actually operate on the
+channel instance.
+
It sits on the channel because the nick structure for this is
+pretty complex and needs to be located in a central place (rather
+on, say, the channel command).
Called before the starting of sending the message to a receiver. This
+is called before any hooks on the receiver itself. If this returns
+None/False, the sending will be aborted.
Parameters
-
msgobj (Msg or TempMsg) – The message to analyze for a pose.
-
sender_string (str) – The name of the sender/poser.
-
**kwargs (dict) – Arbitrary, optional arguments for users
-overriding the call (unused by default).
+
message (str) – The message to send.
+
**kwargs (any) – Keywords passed on from .msg. This includes
+senders.
Returns
-
string (str) –
+
str, False or None –
-
A message that combines the sender_string
component with msg in different ways depending on if a
-pose was performed or not (this must be analyzed by the
-hook).
Hook method. Used for formatting external messages. This is
-needed as a separate operation because the senders of external
-messages may not be in-game objects/accounts, and so cannot
-have things like custom user preferences.
emit (bool, optional) – A sender-agnostic message or not.
-
**kwargs (dict) – Arbitrary, optional arguments for users
-overriding the call (unused by default).
+
message (str) – The message to send.
+
senders (Object, Account or list, optional) – If not given, there is
+no way to associate one or more senders with the message (like
+a broadcast message or similar).
+
bypass_mute (bool, optional) – If set, always send, regardless of
+individual mute-state of subscriber. This can be used for
+global announcements or warnings/alerts.
+
**kwargs (any) – This will be passed on to all hooks. Use no_prefix
+to exclude the channel prefix.
-
Returns
-
transformed (str) – A formatted string.
-
+
Notes
+
The call hook calling sequence is:
+
+
msg = channel.at_pre_msg(message, **kwargs) (aborts for all if return None)
+
msg = receiver.at_pre_channel_msg(msg, channel, **kwargs) (aborts for receiver if return None)
Hook method. Runs before a message is sent to the channel and
-should return the message object, after any transformations.
-If the message is to be discarded, return a false value.
For this to work, the developer must have defined a named view somewhere
in urls.py that follows the format ‘modelname-action’, so in this case
a named view of ‘channel-detail’ would be referenced by this method.
If no View has been created and defined in urls.py, returns an
HTML anchor.
This method is naive and simply returns a path. Securing access to
@@ -640,11 +674,11 @@ object.
For this to work, the developer must have defined a named view somewhere
in urls.py that follows the format ‘modelname-action’, so in this case
a named view of ‘channel-update’ would be referenced by this method.
If no View has been created and defined in urls.py, returns an
HTML anchor.
This method is naive and simply returns a path. Securing access to
@@ -691,11 +725,11 @@ this object.
For this to work, the developer must have defined a named view somewhere
in urls.py that follows the format ‘modelname-action’, so in this case
a named view of ‘channel-detail’ would be referenced by this method.
-
diff --git a/docs/0.9.5/api/evennia.comms.html b/docs/0.9.5/api/evennia.comms.html
index fae4a7dbf8..f0091b3726 100644
--- a/docs/0.9.5/api/evennia.comms.html
+++ b/docs/0.9.5/api/evennia.comms.html
@@ -96,7 +96,6 @@ as code related to external communication like IRC or RSS.
-
diff --git a/docs/0.9.5/api/evennia.comms.managers.html b/docs/0.9.5/api/evennia.comms.managers.html
index fd330f04c9..b342b220bb 100644
--- a/docs/0.9.5/api/evennia.comms.managers.html
+++ b/docs/0.9.5/api/evennia.comms.managers.html
@@ -135,19 +135,15 @@ of which may be Channels).
This is a non-persistent object for sending temporary messages
-that will not be stored. It mimics the “real” Msg object, but
-doesn’t require sender to be given.
+
This is a non-persistent object for sending temporary messages that will not be stored. It
+mimics the “real” Msg object, but doesn’t require sender to be given.
@@ -646,22 +596,6 @@ class built by **create_forward_many_to_many_manager()** define
get_previous_by_db_date_created(*, field=<django.db.models.fields.DateTimeField: db_date_created>, is_next=False, **kwargs)¶
-
diff --git a/docs/0.9.5/api/evennia.contrib.barter.html b/docs/0.9.5/api/evennia.contrib.barter.html
index 8a371d292c..b1c1b4e224 100644
--- a/docs/0.9.5/api/evennia.contrib.barter.html
+++ b/docs/0.9.5/api/evennia.contrib.barter.html
@@ -403,6 +403,11 @@ available to the command
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'command', 'tags': '', 'text': '\n Base command for Trade commands to inherit from. Implements the\n custom parsing.\n '}¶
+
+
@@ -446,6 +451,11 @@ available to the command
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'trade', 'key': 'trade help', 'tags': '', 'text': '\n help command for the trade system.\n\n Usage:\n trade help\n\n Displays help for the trade commands.\n '}¶
+search_index_entry = {'aliases': '', 'category': 'trading', 'key': 'offer', 'tags': '', 'text': '\n offer one or more items in trade.\n\n Usage:\n offer <object> [, object2, ...][:emote]\n\n Offer objects in trade. This will replace the currently\n standing offer.\n '}¶
+
+
@@ -537,6 +552,11 @@ the current offer using the ‘offers’ command.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': 'agree', 'category': 'trading', 'key': 'accept', 'tags': '', 'text': "\n accept the standing offer\n\n Usage:\n accept [:emote]\n agreee [:emote]\n\n This will accept the current offer. The other party must also accept\n for the deal to go through. You can use the 'decline' command to change\n your mind as long as the other party has not yet accepted. You can inspect\n the current offer using the 'offers' command.\n "}¶
+
+
@@ -583,6 +603,11 @@ decline the old offer.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'trading', 'key': 'decline', 'tags': '', 'text': "\n decline the standing offer\n\n Usage:\n decline [:emote]\n\n This will decline a previously 'accept'ed offer (so this allows you to\n change your mind). You can only use this as long as the other party\n has not yet accepted the deal. Also, changing the offer will automatically\n decline the old offer.\n "}¶
+
+
@@ -627,6 +652,11 @@ determine if it’s worth your while.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': 'eval', 'category': 'trading', 'key': 'evaluate', 'tags': '', 'text': "\n evaluate objects on offer\n\n Usage:\n evaluate <offered object>\n\n This allows you to examine any object currently on offer, to\n determine if it's worth your while.\n "}¶
+
+
@@ -675,6 +705,11 @@ try to influence the other part in the deal.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': 'deal offers', 'category': 'trading', 'key': 'status', 'tags': '', 'text': "\n show a list of the current deal\n\n Usage:\n status\n deal\n offers\n\n Shows the currently suggested offers on each sides of the deal. To\n accept the current deal, use the 'accept' command. Use 'offer' to\n change your deal. You might also want to use 'say', 'emote' etc to\n try to influence the other part in the deal.\n "}¶
+search_index_entry = {'aliases': 'finish trade', 'category': 'trading', 'key': 'end trade', 'tags': '', 'text': '\n end the trade prematurely\n\n Usage:\n end trade [:say]\n finish trade [:say]\n\n This ends the trade prematurely. No trade will take place.\n\n '}¶
+
+
@@ -793,6 +833,11 @@ info to your choice.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': 'barter', 'category': 'general', 'key': 'trade', 'tags': '', 'text': '\n Initiate trade with another party\n\n Usage:\n trade <other party> [:say]\n trade <other party> accept [:say]\n trade <other party> decline [:say]\n\n Initiate trade with another party. The other party needs to repeat\n this command with trade accept/decline within a minute in order to\n properly initiate the trade action. You can use the decline option\n yourself if you want to retract an already suggested trade. The\n optional say part works like the say command and allows you to add\n info to your choice.\n '}¶
+search_index_entry = {'aliases': '', 'category': 'general', 'key': '__nomatch_command', 'tags': '', 'text': 'No input has been found.'}¶
+
+
@@ -861,6 +871,11 @@ set in self.parse())
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'general', 'key': '@edit', 'tags': '', 'text': "\n Generic building command.\n\n Syntax:\n @edit [object]\n\n Open a building menu to edit the specified object. This menu allows to\n change the object's key and description.\n\n Examples:\n @edit here\n @edit self\n @edit #142\n\n "}¶
-
diff --git a/docs/0.9.5/api/evennia.contrib.chargen.html b/docs/0.9.5/api/evennia.contrib.chargen.html
index 780a5e5f86..32a3d0e683 100644
--- a/docs/0.9.5/api/evennia.contrib.chargen.html
+++ b/docs/0.9.5/api/evennia.contrib.chargen.html
@@ -77,7 +77,7 @@ at them with this command.
@@ -107,6 +107,11 @@ that is checked by the @ic command directly.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': 'ls l', 'category': 'general', 'key': 'look', 'tags': '', 'text': '\n ooc look\n\n Usage:\n look\n look <character>\n\n This is an OOC version of the look command. Since an Account doesn\'t\n have an in-game existence, there is no concept of location or\n "self".\n\n If any characters are available for you to control, you may look\n at them with this command.\n '}¶
+
+
@@ -152,6 +157,11 @@ attribute on ourselves to remember it.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'create', 'tags': '', 'text': '\n creates a character\n\n Usage:\n create <character name>\n\n This will create a new character, assuming\n the given character name does not already exist.\n '}¶
+
+
@@ -217,7 +227,6 @@ attribute on ourselves to remember it.
-
diff --git a/docs/0.9.5/api/evennia.contrib.clothing.html b/docs/0.9.5/api/evennia.contrib.clothing.html
index f7fa5e0ecf..44833552b2 100644
--- a/docs/0.9.5/api/evennia.contrib.clothing.html
+++ b/docs/0.9.5/api/evennia.contrib.clothing.html
@@ -359,6 +359,11 @@ provide will be displayed after the clothing’s name.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'clothing', 'key': 'wear', 'tags': '', 'text': "\n Puts on an item of clothing you are holding.\n\n Usage:\n wear <obj> [wear style]\n\n Examples:\n wear shirt\n wear scarf wrapped loosely about the shoulders\n\n All the clothes you are wearing are appended to your description.\n If you provide a 'wear style' after the command, the message you\n provide will be displayed after the clothing's name.\n "}¶
+
+
@@ -399,6 +404,11 @@ off the covering item first.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'clothing', 'key': 'remove', 'tags': '', 'text': "\n Takes off an item of clothing.\n\n Usage:\n remove <obj>\n\n Removes an item of clothing you are wearing. You can't remove\n clothes that are covered up by something else - you must take\n off the covering item first.\n "}¶
+
+
@@ -439,6 +449,11 @@ You can’t remove an item of clothing if it’s covered.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'clothing', 'key': 'cover', 'tags': '', 'text': "\n Covers a worn item of clothing with another you're holding or wearing.\n\n Usage:\n cover <obj> [with] <obj>\n\n When you cover a clothing item, it is hidden and no longer appears in\n your description until it's uncovered or the item covering it is removed.\n You can't remove an item of clothing if it's covered.\n "}¶
+
+
@@ -480,6 +495,11 @@ it is also covered by something else.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'clothing', 'key': 'uncover', 'tags': '', 'text': "\n Reveals a worn item of clothing that's currently covered up.\n\n Usage:\n uncover <obj>\n\n When you uncover an item of clothing, you allow it to appear in your\n description without having to take off the garment that's currently\n covering it. You can't uncover an item of clothing if the item covering\n it is also covered by something else.\n "}¶
+
+
@@ -529,6 +549,11 @@ location you are currently in.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'drop', 'tags': '', 'text': '\n drop something\n\n Usage:\n drop <obj>\n\n Lets you drop an object from your inventory into the\n location you are currently in.\n '}¶
+
+
@@ -578,6 +603,11 @@ placing it in their inventory.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'give', 'tags': '', 'text': '\n give away something to someone\n\n Usage:\n give <inventory obj> = <target>\n\n Gives an items from your inventory to another character,\n placing it in their inventory.\n '}¶
-
diff --git a/docs/0.9.5/api/evennia.contrib.custom_gametime.html b/docs/0.9.5/api/evennia.contrib.custom_gametime.html
index 356e559117..57e80e9d5d 100644
--- a/docs/0.9.5/api/evennia.contrib.custom_gametime.html
+++ b/docs/0.9.5/api/evennia.contrib.custom_gametime.html
@@ -313,7 +313,6 @@ The time is given in units as keyword arguments.
-
diff --git a/docs/0.9.5/api/evennia.contrib.dice.html b/docs/0.9.5/api/evennia.contrib.dice.html
index d6cb2e2b02..04b8a5a026 100644
--- a/docs/0.9.5/api/evennia.contrib.dice.html
+++ b/docs/0.9.5/api/evennia.contrib.dice.html
@@ -173,6 +173,11 @@ everyone but the person rolling.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': '@dice roll', 'category': 'general', 'key': 'dice', 'tags': '', 'text': "\n roll dice\n\n Usage:\n dice[/switch] <nr>d<sides> [modifier] [success condition]\n\n Switch:\n hidden - tell the room the roll is being done, but don't show the result\n secret - don't inform the room about neither roll nor result\n\n Examples:\n dice 3d6 + 4\n dice 1d100 - 2 < 50\n\n This will roll the given number of dice with given sides and modifiers.\n So e.g. 2d6 + 3 means to 'roll a 6-sided die 2 times and add the result,\n then add 3 to the total'.\n Accepted modifiers are +, -, * and /.\n A success condition is given as normal Python conditionals\n (<,>,<=,>=,==,!=). So e.g. 2d6 + 3 > 10 means that the roll will succeed\n only if the final result is above 8. If a success condition is given, the\n outcome (pass/fail) will be echoed along with how much it succeeded/failed\n with. The hidden/secret switches will hide all or parts of the roll from\n everyone but the person rolling.\n "}¶
+
+
@@ -239,7 +244,6 @@ Add with @py self.cmdset.add(“contrib.dice.DiceCmdSet”)
@@ -102,6 +102,11 @@ there is no object yet before the account has logged in)
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': 'co con conn', 'category': 'general', 'key': 'connect', 'tags': '', 'text': '\n Connect to the game.\n\n Usage (at login screen):\n connect <email> <password>\n\n Use the create command to first create an account before logging in.\n '}¶
+
+
@@ -155,6 +160,11 @@ name enclosed in quotes:
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': 'cre cr', 'category': 'general', 'key': 'create', 'tags': '', 'text': '\n Create a new account.\n\n Usage (at login screen):\n create "accountname" <email> <password>\n\n This creates a new account account.\n\n '}¶
+
+
@@ -195,6 +205,11 @@ version is a bit more complicated.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': 'qu q', 'category': 'general', 'key': 'quit', 'tags': '', 'text': '\n We maintain a different version of the `quit` command\n here for unconnected accounts for the sake of simplicity. The logged in\n version is a bit more complicated.\n '}¶
+
+
@@ -235,6 +250,11 @@ All it does is display the connect screen.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': 'l look', 'category': 'general', 'key': '__unloggedin_look_command', 'tags': '', 'text': '\n This is an unconnected version of the `look` command for simplicity.\n\n This is called by the server and kicks everything in gear.\n All it does is display the connect screen.\n '}¶
+
+
@@ -250,7 +270,7 @@ for simplicity. It shows a pane of info.
@@ -274,6 +294,11 @@ for simplicity. It shows a pane of info.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': '? h', 'category': 'general', 'key': 'help', 'tags': '', 'text': '\n This is an unconnected version of the help command,\n for simplicity. It shows a pane of info.\n '}¶
+
+
@@ -321,7 +346,6 @@ for simplicity. It shows a pane of info.
+search_index_entry = {'aliases': 'l ls', 'category': 'general', 'key': 'look', 'tags': '', 'text': '\n look\n\n Usage:\n look\n look <obj>\n look <room detail>\n look *<account>\n\n Observes your location, details at your location or objects in your vicinity.\n '}¶
+
+
@@ -357,6 +362,11 @@ version of the desc command.
lock_storage = 'cmd:perm(desc) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': 'describe', 'category': 'building', 'key': 'desc', 'tags': '', 'text': '\n `desc` - describe an object or room.\n\n Usage:\n desc[/switch] [<obj> =] <description>\n\n Switches for `desc`:\n spring - set description for <season> in current room.\n summer\n autumn\n winter\n\n Sets the "desc" attribute on an object. If an object is not given,\n describe the current room.\n\n You can also embed special time markers in your room description, like this:\n\n ```\n <night>In the darkness, the forest looks foreboding.</night>.\n ```\n\n Text marked this way will only display when the server is truly at the given\n timeslot. The available times are night, morning, afternoon and evening.\n\n Note that seasons and time-of-day slots only work on rooms in this\n version of the `desc` command.\n\n '}¶
+
+
@@ -415,6 +425,11 @@ to all the variables defined therein.
lock_storage = 'cmd:perm(Builder)'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'building', 'key': '@detail', 'tags': '', 'text': '\n sets a detail on a room\n\n Usage:\n @detail[/del] <key> [= <description>]\n @detail <key>;<alias>;... = description\n\n Example:\n @detail\n @detail walls = The walls are covered in ...\n @detail castle;ruin;tower = The distant ruin ...\n @detail/del wall\n @detail/del castle;ruin;tower\n\n This command allows to show the current room details if you enter it\n without any argument. Otherwise, sets or deletes a detail on the current\n room, if this room supports details like an extended room. To add new\n detail, just use the @detail command, specifying the key, an equal sign\n and the description. You can assign the same description to several\n details using the alias syntax (replace key by alias1;alias2;alias3;...).\n To remove one or several details, use the @detail/del switch.\n\n '}¶
+
+
@@ -458,6 +473,11 @@ to all the variables defined therein.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'time', 'tags': '', 'text': '\n Check the game time\n\n Usage:\n time\n\n Shows the current in-game time and season.\n '}¶
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'testmenu', 'tags': '', 'text': "\n This test command will initialize a menu that presents you with a form.\n You can fill out the fields of this form in any order, and then type in\n 'send' to send a message to another online player, which will reach them\n after a delay you specify.\n\n Usage:\n <field> = <new value>\n clear <field>\n help\n look\n quit\n send\n "}¶
-
diff --git a/docs/0.9.5/api/evennia.contrib.health_bar.html b/docs/0.9.5/api/evennia.contrib.health_bar.html
index e3662ed471..2749337f73 100644
--- a/docs/0.9.5/api/evennia.contrib.health_bar.html
+++ b/docs/0.9.5/api/evennia.contrib.health_bar.html
@@ -141,7 +141,6 @@ readers will be unable to read the graphical aspect of the bar.
-
diff --git a/docs/0.9.5/api/evennia.contrib.html b/docs/0.9.5/api/evennia.contrib.html
index fc7117bfd8..f13b345abd 100644
--- a/docs/0.9.5/api/evennia.contrib.html
+++ b/docs/0.9.5/api/evennia.contrib.html
@@ -105,7 +105,10 @@ useful but are deemed too game-specific to go into the core library.
-
diff --git a/docs/0.9.5/api/evennia.contrib.ingame_python.eventfuncs.html b/docs/0.9.5/api/evennia.contrib.ingame_python.eventfuncs.html
index d24125b113..a5363a745f 100644
--- a/docs/0.9.5/api/evennia.contrib.ingame_python.eventfuncs.html
+++ b/docs/0.9.5/api/evennia.contrib.ingame_python.eventfuncs.html
@@ -151,7 +151,6 @@ to be called from inside another event.
Note that this hook is called every time the server restarts
(including when it’s reloaded). This hook performs the following
@@ -440,7 +440,6 @@ restart only twice.
-
diff --git a/docs/0.9.5/api/evennia.contrib.ingame_python.utils.html b/docs/0.9.5/api/evennia.contrib.ingame_python.utils.html
index 575050c618..3182edf9c1 100644
--- a/docs/0.9.5/api/evennia.contrib.ingame_python.utils.html
+++ b/docs/0.9.5/api/evennia.contrib.ingame_python.utils.html
@@ -204,7 +204,6 @@ either “yes” or “okay” (maybe ‘say I don’t like it, but okay’).0.9.5 (v0.9.5 branch)
-
diff --git a/docs/0.9.5/api/evennia.contrib.mail.html b/docs/0.9.5/api/evennia.contrib.mail.html
index 67d4fb3510..adccb92052 100644
--- a/docs/0.9.5/api/evennia.contrib.mail.html
+++ b/docs/0.9.5/api/evennia.contrib.mail.html
@@ -218,6 +218,11 @@ the newly created mails.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': 'mail', 'category': 'general', 'key': '@mail', 'tags': '', 'text': '\n Communicate with others by sending mail.\n\n Usage:\n @mail - Displays all the mail an account has in their mailbox\n @mail <#> - Displays a specific message\n @mail <accounts>=<subject>/<message>\n - Sends a message to the comma separated list of accounts.\n @mail/delete <#> - Deletes a specific message\n @mail/forward <account list>=<#>[/<Message>]\n - Forwards an existing message to the specified list of accounts,\n original message is delivered with optional Message prepended.\n @mail/reply <#>=<message>\n - Replies to a message #. Prepends message to the original\n message text.\n Switches:\n delete - deletes a message\n forward - forward a received message to another object with an optional message attached.\n reply - Replies to a received message, appending the original message to the bottom.\n Examples:\n @mail 2\n @mail Griatch=New mail/Hey man, I am sending you a message!\n @mail/delete 6\n @mail/forward feend78 Griatch=4/You guys should read this.\n @mail/reply 9=Thanks for the info!\n\n '}¶
+
+
@@ -286,6 +291,11 @@ reply - Replies to a received message, appending the original message to the b
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': 'mail', 'category': 'general', 'key': '@mail', 'tags': '', 'text': '\n Communicate with others by sending mail.\n\n Usage:\n @mail - Displays all the mail an account has in their mailbox\n @mail <#> - Displays a specific message\n @mail <accounts>=<subject>/<message>\n - Sends a message to the comma separated list of accounts.\n @mail/delete <#> - Deletes a specific message\n @mail/forward <account list>=<#>[/<Message>]\n - Forwards an existing message to the specified list of accounts,\n original message is delivered with optional Message prepended.\n @mail/reply <#>=<message>\n - Replies to a message #. Prepends message to the original\n message text.\n Switches:\n delete - deletes a message\n forward - forward a received message to another object with an optional message attached.\n reply - Replies to a received message, appending the original message to the bottom.\n Examples:\n @mail 2\n @mail Griatch=New mail/Hey man, I am sending you a message!\n @mail/delete 6\n @mail/forward feend78 Griatch=4/You guys should read this.\n @mail/reply 9=Thanks for the info!\n\n '}¶
+
+
@@ -333,7 +343,6 @@ reply - Replies to a received message, appending the original message to the b
-
diff --git a/docs/0.9.5/api/evennia.contrib.multidescer.html b/docs/0.9.5/api/evennia.contrib.multidescer.html
index ddf50da9df..a82cb49109 100644
--- a/docs/0.9.5/api/evennia.contrib.multidescer.html
+++ b/docs/0.9.5/api/evennia.contrib.multidescer.html
@@ -116,6 +116,11 @@ description in use and db.multidesc to store all descriptions.<
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': 'desc', 'category': 'general', 'key': '+desc', 'tags': '', 'text': '\n Manage multiple descriptions\n\n Usage:\n +desc [key] - show current desc desc with <key>\n +desc <key> = <text> - add/replace desc with <key>\n +desc/list - list descriptions (abbreviated)\n +desc/list/full - list descriptions (full texts)\n +desc/edit <key> - add/edit desc <key> in line editor\n +desc/del <key> - delete desc <key>\n +desc/swap <key1>-<key2> - swap positions of <key1> and <key2> in list\n +desc/set <key> [+key+...] - set desc as default or combine multiple descs\n\n Notes:\n When combining multiple descs with +desc/set <key> + <key2> + ...,\n any keys not matching an actual description will be inserted\n as plain text. Use e.g. ansi line break ||/ to add a new\n paragraph and + + or ansi space ||_ to add extra whitespace.\n\n '}¶
+
+
@@ -163,7 +168,6 @@ description in use and db.multidesc to store all descriptions.<
-
diff --git a/docs/0.9.5/api/evennia.contrib.puzzles.html b/docs/0.9.5/api/evennia.contrib.puzzles.html
index 45c205d49f..c04043eddc 100644
--- a/docs/0.9.5/api/evennia.contrib.puzzles.html
+++ b/docs/0.9.5/api/evennia.contrib.puzzles.html
@@ -206,6 +206,11 @@ to all the variables defined therein.
lock_storage = 'cmd:perm(puzzle) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': '@puzzlerecipe', 'category': 'puzzles', 'key': '@puzzle', 'tags': '', 'text': "\n Creates a puzzle recipe. A puzzle consists of puzzle-parts that\n the player can 'use' together to create a specified result.\n\n Usage:\n @puzzle name,<part1[,part2,...>] = <result1[,result2,...]>\n\n Example:\n create/drop balloon\n create/drop glass of water\n create/drop water balloon\n @puzzle waterballon,balloon,glass of water = water balloon\n @del ballon, glass of water, water balloon\n @armpuzzle #1\n\n Notes:\n Each part and result are objects that must (temporarily) exist and be placed in their\n corresponding location in order to create the puzzle. After the creation of the puzzle,\n these objects are not needed anymore and can be deleted. Components of the puzzle\n will be re-created by use of the `@armpuzzle` command later.\n\n "}¶
+
+
@@ -269,6 +274,11 @@ to all the variables defined therein.
lock_storage = 'cmd:perm(puzzleedit) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'puzzles', 'key': '@puzzleedit', 'tags': '', 'text': "\n Edits puzzle properties\n\n Usage:\n @puzzleedit[/delete] <#dbref>\n @puzzleedit <#dbref>/use_success_message = <Custom message>\n @puzzleedit <#dbref>/use_success_location_message = <Custom message from {caller} producing {result_names}>\n @puzzleedit <#dbref>/mask = attr1[,attr2,...]>\n @puzzleedit[/addpart] <#dbref> = <obj[,obj2,...]>\n @puzzleedit[/delpart] <#dbref> = <obj[,obj2,...]>\n @puzzleedit[/addresult] <#dbref> = <obj[,obj2,...]>\n @puzzleedit[/delresult] <#dbref> = <obj[,obj2,...]>\n\n Switches:\n addpart - adds parts to the puzzle\n delpart - removes parts from the puzzle\n addresult - adds results to the puzzle\n delresult - removes results from the puzzle\n delete - deletes the recipe. Existing parts and results aren't modified\n\n mask - attributes to exclude during matching (e.g. location, desc, etc.)\n use_success_location_message containing {result_names} and {caller} will\n automatically be replaced with correct values. Both are optional.\n\n When removing parts/results, it's possible to remove all.\n\n "}¶
+
+
@@ -316,6 +326,11 @@ to all the variables defined therein.
lock_storage = 'cmd:perm(armpuzzle) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'puzzles', 'key': '@armpuzzle', 'tags': '', 'text': '\n Arms a puzzle by spawning all its parts.\n\n Usage:\n @armpuzzle <puzzle #dbref>\n\n Notes:\n Create puzzles with `@puzzle`; get list of\n defined puzzles using `@lspuzzlerecipes`.\n\n '}¶
+
+
@@ -364,6 +379,11 @@ to all the variables defined therein.
lock_storage = 'cmd:pperm(use) or pperm(Player)'¶
+
+
+search_index_entry = {'aliases': 'combine', 'category': 'puzzles', 'key': 'use', 'tags': '', 'text': '\n Use an object, or a group of objects at once.\n\n\n Example:\n You look around you and see a pole, a long string, and a needle.\n\n use pole, long string, needle\n\n Genius! You built a fishing pole.\n\n\n Usage:\n use <obj1> [,obj2,...]\n '}¶
+
+
@@ -408,6 +428,11 @@ to all the variables defined therein.
lock_storage = 'cmd:perm(lspuzzlerecipes) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'puzzles', 'key': '@lspuzzlerecipes', 'tags': '', 'text': '\n Searches for all puzzle recipes\n\n Usage:\n @lspuzzlerecipes\n '}¶
+
+
@@ -452,6 +477,11 @@ to all the variables defined therein.
lock_storage = 'cmd:perm(lsarmedpuzzles) or perm(Builder)'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'puzzles', 'key': '@lsarmedpuzzles', 'tags': '', 'text': '\n Searches for all armed puzzles\n\n Usage:\n @lsarmedpuzzles\n '}¶
-
diff --git a/docs/0.9.5/api/evennia.contrib.rplanguage.html b/docs/0.9.5/api/evennia.contrib.rplanguage.html
index e6ed24c56c..73926c618a 100644
--- a/docs/0.9.5/api/evennia.contrib.rplanguage.html
+++ b/docs/0.9.5/api/evennia.contrib.rplanguage.html
@@ -98,18 +98,46 @@ words compared to the original and can help change the “feel” for
the language you are creating. You can also add your own
dictionary and “fix” random words for a list of input words.
Below is an example of “elvish”, using “rounder” vowels and sounds:
-
phonemes="oi oh ee ae aa eh ah ao aw ay er ey ow ia ih iy ""oy ua uh uw y p b t d f v t dh s z sh zh ch jh k ""ng g m n l r w",
+
# vowel/consonant grammar possibilities
+grammar=("v vv vvc vcc vvcc cvvc vccv vvccv vcvccv vcvcvcc vvccvvcc "
+ "vcvvccvvc cvcvvcvvcc vcvcvvccvcvv")
+
+# all not in this group is considered a consonantvowels="eaoiuy"
-grammar="v vv vvc vcc vvcc cvvc vccv vvccv vcvccv vcvcvcc vvccvvcc ""vcvvccvvc cvcvvcvvcc vcvcvvccvcvv",
+
+# you need a representative of all of the minimal grammars here, so if a
+# grammar v exists, there must be atleast one phoneme available with only
+# one vowel in it
+phonemes=("oi oh ee ae aa eh ah ao aw ay er ey ow ia ih iy "
+ "oy ua uh uw y p b t d f v t dh s z sh zh ch jh k "
+ "ng g m n l r w")
+
+# how much the translation varies in length compared to the original. 0 is
+# smallest, higher values give ever bigger randomness (including removing
+# short words entirely)word_length_variance=1
+
+# if a proper noun (word starting with capitalized letter) should be
+# translated or not. If not (default) it means e.g. names will remain
+# unchanged across languages.
+noun_translate=False
+
+# all proper nouns (words starting with a capital letter not at the beginning
+# of a sentence) can have either a postfix or -prefix added at all timesnoun_postfix="'la"
+
+# words in dict will always be translated this way. The 'auto_translations'
+# is instead a list or filename to file with words to use to help build a
+# bigger dictionary by creating random translations of each word in the
+# list *once* and saving the result for subsequent use.manual_translations={"the":"y'e","we":"uyi","she":"semi","he":"emi","you":"do",'me':'mi','i':'me','be':"hy'e",'and':'y'}rplanguage.add_language(key="elvish",phonemes=phonemes,grammar=grammar,word_length_variance=word_length_variance,
+ noun_translate=noun_translate,noun_postfix=noun_postfix,vowels=vowels,
- manual_translations=manual_translations
+ manual_translations=manual_translations,auto_translations="my_word_file.txt")
@@ -190,9 +218,13 @@ cvcvccc would be c+v+c+v+cc+c (a word like ‘galosch’).
0 means a minimal variance, higher variance may mean words
have wildly varying length; this strongly affects how the
language “looks”.
-
noun_translate (bool, optional) – If a proper noun, identified as a
-capitalized word, should be translated or not. By default they
-will not, allowing for e.g. the names of characters to be understandable.
+
noun_translate (bool, optional) – If a proper noun should be translated or
+not. By default they will not, allowing for e.g. the names of characters
+to be understandable. A ‘noun’ is identified as a capitalized word
+not at the start of a sentence. This simple metric means that names
+starting a sentence always will be translated (- but hey, maybe
+the fantasy language just never uses a noun at the beginning of
+sentences, who knows?)
noun_prefix (str, optional) – A prefix to go before every noun
in this language (if any).
noun_postfix (str, optuonal) – A postfix to go after every noun
@@ -379,7 +411,6 @@ means fully obscured.
@@ -604,6 +612,11 @@ a different language.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': ':', 'category': 'general', 'key': 'emote', 'tags': '', 'text': '\n Emote an action, allowing dynamic replacement of\n text in the emote.\n\n Usage:\n emote text\n\n Example:\n emote /me looks around.\n emote With a flurry /me attacks /tall man with his sword.\n emote "Hello", /me says.\n\n Describes an event in the world. This allows the use of /ref\n markers to replace with the short descriptions or recognized\n strings of objects in the same room. These will be translated to\n emotes to match each person seeing it. Use "..." for saying\n things and langcode"..." without spaces to say something in\n a different language.\n\n '}¶
@@ -647,6 +660,11 @@ a different language.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': '\' "', 'category': 'general', 'key': 'say', 'tags': '', 'text': '\n speak as your character\n\n Usage:\n say <message>\n\n Talk to those in your current location.\n '}¶
+
+
@@ -690,6 +708,11 @@ a different language.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'sdesc', 'tags': '', 'text': '\n Assign yourself a short description (sdesc).\n\n Usage:\n sdesc <short description>\n\n Assigns a short description to yourself.\n\n '}¶
+
+
@@ -748,6 +771,11 @@ sdesc in the emote, regardless of who is seeing it.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'pose', 'tags': '', 'text': "\n Set a static pose\n\n Usage:\n pose <pose>\n pose default <pose>\n pose reset\n pose obj = <pose>\n pose default obj = <pose>\n pose reset obj =\n\n Examples:\n pose leans against the tree\n pose is talking to the barkeep.\n pose box = is sitting on the floor.\n\n Set a static pose. This is the end of a full sentence that starts\n with your sdesc. If no full stop is given, it will be added\n automatically. The default pose is the pose you get when using\n pose reset. Note that you can use sdescs/recogs to reference\n people in your pose, but these always appear as that person's\n sdesc in the emote, regardless of who is seeing it.\n\n "}¶
+
+
@@ -798,6 +826,11 @@ Using the command without arguments will list all current recogs.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': 'recognize forget', 'category': 'general', 'key': 'recog', 'tags': '', 'text': '\n Recognize another person in the same room.\n\n Usage:\n recog\n recog sdesc as alias\n forget alias\n\n Example:\n recog tall man as Griatch\n forget griatch\n\n This will assign a personal alias for a person, or forget said alias.\n Using the command without arguments will list all current recogs.\n\n '}¶
+
+
@@ -842,6 +875,11 @@ set in self.parse())
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': 'unmask', 'category': 'general', 'key': 'mask', 'tags': '', 'text': "\n Wear a mask\n\n Usage:\n mask <new sdesc>\n unmask\n\n This will put on a mask to hide your identity. When wearing\n a mask, your sdesc will be replaced by the sdesc you pick and\n people's recognitions of you will be disabled.\n\n "}¶
+
+
@@ -1250,7 +1288,6 @@ the evennia.contrib.rplanguage module.
-
diff --git a/docs/0.9.5/api/evennia.contrib.security.auditing.outputs.html b/docs/0.9.5/api/evennia.contrib.security.auditing.outputs.html
index 88b9255ebe..eb7ef25c07 100644
--- a/docs/0.9.5/api/evennia.contrib.security.auditing.outputs.html
+++ b/docs/0.9.5/api/evennia.contrib.security.auditing.outputs.html
@@ -126,7 +126,6 @@ compromised or taken down, losing your logs along with it is no help!).
+search_index_entry = {'aliases': '', 'category': 'building', 'key': 'open', 'tags': '', 'text': '\n open a new exit from the current room\n\n Usage:\n open <new exit>[;alias;alias..][:typeclass] [,<return exit>[;alias;..][:typeclass]]] = <destination>\n\n Handles the creation of exits. If a destination is given, the exit\n will point there. The <return exit> argument sets up an exit at the\n destination leading back to the current room. Destination name\n can be given both as a #dbref and a name, if that name is globally\n unique.\n\n '}¶
+
+
@@ -223,6 +228,11 @@ close <door>
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': 'close', 'category': 'general', 'key': 'open', 'tags': '', 'text': '\n Open and close a door\n\n Usage:\n open <door>\n close <door>\n\n '}¶
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'setspeed', 'tags': '', 'text': "\n set your movement speed\n\n Usage:\n setspeed stroll|walk|run|sprint\n\n This will set your movement speed, determining how long time\n it takes to traverse exits. If no speed is set, 'walk' speed\n is assumed.\n "}¶
+
+
@@ -173,6 +178,11 @@ stored deferred from the exit traversal above.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'stop', 'tags': '', 'text': '\n stop moving\n\n Usage:\n stop\n\n Stops the current movement, if any.\n '}¶
+
+
@@ -220,7 +230,6 @@ stored deferred from the exit traversal above.
-
diff --git a/docs/0.9.5/api/evennia.contrib.talking_npc.html b/docs/0.9.5/api/evennia.contrib.talking_npc.html
index 4650e2be3c..1a9a8ab6e7 100644
--- a/docs/0.9.5/api/evennia.contrib.talking_npc.html
+++ b/docs/0.9.5/api/evennia.contrib.talking_npc.html
@@ -121,6 +121,11 @@ that NPC and give you options on what to talk about.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'talk', 'tags': '', 'text': '\n Talks to an npc\n\n Usage:\n talk\n\n This command is only available if a talkative non-player-character\n (NPC) is actually present. It will strike up a conversation with\n that NPC and give you options on what to talk about.\n '}¶
+
+
@@ -227,7 +232,6 @@ the conversation defined above.
-
diff --git a/docs/0.9.5/api/evennia.contrib.tree_select.html b/docs/0.9.5/api/evennia.contrib.tree_select.html
index 0a6bb538a9..68cf9ad4ac 100644
--- a/docs/0.9.5/api/evennia.contrib.tree_select.html
+++ b/docs/0.9.5/api/evennia.contrib.tree_select.html
@@ -370,6 +370,11 @@ set in self.parse())
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'namecolor', 'tags': '', 'text': '\n Set or remove a special color on your name. Just an example for the\n easy menu selection tree contrib.\n '}¶
+
+
@@ -434,7 +439,6 @@ to determine the color the player chose.
-
diff --git a/docs/0.9.5/api/evennia.contrib.turnbattle.tb_basic.html b/docs/0.9.5/api/evennia.contrib.turnbattle.tb_basic.html
index 20175fbd6b..a0a39adf74 100644
--- a/docs/0.9.5/api/evennia.contrib.turnbattle.tb_basic.html
+++ b/docs/0.9.5/api/evennia.contrib.turnbattle.tb_basic.html
@@ -501,6 +501,11 @@ When it’s your turn, you can attack other characters.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'combat', 'key': 'fight', 'tags': '', 'text': "\n Starts a fight with everyone in the same room as you.\n\n Usage:\n fight\n\n When you start a fight, everyone in the room who is able to\n fight is added to combat, and a turn order is randomly rolled.\n When it's your turn, you can attack other characters.\n "}¶
+
+
@@ -540,6 +545,11 @@ a chance to hit, and if successful, will deal damage.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'combat', 'key': 'attack', 'tags': '', 'text': '\n Attacks another character.\n\n Usage:\n attack <target>\n\n When in a fight, you may attack another character. The attack has\n a chance to hit, and if successful, will deal damage.\n '}¶
+
+
@@ -579,6 +589,11 @@ if there are still any actions you can take.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': 'wait hold', 'category': 'combat', 'key': 'pass', 'tags': '', 'text': '\n Passes on your turn.\n\n Usage:\n pass\n\n When in a fight, you can use this command to end your turn early, even\n if there are still any actions you can take.\n '}¶
+search_index_entry = {'aliases': 'spare', 'category': 'combat', 'key': 'disengage', 'tags': '', 'text': "\n Passes your turn and attempts to end combat.\n\n Usage:\n disengage\n\n Ends your turn early and signals that you're trying to end\n the fight. If all participants in a fight disengage, the\n fight ends.\n "}¶
+
+
@@ -658,6 +678,11 @@ rest if you’re not in a fight.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'combat', 'key': 'rest', 'tags': '', 'text': "\n Recovers damage.\n\n Usage:\n rest\n\n Resting recovers your HP to its maximum, but you can only\n rest if you're not in a fight.\n "}¶
+
+
@@ -699,6 +724,11 @@ topics related to the game.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': '?', 'category': 'general', 'key': 'help', 'tags': '', 'text': '\n View help or a list of topics\n\n Usage:\n help <topic or command>\n help list\n help all\n\n This will search for help on commands and other\n topics related to the game.\n '}¶
-
diff --git a/docs/0.9.5/api/evennia.contrib.turnbattle.tb_equip.html b/docs/0.9.5/api/evennia.contrib.turnbattle.tb_equip.html
index 4eac6b6cf7..63d9742d2d 100644
--- a/docs/0.9.5/api/evennia.contrib.turnbattle.tb_equip.html
+++ b/docs/0.9.5/api/evennia.contrib.turnbattle.tb_equip.html
@@ -618,6 +618,11 @@ When it’s your turn, you can attack other characters.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'combat', 'key': 'fight', 'tags': '', 'text': "\n Starts a fight with everyone in the same room as you.\n\n Usage:\n fight\n\n When you start a fight, everyone in the room who is able to\n fight is added to combat, and a turn order is randomly rolled.\n When it's your turn, you can attack other characters.\n "}¶
+
+
@@ -657,6 +662,11 @@ a chance to hit, and if successful, will deal damage.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'combat', 'key': 'attack', 'tags': '', 'text': '\n Attacks another character.\n\n Usage:\n attack <target>\n\n When in a fight, you may attack another character. The attack has\n a chance to hit, and if successful, will deal damage.\n '}¶
+
+
@@ -696,6 +706,11 @@ if there are still any actions you can take.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': 'wait hold', 'category': 'combat', 'key': 'pass', 'tags': '', 'text': '\n Passes on your turn.\n\n Usage:\n pass\n\n When in a fight, you can use this command to end your turn early, even\n if there are still any actions you can take.\n '}¶
+search_index_entry = {'aliases': 'spare', 'category': 'combat', 'key': 'disengage', 'tags': '', 'text': "\n Passes your turn and attempts to end combat.\n\n Usage:\n disengage\n\n Ends your turn early and signals that you're trying to end\n the fight. If all participants in a fight disengage, the\n fight ends.\n "}¶
+
+
@@ -775,6 +795,11 @@ rest if you’re not in a fight.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'combat', 'key': 'rest', 'tags': '', 'text': "\n Recovers damage.\n\n Usage:\n rest\n\n Resting recovers your HP to its maximum, but you can only\n rest if you're not in a fight.\n "}¶
+
+
@@ -816,6 +841,11 @@ topics related to the game.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': '?', 'category': 'general', 'key': 'help', 'tags': '', 'text': '\n View help or a list of topics\n\n Usage:\n help <topic or command>\n help list\n help all\n\n This will search for help on commands and other\n topics related to the game.\n '}¶
+
+
@@ -859,6 +889,11 @@ currently wielding.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'combat', 'key': 'wield', 'tags': '', 'text': '\n Wield a weapon you are carrying\n\n Usage:\n wield <weapon>\n\n Select a weapon you are carrying to wield in combat. If\n you are already wielding another weapon, you will switch\n to the weapon you specify instead. Using this command in\n combat will spend your action for your turn. Use the\n "unwield" command to stop wielding any weapon you are\n currently wielding.\n '}¶
+
+
@@ -898,6 +933,11 @@ weapon you are currently wielding and become unarmed.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'combat', 'key': 'unwield', 'tags': '', 'text': '\n Stop wielding a weapon.\n\n Usage:\n unwield\n\n After using this command, you will stop wielding any\n weapon you are currently wielding and become unarmed.\n '}¶
+
+
@@ -938,6 +978,11 @@ command to remove any armor you are wearing.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'combat', 'key': 'don', 'tags': '', 'text': '\n Don armor that you are carrying\n\n Usage:\n don <armor>\n\n Select armor to wear in combat. You can\'t use this\n command in the middle of a fight. Use the "doff"\n command to remove any armor you are wearing.\n '}¶
+
+
@@ -978,6 +1023,11 @@ You can’t use this command in combat.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'combat', 'key': 'doff', 'tags': '', 'text': "\n Stop wearing armor.\n\n Usage:\n doff\n\n After using this command, you will stop wearing any\n armor you are currently using and become unarmored.\n You can't use this command in combat.\n "}¶
+
+
@@ -1048,7 +1098,6 @@ You can’t use this command in combat.
-
diff --git a/docs/0.9.5/api/evennia.contrib.turnbattle.tb_items.html b/docs/0.9.5/api/evennia.contrib.turnbattle.tb_items.html
index 7f38ac5186..879c0e3e8d 100644
--- a/docs/0.9.5/api/evennia.contrib.turnbattle.tb_items.html
+++ b/docs/0.9.5/api/evennia.contrib.turnbattle.tb_items.html
@@ -652,6 +652,11 @@ When it’s your turn, you can attack other characters.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'combat', 'key': 'fight', 'tags': '', 'text': "\n Starts a fight with everyone in the same room as you.\n\n Usage:\n fight\n\n When you start a fight, everyone in the room who is able to\n fight is added to combat, and a turn order is randomly rolled.\n When it's your turn, you can attack other characters.\n "}¶
+
+
@@ -691,6 +696,11 @@ a chance to hit, and if successful, will deal damage.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'combat', 'key': 'attack', 'tags': '', 'text': '\n Attacks another character.\n\n Usage:\n attack <target>\n\n When in a fight, you may attack another character. The attack has\n a chance to hit, and if successful, will deal damage.\n '}¶
+
+
@@ -730,6 +740,11 @@ if there are still any actions you can take.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': 'wait hold', 'category': 'combat', 'key': 'pass', 'tags': '', 'text': '\n Passes on your turn.\n\n Usage:\n pass\n\n When in a fight, you can use this command to end your turn early, even\n if there are still any actions you can take.\n '}¶
+search_index_entry = {'aliases': 'spare', 'category': 'combat', 'key': 'disengage', 'tags': '', 'text': "\n Passes your turn and attempts to end combat.\n\n Usage:\n disengage\n\n Ends your turn early and signals that you're trying to end\n the fight. If all participants in a fight disengage, the\n fight ends.\n "}¶
+
+
@@ -809,6 +829,11 @@ rest if you’re not in a fight.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'combat', 'key': 'rest', 'tags': '', 'text': "\n Recovers damage.\n\n Usage:\n rest\n\n Resting recovers your HP to its maximum, but you can only\n rest if you're not in a fight.\n "}¶
+
+
@@ -850,6 +875,11 @@ topics related to the game.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': '?', 'category': 'general', 'key': 'help', 'tags': '', 'text': '\n View help or a list of topics\n\n Usage:\n help <topic or command>\n help list\n help all\n\n This will search for help on commands and other\n topics related to the game.\n '}¶
+
+
@@ -890,6 +920,11 @@ to attack others, and as such can only be used in combat.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'combat', 'key': 'use', 'tags': '', 'text': '\n Use an item.\n\n Usage:\n use <item> [= target]\n\n An item can have various function - looking at the item may\n provide information as to its effects. Some items can be used\n to attack others, and as such can only be used in combat.\n '}¶
+
+
@@ -1043,7 +1078,6 @@ items using the same function work differently.
-
diff --git a/docs/0.9.5/api/evennia.contrib.turnbattle.tb_magic.html b/docs/0.9.5/api/evennia.contrib.turnbattle.tb_magic.html
index bccc5d630d..6d63039b72 100644
--- a/docs/0.9.5/api/evennia.contrib.turnbattle.tb_magic.html
+++ b/docs/0.9.5/api/evennia.contrib.turnbattle.tb_magic.html
@@ -524,6 +524,11 @@ When it’s your turn, you can attack other characters.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'combat', 'key': 'fight', 'tags': '', 'text': "\n Starts a fight with everyone in the same room as you.\n\n Usage:\n fight\n\n When you start a fight, everyone in the room who is able to\n fight is added to combat, and a turn order is randomly rolled.\n When it's your turn, you can attack other characters.\n "}¶
+
+
@@ -563,6 +568,11 @@ a chance to hit, and if successful, will deal damage.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'combat', 'key': 'attack', 'tags': '', 'text': '\n Attacks another character.\n\n Usage:\n attack <target>\n\n When in a fight, you may attack another character. The attack has\n a chance to hit, and if successful, will deal damage.\n '}¶
+
+
@@ -602,6 +612,11 @@ if there are still any actions you can take.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': 'wait hold', 'category': 'combat', 'key': 'pass', 'tags': '', 'text': '\n Passes on your turn.\n\n Usage:\n pass\n\n When in a fight, you can use this command to end your turn early, even\n if there are still any actions you can take.\n '}¶
+search_index_entry = {'aliases': 'spare', 'category': 'combat', 'key': 'disengage', 'tags': '', 'text': "\n Passes your turn and attempts to end combat.\n\n Usage:\n disengage\n\n Ends your turn early and signals that you're trying to end\n the fight. If all participants in a fight disengage, the\n fight ends.\n "}¶
+search_index_entry = {'aliases': '', 'category': 'magic', 'key': 'learnspell', 'tags': '', 'text': "\n Learn a magic spell.\n\n Usage:\n learnspell <spell name>\n\n Adds a spell by name to your list of spells known.\n\n The following spells are provided as examples:\n\n |wmagic missile|n (3 MP): Fires three missiles that never miss. Can target\n up to three different enemies.\n\n |wflame shot|n (3 MP): Shoots a high-damage jet of flame at one target.\n\n |wcure wounds|n (5 MP): Heals damage on one target.\n\n |wmass cure wounds|n (10 MP): Like 'cure wounds', but can heal up to 5\n targets at once.\n\n |wfull heal|n (12 MP): Heals one target back to full HP.\n\n |wcactus conjuration|n (2 MP): Creates a cactus.\n "}¶
+search_index_entry = {'aliases': '', 'category': 'magic', 'key': 'cast', 'tags': '', 'text': "\n Cast a magic spell that you know, provided you have the MP\n to spend on its casting.\n\n Usage:\n cast <spellname> [= <target1>, <target2>, etc...]\n\n Some spells can be cast on multiple targets, some can be cast\n on only yourself, and some don't need a target specified at all.\n Typing 'cast' by itself will give you a list of spells you know.\n "}¶
+
+
@@ -781,6 +811,11 @@ only rest if you’re not in a fight.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'combat', 'key': 'rest', 'tags': '', 'text': "\n Recovers damage and restores MP.\n\n Usage:\n rest\n\n Resting recovers your HP and MP to their maximum, but you can\n only rest if you're not in a fight.\n "}¶
+
+
@@ -820,6 +855,11 @@ other targets in combat.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'combat', 'key': 'status', 'tags': '', 'text': '\n Gives combat information.\n\n Usage:\n status\n\n Shows your current and maximum HP and your distance from\n other targets in combat.\n '}¶
+
+
@@ -861,6 +901,11 @@ topics related to the game.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': '?', 'category': 'general', 'key': 'help', 'tags': '', 'text': '\n View help or a list of topics\n\n Usage:\n help <topic or command>\n help list\n help all\n\n This will search for help on commands and other\n topics related to the game.\n '}¶
+
+
@@ -982,7 +1027,6 @@ instead of creating objects directly.
-
diff --git a/docs/0.9.5/api/evennia.contrib.turnbattle.tb_range.html b/docs/0.9.5/api/evennia.contrib.turnbattle.tb_range.html
index d7e6be2f4e..3207b565ef 100644
--- a/docs/0.9.5/api/evennia.contrib.turnbattle.tb_range.html
+++ b/docs/0.9.5/api/evennia.contrib.turnbattle.tb_range.html
@@ -815,6 +815,11 @@ When it’s your turn, you can attack other characters.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'combat', 'key': 'fight', 'tags': '', 'text': "\n Starts a fight with everyone in the same room as you.\n\n Usage:\n fight\n\n When you start a fight, everyone in the room who is able to\n fight is added to combat, and a turn order is randomly rolled.\n When it's your turn, you can attack other characters.\n "}¶
+
+
@@ -856,6 +861,11 @@ you. Use the ‘approach’ command to get closer to a target.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'combat', 'key': 'attack', 'tags': '', 'text': "\n Attacks another character in melee.\n\n Usage:\n attack <target>\n\n When in a fight, you may attack another character. The attack has\n a chance to hit, and if successful, will deal damage. You can only\n attack engaged targets - that is, targets that are right next to\n you. Use the 'approach' command to get closer to a target.\n "}¶
+search_index_entry = {'aliases': '', 'category': 'combat', 'key': 'shoot', 'tags': '', 'text': "\n Attacks another character from range.\n\n Usage:\n shoot <target>\n\n When in a fight, you may shoot another character. The attack has\n a chance to hit, and if successful, will deal damage. You can attack\n any target in combat by shooting, but can't shoot if there are any\n targets engaged with you. Use the 'withdraw' command to retreat from\n nearby enemies.\n "}¶
+
+
@@ -937,6 +952,11 @@ characters you are 0 spaces away from.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'combat', 'key': 'approach', 'tags': '', 'text': '\n Approaches an object.\n\n Usage:\n approach <target>\n\n Move one space toward a character or object. You can only attack\n characters you are 0 spaces away from.\n '}¶
+
+
@@ -975,6 +995,11 @@ characters you are 0 spaces away from.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'combat', 'key': 'withdraw', 'tags': '', 'text': '\n Moves away from an object.\n\n Usage:\n withdraw <target>\n\n Move one space away from a character or object.\n '}¶
+
+
@@ -1014,6 +1039,11 @@ if there are still any actions you can take.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': 'wait hold', 'category': 'combat', 'key': 'pass', 'tags': '', 'text': '\n Passes on your turn.\n\n Usage:\n pass\n\n When in a fight, you can use this command to end your turn early, even\n if there are still any actions you can take.\n '}¶
+search_index_entry = {'aliases': 'spare', 'category': 'combat', 'key': 'disengage', 'tags': '', 'text': "\n Passes your turn and attempts to end combat.\n\n Usage:\n disengage\n\n Ends your turn early and signals that you're trying to end\n the fight. If all participants in a fight disengage, the\n fight ends.\n "}¶
+
+
@@ -1093,6 +1128,11 @@ rest if you’re not in a fight.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'combat', 'key': 'rest', 'tags': '', 'text': "\n Recovers damage.\n\n Usage:\n rest\n\n Resting recovers your HP to its maximum, but you can only\n rest if you're not in a fight.\n "}¶
+
+
@@ -1132,6 +1172,11 @@ other targets in combat.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'combat', 'key': 'status', 'tags': '', 'text': '\n Gives combat information.\n\n Usage:\n status\n\n Shows your current and maximum HP and your distance from\n other targets in combat.\n '}¶
+
+
@@ -1173,6 +1218,11 @@ topics related to the game.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': '?', 'category': 'general', 'key': 'help', 'tags': '', 'text': '\n View help or a list of topics\n\n Usage:\n help <topic or command>\n help list\n help all\n\n This will search for help on commands and other\n topics related to the game.\n '}¶
-
diff --git a/docs/0.9.5/api/evennia.contrib.tutorial_examples.bodyfunctions.html b/docs/0.9.5/api/evennia.contrib.tutorial_examples.bodyfunctions.html
index da711e0725..743f45ccd3 100644
--- a/docs/0.9.5/api/evennia.contrib.tutorial_examples.bodyfunctions.html
+++ b/docs/0.9.5/api/evennia.contrib.tutorial_examples.bodyfunctions.html
@@ -139,7 +139,6 @@ a random check here so as to only return 33% of the time.
This defines the cmdset for the red_button. Here we have defined
-the commands and the cmdset in the same module, but if you
-have many different commands to merge it is often better
-to define the cmdset separately, picking and choosing from
-among the available commands as to what should be included in the
-cmdset - this way you can often re-use the commands too.
Note that we choose to implement this with checking for
-if the lid is open/closed. This is because this command
-is likely to be tried regardless of the state of the lid.
-
An alternative would be to make two versions of this command
-and tuck them into the cmdset linked to the Open and Closed
-lid-state respectively.
The default cmdset always sits
-on the button object and whereas other
-command sets may be added/merge onto it
-and hide it, removing them will always
-bring it back. It’s added to the object
-using obj.cmdset.add_default().
It contains the commands that launches the other
-command sets, making the red button a self-contained
-item (i.e. you don’t have to manually add any
-scripts etc to it when creating it).
-
diff --git a/docs/0.9.5/api/evennia.contrib.tutorial_examples.red_button.html b/docs/0.9.5/api/evennia.contrib.tutorial_examples.red_button.html
index 6158de0a2b..a52fafef8b 100644
--- a/docs/0.9.5/api/evennia.contrib.tutorial_examples.red_button.html
+++ b/docs/0.9.5/api/evennia.contrib.tutorial_examples.red_button.html
@@ -44,74 +44,627 @@ script.examples as well as commands.examples to make an interactive
button typeclass.
Create this button with
-
@create/drop examples.red_button.RedButton
+
create/drop red_button.RedButton
Note that you must drop the button before you can see its messages!
The button’s functionality is controlled by CmdSets that gets added and removed
+depending on the ‘state’ the button is in.
+
+
Lid-closed state: In this state the button is covered by a glass cover and trying
+to ‘push’ it will fail. You can ‘nudge’, ‘smash’ or ‘open’ the lid.
+
Lid-open state: In this state the lid is open but will close again after a certain
+time. Using ‘push’ now will press the button and trigger the Blind-state.
+
Blind-state: In this mode you are blinded by a bright flash. This will affect your
+normal commands like ‘look’ and help until the blindness wears off after a certain
+time.
+
+
Timers are handled by persistent delays on the button. These are examples of
+evennia.utils.utils.delay calls that wait a certain time before calling a method -
+such as when closing the lid and un-blinding a character.
This is the version of push used when the lid is closed.
+
An alternative would be to make a ‘push’ command in a default cmdset
+that is always available on the button and then use if-statements to
+check if the lid is open or closed.
+search_index_entry = {'aliases': 'nudge', 'category': 'general', 'key': 'nudge lid', 'tags': '', 'text': "\n Try to nudge the button's lid.\n\n Usage:\n nudge lid\n\n This command will have you try to push the lid of the button away.\n\n "}¶
It contains the commands that launches the other
+command sets, making the red button a self-contained
+item (i.e. you don’t have to manually add any
+scripts etc to it when creating it).
+
Note that this is given with a key_mergetype set. This
+is set up so that the cmdset with merge with Union merge type
+except if the other cmdset to merge with is LidOpenCmdSet,
+in which case it will Replace that. So these two cmdsets will
+be mutually exclusive.
This is the actual executing part of the command. It is
+called directly after self.parse(). See the docstring of this
+module for which object properties are available (beyond those
+set in self.parse())
+search_index_entry = {'aliases': 'close', 'category': 'general', 'key': 'close lid', 'tags': '', 'text': '\n Close the lid\n\n Usage:\n close lid\n\n Closes the lid of the red button.\n '}¶
Note that this is given with a key_mergetype set. This
+is set up so that the cmdset with merge with Union merge type
+except if the other cmdset to merge with is LidClosedCmdSet,
+in which case it will Replace that. So these two cmdsets will
+be mutually exclusive.
+search_index_entry = {'aliases': 'get l feel listen examine ex', 'category': 'general', 'key': 'look', 'tags': '', 'text': "\n Looking around in darkness\n\n Usage:\n look <obj>\n\n ... not that there's much to see in the dark.\n\n "}¶
+search_index_entry = {'aliases': 'h', 'category': 'general', 'key': 'help', 'tags': '', 'text': '\n Help function while in the blinded state\n\n Usage:\n help\n\n '}¶
This is the cmdset added to the account when
+the button is pushed.
+
Since this has mergetype Replace it will completely remove the commands of
+all other cmdsets while active. To allow some limited interaction
+(pose/say) we import those default commands and add them too.
+
We also disable all exit-commands generated by exits and
+object-interactions while blinded by setting no_exits and no_objs flags
+on the cmdset. This is to avoid the player walking off or interfering with
+other objects while blinded. Account-level commands however (channel messaging
+etc) will not be affected by the blinding.
This class describes an evil red button. It will use the script
-definition in contrib/examples/red_button_scripts to blink at regular
-intervals. It also uses a series of script and commands to handle
-pushing the button and causing effects when doing so.
-
-
The following attributes can be set on the button:
desc_lid_open - description when lid is open
-desc_lid_closed - description when lid is closed
-desc_lamp_broken - description when lamp is broken
-
-
+
This class describes an evil red button. It will blink invitingly and
+temporarily blind whomever presses it.
+
The button can take a few optional attributes controlling how things will
+be displayed in its various states. This is a useful way to give builders
+the option to customize a complex object from in-game. Actual return messages
+to event-actions are (in this example) left with each command, but one could
+also imagine having those handled via Attributes as well, if one wanted a
+completely in-game customizable button without needing to tweak command
+classes.
+
Attributes:
+- desc_closed_lid: This is the description to show of the button
+
+
when the lid is closed.
+
+
+
desc_open_lid”: Shown when the lid is open
+
auto_close_msg: Message to show when lid auto-closes
+
desc_add_lamp_broken: Extra desc-line added after normal desc when lamp
+is broken.
+
blink_msg: A list of strings to randomly choose from when the lamp
+blinks.
+
+
Notes:
+The button starts with lid closed. To set the initial description,
+you can either set desc after creating it or pass a desc attribute
+when creating it, such as
+button = create_object(RedButton, …, attributes=[(‘desc’, ‘my desc’)]).
+
+
+desc_closed_lid = 'This is a large red button, inviting yet evil-looking. A closed glass lid protects it.'¶
+
+
+
+
+desc_open_lid = 'This is a large red button, inviting yet evil-looking. Its glass cover is open and the button exposed.'¶
+
+
+
+
+auto_close_msg = "The button's glass lid silently slides back in place."¶
+
+
+
+
+lamp_breaks_msg = 'The lamp flickers, the button going dark.'¶
+
+
+
+
+desc_add_lamp_broken = '\nThe big red button has stopped blinking for the time being.'¶
+
+
+
+
+blink_msgs = ['The red button flashes briefly.', 'The red button blinks invitingly.', 'The red button flashes. You know you wanna push it!']¶
msg (str, optional) – If given, display a message to the room
+
lid closes. (when) –
+
+
+
+
This will first try to get the Attribute (self.db.desc_closed_lid) in
+case it was set by a builder and if that was None, it will fall back to
+self.desc_closed_lid, the default description (note that lack of .db).
Close the glass lid. This validates all scripts on the button,
-which means that scripts only being valid when the lid is open
-will go away automatically.
The script system will regularly call this
-function to make the button blink. Now and then
-it won’t blink at all though, to add some randomness
-to how often the message is echoed.
These are scripts intended for a particular object - the
-red_button object type in contrib/examples. A few variations
-on uses of scripts are included.
This manages the cmdset for the “closed” button state. What this
-means is that while this script is valid, we add the RedButtonClosed
-cmdset to it (with commands like open, nudge lid etc)
This is called once every server restart, so we want to add the
-(memory-resident) cmdset to the object here. is_valid is automatically
-checked so we don’t need to worry about adding the script to an
-open lid.
This is called once every server restart, so we want to add the
-(memory-resident) cmdset to the object here. is_valid is
-automatically checked, so we don’t need to worry about
-adding the cmdset to a closed lid-button.
This adds a (very limited) cmdset TO THE ACCOUNT, during a certain time,
-after which the script will close and all functions are
-restored. It’s up to the function starting the script to actually
-set it on the right account object.
Note that the RedButtonBlind cmdset is defined to completly
-replace the other cmdsets on the stack while it is active
-(this means that while blinded, only operations in this cmdset
-will be possible for the account to perform). It is however
-not persistent, so should there be a bug in it, we just need
-to restart the server to clear out of it during development.
This event closes the glass lid over the button
-some time after it was opened. It’s a one-off
-script that should be started/created when the
-lid is opened.
Called when script object is first created. Sets things up.
-We want to have a lid on the button that the user can pull
-aside in order to make the button ‘pressable’. But after a set
-time that lid should auto-close again, making the button safe
-from pressing (and deleting this command).
This script can only operate if the lid is open; if it
-is already closed, the script is clearly invalid.
-
Note that we are here relying on an self.obj being
-defined (and being a RedButton object) - this we should be able to
-expect since this type of script is always tied to one individual
-red button object and not having it would be an error.
Called after self.interval seconds. It closes the lid. Before this method is
-called, self.is_valid() is automatically checked, so there is no need to
-check this manually.
This deactivates the button for a short while (it won’t blink, won’t
-close its lid etc). It is meant to be called when the button is pushed
-and run as long as the blinded effect lasts. We cannot put these methods
-in the AddBlindedCmdSet script since that script is defined on the account
-whereas this one must be defined on the button.
Deactivate the button. Observe that this method is always
-called directly, regardless of the value of self.start_delay
-(that just controls when at_repeat() is called)
-
diff --git a/docs/0.9.5/api/evennia.contrib.tutorial_world.mob.html b/docs/0.9.5/api/evennia.contrib.tutorial_world.mob.html
index b492652173..ba60f71d5a 100644
--- a/docs/0.9.5/api/evennia.contrib.tutorial_world.mob.html
+++ b/docs/0.9.5/api/evennia.contrib.tutorial_world.mob.html
@@ -88,6 +88,11 @@ to turn on/off the mob.”
lock_storage = 'cmd:superuser()'¶
+
+
+search_index_entry = {'aliases': 'moboff', 'category': 'general', 'key': 'mobon', 'tags': '', 'text': "\n Activates/deactivates Mob\n\n Usage:\n mobon <mob>\n moboff <mob>\n\n This turns the mob from active (alive) mode\n to inactive (dead) mode. It is used during\n building to activate the mob once it's\n prepared.\n "}¶
+
+
@@ -123,7 +128,7 @@ the way it came. If unset, the mob will remain
stationary (idling) until attacked.
aggressive: if set, will attack Characters in
the same room using whatever Weapon it
-carries (see tutorial_world.objects.Weapon).
+carries (see tutorial_world.objects.TutorialWeapon).
if unset, the mob will never engage in combat
no matter what.
@@ -319,7 +324,6 @@ right away, also when patrolling on a very slow ticker.
@@ -714,6 +739,11 @@ parry - forgoes your attack but will make you harder to hit on next
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': 'hit fight defend thrust pierce slash stab kill parry bash chop', 'category': 'tutorialworld', 'key': 'attack', 'tags': '', 'text': '\n Attack the enemy. Commands:\n\n stab <enemy>\n slash <enemy>\n parry\n\n stab - (thrust) makes a lot of damage but is harder to hit with.\n slash - is easier to land, but does not make as much damage.\n parry - forgoes your attack but will make you harder to hit on next\n enemy attack.\n\n '}¶
+
+
@@ -735,8 +765,8 @@ parry - forgoes your attack but will make you harder to hit on next
+search_index_entry = {'aliases': '', 'category': 'tutorialworld', 'key': 'get weapon', 'tags': '', 'text': '\n Usage:\n get weapon\n\n This will try to obtain a weapon from the container.\n '}¶
This object represents a weapon store. When people use the
“get weapon” command on this rack, it will produce one
@@ -871,14 +906,14 @@ grab another one.
This will produce a new weapon from the rack,
assuming the caller hasn’t already gotten one. When
doing so, the caller will get Tagged with the id
@@ -887,25 +922,25 @@ pulling weapons from it indefinitely.
-
diff --git a/docs/0.9.5/api/evennia.contrib.tutorial_world.rooms.html b/docs/0.9.5/api/evennia.contrib.tutorial_world.rooms.html
index 4c69b7cac2..a6908aedac 100644
--- a/docs/0.9.5/api/evennia.contrib.tutorial_world.rooms.html
+++ b/docs/0.9.5/api/evennia.contrib.tutorial_world.rooms.html
@@ -87,6 +87,11 @@ called tutorial_info and display that.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': 'tut', 'category': 'tutorialworld', 'key': 'tutorial', 'tags': '', 'text': '\n Get help during the tutorial\n\n Usage:\n tutorial [obj]\n\n This command allows you to get behind-the-scenes info\n about an object or the current location.\n\n '}¶
+
+
@@ -140,6 +145,11 @@ the set_detail method and uses it.
lock_storage = 'cmd:perm(Builder)'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'tutorialworld', 'key': '@detail', 'tags': '', 'text': '\n sets a detail on a room\n\n Usage:\n @detail <key> = <description>\n @detail <key>;<alias>;... = description\n\n Example:\n @detail walls = The walls are covered in ...\n @detail castle;ruin;tower = The distant ruin ...\n\n This sets a "detail" on the object this command is defined on\n (TutorialRoom for this tutorial). This detail can be accessed with\n the TutorialRoomLook command sitting on TutorialRoom objects (details\n are set as a simple dictionary on the room). This is a Builder command.\n\n We custom parse the key for the ;-separator in order to create\n multiple aliases to the detail all at once.\n '}¶
+
+
@@ -187,6 +197,11 @@ code except for adding in the details.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': 'l ls', 'category': 'tutorialworld', 'key': 'look', 'tags': '', 'text': '\n looks at the room and on details\n\n Usage:\n look <obj>\n look <room detail>\n look *<account>\n\n Observes your location, details at your location or objects\n in your vicinity.\n\n Tutorial: This is a child of the default Look command, that also\n allows us to look at "details" in the room. These details are\n things to examine and offers some extra description without\n actually having to be actual database objects. It uses the\n return_detail() hook on TutorialRooms for this.\n '}¶
+
+
@@ -223,6 +238,11 @@ to all the variables defined therein.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': 'abort', 'category': 'general', 'key': 'give up', 'tags': '', 'text': '\n Give up the tutorial-world quest and return to Limbo, the start room of the\n server.\n\n '}¶
+
+
@@ -428,6 +448,11 @@ set in self.parse())
lock_storage = 'cmd:all();'¶
@@ -557,6 +582,11 @@ on the bridge, 0 - 4.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': 'e', 'category': 'tutorialworld', 'key': 'east', 'tags': '', 'text': '\n Go eastwards across the bridge.\n\n Tutorial info:\n This command relies on the caller having two Attributes\n (assigned by the room when entering):\n - east_exit: a unique name or dbref to the room to go to\n when exiting east.\n - west_exit: a unique name or dbref to the room to go to\n when exiting west.\n The room must also have the following Attributes\n - tutorial_bridge_posistion: the current position on\n on the bridge, 0 - 4.\n\n '}¶
+
+
@@ -615,6 +645,11 @@ on the bridge, 0 - 4.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': 'w', 'category': 'tutorialworld', 'key': 'west', 'tags': '', 'text': '\n Go westwards across the bridge.\n\n Tutorial info:\n This command relies on the caller having two Attributes\n (assigned by the room when entering):\n - east_exit: a unique name or dbref to the room to go to\n when exiting east.\n - west_exit: a unique name or dbref to the room to go to\n when exiting west.\n The room must also have the following property:\n - tutorial_bridge_posistion: the current position on\n on the bridge, 0 - 4.\n\n '}¶
+
+
@@ -659,6 +694,11 @@ if they fall off the bridge.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': 'l', 'category': 'tutorialworld', 'key': 'look', 'tags': '', 'text': '\n looks around at the bridge.\n\n Tutorial info:\n This command assumes that the room has an Attribute\n "fall_exit", a unique name or dbref to the place they end upp\n if they fall off the bridge.\n '}¶
@@ -846,6 +891,11 @@ random chance of eventually finding a light source.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': 'search feel l fiddle feel around', 'category': 'tutorialworld', 'key': 'look', 'tags': '', 'text': '\n Look around in darkness\n\n Usage:\n look\n\n Look around in the darkness, trying\n to find something.\n '}¶
+
+
@@ -884,6 +934,11 @@ random chance of eventually finding a light source.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'tutorialworld', 'key': 'help', 'tags': '', 'text': '\n Help command for the dark state.\n '}¶
+search_index_entry = {'aliases': '', 'category': 'general', 'key': '__nomatch_command', 'tags': '', 'text': "\n This is a system command. Commands with special keys are used to\n override special sitations in the game. The CMD_NOMATCH is used\n when the given command is not found in the current command set (it\n replaces Evennia's default behavior or offering command\n suggestions)\n "}¶
+
+
@@ -1197,7 +1257,6 @@ overriding the call (unused by default).
-
diff --git a/docs/0.9.5/api/evennia.contrib.unixcommand.html b/docs/0.9.5/api/evennia.contrib.unixcommand.html
index 781dd78ee8..bf0f7a323d 100644
--- a/docs/0.9.5/api/evennia.contrib.unixcommand.html
+++ b/docs/0.9.5/api/evennia.contrib.unixcommand.html
@@ -318,6 +318,11 @@ use its add_argument method.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'command', 'tags': '', 'text': '\n Unix-type commands, supporting short and long options.\n\n This command syntax uses the Unix-style commands with short options\n (-X) and long options (--something). The `argparse` module is\n used to parse the command.\n\n In order to use it, you should override two methods:\n - `init_parser`: this method is called when the command is created.\n It can be used to set options in the parser. `self.parser`\n contains the `argparse.ArgumentParser`, so you can add arguments\n here.\n - `func`: this method is called to execute the command, but after\n the parser has checked the arguments given to it are valid.\n You can access the namespace of valid arguments in `self.opts`\n at this point.\n\n The help of UnixCommands is derived from the docstring, in a\n slightly different way than usual: the first line of the docstring\n is used to represent the program description (the very short\n line at the top of the help message). The other lines below are\n used as the program\'s "epilog", displayed below the options. It\n means in your docstring, you don\'t have to write the options.\n They will be automatically provided by the parser and displayed\n accordingly. The `argparse` module provides a default \'-h\' or\n \'--help\' option on the command. Typing |whelp commandname|n will\n display the same as |wcommandname -h|n, though this behavior can\n be changed.\n\n '}¶
+
+
@@ -365,7 +370,6 @@ use its add_argument method.
-fieldsets = ((None, {'fields': (('db_key', 'db_help_category'), 'db_entrytext', 'db_lock_storage'), 'description': 'Sets a Help entry. Set lock to <i>view:all()</I> unless you want to restrict it.'}),)¶
Determines if another object has permission to access.
-accessing_obj - object trying to access this one
-access_type - type of access sought
-default - what to return if no lock of access_type was found
Property for easily retaining a search index entry for this object.
@@ -161,8 +178,10 @@ instances of this object.
For this to work, the developer must have defined a named view somewhere
in urls.py that follows the format ‘modelname-action’, so in this case
a named view of ‘character-create’ would be referenced by this method.
If no View has been created and defined in urls.py, returns an
HTML anchor.
This method is naive and simply returns a path. Securing access to
@@ -184,11 +203,11 @@ this object.
For this to work, the developer must have defined a named view somewhere
in urls.py that follows the format ‘modelname-action’, so in this case
a named view of ‘character-detail’ would be referenced by this method.
If no View has been created and defined in urls.py, returns an
HTML anchor.
This method is naive and simply returns a path. Securing access to
@@ -210,11 +229,11 @@ object.
For this to work, the developer must have defined a named view somewhere
in urls.py that follows the format ‘modelname-action’, so in this case
a named view of ‘character-update’ would be referenced by this method.
If no View has been created and defined in urls.py, returns an
HTML anchor.
This method is naive and simply returns a path. Securing access to
@@ -235,11 +254,11 @@ responsibility.
For this to work, the developer must have defined a named view somewhere
in urls.py that follows the format ‘modelname-action’, so in this case
a named view of ‘character-detail’ would be referenced by this method.
If no View has been created and defined in urls.py, returns an
HTML anchor.
This method is naive and simply returns a path. Securing access to
@@ -261,11 +280,11 @@ this object.
For this to work, the developer must have defined a named view somewhere
in urls.py that follows the format ‘modelname-action’, so in this case
a named view of ‘character-detail’ would be referenced by this method.
-
diff --git a/docs/0.9.5/api/evennia.locks.html b/docs/0.9.5/api/evennia.locks.html
index a91dbaa7f5..2f69d0913c 100644
--- a/docs/0.9.5/api/evennia.locks.html
+++ b/docs/0.9.5/api/evennia.locks.html
@@ -44,10 +44,7 @@ lock strings are processed through the lockhandler in this package. It
also contains the default lock functions used in lock definitions.
-
diff --git a/docs/0.9.5/api/evennia.locks.lockfuncs.html b/docs/0.9.5/api/evennia.locks.lockfuncs.html
index 484a9f98f2..0cce1ff588 100644
--- a/docs/0.9.5/api/evennia.locks.lockfuncs.html
+++ b/docs/0.9.5/api/evennia.locks.lockfuncs.html
@@ -48,106 +48,6 @@ Run the access() method of the handler to execute the lock chec
Note that accessing_obj and accessed_obj can be any object type
with a lock variable/field, so be careful to not expect
a certain object type.
-
Appendix: MUX locks
-
Below is a list nicked from the MUX help file on the locks available
-in standard MUX. Most of these are not relevant to core Evennia since
-locks in Evennia are considerably more flexible and can be implemented
-on an individual command/typeclass basis rather than as globally
-available like the MUX ones. So many of these are not available in
-basic Evennia, but could all be implemented easily if needed for the
-individual game.
DefaultLock: Exits: controls who may traverse the exit to
-
-
-
its destination.
Evennia: “traverse:<lockfunc()>”
-
-
-
-
-
Rooms: controls whether the account sees the
SUCC or FAIL message for the room
-following the room description when
-looking at the room.
-
-
Evennia: Custom typeclass
-
-
-
Accounts/Things: controls who may GET the object.
Evennia: “get:<lockfunc()”
-
-
-
-
-
EnterLock: Accounts/Things: controls who may ENTER the object
Evennia:
-
-
GetFromLock: All but Exits: controls who may gets things from a
-
given location.
Evennia:
-
-
-
-
GiveLock: Accounts/Things: controls who may give the object.
Evennia:
-
-
LeaveLock: Accounts/Things: controls who may LEAVE the object.
Evennia:
-
-
LinkLock: All but Exits: controls who may link to the location
if the location is LINK_OK (for linking
-exits or setting drop-tos) or ABODE (for
-setting homes)
-
-
Evennia:
-
-
-
MailLock: Accounts: controls who may @mail the account.
Evennia:
-
-
OpenLock: All but Exits: controls who may open an exit.
Evennia:
-
-
PageLock: Accounts: controls who may page the account.
Evennia: “send:<lockfunc()>”
-
-
ParentLock: All: controls who may make @parent links to
-
the object.
Evennia: Typeclasses and
-
-
-
“puppet:<lockstring()>”
-
-
ReceiveLock: Accounts/Things: controls who may give things to the
-
object.
Evennia:
-
-
-
-
SpeechLock: All but Exits: controls who may speak in that location
Evennia:
-
-
TeloutLock: All but Exits: controls who may teleport out of the
-
location.
Evennia:
-
-
-
-
TportLock: Rooms/Things: controls who may teleport there
Evennia:
-
-
UseLock: All but Exits: controls who may USE the object, GIVE
the object money and have the PAY
-attributes run, have their messages
-heard and possibly acted on by LISTEN
-and AxHEAR, and invoke $-commands
-stored on the object.
-
-
Evennia: Commands and Cmdsets.
-
-
-
DropLock: All but rooms: controls who may drop that object.
Evennia:
-
-
VisibleLock: All: Controls object visibility when the
object is not dark and the looker
-passes the lock. In DARK locations, the
-object must also be set LIGHT and the
-viewer must pass the VisibleLock.
Only accepts an accesing_obj that is superuser (e.g. user #1)
-
Since a superuser would not ever reach this check (superusers
-bypass the lock entirely), any user who gets this far cannot be a
-superuser, hence we just return False. :)
diff --git a/docs/0.9.5/api/evennia.locks.lockhandler.html b/docs/0.9.5/api/evennia.locks.lockhandler.html
index 769dc544b4..3b6a706d7f 100644
--- a/docs/0.9.5/api/evennia.locks.lockhandler.html
+++ b/docs/0.9.5/api/evennia.locks.lockhandler.html
@@ -456,7 +456,6 @@ among the locks defined by lockstring.
-
diff --git a/docs/0.9.5/api/evennia.objects.objects.html b/docs/0.9.5/api/evennia.objects.objects.html
index 1fef2de4d8..d73bbec6cf 100644
--- a/docs/0.9.5/api/evennia.objects.objects.html
+++ b/docs/0.9.5/api/evennia.objects.objects.html
@@ -43,12 +43,12 @@
DefaultCharacter, DefaultAccount, DefaultRoom and DefaultExit.
These are the (default) starting points for all in-game visible
entities.
+
This is the v1.0 develop version (for ref in doc building).
class evennia.objects.objects.ObjectSessionHandler(obj)[source]¶
Bases: object
-
Handles the get/setting of the sessid
-comma-separated integer field
+
Handles the get/setting of the sessid comma-separated integer field
Returns an Object matching a search string/condition
Perform a standard object search in the database, handling
multiple results and lack thereof gracefully. By default, only
@@ -336,7 +346,9 @@ for a broader search.
to search. Note that this is used to query the contents of a
location and will not match for the location itself -
if you want that, don’t set this or use candidates to specify
-exactly which objects should be searched.
+exactly which objects should be searched. If this nor candidates are
+given, candidates will include caller’s inventory, current location and
+all objects in the current location.
attribute_name (str) – Define which property to search. If set, no
key+alias search will be performed. This can be used
to search database fields (db_ will be automatically
@@ -364,14 +376,22 @@ to find an object (globally) by its database-id 123. If False,
will be treated like a normal string. If None (default), the ability to query by
#dbref is turned on if self has the permission ‘Builder’ and is turned off
otherwise.
+
stacked (int, optional) – If > 0, multimatches will be analyzed to determine if they
+only contains identical objects; these are then assumed ‘stacked’ and no multi-match
+error will be generated, instead stacked number of matches will be returned. If
+stacked is larger than number of matches, returns that number of matches. If
+the found stack is a mix of objects, return None and handle the multi-match
+error depending on the value of quiet.
Returns
-
match (Object, None or list) –
-
-
will return an Object/None if quiet=False,
otherwise it will return a list of 0, 1 or more matches.
-
-
+
Object – If finding a match an quiet=False
+None: If not finding a unique match and quiet=False.
+list: With 0, 1 or more matching objects if quiet=True
+list: With 2 or more matching objects if stacked is a positive integer and
+
+
the matched stack has only object-copies.
+
@@ -526,47 +546,66 @@ function on.
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 Arguments
-
-
arguments will be passed on to obj.msg() for all (Keyword) –
-
objects. (messaged) –
+{“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.
-
Example
-
Say Char is a Character object and Npc is an NPC object:
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
+
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.
+
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.
+
Examples
+
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”
Called by DefaultObject.copy(). Meant to be overloaded. In case there’s extra data not covered by
-.copy(), this can be used to deal with it.
+
Called by DefaultObject.copy(). Meant to be overloaded. In case there’s extra data not
+covered by .copy(), this can be used to deal with it.
Parameters
new_obj (Object) – The new Copy of this object.
@@ -1367,7 +1406,8 @@ text from the object. Returning None aborts the command.
a say. This is sent by the whisper command by default.
Other verbal commands could use this hook in similar
ways.
-
receivers (Object or iterable) – If set, this is the target or targets for the say/whisper.
+
receivers (Object or iterable) – If set, this is the target or targets for the
+say/whisper.
Returns
@@ -1392,8 +1432,8 @@ re-writing it completely.
msg_self (bool or str, optional) – If boolean True, echo message to self. If a string,
return that message. If False or unset, don’t echo to self.
msg_location (str, optional) – The message to echo to self’s location.
-
receivers (Object or iterable, optional) – An eventual receiver or receivers of the message
-(by default only used by whispers).
+
receivers (Object or iterable, optional) – An eventual receiver or receivers of the
+message (by default only used by whispers).
msg_receivers (str) – Specific message to pass to the receiver(s). This will parsed
with the {receiver} placeholder replaced with the given receiver.
@@ -1495,6 +1535,37 @@ errors (list): A list of errors in string form, if any.
Normalize the character name prior to creating. Note that this should be refactored to
+support i18n for non-latin scripts, but as we (currently) have no bug reports requesting
+better support of non-latin character sets, requiring character names to be latinified is an
+acceptable option.
@@ -1699,7 +1770,8 @@ overriding the call (unused by default).
Returns
-
A string with identifying information to disambiguate the command, conventionally with a preceding space.
+
A string with identifying information to disambiguate the command, conventionally with a
+preceding space.
@@ -1724,6 +1796,11 @@ overriding the call (unused by default).
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'command', 'tags': '', 'text': '\n This is a command that simply cause the caller to traverse\n the object it is attached to.\n\n '}¶
+
+
@@ -1933,7 +2010,6 @@ read for an error string instead.
-
diff --git a/docs/0.9.5/api/evennia.prototypes.menus.html b/docs/0.9.5/api/evennia.prototypes.menus.html
index 58d4a61b07..98707d16fe 100644
--- a/docs/0.9.5/api/evennia.prototypes.menus.html
+++ b/docs/0.9.5/api/evennia.prototypes.menus.html
@@ -197,7 +197,6 @@ prototype rather than creating a new one.
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.
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 spawn
before any object is created/updated. It must otherwise return the value to store for the specified
prototype key (this value must be possible to serialize in an Attribute).
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.
-
diff --git a/docs/0.9.5/api/evennia.prototypes.prototypes.html b/docs/0.9.5/api/evennia.prototypes.prototypes.html
index 6292a9bd7d..5816682e6e 100644
--- a/docs/0.9.5/api/evennia.prototypes.prototypes.html
+++ b/docs/0.9.5/api/evennia.prototypes.prototypes.html
@@ -340,7 +340,7 @@ with (it may still be useful as a mix-in prototype).
Parse a prototype value string for a protfunc and process it.
Available protfuncs are specified as callables in one of the modules of
settings.PROTFUNC_MODULES, or specified on the command line.
@@ -351,8 +351,6 @@ with (it may still be useful as a mix-in prototype).
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.
@@ -361,22 +359,15 @@ behave differently.
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.
@@ -423,18 +414,22 @@ eventual object #dbrefs in the output from the protfunc.
Analyze the prototype value and produce a value useful at the point of spawning.
Parameters
value (any) –
This can be:
callable - will be called as callable()
-(callable, (args,)) - will be called as callable(*args)
+(callable, (args,)) - will be called as callable(*args)
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
@@ -500,7 +495,6 @@ validator (callable, optional): If given, this will be called with the value to<
-
diff --git a/docs/0.9.5/api/evennia.prototypes.spawner.html b/docs/0.9.5/api/evennia.prototypes.spawner.html
index 43fa7355ef..012965e697 100644
--- a/docs/0.9.5/api/evennia.prototypes.spawner.html
+++ b/docs/0.9.5/api/evennia.prototypes.spawner.html
@@ -80,7 +80,7 @@ prototype_locks (str, optional): locks for restricting access to this prototype.
prototype_tags(list, optional): List of tags or tuples (tag, category) used to group prototype
in listings
-
prototype_parent (str, tuple or callable, optional): name (prototype_key) of eventual parent prototype, or
a list of parents, for multiple left-to-right inheritance.
+
prototype_parent (str, tuple or callable, optional): name (prototype_key) of eventual parent
prototype, or a list of parents, for multiple left-to-right inheritance.
prototype: Deprecated. Same meaning as ‘parent’.
@@ -224,8 +224,8 @@ attr/tag (for example) are represented by a tuple.
This is most useful for displaying.
implicit_keep (bool, optional) – If set, the resulting diff will assume KEEP unless the new
prototype explicitly change them. That is, if a key exists in prototype1 and
-not in prototype2, it will not be REMOVEd but set to KEEP instead. This is particularly
-useful for auto-generated prototypes when updating objects.
+not in prototype2, it will not be REMOVEd but set to KEEP instead. This is
+particularly useful for auto-generated prototypes when updating objects.
Returns
@@ -349,7 +349,7 @@ of the olc _format_diff_text_and_options without the options.
Update existing objects with the latest version of the prototype.
Parameters
@@ -366,6 +366,7 @@ expected - for example, one usually do not want to remove the object’s locatio
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.
@@ -445,6 +446,7 @@ dictionary. These will be batched-spawned as one object each.
Keyword Arguments
+
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
@@ -518,7 +520,6 @@ custom prototype_parents are given to this function.
-
diff --git a/docs/0.9.5/api/evennia.scripts.monitorhandler.html b/docs/0.9.5/api/evennia.scripts.monitorhandler.html
index 2153ee743e..88baf7cd28 100644
--- a/docs/0.9.5/api/evennia.scripts.monitorhandler.html
+++ b/docs/0.9.5/api/evennia.scripts.monitorhandler.html
@@ -200,7 +200,6 @@ all kwargs must be possible to pickle!
-
diff --git a/docs/0.9.5/api/evennia.scripts.scripthandler.html b/docs/0.9.5/api/evennia.scripts.scripthandler.html
index 4c8f4c4c2e..24e4967369 100644
--- a/docs/0.9.5/api/evennia.scripts.scripthandler.html
+++ b/docs/0.9.5/api/evennia.scripts.scripthandler.html
@@ -138,25 +138,6 @@ If no key is given, delete all scripts on the object!
Runs a validation on this object’s scripts only. This should
-be called regularly to crank the wheels.
-
-
Parameters
-
init_mode (str, optional) –
-
This is used during server
-
-
upstart and can have three values:
-- False (no init mode). Called during run.
-- “reset” - server reboot. Kill non-persistent scripts
-- “reload” - server reload. Keep non-persistent scripts.
-
-
-
-
-
@@ -204,7 +185,6 @@ be called regularly to crank the wheels.
-
diff --git a/docs/0.9.5/api/evennia.scripts.scripts.html b/docs/0.9.5/api/evennia.scripts.scripts.html
index 7d2bed16b4..f2596b2f92 100644
--- a/docs/0.9.5/api/evennia.scripts.scripts.html
+++ b/docs/0.9.5/api/evennia.scripts.scripts.html
@@ -70,182 +70,20 @@ errors (list): A list of errors in string form, if any.
Get time until the script fires it at_repeat hook again.
-
-
Returns
-
next (int) –
-
-
Time in seconds until the script runs again.
If not a timed script, return None.
-
-
-
-
-
-
Notes
-
This hook is not used in any way by the script’s stepping
-system; it’s only here for the user to be able to check in
-on their scripts and when they will next be run.
Called every time the script is started (for persistent
-scripts, this is usually once every server start)
-
-
Parameters
-
force_restart (bool, optional) – Normally an already
-started script will not be started again. if
-force_restart=True, the script will always restart
-the script, regardless of if it has started before.
Restart a paused script. This WILL call the at_start() hook.
-
-
Parameters
-
manual_unpause (bool, optional) – This is False if unpause is
-called by the server reload/reset mechanism.
-
-
Returns
-
result (bool) – True if unpause was triggered, False otherwise.
-
-
Raises
-
RuntimeError – If trying to automatically resart this script
-(usually after a reset/reload), but it was manually paused,
-and so should not the auto-unpaused.
Restarts an already existing/running Script from the
-beginning, optionally using different settings. This will
-first call the stop hooks, and then the start hooks again.
-:param interval: Allows for changing the interval
-
-
of the Script. Given in seconds. if None, will use the already stored interval.
-
-
-
Parameters
-
-
repeats (int, optional) – The number of repeats. If unset, will
-use the previous setting.
-
start_delay (bool, optional) – If we should wait interval seconds
-before starting or not. If None, re-use the previous setting.
Is called to check if the script is valid to run at this time.
-Should return a boolean. The method is assumed to collect all
-needed information from its related self.obj.
+
Is called to check if the script’s timer is valid to run at this time.
+Should return a boolean. If False, the timer will be stopped.
Called whenever the script is started, which for persistent
-scripts is at least once every server start. It will also be
-called when starting again after a pause (such as after a
-server reload)
+
Called whenever the script timer is started, which for persistent
+timed scripts is at least once every server start. It will also be
+called when starting again after a pause (including after a
+server reload).
Parameters
**kwargs (dict) – Arbitrary, optional arguments for users
@@ -266,20 +104,39 @@ overriding the call (unused by default).
Called whenever when it’s time for this script to stop (either
-because is_valid returned False or it runs out of iterations)
-
-
Args
-
**kwargs (dict): Arbitrary, optional arguments for users
overriding the call (unused by default).
-
-
+
Called whenever when it’s time for this script’s timer to stop (either
+because is_valid returned False, it ran out of iterations or it was manuallys
+stopped.
+
+
Parameters
+
**kwargs (dict) – Arbitrary, optional arguments for users
+overriding the call (unused by default).
This hook is called after the server has started. It can be used to add
+post-startup setup for Scripts without a timer component (for which at_start
+could be used).
-
diff --git a/docs/0.9.5/api/evennia.scripts.taskhandler.html b/docs/0.9.5/api/evennia.scripts.taskhandler.html
index 4585ea7129..94ea9a847a 100644
--- a/docs/0.9.5/api/evennia.scripts.taskhandler.html
+++ b/docs/0.9.5/api/evennia.scripts.taskhandler.html
@@ -139,7 +139,7 @@ called(self): A task attribute to check if the deferred instance of a task has b
diff --git a/docs/0.9.5/api/evennia.server.amp_client.html b/docs/0.9.5/api/evennia.server.amp_client.html
index 7f844d53e1..b955912945 100644
--- a/docs/0.9.5/api/evennia.server.amp_client.html
+++ b/docs/0.9.5/api/evennia.server.amp_client.html
@@ -266,7 +266,6 @@ operation, as defined by the global variables in
-
diff --git a/docs/0.9.5/api/evennia.server.evennia_launcher.html b/docs/0.9.5/api/evennia.server.evennia_launcher.html
index 6093cb989e..155cd1bfd1 100644
--- a/docs/0.9.5/api/evennia.server.evennia_launcher.html
+++ b/docs/0.9.5/api/evennia.server.evennia_launcher.html
@@ -583,7 +583,6 @@ settings here. The result will be printed to the terminal.
-
diff --git a/docs/0.9.5/api/evennia.server.game_index_client.client.html b/docs/0.9.5/api/evennia.server.game_index_client.client.html
index 2991dabcb0..bcc503d0d0 100644
--- a/docs/0.9.5/api/evennia.server.game_index_client.client.html
+++ b/docs/0.9.5/api/evennia.server.game_index_client.client.html
@@ -195,7 +195,6 @@ to this Protocol. The connection has been closed.
-
diff --git a/docs/0.9.5/api/evennia.server.manager.html b/docs/0.9.5/api/evennia.server.manager.html
index e1ac547375..035c1c38aa 100644
--- a/docs/0.9.5/api/evennia.server.manager.html
+++ b/docs/0.9.5/api/evennia.server.manager.html
@@ -122,7 +122,6 @@ value (str): If key was given, this is the stored value, or
-
diff --git a/docs/0.9.5/api/evennia.server.models.html b/docs/0.9.5/api/evennia.server.models.html
index cc0402e317..f7262727f9 100644
--- a/docs/0.9.5/api/evennia.server.models.html
+++ b/docs/0.9.5/api/evennia.server.models.html
@@ -176,7 +176,6 @@ object the first time, the query is executed.
-
diff --git a/docs/0.9.5/api/evennia.server.portal.amp_server.html b/docs/0.9.5/api/evennia.server.portal.amp_server.html
index 0bf574c16a..ba8ecc2d4d 100644
--- a/docs/0.9.5/api/evennia.server.portal.amp_server.html
+++ b/docs/0.9.5/api/evennia.server.portal.amp_server.html
@@ -77,7 +77,7 @@ these are the Evennia Server and the evennia launcher).
Parameters
-
portal (Portal) – The Evennia Portal service instance.
+
portal (Portal) – The Evennia Portal service instance.
protocol (Protocol) – The protocol the factory creates
instances of.
@@ -307,7 +307,6 @@ global variables in evennia/server/amp.py.
This implements the MCCP v2 telnet protocol as per
+http://tintin.sourceforge.net/mccp/. MCCP allows for the server to
+compress data when sending to supporting clients, reducing bandwidth
+by 70-90%.. The compression is done using Python’s builtin zlib
+library. If the client doesn’t support MCCP, server sends uncompressed
+as normal. Note: On modern hardware you are not likely to notice the
+effect of MCCP unless you have extremely heavy traffic or sits on a
+terribly slow connection.
+
This protocol is implemented by the telnet protocol importing
+mccp_compress and calling it from its write methods.
initialize MCCP by storing protocol on
+ourselves and calling the client to see if
+it supports MCCP. Sets callbacks to
+start zlib compression in that case.
+
+
Parameters
+
protocol (Protocol) – The active protocol instance.
This implements the MSSP telnet protocol as per
+http://tintin.sourceforge.net/mssp/. MSSP allows web portals and
+listings to have their crawlers find the mud and automatically
+extract relevant information about it, such as genre, how many
+active players and so on.
Partial implementation of the MXP protocol.
+The MXP protocol allows more advanced formatting options for telnet clients
+that supports it (mudlet, zmud, mushclient are a few)
This module implements the main Evennia server process, the core of
+the game engine.
+
This module should be started with the ‘twistd’ executable since it
+sets up all the networking features. (this is done automatically
+by game/evennia.py).
_reactor_stopping (bool, optional) – This is set if server
+is already in the process of shutting down; in this case
+we don’t need to stop it again.
+
_stop_server (bool, optional) – Only used in portal-interactive mode;
+makes sure to stop the Server cleanly.
+
+
+
+
Note that restarting (regardless of the setting) will not work
+if the Portal is currently running in daemon mode. In that
+case it always needs to be restarted manually.
-
diff --git a/docs/0.9.5/api/evennia.server.portal.ssh.html b/docs/0.9.5/api/evennia.server.portal.ssh.html
index 8f4b82cea0..4c79fac487 100644
--- a/docs/0.9.5/api/evennia.server.portal.ssh.html
+++ b/docs/0.9.5/api/evennia.server.portal.ssh.html
@@ -225,18 +225,18 @@ reaching this point.
are considered.
Keyword Arguments
-
options (dict) –
Send-option flags:
+
options (dict) –
Send-option flags (booleans)
-
mxp: Enforce MXP link support.
-
ansi: Enforce no ANSI colors.
-
xterm256: Enforce xterm256 colors, regardless of TTYPE setting.
-
nocolor: Strip all colors.
-
raw: Pass string through without any ansi processing
-(i.e. include Evennia ansi markers but do not
+
mxp: enforce mxp link support.
+
ansi: enforce no ansi colors.
+
xterm256: enforce xterm256 colors, regardless of ttype setting.
+
nocolor: strip all colors.
+
raw: pass string through without any ansi processing
+(i.e. include evennia ansi markers but do not
convert them into ansi tokens)
-
echo: Turn on/off line echo on the client. Turn
+
echo: turn on/off line echo on the client. turn
off line echo for client, for example for password.
-Note that it must be actively turned back on again!
+note that it must be actively turned back on again!
@@ -405,7 +405,6 @@ do not exist, the keypair is created.
This supports suppressing or activating Evennia
+the GO-AHEAD telnet operation after every server reply.
+If the client sends no explicit DONT SUPRESS GO-AHEAD,
+Evennia will default to supressing it since many clients
+will fail to use it and has no knowledge of this standard.
Each player connecting over telnet (ie using most traditional mud
+clients) gets a telnet protocol instance assigned to them. All
+communication between game and player goes through here.
Allow to toggle the NOP keepalive for those sad clients that
+can’t even handle a NOP instruction. This is turned off by the
+protocol_flag NOPKEEPALIVE (settable e.g. by the default
+option command).
This is called by all telnet extensions once they are finished.
+When all have reported, a sync with the server is performed.
+The system will force-call this sync after a small time to handle
+clients that don’t reply to handshakes at all.
Signal a programming error by raising an exception.
+
L{enableRemote} must return true for the given value of C{option} in
+order for this method to be called. If a subclass of L{Telnet}
+overrides enableRemote to allow certain options to be enabled, it must
+also override disableRemote tto disable those options.
OOB protocols allow for asynchronous communication between Evennia and
+compliant telnet clients. The “text” type of send command will always
+be sent “in-band”, appearing in the client’s main text output. OOB
+commands, by contrast, can have many forms and it is up to the client
+how and if they are handled. Examples of OOB instructions could be to
+instruct the client to play sounds or to update a graphical health
+bar.
+
Note that in Evennia’s Web client, all send commands are “OOB
+commands”, (including the “text” one), there is no equivalence to
+MSDP/GMCP for the webclient since it doesn’t need it.
+
This implements the following telnet OOB communication protocols:
GMCP messages will be outgoing on the following
+form (the non-JSON cmdname at the start is what
+IRE games use, supposedly, and what clients appear
+to have adopted). A cmdname without Package will end
+up in the Core package, while Core package names will
+be stripped on the Evennia side.
Observe that all MSDP_VARS are used to identify cmdnames,
+so if there are multiple arrays with the same cmdname
+given, they will be merged into one argument array, same
+for tables. Different MSDP_VARS (outside tables) will be
+identified as separate cmdnames.
This allows for running the telnet communication over an encrypted SSL tunnel. To use it, requires a
+client supporting Telnet SSL.
+
The protocol will try to automatically create the private key and certificate on the server side
+when starting and will warn if this was not possible. These will appear as files ssl.key and
+ssl.cert in mygame/server/.
This module implements the TTYPE telnet protocol as per
+http://tintin.sourceforge.net/mtts/. It allows the server to ask the
+client about its capabilities. If the client also supports TTYPE, it
+will return with information such as its name, if it supports colour
+etc. If the client does not support TTYPE, this will be ignored.
+
All data will be stored on the protocol’s protocol_flags dictionary,
+under the ‘TTYPE’ key.
Handles negotiation of the ttype protocol once the client has
+confirmed that it will respond with the ttype protocol.
+
+
Parameters
+
option (Option) – Not used.
+
+
+
Notes
+
The negotiation proceeds in several steps, each returning a
+certain piece of information about the client. All data is
+stored on protocol.protocol_flags under the TTYPE key.
-
diff --git a/docs/0.9.5/api/evennia.server.profiling.dummyrunner.html b/docs/0.9.5/api/evennia.server.profiling.dummyrunner.html
index d479a0c6cf..80eb5c460b 100644
--- a/docs/0.9.5/api/evennia.server.profiling.dummyrunner.html
+++ b/docs/0.9.5/api/evennia.server.profiling.dummyrunner.html
@@ -67,13 +67,83 @@ change which actions by adding a path to
in your settings. See utils.dummyrunner_actions.py
for instructions on how to define this module.
This is the actual executing part of the command. It is
+called directly after self.parse(). See the docstring of this
+module for which object properties are available (beyond those
+set in self.parse())
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'dummyrunner_echo_response', 'tags': '', 'text': '\n Dummyrunner command measuring the round-about response time\n from sending to receiving a result.\n\n Usage:\n dummyrunner_echo_response <timestamp>\n\n Responds with\n dummyrunner_echo_response:<timestamp>,<current_time>\n\n The dummyrunner will send this and then compare the send time\n with the receive time on both ends.\n\n '}¶
This module defines dummyrunner settings and sets up
the actions available to dummy accounts.
The settings are global variables:
-
TIMESTEP - time in seconds between each ‘tick’
-CHANCE_OF_ACTION - chance 0-1 of action happening
-CHANCE_OF_LOGIN - chance 0-1 of login happening
-TELNET_PORT - port to use, defaults to settings.TELNET_PORT
-ACTIONS - see below
+
+
TIMESTEP - time in seconds between each ‘tick’. 1 is a good start.
+
CHANCE_OF_ACTION - chance 0-1 of action happening. Default is 0.5.
+
CHANCE_OF_LOGIN - chance 0-1 of login happening. 0.01 is a good number.
+
TELNET_PORT - port to use, defaults to settings.TELNET_PORT
where the first entry is the function to call on first connect, with a
chance of occurring given by CHANCE_OF_LOGIN. This function is usually
responsible for logging in the account. The second entry is always
called when the dummyrunner disconnects from the server and should
-thus issue a logout command. The other entries are tuples (chance,
+thus issue a logout command. The other entries are tuples (chance,
func). They are picked randomly, their commonality based on the
cumulative chance given (the chance is normalized between all options
-so if will still work also if the given chances don’t add up to 1).
-Since each function can return a list of game-command strings, each
-function may result in multiple operations.
+so if will still work also if the given chances don’t add up to 1).
+
The PROFILE variable define pre-made ACTION tuples for convenience.
+
Each function should return an iterable of one or more command-call
+strings (like “look here”), so each can group multiple command operations.
An action-function is called with a “client” argument which is a
-reference to the dummy client currently performing the action. It
-returns a string or a list of command strings to execute. Use the
-client object for optionally saving data between actions.
+reference to the dummy client currently performing the action.
The client object has the following relevant properties and methods:
-
-
key - an optional client key. This is only used for dummyrunner output.
Default is “Dummy-<cid>”
-
-
-
+
key - an optional client key. This is only used for dummyrunner output.
+Default is “Dummy-<cid>”
cid - client id
gid - globally unique id, hashed with time stamp
istep - the current step
exits - an empty list. Can be used to store exit names
objs - an empty list. Can be used to store object names
-
-
counter() - returns a unique increasing id, hashed with time stamp
to make it unique also between dummyrunner instances.
-
-
-
+
counter() - returns a unique increasing id, hashed with time stamp
+to make it unique also between dummyrunner instances.
The return should either be a single command string or a tuple of
command strings. This list of commands will always be executed every
TIMESTEP with a chance given by CHANCE_OF_ACTION by in the order given
(no randomness) and allows for setting up a more complex chain of
commands (such as creating an account and logging in).
Special dummyrunner command, injected in c_login. It measures
+response time. Including this in the ACTION tuple will give more
+dummyrunner output about just how fast commands are being processed.
+
The dummyrunner will treat this special and inject the
+{timestamp} just before sending.
+
+
@@ -217,7 +224,6 @@ commands (such as creating an account and logging in).
-
diff --git a/docs/0.9.5/api/evennia.server.profiling.memplot.html b/docs/0.9.5/api/evennia.server.profiling.memplot.html
index e696e0402d..b3264ff00c 100644
--- a/docs/0.9.5/api/evennia.server.profiling.memplot.html
+++ b/docs/0.9.5/api/evennia.server.profiling.memplot.html
@@ -129,7 +129,6 @@ the script will append to this file if it already exists.
-
diff --git a/docs/0.9.5/api/evennia.server.profiling.timetrace.html b/docs/0.9.5/api/evennia.server.profiling.timetrace.html
index d05a7d3c99..28b01ded7b 100644
--- a/docs/0.9.5/api/evennia.server.profiling.timetrace.html
+++ b/docs/0.9.5/api/evennia.server.profiling.timetrace.html
@@ -102,7 +102,6 @@ This message will get attached time stamp.
This module implements the main Evennia server process, the core of
-the game engine.
-
This module should be started with the ‘twistd’ executable since it
-sets up all the networking features. (this is done automatically
-by evennia/server/server_runner.py).
+
This module implements the main Evennia server process, the core of the game
+engine.
+
This module should be started with the ‘twistd’ executable since it sets up all
+the networking features. (this is done automatically by
+evennia/server/server_runner.py).
class evennia.server.server.Evennia(application)[source]¶
@@ -100,20 +100,20 @@ Once finished the last_initial_setup_step is set to -1.
'reload' (-) – server restarts, no “persistent” scripts
+
+
mode - sets the server restart mode.
+
‘reload’ - server restarts, no “persistent” scripts
are stopped, at_reload hooks called.
-
'reset' - server restarts, non-persistent scripts stopped, (-) – at_shutdown hooks called but sessions will not
+
‘reset’ - server restarts, non-persistent scripts stopped,
+at_shutdown hooks called but sessions will not
be disconnected.
-
- like reset, but server will not auto-restart. (-'shutdown') –
-
_reactor_stopping – This is set if server is stopped by a kill
-command OR this method was already called
-once - in both cases the reactor is dead/stopping already.
+
‘shutdown’ - like reset, but server will not auto-restart.
+
_reactor_stopping - this is set if server is stopped by a kill
command OR this method was already called
+once - in both cases the reactor is
+dead/stopping already.
+
@@ -150,7 +150,7 @@ of it is fore a reload, reset or shutdown.
after reconnecting.
Parameters
-
mode (str) – One of reload, reset or shutdown.
+
mode (str) – One of ‘reload’, ‘reset’ or ‘shutdown’.
-
diff --git a/docs/0.9.5/api/evennia.server.serversession.html b/docs/0.9.5/api/evennia.server.serversession.html
index e0173eaa85..99ae4aadd8 100644
--- a/docs/0.9.5/api/evennia.server.serversession.html
+++ b/docs/0.9.5/api/evennia.server.serversession.html
@@ -44,126 +44,6 @@ a connection to the outside world but don’t know any details about how the
connection actually happens (so it’s the same for telnet, web, ssh etc).
It is stored on the Server side (as opposed to protocol-specific sessions which
are stored on the Portal side)
NAttributeHandler version without recache protection.
-This stand-alone handler manages non-database saving.
-It is similar to AttributeHandler and is used
-by the .ndb handler in the same way as .db does
-for the AttributeHandler.
-
diff --git a/docs/0.9.5/api/evennia.server.session.html b/docs/0.9.5/api/evennia.server.session.html
index 24656fbab2..0ed40c9b4f 100644
--- a/docs/0.9.5/api/evennia.server.session.html
+++ b/docs/0.9.5/api/evennia.server.session.html
@@ -197,7 +197,6 @@ should overload this to format/handle the outgoing data as needed.
kwargs (dict) – the name of the instruction (like “text”). Suitable values for each keyword are:
+- arg -> [[arg], {}]
+- [args] -> [[args], {}]
+- {kwargs} -> [[], {kwargs}]
+- [args, {kwargs}] -> [[arg], {kwargs}]
+- [[args], {kwargs}] -> [[args], {kwargs}]
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
+
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 funcparser parsing has been
applied.
-
-
-
@@ -153,13 +143,10 @@ applied.
class evennia.server.sessionhandler.ServerSessionHandler(*args, **kwargs)[source]¶
This object holds the stack of sessions active in the game at
-any time.
-
A session register with the handler in two steps, first by
-registering itself with the connect() method. This indicates an
-non-authenticated session. Whenever the session is authenticated
-the session together with the related account is sent to the login()
-method.
+
This object holds the stack of sessions active in the game at any time.
+
A session register with the handler in two steps, first by registering itself with the connect()
+method. This indicates an non-authenticated session. Whenever the session is authenticated the
+session together with the related account is sent to the login() method.
Log in the previously unloggedin session and the account we by
-now should know is connected to it. After this point we assume
-the session to be logged in one way or another.
+
Log in the previously unloggedin session and the account we by now should know is connected
+to it. After this point we assume the session to be logged in one way or another.
Parameters
@@ -463,7 +449,7 @@ object.
Returns.
-
sessions (Session or list): Can be more than one of Object is controlled by
more than one Session (MULTISESSION_MODE > 1).
+
sessions (Session or list): Can be more than one of Object is controlled by more than
one Session (MULTISESSION_MODE > 1).
@@ -481,7 +467,7 @@ object.
Returns.
-
sessions (Session or list): Can be more than one of Object is controlled by
more than one Session (MULTISESSION_MODE > 1).
+
sessions (Session or list): Can be more than one of Object is controlled by more than
Split incoming data into its inputfunc counterparts.
-This should be called by the serversession.data_in
-as sessionhandler.call_inputfunc(self, **kwargs).
+
Split incoming data into its inputfunc counterparts. This should be
+called by the serversession.data_in as
+sessionhandler.call_inputfunc(self, **kwargs).
This module brings Django Signals into Evennia. These are events that
-can be subscribed to by importing a given Signal and using the
-following code.
-
THIS_SIGNAL.connect(callback,sender_object**)
+
This module brings Django Signals into Evennia. These are events that can be
+subscribed to by importing a given Signal and using the following code.
+
THIS_SIGNAL.connect(callback,sender_object)
-
When other code calls THIS_SIGNAL.send(sender, **kwargs), the callback
-will be triggered.
-
Callbacks must be in the following format:
+
When other code calls THIS_SIGNAL.send(sender, **kwargs), the callback will
+be triggered.
+
Callbacks must be on the following format:
defmy_callback(sender,**kwargs):
- ...
+ # ...
-
This is used on top of hooks to make certain features easier to
-add to contribs without necessitating a full takeover of hooks
-that may be in high demand.
+
This is used on top of hooks to make certain features easier to add to contribs
+without necessitating a full takeover of hooks that may be in high demand.
This version of the throttle is usable by both the terminal server as well
as the web server, imposes limits on memory consumption by using deques
-with length limits instead of open-ended lists, and removes sparse keys when
-no recent failures have been recorded.
+with length limits instead of open-ended lists, and uses native Django
+caches for automatic key eviction and persistence configurability.
error_msg = 'Too many failed attempts; you must wait a few minutes before trying again.'¶
@@ -62,7 +62,9 @@ no recent failures have been recorded.
Keyword Arguments
-
limit (int) – Max number of failures before imposing limiter
+
name (str) – Name of this throttle.
+
limit (int) – Max number of failures before imposing limiter. If None,
+the throttle is disabled.
timeout (int) – number of timeout seconds after
max number of tries has been reached.
cache_size (int) – Max number of attempts to record per IP within a
@@ -73,6 +75,25 @@ the throttle is imposed!
-
diff --git a/docs/0.9.5/api/evennia.settings_default.html b/docs/0.9.5/api/evennia.settings_default.html
index ebbc2bcdbc..106b2487f5 100644
--- a/docs/0.9.5/api/evennia.settings_default.html
+++ b/docs/0.9.5/api/evennia.settings_default.html
@@ -92,7 +92,6 @@ always be sure of what you have changed and what is default behaviour.
This form overrides the base behavior of the ModelForm that would be used for a
-Tag-through-model. Since the through-models only have access to the foreignkeys of the Tag and
-the Object that they’re attached to, we need to spoof the behavior of it being a form that would
-correspond to its tag, or the creation of a tag. Instead of being saved, we’ll call to the
-Object’s handler, which will handle the creation, change, or deletion of a tag for us, as well
-as updating the handler’s cache so that all changes are instantly updated in-game.
If we have a tag, then we’ll prepopulate our instance with the fields we’d expect it
-to have based on the tag. tag_key, tag_category, tag_type, and tag_data all refer to
-the corresponding tag fields. The initial data of the form fields will similarly be
-populated.
One thing we want to do here is the or None checks, because forms are saved with an empty
-string rather than null from forms, usually, and the Handlers may handle empty strings
-differently than None objects. So for consistency with how things are handled in game,
-we’ll try to make sure that empty form fields will be None, rather than ‘’.
The Formset handles all the inline forms that are grouped together on the change page of the
-corresponding object. All the tags will appear here, and we’ll save them by overriding the
-formset’s save method. The forms will similarly spoof their save methods to return an instance
-which hasn’t been saved to the database, but have the relevant fields filled out based on the
-contents of the cleaned form. We’ll then use that to call to the handler of the corresponding
-Object, where the handler is an AliasHandler, PermissionsHandler, or TagHandler, based on the
-type of tag.
A handler for inline Tags. This class should be subclassed in the admin of your models,
-and the ‘model’ and ‘related_field’ class attributes must be set. model should be the
-through model (ObjectDB_db_tag’, for example), while related field should be the name
-of the field on that through model which points to the model being used: ‘objectdb’,
-‘msg’, ‘accountdb’, etc.
get_formset has to return a class, but we need to make the class that we return
-know about the related_field that we’ll use. Returning the class itself rather than
-a proxy isn’t threadsafe, since it’d be the base class and would change if multiple
-people used the admin at the same time
This form overrides the base behavior of the ModelForm that would be used for a Attribute-through-model.
-Since the through-models only have access to the foreignkeys of the Attribute and the Object that they’re
-attached to, we need to spoof the behavior of it being a form that would correspond to its Attribute,
-or the creation of an Attribute. Instead of being saved, we’ll call to the Object’s handler, which will handle
-the creation, change, or deletion of an Attribute for us, as well as updating the handler’s cache so that all
-changes are instantly updated in-game.
If we have an Attribute, then we’ll prepopulate our instance with the fields we’d expect it
-to have based on the Attribute. attr_key, attr_category, attr_value, attr_type,
-and attr_lockstring all refer to the corresponding Attribute fields. The initial data of the form fields will
-similarly be populated.
One thing we want to do here is the or None checks, because forms are saved with an empty
-string rather than null from forms, usually, and the Handlers may handle empty strings
-differently than None objects. So for consistency with how things are handled in game,
-we’ll try to make sure that empty form fields will be None, rather than ‘’.
A handler for inline Attributes. This class should be subclassed in the admin of your models,
-and the ‘model’ and ‘related_field’ class attributes must be set. model should be the
-through model (ObjectDB_db_tag’, for example), while related field should be the name
-of the field on that through model which points to the model being used: ‘objectdb’,
-‘msg’, ‘accountdb’, etc.
get_formset has to return a class, but we need to make the class that we return
-know about the related_field that we’ll use. Returning the class itself rather than
-a proxy isn’t threadsafe, since it’d be the base class and would change if multiple
-people used the admin at the same time
diff --git a/docs/0.9.5/api/evennia.typeclasses.attributes.html b/docs/0.9.5/api/evennia.typeclasses.attributes.html
index f76075acde..aa8aa41bc7 100644
--- a/docs/0.9.5/api/evennia.typeclasses.attributes.html
+++ b/docs/0.9.5/api/evennia.typeclasses.attributes.html
@@ -45,9 +45,9 @@ both pure-string values and pickled arbitrary data.
the Attribute- and NickHandlers as well as the NAttributeHandler,
which is a non-db version of Attributes.
Attributes are things that are specific to different types of objects. For
example, a drink container needs to store its fill level, whereas an exit
needs to store its open/closed/locked/unlocked state. These are done via
@@ -82,6 +82,106 @@ attributes on the fly as we like.
+
This class is an API/Interface/Abstract base class; do not instantiate it directly.
Determines if another object has permission to access.
+
+
Parameters
+
+
accessing_obj (object) – Entity trying to access this one.
+
access_type (str, optional) – Type of access sought, see
+the lock documentation.
+
default (bool, optional) – What result to return if no lock
+of access_type was found. The default, False, means a lockdown
+policy, only allowing explicit access.
+
kwargs (any, optional) – Not used; here to make the API consistent with
+other access calls.
pk (int) – This is a fake ‘primary key’ / id-field. It doesn’t actually have to be
+unique, but is fed an incrementing number from the InMemoryBackend by default. This
+is needed only so Attributes can be sorted. Some parts of the API also see the lack
+of a .pk field as a sign that the Attribute was deleted.
+
**kwargs – Other keyword arguments are used to construct the actual Attribute.
Determines if another object has permission to access.
-
-
Parameters
-
-
accessing_obj (object) – Entity trying to access this one.
-
access_type (str, optional) – Type of access sought, see
-the lock documentation.
-
default (bool, optional) – What result to return if no lock
-of access_type was found. The default, False, means a lockdown
-policy, only allowing explicit access.
-
kwargs (any, optional) – Not used; here to make the API consistent with
-other access calls.
RuntimeError – If trying to pass a non-iterable as argument.
+
+
+
Notes
+
The indata tuple order matters, so if you want a lockstring but no
+category, set the category to None. This method does not have the
+ability to check editing permissions and is mainly used internally.
+It does not use the normal self.add but applies the Attributes
+directly to the database.
Given a list of attributes, deletes them all.
+The default implementation is fine, but this is overridable since some databases may allow
+for a better method.
category (str, optional) – If given, clear only Attributes
+of this category.
+
accessing_obj (object, optional) – If given, check the
+attredit lock on each Attribute before continuing.
+
default_access (bool, optional) – Use this permission as
+fallback if access_obj is given but there is no lock of
+type attredit on the Attribute in question.
This Backend for Attributes stores NOTHING in the database. Everything is kept in memory, and
+normally lost on a crash, reload, shared memory flush, etc. It generates IDs for the Attributes
+it manages, but these are of little importance beyond sorting and satisfying the caching logic
+to know an Attribute hasn’t been deleted out from under the cache’s nose.
obj (TypedObject) – An Account, Object, Channel, ServerSession (not technically a typed
+object), etc. backend_class (IAttributeBackend class): The class of the backend to
+use.
+
+
@@ -386,7 +988,8 @@ permission lock will be checked before returning each
looked-after Attribute.
default_access (bool, optional) – If no attrread lock is set on
object, this determines if the lock should then be passed or not.
-
return_list (bool, optional) –
+
return_list (bool, optional) – Always return a list, also if there is only
+one or zero matches found.
Returns
@@ -444,14 +1047,12 @@ repeat-calling add when having many Attributes to add.
*args (tuple) –
Each argument should be a tuples (can be of varying
length) representing the Attribute to add to this object.
Supported tuples are
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.
+
pattern (str) – The pattern to be used for nick recognition. This will
+be parsed for shell patterns into a regex, unless pattern_is_regex
+is True, in which case it must be an already valid regex string. In
+this case, instead of $N, numbered arguments must instead be given
+as matching groups named as argN, such as (?P<arg1>.+?).
+
replacement (str) – The template to be used to replace the string
+matched by the pattern. This can contain $N markers and is never
+parsed into a regex.
+
pattern_is_regex (bool) – If set, pattern is a full regex string
+instead of containing shell patterns.
Returns
-
(regex, str) –
+
regex, template (str) –
-
Regex to match against strings and a template
Template with markers {arg1}, {arg2}, etc for
-replacement using the standard .format method.
+
Regex to match against strings and template
with markers **{arg1}, {arg2}**, etc for replacement using the standard
+.format method.
obj (TypedObject) – An Account, Object, Channel, ServerSession (not technically a typed
+object), etc. backend_class (IAttributeBackend class): The class of the backend to
+use.
+
+
@@ -659,25 +1312,51 @@ a string.
kwargs (any, optional) – These are passed on to AttributeHandler.get.
+
Returns
+
str or tuple – The nick replacement string or nick tuple.
key (str) – A key (or template) for the nick to match for.
-
replacement (str) – The string (or template) to replace key with (the “nickname”).
+
pattern (str) – A pattern to match for. This will be parsed for
+shell patterns using the fnmatch library and can contain
+$N-markers to indicate the locations of arguments to catch. If
+pattern_is_regex=True, this must instead be a valid regular
+expression and the $N-markers must be named argN that matches
+numbered regex groups (see examples).
+
replacement (str) – The string (or template) to replace key with
+(the “nickname”). This may contain $N markers to indicate where to
+place the argument-matches
category (str, optional) – the category within which to
retrieve the nick. The “inputline” means replacing data
sent by the user.
-
kwargs (any, optional) – These are passed on to AttributeHandler.get.
+
pattern_is_regex (bool) – If True, the pattern will be parsed as a
+raw regex string. Instead of using $N markers in this string, one
+then must mark numbered arguments as a named regex-groupd named argN.
+For example, (?P<arg1>.+?) will match the behavior of using $1
+in the shell pattern.
+
**kwargs (any, optional) – These are passed on to AttributeHandler.get.
+
Notes
+
For most cases, the shell-pattern is much shorter and easier. The
+regex pattern form can be useful for more complex matchings though,
+for example in order to add optional arguments, such as with
+(?P<argN>.*?).
+
Example
+
+
pattern (default shell syntax): “gr $1 at $2”
+
pattern (with pattern_is_regex=True): r”gr (?P<arg1>.+?) at (?P<arg2>.+?)”
+
replacement: “emote With a flourish, $1 grins at $2.”
+
@@ -727,102 +1406,6 @@ with nicks stored on the Account level.
This stand-alone handler manages non-database saving.
-It is similar to AttributeHandler and is used
-by the .ndb handler in the same way as .db does
-for the AttributeHandler.
-
diff --git a/docs/0.9.5/api/evennia.typeclasses.html b/docs/0.9.5/api/evennia.typeclasses.html
index c55f38ed3d..df8d6503f6 100644
--- a/docs/0.9.5/api/evennia.typeclasses.html
+++ b/docs/0.9.5/api/evennia.typeclasses.html
@@ -99,7 +99,6 @@ Attribute and Tag models are defined along with their handlers.
Return Attribute objects by key, by category, by value, by
-strvalue, by object (it is stored on) or with a combination of
-those criteria.
+
Return Attribute objects by key, by category, by value, by strvalue, by
+object (it is stored on) or with a combination of those criteria.
Parameters
-
key (str, optional) – The attribute’s key to search for.
-
category (str, optional) – The category of the attribute(s)
-to search for.
+
key (str, optional) – The attribute’s key to search for
+
category (str, optional) – The category of the attribute(s) to search for.
value (str, optional) – The attribute value to search for.
Note that this is not a very efficient operation since it
will query for a pickled entity. Mutually exclusive to
@@ -69,14 +67,14 @@ mutually exclusive to the value keyword and will take
precedence if given.
obj (Object, optional) – On which object the Attribute to
search for is.
-
attrtype (str, optional) – An attribute-type to search for.
+
attrype (str, optional) – An attribute-type to search for.
By default this is either None (normal Attributes) or
“nick”.
-
kwargs (any) – Currently unused. Reserved for future use.
+
**kwargs (any) – Currently unused. Reserved for future use.
Returns
-
attributes (list) – The matching Attributes.
+
list – The matching Attributes.
@@ -168,7 +166,7 @@ stored on) or with a combination of those criteria.
to search for.
obj (Object, optional) – On which object the Tag to
search for is.
-
tagtype (str, optional) – One of None (normal tags),
+
tagtype (str, optional) – One of None (normal tags),
“alias” or “permission”
global_search (bool, optional) – Include all possible tags,
not just tags on this object
@@ -493,7 +491,6 @@ Mutually exclusive to include_children.
-
diff --git a/docs/0.9.5/api/evennia.typeclasses.models.html b/docs/0.9.5/api/evennia.typeclasses.models.html
index d0da709195..2ca866d0ed 100644
--- a/docs/0.9.5/api/evennia.typeclasses.models.html
+++ b/docs/0.9.5/api/evennia.typeclasses.models.html
@@ -60,7 +60,6 @@ The admin should usually not have to deal directly with the database object
layer.
This module also contains the Managers for the respective models; inherit from
these to create custom managers.
-
class evennia.typeclasses.models.TypedObject(*args, **kwargs)[source]¶
@@ -68,18 +67,18 @@ these to create custom managers.
Abstract Django model.
This is the basis for a typed object. It also contains all the
mechanics for managing connected attributes.
-
-
The TypedObject has the following properties:
key - main name
-name - alias for key
-typeclass_path - the path to the decorating typeclass
-typeclass - auto-linked typeclass
-date_created - time stamp of object creation
-permissions - perm strings
-dbref - #id of object
-db - persistent attribute storage
-ndb - non-persistent attribute storage
-
-
+
The TypedObject has the following properties:
+
+
key - main name
+
name - alias for key
+
typeclass_path - the path to the decorating typeclass
@@ -159,10 +158,10 @@ a class based on the db_typeclass_path database field rather
than use the one in the model.
Parameters
-
-
*args – Passed through to parent.
-
**kwargs – Passed through to parent.
-
+
through to parent. (Passed) –
+
+
Keyword Arguments
+
through to parent. (Passed) –
Notes
@@ -368,7 +367,7 @@ superuser lock bypass (be careful with this one).
Keyword Arguments
-
kwargs (any) – Ignored, but is there to make the api
+
kwar (any) – Ignored, but is there to make the api
consistent with the object-typeclass method access, which
use it to feed to its hook methods.
@@ -401,36 +400,30 @@ without involving any locks.
property db¶
Attribute handler wrapper. Allows for the syntax
obj.db.attrname=value
- and
+# andvalue=obj.db.attrname
- and
+# anddelobj.db.attrname
- and
+# andall_attr=obj.db.all()
+# (unless there is an attribute
+# named 'all', in which case that will be returned instead).
-
(unless there is an attribute named ‘all’, in which case that will be
-returned instead).
A non-attr_obj store (NonDataBase). Everything stored to this is
-guaranteed to be cleared when a server is shutdown. Syntax is same as
-for the .db property, e.g.
-
obj.ndb.attrname=value
- and
-value=obj.ndb.attrname
- and
-delobj.ndb.attrname
- and
-all_attr=obj.ndb.all()
-
-
-
What makes this preferable over just assigning properties directly on
-the object is that Evennia can track caching for these properties and
-for example avoid wiping objects with set .ndb data on cache flushes.
+
NonDataBase). Everything stored
+to this is guaranteed to be cleared when a server is shutdown.
+Syntax is same as for the _get_db_holder() method and
+property, e.g. obj.ndb.attr = value etc.
+
+
Type
+
A non-attr_obj store (ndb
+
+
@@ -517,141 +510,22 @@ at/getting information for this object.
classmethod web_get_create_url()[source]¶
Returns the URI path for a View that allows users to create new
instances of this object.
+
ex. Chargen = ‘/characters/create/’
+
For this to work, the developer must have defined a named view somewhere
+in urls.py that follows the format ‘modelname-action’, so in this case
+a named view of ‘character-create’ would be referenced by this method.
If no View has been created and defined in urls.py, returns an
+HTML anchor.
+
This method is naive and simply returns a path. Securing access to
+the actual view and limiting who can create new objects is the
+developer’s responsibility.
Returns
path (str) – URI path to object creation page, if defined.
-
Examples
-
Chargen='/characters/create/'
-
-
-
For this to work, the developer must have defined a named view somewhere
-in urls.py that follows the format ‘modelname-action’, so in this case
-a named view of ‘character-create’ would be referenced by this method.
If no View has been created and defined in urls.py, returns an
-HTML anchor.
-
Notes
-
This method is naive and simply returns a path. Securing access to
-the actual view and limiting who can create new objects is the
-developer’s responsibility.
Returns the URI path for a View that allows users to view details for
-this object.
-
-
Returns
-
path (str) – URI path to object detail page, if defined.
-
-
-
Examples
-
Oscar(Character)='/characters/oscar/1/'
-
-
-
For this to work, the developer must have defined a named view somewhere
-in urls.py that follows the format ‘modelname-action’, so in this case
-a named view of ‘character-detail’ would be referenced by this method.
If no View has been created and defined in urls.py, returns an
-HTML anchor.
-
Notes
-
This method is naive and simply returns a path. Securing access to
-the actual view and limiting who can view this object is the
-developer’s responsibility.
Returns the URI path for a View that allows users to puppet a specific
-object.
-
-
Returns
-
path (str) – URI path to object puppet page, if defined.
-
-
-
Examples
-
Oscar(Character)='/characters/oscar/1/puppet/'
-
-
-
For this to work, the developer must have defined a named view somewhere
-in urls.py that follows the format ‘modelname-action’, so in this case
-a named view of ‘character-puppet’ would be referenced by this method.
If no View has been created and defined in urls.py, returns an
-HTML anchor.
-
Notes
-
This method is naive and simply returns a path. Securing access to
-the actual view and limiting who can view this object is the developer’s
-responsibility.
Returns the URI path for a View that allows users to update this
-object.
-
-
Returns
-
path (str) – URI path to object update page, if defined.
-
-
-
Examples
-
Oscar(Character)='/characters/oscar/1/change/'
-
-
-
For this to work, the developer must have defined a named view somewhere
-in urls.py that follows the format ‘modelname-action’, so in this case
-a named view of ‘character-update’ would be referenced by this method.
If no View has been created and defined in urls.py, returns an
-HTML anchor.
-
Notes
-
This method is naive and simply returns a path. Securing access to
-the actual view and limiting who can modify objects is the developer’s
-responsibility.
Returns the URI path for a View that allows users to delete this object.
-
-
Returns
-
path (str) – URI path to object deletion page, if defined.
-
-
-
Examples
-
Oscar(Character)='/characters/oscar/1/delete/'
-
-
-
For this to work, the developer must have defined a named view somewhere
-in urls.py that follows the format ‘modelname-action’, so in this case
-a named view of ‘character-detail’ would be referenced by this method.
If no View has been created and defined in urls.py, returns an
-HTML anchor.
-
Notes
-
This method is naive and simply returns a path. Securing access to
-the actual view and limiting who can delete this object is the developer’s
-responsibility.
@@ -660,34 +534,6 @@ responsibility.
A wrapper for getting database field db_date_created.
Returns the URI path for a View that allows users to view details for
-this object.
-
-
Returns
-
path (str) – URI path to object detail page, if defined.
-
-
-
Examples
-
Oscar(Character)='/characters/oscar/1/'
-
-
-
For this to work, the developer must have defined a named view somewhere
-in urls.py that follows the format ‘modelname-action’, so in this case
-a named view of ‘character-detail’ would be referenced by this method.
If no View has been created and defined in urls.py, returns an
-HTML anchor.
-
Notes
-
This method is naive and simply returns a path. Securing access to
-the actual view and limiting who can view this object is the
-developer’s responsibility.
Returns the URI path for a View that allows users to view details for
+this object.
+
+
Returns
+
path (str) – URI path to object detail page, if defined.
+
+
+
Examples
+
Oscar(Character)='/characters/oscar/1/'
+
+
+
For this to work, the developer must have defined a named view somewhere
+in urls.py that follows the format ‘modelname-action’, so in this case
+a named view of ‘character-detail’ would be referenced by this method.
If no View has been created and defined in urls.py, returns an
+HTML anchor.
+
This method is naive and simply returns a path. Securing access to
+the actual view and limiting who can view this object is the
+developer’s responsibility.
Returns the URI path for a View that allows users to puppet a specific
+object.
+
+
Returns
+
str – URI path to object puppet page, if defined.
+
+
+
Examples
+
Oscar(Character)='/characters/oscar/1/puppet/'
+
+
+
For this to work, the developer must have defined a named view somewhere
+in urls.py that follows the format ‘modelname-action’, so in this case
+a named view of ‘character-puppet’ would be referenced by this method.
If no View has been created and defined in urls.py, returns an
+HTML anchor.
+
This method is naive and simply returns a path. Securing access to
+the actual view and limiting who can view this object is the developer’s
+responsibility.
Returns the URI path for a View that allows users to update this
+object.
+
+
Returns
+
str – URI path to object update page, if defined.
+
+
+
Examples
+
Oscar(Character)='/characters/oscar/1/change/'
+
+
+
For this to work, the developer must have defined a named view somewhere
+in urls.py that follows the format ‘modelname-action’, so in this case
+a named view of ‘character-update’ would be referenced by this method.
If no View has been created and defined in urls.py, returns an
+HTML anchor.
+
This method is naive and simply returns a path. Securing access to
+the actual view and limiting who can modify objects is the developer’s
+responsibility.
Returns the URI path for a View that allows users to delete this object.
+
+
Returns
+
path (str) – URI path to object deletion page, if defined.
+
+
+
Examples
+
Oscar(Character)='/characters/oscar/1/delete/'
+
+
+
For this to work, the developer must have defined a named view
+somewhere in urls.py that follows the format ‘modelname-action’, so
+in this case a named view of ‘character-detail’ would be referenced
+by this method.
If no View has been created and defined in urls.py, returns an HTML
+anchor.
+
This method is naive and simply returns a path. Securing access to
+the actual view and limiting who can delete this object is the
+developer’s responsibility.
Returns the URI path for a View that allows users to view details for
+this object.
+
+
Returns
+
path (str) – URI path to object detail page, if defined.
+
+
+
Examples
+
Oscar(Character)='/characters/oscar/1/'
+
+
+
For this to work, the developer must have defined a named view somewhere
+in urls.py that follows the format ‘modelname-action’, so in this case
+a named view of ‘character-detail’ would be referenced by this method.
If no View has been created and defined in urls.py, returns an
+HTML anchor.
+
This method is naive and simply returns a path. Securing access to
+the actual view and limiting who can view this object is the
+developer’s responsibility.
-
diff --git a/docs/0.9.5/api/evennia.typeclasses.tags.html b/docs/0.9.5/api/evennia.typeclasses.tags.html
index 1f07bee11e..c50b90f97a 100644
--- a/docs/0.9.5/api/evennia.typeclasses.tags.html
+++ b/docs/0.9.5/api/evennia.typeclasses.tags.html
@@ -270,6 +270,33 @@ existing Tag object, this will be re-used and no new Tag
will be created.
+
*args (tuple or str) – Each argument should be a tagstr keys or tuple (keystr, category) or
-(keystr, category, data). It’s possible to mix input types.
+
*args (tuple or str) – Each argument should be a tagstr keys or tuple
+(keystr, category) or (keystr, category, data). It’s possible to mix input
+types.
Notes
@@ -436,7 +464,6 @@ of a latter tuple with the same category).
Use the codes defined in ANSIPARSER in your text to apply colour to text
-according to the ANSI standard.
-
Examples:
+
Use the codes defined in the ANSIParser class to apply colour to text. The
+parse_ansi function in this module parses text for markup and strip_ansi
+removes it.
+
You should usually not need to call parse_ansi explicitly; it is run by
+Evennia just before returning data to/from the user. Alternative markup is
+possible by overriding the parser class (see also contrib/ for deprecated
+markup schemes).
+
Supported standards:
+
+
ANSI 8 bright and 8 dark fg (foreground) colors
+
ANSI 8 dark bg (background) colors
+
‘ANSI’ 8 bright bg colors ‘faked’ with xterm256 (bright bg not included in ANSI standard)
ANSI colors: r ed, g reen, y ellow, b lue, m agenta, c yan, n ormal (no color).
+Capital letters indicate the ‘dark’ variant.
+
+
|r fg bright red
+
|R fg dark red
+
|[r bg bright red
+
|[R bg dark red
+
|[R|g bg dark red, fg bright green
+
"This is |rRed text|n and this is normal again."
-
Mostly you should not need to call parse_ansi() explicitly; it is run by
-Evennia just before returning data to/from the user. Depreciated example forms
-are available by extending the ansi mapping.
+
Xterm256 colors are given as RGB (Red-Green-Blue), with values 0-5:
+
+
|500 fg bright red
+
|050 fg bright green
+
|005 fg bright blue
+
|110 fg dark brown
+
|425 fg pink
+
|[431 bg orange
+
+
Xterm256 greyscale:
+
+
|=a fg black
+
|=g fg dark grey
+
|=o fg middle grey
+
|=v fg bright grey
+
|=z fg white
+
|[=r bg middle grey
+
+
"This is |500Red text|n and this is normal again."
+"This is |[=jText on dark grey background"
+
This module contains the core methods for the Batch-command- and
-Batch-code-processors respectively. In short, these are two different
-ways to build a game world using a normal text-editor without having
-to do so ‘on the fly’ in-game. They also serve as an automatic backup
-so you can quickly recreate a world also after a server reset. The
-functions in this module is meant to form the backbone of a system
-called and accessed through game commands.
-
The Batch-command processor is the simplest. It simply runs a list of
-in-game commands in sequence by reading them from a text file. The
-advantage of this is that the builder only need to remember the normal
-in-game commands. They are also executing with full permission checks
-etc, making it relatively safe for builders to use. The drawback is
-that in-game there is really a builder-character walking around
-building things, and it can be important to create rooms and objects
-in the right order, so the character can move between them. Also
-objects that affects players (such as mobs, dark rooms etc) will
-affect the building character too, requiring extra care to turn
-off/on.
+Batch-code-processors respectively. In short, these are two different ways to
+build a game world using a normal text-editor without having to do so ‘on the
+fly’ in-game. They also serve as an automatic backup so you can quickly
+recreate a world also after a server reset. The functions in this module is
+meant to form the backbone of a system called and accessed through game
+commands.
+
The Batch-command processor is the simplest. It simply runs a list of in-game
+commands in sequence by reading them from a text file. The advantage of this is
+that the builder only need to remember the normal in-game commands. They are
+also executing with full permission checks etc, making it relatively safe for
+builders to use. The drawback is that in-game there is really a
+builder-character walking around building things, and it can be important to
+create rooms and objects in the right order, so the character can move between
+them. Also objects that affects players (such as mobs, dark rooms etc) will
+affect the building character too, requiring extra care to turn off/on.
The Batch-code processor is a more advanced system that accepts full
Python code, executing in chunks. The advantage of this is much more
power; practically anything imaginable can be coded and handled using
@@ -69,33 +67,38 @@ etc. You also need to know Python and Evennia’s API. Hence it’s
recommended that the batch-code processor is limited only to
superusers or highly trusted staff.
The batch-command processor accepts ‘batchcommand files’ e.g
batch.ev, containing a sequence of valid Evennia commands in a
simple format. The engine runs each command in sequence, as if they
had been run at the game prompt.
Each Evennia command must be delimited by a line comment to mark its
-end. This way entire game worlds can be created and planned offline; it is
+end.
+
look
+# delimiting comment
+create/dropbox
+# another required comment
+
+
+
One can also inject another batchcmdfile:
+
#INSERT path.batchcmdfile
+
+
+
This way entire game worlds can be created and planned offline; it is
especially useful in order to create long room descriptions where a
real offline text editor is often much better than any online text
editor or prompt.
-
There is only one batchcommand-specific entry to use in a batch-command
-files (all others are just like in-game commands):
-
-
#INSERT path.batchcmdfile - this as the first entry on a line will
-import and run a batch.ev file in this position, as if it was
-written in this file.
# batch file# all lines starting with # are comments; they also indicate# that a command definition is over.
-@createbox
+createbox# this comment ends the @create command.
-@setbox/desc=Alargebox.
+setbox/desc=Alargebox.Insidearesomescatteredpilesofclothing.
@@ -108,24 +111,25 @@ written in this file.
# (so two empty lines becomes a new paragraph).
-@teleport#221
+teleport#221# (Assuming #221 is a warehouse or something.)# (remember, this comment ends the @teleport command! Don'f forget it)# Example of importing another file at this point.
-#INSERT examples.batch
+#IMPORT examples.batch
-@dropbox
+dropbox# Done, the box is in the warehouse! (this last comment is not necessary to
-# close the @drop command since it's the end of the file)
+# close the drop command since it's the end of the file)
An example batch file is contrib/examples/batch_example.ev.
The Batch-code processor accepts full python modules (e.g. batch.py)
that looks identical to normal Python files. The difference from
importing and running any Python module is that the batch-code module
@@ -156,13 +160,17 @@ this file.
Importing works as normal. The following variables are automatically
made available in the script namespace.
-
caller - The object executing the batchscript
-
DEBUG - This is a boolean marking if the batchprocessor is running
-in debug mode. It can be checked to e.g. delete created objects
+
caller - The object executing the batchscript
+
+
DEBUG - This is a boolean marking if the batchprocessor is running
in debug mode. It can be checked to e.g. delete created objects
when running a CODE block multiple times during testing.
-(avoids creating a slew of same-named db objects)
+(avoids creating a slew of same-named db objects)
+
This parses the lines of a batchfile according to the following
-rules:
+
This parses the lines of a batch-command-file.
+
+
Parameters
+
pythonpath (str) – The dot-python path to the file.
+
+
Returns
+
list – A list of all parsed commands with arguments, as strings.
+
+
+
Notes
+
Parsing follows the following rules:
-
# at the beginning of a line marks the end of the command before
+
A # at the beginning of a line marks the end of the command before
it. It is also a comment and any number of # can exist on
subsequent lines (but not inside comments).
-
#INSERT at the beginning of a line imports another
+
#INSERT at the beginning of a line imports another
batch-cmd file file and pastes it into the batch file as if
it was written there.
Commands are placed alone at the beginning of a line and their
@@ -264,30 +280,28 @@ a newline (so two empty lines is a paragraph).
This parses the lines of a batchfile according to the following
-rules:
+
This parses the lines of a batch-code file
Parameters
pythonpath (str) – The dot-python path to the file.
Returns
-
codeblocks (list) –
+
list –
-
A list of all #CODE blocks, each with
prepended #HEADER data. If no #CODE blocks were found,
-this will be a list of one element.
+
A list of all #CODE blocks, each with
prepended #HEADER block data. If no #CODE
+blocks were found, this will be a list of one element
+containing all code in the file (so a normal Python file).
Notes
+
Parsing is done according to the following rules:
-
-
Code before a #CODE/HEADER block are considered part of
the first code/header block or is the ONLY block if no
-#CODE/HEADER blocks are defined.
-
-
-
+
Code before a #CODE/HEADER block are considered part of
+the first code/header block or is the ONLY block if no
+#CODE/HEADER blocks are defined.
Lines starting with #HEADER starts a header block (ends other blocks)
Lines starting with #CODE begins a code block (ends other blocks)
Lines starting with #INSERT are on form #INSERT filename. Code from
@@ -320,6 +334,7 @@ namespace.
This module gathers all the essential database-creation
-functions for the game engine’s various object types.
-
Only objects created ‘stand-alone’ are in here, e.g. object Attributes
-are always created directly through their respective objects.
-
Each creation_* function also has an alias named for the entity being
-created, such as create_object() and object(). This is for
-consistency with the utils.search module and allows you to do the
-shorter “create.object()”.
-
The respective object managers hold more methods for manipulating and
-searching objects already existing in the database.
This module gathers all the essential database-creation functions for the game
+engine’s various object types.
+
Only objects created ‘stand-alone’ are in here. E.g. object Attributes are
+always created through their respective objects handlers.
+
Each creation_* function also has an alias named for the entity being created,
+such as create_object() and object(). This is for consistency with the
+utils.search module and allows you to do the shorter create.object().
+
The respective object managers hold more methods for manipulating and searching
+objects already existing in the database.
typeclass (class or str) – Class or python path to a typeclass.
key (str) – Name of the new object. If not set, a name of
-#dbref will be set.
+#dbref will be set.
home (Object or str) – Obj or #dbref to use as the object’s
home location.
permissions (list) – A list of permission strings or tuples (permstring, category).
locks (str) – one or more lockstrings, separated by semicolons.
aliases (list) – A list of alternative keys or tuples (aliasstring, category).
tags (list) – List of tag keys or tuples (tagkey, category) or (tagkey, category, data).
-
destination (Object or str) – Obj or #dbref to use as an Exit’s
-target.
+
destination (Object or str) – Obj or #dbref to use as an Exit’s target.
report_to (Object) – The object to return error messages to.
nohome (bool) – This allows the creation of objects without a
default home location; only used when creating the default
@@ -91,10 +80,8 @@ adding this rarely makes sense since this data will not survive a reload.
object (Object) – A newly created object of the given typeclass.
Create a new communication Msg. Msgs represent a unit of
database-persistent communication between entites.
Parameters
-
senderobj (Object or Account) – The entity sending the Msg.
+
senderobj (Object, Account, Script, str or list) – The entity (or
+entities) sending the Msg. If a str, this is the id-string
+for an external sender type.
message (str) – Text with the message. Eventual headers, titles
etc should all be included in this text string. Formatting
will be retained.
-
channels (Channel, key or list) – A channel or a list of channels to
-send to. The channels may be actual channel objects or their
-unique key strings.
-
receivers (Object, Account, str or list) – An Account/Object to send
-to, or a list of them. May be Account objects or accountnames.
+
receivers (Object, Account, Script, str or list) – An Account/Object to send
+to, or a list of them. If a string, it’s an identifier for an external
+receiver.
locks (str) – Lock definition string.
tags (list) – A list of tags or tuples (tag, category).
header (str) – Mime-type or other optional information for the message
@@ -194,10 +180,9 @@ to, or a list of them. May be Account objects or accountnames.
Notes
-
The Comm system is created very open-ended, so it’s fully possible
-to let a message both go to several channels and to several
-receivers at the same time, it’s up to the command definitions to
-limit this as desired.
+
The Comm system is created to be very open-ended, so it’s fully
+possible to let a message both go several receivers at the same time,
+it’s up to the command definitions to limit this as desired.
@@ -315,7 +300,6 @@ operations and is thus not suitable for play-testing the game.
-
diff --git a/docs/0.9.5/api/evennia.utils.dbserialize.html b/docs/0.9.5/api/evennia.utils.dbserialize.html
index 5fbade71d1..1d363e0bc4 100644
--- a/docs/0.9.5/api/evennia.utils.dbserialize.html
+++ b/docs/0.9.5/api/evennia.utils.dbserialize.html
@@ -88,7 +88,7 @@ will save to when they update. It must have a ‘value’ property
that saves assigned data to the database. Skip if not
serializing onto a given object. If db_obj is given, this
function will convert lists, dicts and sets to their
-_SaverList, _SaverDict and _SaverSet counterparts.
+_SaverList, _SaverDict and _SaverSet counterparts.
Returns
@@ -166,7 +166,6 @@ function will convert lists, dicts and sets to their
This implements an advanced line editor for editing longer texts
-in-game. The editor mimics the command mechanisms of the “VI” editor
-(a famous line-by-line editor) as far as reasonable.
+
This implements an advanced line editor for editing longer texts in-game. The
+editor mimics the command mechanisms of the “VI” editor (a famous line-by-line
+editor) as far as reasonable.
Features of the editor:
-
-
+
undo/redo.
edit/replace on any line of the buffer.
search&replace text anywhere in buffer.
formatting of buffer, or selection, to certain width + indentations.
allow to echo the input or not, depending on your client.
+
in-built help
-
-
To use the editor, just import EvEditor from this module
-and initialize it:
+
To use the editor, just import EvEditor from this module and initialize it:
fromevennia.utils.eveditorimportEvEditor
-EvEditor(caller,loadfunc=None,savefunc=None,quitfunc=None,key="",persistent=True)
+
+# set up an editor to edit the caller's 'desc' Attribute
+def_loadfunc(caller):
+ returncaller.db.desc
+
+def_savefunc(caller,buffer):
+ caller.db.desc=buffer.strip()
+ returnTrue
+
+def_quitfunc(caller):
+ caller.msg("Custom quit message")
+
+# start the editor
+EvEditor(caller,loadfunc=None,savefunc=None,quitfunc=None,key="",
+ persistent=True,code=False)
-
-
caller is the user of the editor, the one to see all feedback.
-
loadfunc(caller) is called when the editor is first launched; the
-return from this function is loaded as the starting buffer in the
-editor.
-
safefunc(caller, buffer) is called with the current buffer when
-saving in the editor. The function should return True/False depending
-on if the saving was successful or not.
-
quitfunc(caller) is called when the editor exits. If this is given,
-no automatic quit messages will be given.
-
key is an optional identifier for the editing session, to be
-displayed in the editor.
-
persistent means the editor state will be saved to the database making it
-survive a server reload. Note that using this mode, the load- save-
-and quit-funcs must all be possible to pickle - notable unusable
-callables are class methods and functions defined inside other
-functions. With persistent=False, no such restriction exists.
-
code set to True activates features on the EvEditor to enter Python code.
-
-
In addition, the EvEditor can be used to enter Python source code,
-and offers basic handling of indentation.
-
+
The editor can also be used to format Python code and be made to
+survive a reload. See the EvEditor class for more details.
class evennia.utils.eveditor.CmdSaveYesNo(**kwargs)[source]¶
@@ -124,6 +116,11 @@ command was given specifically as “no” or “n”.
lock_storage = 'cmd:all()'¶
+
+
+search_index_entry = {'aliases': '__noinput_command', 'category': 'general', 'key': '__nomatch_command', 'tags': '', 'text': '\n Save the editor state on quit. This catches\n nomatches (defaults to Yes), and avoid saves only if\n command was given specifically as "no" or "n".\n '}¶
+
+
@@ -182,17 +179,19 @@ command was given specifically as “no” or “n”.
+search_index_entry = {'aliases': '__noinput_command', 'category': 'general', 'key': '__nomatch_command', 'tags': '', 'text': '\n No command match - Inputs line of text into buffer.\n\n '}¶
Where caller is the Object to use the menu on - it will get a new
-cmdset while using the Menu. The menu_module_path is the python path
-to a python module containing function definitions. By adjusting the
+cmdset while using the Menu. The menu_module_path is the python path
+to a python module containing function definitions. By adjusting the
keyword options of the Menu() initialization call you can start the
menu at different places in the menu definition file, adjust if the
menu command should overload the normal commands or not, etc.
@@ -78,7 +79,7 @@ command definition too) with function definitions:
returntext,options
-
Where caller is the object using the menu and input_string is the
+
Where caller is the object using the menu and input_string is the
command entered by the user on the previous node (the command
entered to get to this node). The node function code will only be
executed once per node-visit and the system will accept nodes with
@@ -92,50 +93,43 @@ deleted when the menu is exited.
returned as None as well. If the options are returned as None, the
menu is immediately exited and the default “look” command is called.
-
text (str, tuple or None): Text shown at this node. If a tuple, the
-second element in the tuple is a help text to display at this
-node when the user enters the menu help command there.
+
+
text (str, tuple or None): Text shown at this node. If a tuple, the
second element in the tuple is a help text to display at this
+node when the user enters the menu help command there.
+
+
+
options (tuple, dict or None): If None, this exits the menu.
If a single dict, this is a single-option node. If a tuple,
-it should be a tuple of option dictionaries. Option dicts have
-the following keys:
+it should be a tuple of option dictionaries. Option dicts have the following keys:
-
-
key (str or tuple, optional): What to enter to choose this option.
If a tuple, it must be a tuple of strings, where the first string is the
+
key (str or tuple, optional): What to enter to choose this option.
+If a tuple, it must be a tuple of strings, where the first string is the
key which will be shown to the user and the others are aliases.
If unset, the options’ number will be used. The special key _default
marks this option as the default fallback when no other option matches
the user input. There can only be one _default option per node. It
-will not be displayed in the list.
-
-
-
+will not be displayed in the list.
desc (str, optional): This describes what choosing the option will do.
-
-
goto (str, tuple or callable): If string, should be the name of node to go to
when this option is selected. If a callable, it has the signature
+
goto (str, tuple or callable): If string, should be the name of node to go to
+when this option is selected. If a callable, it has the signature
callable(caller[,raw_input][,**kwargs]). If a tuple, the first element
-is the callable and the second is a dict with the kwargs to pass to
+is the callable and the second is a dict with the **kwargs to pass to
the callable. Those kwargs will also be passed into the next node if possible.
Such a callable should return either a str or a (str, dict), where the
string is the name of the next node to go to and the dict is the new,
(possibly modified) kwarg to pass into the next node. If the callable returns
-None or the empty string, the current node will be revisited.
-
-
-
-
-
exec (str, callable or tuple, optional): This takes the same input as goto above
and runs before it. If given a node name, the node will be executed but will not
+None or the empty string, the current node will be revisited.
+
exec (str, callable or tuple, optional): This takes the same input as goto above
+and runs before it. If given a node name, the node will be executed but will not
be considered the next node. If node/callback returns str or (str, dict), these will
replace the goto step (goto callbacks will not fire), with the string being the
next node name and the optional dict acting as the kwargs-input for the next node.
-If an exec callable returns None, the current node is re-run.
-
-
-
+If an exec callable returns the empty string (only), the current node is re-run.
-
If key is not given, the option will automatically be identified by
+
If key is not given, the option will automatically be identified by
its number 1..N.
Example:
# in menu_module.py
@@ -189,9 +183,8 @@ same Using help will show the help text, otherwise a list of
available commands while in menu mode.
The menu tree is exited either by using the in-menu quit command or by
reaching a node without any options.
-
For a menu demo, import CmdTestMenu from this module and add it to
-your default cmdset. Run it with this module, like testmenu
-evennia.utils.evmenu.
+
For a menu demo, import CmdTestMenu from this module and add it to
+your default cmdset. Run it with this module, like testmenu evennia.utils.evmenu.
In evmenu.py is a helper function parse_menu_template that parses a
@@ -281,9 +274,9 @@ allowed, these will be added to the **kwargs going into the cal
strings is only needed if wanting to pass strippable spaces, otherwise the
key:values will be converted to strings/numbers with literal_eval before passed
into the callable.
-
The “> ” option takes a glob or regex to perform different actions depending on user
-input. Make sure to sort these in increasing order of generality since they
-will be tested in sequence.
+
The > option takes a glob or regex to perform different actions depending
+on user input. Make sure to sort these in increasing order of generality since
+they will be tested in sequence.
@@ -366,6 +359,11 @@ commands the caller can use.
lock_storage = 'cmd:all()'¶
@@ -497,14 +495,12 @@ menu will not be using this same session anymore after a reload.persistent flag is deactivated.
+
**kwargs – All kwargs will become initialization variables on caller.ndb._menutree,
+to be available at run.
-
Keyword Arguments
-
any (any) – All kwargs will become initialization variables on caller.ndb._evmenu,
-to be available at run.
-
-
Raises
-
EvMenuError – If the start/end node is not found in menu tree.
+
Raises
+
EvMenuError – If the start/end node is not found in menu tree.
Notes
@@ -604,11 +600,9 @@ a (“nodename”, kwargs) tuple.
raw_string (str) – The raw default string entered on the
previous node (only used if the node accepts it as an
argument)
+
**kwargs – Extra arguments to goto callables.
-
Keyword Arguments
-
any – Extra arguments to goto callables.
-
@@ -753,34 +747,49 @@ prepending those options added in the node.
option_generator (callable or list) – A list of strings indicating the options, or a callable
that is called as option_generator(caller) to produce such a list.
select (callable or str, optional) – Node to redirect a selection to. Its **kwargs will
-contain the available_choices list and selection will hold one
-of the elements in that list. If a callable, it will be called as
-select(caller, menuchoice, **kwargs) where menuchoice is the
-chosen option as a string and available_choices is the list of available
-options offered by the option_generator. The callable whould return
-the name of the target node to goto after this selection (or None to repeat the
-list-node). Note that if this is not given, the decorated node
-must itself provide a way to continue from the node!
+contain the available_choices list and selection will hold one of the elements in
+that list. If a callable, it will be called as
+select(caller, menuchoice, **kwargs) where menuchoice is the chosen option as a
+string and available_choices is a kwarg mapping the option keys to the choices
+offered by the option_generator. The callable whould return the name of the target node
+to goto after this selection (or None to repeat the list-node). Note that if this is not
+given, the decorated node must itself provide a way to continue from the node!
pagesize (int) – How many options to show per page.
Example
-
def_selectfunc(caller,menuchoice,**kwargs):
- # menuchoice would be either 'foo' or 'bar' here
- # kwargs['available_choices'] would be the list ['foo', 'bar']
- return"the_next_node_to_go_to"
+
defselect(caller,selection,available_choices=None,**kwargs):
+ '''
+ Args:
+ caller (Object or Account): User of the menu.
+ selection (str): What caller chose in the menu
+ available_choices (list): The keys of elements available on the *current listing
+ page*.
+ **kwargs: Kwargs passed on from the node.
+ Returns:
+ tuple, str or None: A tuple (nextnodename, **kwargs) or just nextnodename. Return
+ **None** to go back to the listnode.
-@list_node(['foo','bar'],_selectfunc)
-defnode_index(caller):
- text="describing the list"
- returntext,[]
+ # (do something with **selection** here)
+
+ return "nextnode", **kwargs
+
+@list_node(['foo', 'bar'], select)
+def node_index(caller):
+ text = "describing the list"
+
+ # optional extra options in addition to the list-options
+ extra_options = []
+
+ return text, extra_options
Notes
-
All normal goto or exec callables returned from the decorated nodes will, if they accept
-**kwargs, get a new kwarg available_choices injected. This is the ordered list of named
-options (descs) visible on the current node page.
+
All normal goto or exec callables returned from the decorated nodes
+will, if they accept **kwargs, get a new kwarg ‘available_choices’
+injected. These are the ordered list of named options (descs) visible
+on the current node page.
@@ -814,6 +823,11 @@ options (descs) visible on the current node page.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': '__noinput_command', 'category': 'general', 'key': '__nomatch_command', 'tags': '', 'text': '\n Enter your data and press return.\n '}¶
+
+
@@ -867,44 +881,29 @@ options (descs) visible on the current node page.
This is a helper function for easily request input from
-the caller.
+
This is a helper function for easily request input from the caller.
Parameters
-
caller (Account or Object) – The entity being asked
-the question. This should usually be an object
-controlled by a user.
-
prompt (str) – This text will be shown to the user,
-in order to let them know their input is needed.
+
caller (Account or Object) – The entity being asked the question. This
+should usually be an object controlled by a user.
+
prompt (str) – This text will be shown to the user, in order to let them
+know their input is needed.
callback (callable) – A function that will be called
-when the user enters a reply. It must take three
-arguments: the caller, the prompt text and the
-result of the input given by the user. If the
-callback doesn’t return anything or return False,
-the input prompt will be cleaned up and exited. If
-returning True, the prompt will remain and continue to
-accept input.
+when the user enters a reply. It must take three arguments: the
+caller, the prompt text and the result of the input given by
+the user. If the callback doesn’t return anything or return False,
+the input prompt will be cleaned up and exited. If returning True,
+the prompt will remain and continue to accept input.
session (Session, optional) – This allows to specify the
-session to send the prompt to. It’s usually only
-needed if caller is an Account in multisession modes
-greater than 2. The session is then updated by the
-command and is available (for example in callbacks)
-through caller.ndb.getinput._session.
-
args (optional) – Extra arguments will be
-passed to the fall back function as a list ‘args’
-and all keyword arguments as a dictionary ‘kwargs’.
-To utilise *args and **kwargs, a value for the
-session argument must be provided (None by default)
-and the callback function must take *args and
-**kwargs as arguments.
-
kwargs (optional) – Extra arguments will be
-passed to the fall back function as a list ‘args’
-and all keyword arguments as a dictionary ‘kwargs’.
-To utilise *args and **kwargs, a value for the
-session argument must be provided (None by default)
-and the callback function must take *args and
-**kwargs as arguments.
+session to send the prompt to. It’s usually only needed if caller
+is an Account in multisession modes greater than 2. The session is
+then updated by the command and is available (for example in
+callbacks) through caller.ndb.getinput._session.
+
*args (any) – Extra arguments to pass to callback. To utilise *args
+(and **kwargs), a value for the session argument must also be
+provided.
+
**kwargs (any) – Extra kwargs to pass to callback.
Raises
@@ -912,23 +911,160 @@ and the callback function must take *args and
Notes
-
The result value sent to the callback is raw and not
-processed in any way. This means that you will get
-the ending line return character from most types of
-client inputs. So make sure to strip that before
-doing a comparison.
-
When the prompt is running, a temporary object
-caller.ndb._getinput is stored; this will be removed
-when the prompt finishes.
-If you need the specific Session of the caller (which
-may not be easy to get if caller is an account in higher
-multisession modes), then it is available in the
-callback through caller.ndb._getinput._session.
-
Chaining get_input functions will result in the caller
-stacking ever more instances of InputCmdSets. Whilst
-they will all be cleared on concluding the get_input
-chain, EvMenu should be considered for anything beyond
-a single question.
+
The result value sent to the callback is raw and not processed in any
+way. This means that you will get the ending line return character from
+most types of client inputs. So make sure to strip that before doing a
+comparison.
+
When the prompt is running, a temporary object caller.ndb._getinput
+is stored; this will be removed when the prompt finishes.
+
If you need the specific Session of the caller (which may not be easy
+to get if caller is an account in higher multisession modes), then it
+is available in the callback through caller.ndb._getinput._session.
+This is why the session is required as input.
+
It’s not recommended to ‘chain’ get_input into a sequence of
+questions. This will result in the caller stacking ever more instances
+of InputCmdSets. While they will all be cleared on concluding the
+get_input chain, EvMenu should be considered for anything beyond a
+single question.
+search_index_entry = {'aliases': 'a y yes __nomatch_command abort no n', 'category': 'general', 'key': '__noinput_command', 'tags': '', 'text': '\n Handle a prompt for yes or no. Press [return] for the default choice.\n\n '}¶
+evennia.utils.evmenu.ask_yes_no(caller, prompt='Yes or No {options}?', yes_action='Yes', no_action='No', default=None, allow_abort=False, session=None, *args, **kwargs)[source]¶
+
A helper question for asking a simple yes/no question. This will cause
+the system to pause and wait for input from the player.
+
+
Parameters
+
+
prompt (str) – The yes/no question to ask. This takes an optional formatting
+marker {options} which will be filled with ‘Y/N’, ‘[Y]/N’ or
+‘Y/[N]’ depending on the setting of default. If allow_abort is set,
+then the ‘A(bort)’ option will also be available.
+
yes_action (callable or str) – If a callable, this will be called
+with (caller, *args, **kwargs) when the Yes-choice is made.
+If a string, this string will be echoed back to the caller.
+
no_action (callable or str) – If a callable, this will be called
+with (caller, *args, **kwargs) when the No-choice is made.
+If a string, this string will be echoed back to the caller.
+
default (str optional) – This is what the user will get if they just press the
+return key without giving any input. One of ‘N’, ‘Y’, ‘A’ or None
+for no default (an explicit choice must be given). If ‘A’ (abort)
+is given, allow_abort kwarg is ignored and assumed set.
+
allow_abort (bool, optional) – If set, the ‘A(bort)’ option is available
+(a third option meaning neither yes or no but just exits the prompt).
+
session (Session, optional) – This allows to specify the
+session to send the prompt to. It’s usually only needed if caller
+is an Account in multisession modes greater than 2. The session is
+then updated by the command and is available (for example in
+callbacks) through caller.ndb._yes_no_question.session.
+
*args – Additional arguments passed on into callables.
+
**kwargs – Additional keyword args passed on into callables.
+
+
+
Raises
+
RuntimeError, FooError – If default and allow_abort clashes.
+
+
+
Example
+
# just returning strings
+ask_yes_no(caller,"Are you happy {options}?",
+ "you answered yes","you answered no")
+# trigger callables
+ask_yes_no(caller,"Are you sad {options}?",
+ _callable_yes,_callable_no,allow_abort=True)
+
+
@@ -1032,7 +1168,6 @@ Must be on the form callable(caller, raw_string, **kwargs).
Where always_page decides if the pager is used also if the text is not long
-enough to need to scroll, session is used to determine which session to relay
-to and justify_kwargs are kwargs to pass to utils.utils.justify in order to
+
The always_page argument decides if the pager is used also if the text is not long
+enough to need to scroll, session is used to determine which session to relay
+to and justify_kwargs are kwargs to pass to utils.utils.justify in order to
change the formatting of the text. The remaining **kwargs will be passed on to
the caller.msg() construct every time the page is updated.
@@ -75,7 +75,7 @@ the caller.msg() construct every time the page is updated.
@@ -99,6 +99,11 @@ the caller.msg() construct every time the page is updated.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': 'a end b e next quit top t q abort n back', 'category': 'general', 'key': '__noinput_command', 'tags': '', 'text': '\n Manipulate the text paging\n '}¶
+
+
@@ -137,6 +142,11 @@ the caller.msg() construct every time the page is updated.
lock_storage = 'cmd:all();'¶
+
+
+search_index_entry = {'aliases': 'l', 'category': 'general', 'key': 'look', 'tags': '', 'text': '\n Override look to display window and prevent OOCLook from firing\n '}¶
If a string, paginage normally. If this text contains
-one or more \f (backslash + f) format symbols, automatic
-pagination and justification are force-disabled and
-page-breaks will only happen after each \f.
+one or more \f format symbol, automatic pagination and justification
+are force-disabled and page-breaks will only happen after each \f.
If EvTable, the EvTable will be paginated with the same
setting on each page if it is too long. The table
decorations will be considered in the size of the page.
@@ -201,8 +210,9 @@ expected to be a line in the final display. Each line
will be run through iter_callable.
-
always_page (bool, optional) – If False, the pager will only kick
-in if inp is too big to fit the screen.
+
always_page (bool, optional) – If False, the
+pager will only kick in if inp is too big
+to fit the screen.
session (Session, optional) – If given, this session will be used
to determine the screen width and will receive all output.
justify (bool, optional) – If set, auto-justify long lines. This must be turned
@@ -218,48 +228,28 @@ exit message will not be shown.
the caller when the more page exits. Note that this will be using whatever
cmdset the user had before the evmore pager was activated (so none of
the evmore commands will be available when this is run).
-
kwargs (any, any) – These will be passed on to the caller.msg method.
+
kwargs (any, optional) – These will be passed on to the caller.msg method.
The input contains \f (backslash + f) markers. We use \f to indicate
-the user wants to enforce their line breaks on their own. If so, we do
-no automatic line-breaking/justification at all.
+
The input contains f markers. We use f to indicate the user wants to
+enforce their line breaks on their own. If so, we do no automatic
+line-breaking/justification at all.
+
+
Parameters
+
text (str) – The string to format with f-markers.
+
+
@@ -378,16 +373,15 @@ strings, querysets, django.Paginator, EvTables and any iterables with strings.
Notes
If overridden, this method must perform the following actions:
-
read and re-store self._data (the incoming data set) if needed
-for pagination to work.
+
read and re-store self._data (the incoming data set) if needed for pagination to
+work.
set self._npages to the total number of pages. Default is 1.
set self._paginator to a callable that will take a page number 1…N and return
the data to display on that page (not any decorations or next/prev buttons). If only
wanting to change the paginator, override self.paginator instead.
-
set self._page_formatter to a callable that will receive the
-page from self._paginator and format it with one element per
-line. Default is str. Or override self.page_formatter
-directly instead.
+
set self._page_formatter to a callable that will receive the page from
+self._paginator and format it with one element per line. Default is str. Or
+override self.page_formatter directly instead.
By default, helper methods are called that perform these actions
depending on supported inputs.
@@ -439,46 +433,69 @@ EvTable across many pages and feed it into EvMore all at once).
If a string, paginage normally. If this text contains
-one or more \f (backslash + f) format symbol, automatic pagination is disabled
-and page-breaks will only happen after each \f.
+one or more \f format symbol, automatic pagination and justification
+are force-disabled and page-breaks will only happen after each \f.
If EvTable, the EvTable will be paginated with the same
setting on each page if it is too long. The table
decorations will be considered in the size of the page.
-
Otherwise text is converted to an iterator, where each step is
-is expected to be a line in the final display, and each line
-will be run through repr().
+
Otherwise inp is converted to an iterator, where each step is
+expected to be a line in the final display. Each line
+will be run through iter_callable.
always_page (bool, optional) – If False, the
-pager will only kick in if text is too big
+pager will only kick in if inp is too big
to fit the screen.
session (Session, optional) – If given, this session will be used
to determine the screen width and will receive all output.
-
justify (bool, optional) – If set, justify long lines in output. Disable for
-fixed-format output, like tables.
-
justify_kwargs (dict, bool or None, optional) – If given, this should
-be valid keyword arguments to the utils.justify() function. If False,
-no justification will be done.
-
exit_on_lastpage (bool, optional) – Immediately exit pager when reaching the last page.
-
use_evtable (bool, optional) – If True, each page will be rendered as an
-EvTable. For this to work, text must be an iterable, where each element
-is the table (list of list) to render on that page.
-
evtable_args (tuple, optional) – The args to use for EvTable on each page.
-
evtable_kwargs (dict, optional) – The kwargs to use for EvTable on each
-page (except table, which is supplied by EvMore per-page).
-
kwargs (any, optional) – These will be passed on
-to the caller.msg method.
+
justify (bool, optional) – If set, auto-justify long lines. This must be turned
+off for fixed-width or formatted output, like tables. It’s force-disabled
+if inp is an EvTable.
+
justify_kwargs (dict, optional) – Keywords for the justifiy function. Used only
+if justify is True. If this is not set, default arguments will be used.
+
exit_on_lastpage (bool, optional) – If reaching the last page without the
+page being completely filled, exit pager immediately. If unset,
+another move forward is required to exit. If set, the pager
+exit message will not be shown.
+
exit_cmd (str, optional) – If given, this command-string will be executed on
+the caller when the more page exits. Note that this will be using whatever
+cmdset the user had before the evmore pager was activated (so none of
+the evmore commands will be available when this is run).
+
kwargs (any, optional) – These will be passed on to the caller.msg method.
This is an advanced ASCII table creator. It was inspired by Prettytable
+(https://code.google.com/p/prettytable/) but shares no code and is considerably
+more advanced, supporting auto-balancing of incomplete tables and ANSI colors among
+other things.
Example usage:
fromevennia.utilsimportevtabletable=evtable.EvTable("Heading1","Heading2",
- table=[[1,2,3],[4,5,6],[7,8,9]],border="cells")
+ table=[[1,2,3],[4,5,6],[7,8,9]],border="cells")table.add_column("This is long data","This is even longer data")table.add_row("This is a single row")printtable
@@ -99,8 +101,9 @@ Here we change the width and alignment of the column at index 3
(Python starts from 0):
+-----------+-------+-----+-----------------------------+---------+|Heading1|Headi||||||ng2||||+~~~~~~~~~~~+~~~~~~~+~~~~~+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+~~~~~~~~~+
@@ -121,14 +124,13 @@ Here we change the width and alignment of the column at index 3
If the height is restricted, cells will be restricted from expanding
vertically. This will lead to text contents being cropped. Each cell
can only shrink to a minimum width and height of 1.
-
EvTable is intended to be used with [ANSIString](evennia.utils.ansi#ansistring)
-for supporting ANSI-coloured string types.
-
When a cell is auto-wrapped across multiple lines, ANSI-reset
-sequences will be put at the end of each wrapped line. This means that
-the colour of a wrapped cell will not “bleed”, but it also means that
-eventual colour outside the table will not transfer “across” a table,
-you need to re-set the color to have it appear on both sides of the
-table string.
+
EvTable is intended to be used with ANSIString for supporting ANSI-coloured
+string types.
+
When a cell is auto-wrapped across multiple lines, ANSI-reset sequences will be
+put at the end of each wrapped line. This means that the colour of a wrapped
+cell will not “bleed”, but it also means that eventual colour outside the table
+will not transfer “across” a table, you need to re-set the color to have it
+appear on both sides of the table string.
@@ -476,8 +478,9 @@ resize individual columns in the vertical direction to fit.
height (int, optional) – Fixed height of table. Defaults to being unset. Width is
still given precedence. If given, table cells will crop text rather
than expand vertically.
-
evenwidth (bool, optional) – Used with the width keyword. Adjusts columns to have as even width as
-possible. This often looks best also for mixed-length tables. Default is False.
+
evenwidth (bool, optional) – Used with the width keyword. Adjusts columns to have as
+even width as possible. This often looks best also for mixed-length tables. Default
+is False.
maxwidth (int, optional) – This will set a maximum width
of the table while allowing it to be smaller. Only if it grows wider than this
size will it be resized by expanding horizontally (or crop height is given).
@@ -649,7 +652,6 @@ given from 0 to Ncolumns-1.
diff --git a/docs/0.9.5/api/evennia.utils.gametime.html b/docs/0.9.5/api/evennia.utils.gametime.html
index 8afffd3831..05f65473df 100644
--- a/docs/0.9.5/api/evennia.utils.gametime.html
+++ b/docs/0.9.5/api/evennia.utils.gametime.html
@@ -273,7 +273,6 @@ the epoch set by settings.TIME_GAME_EPOCH will still apply.
-
diff --git a/docs/0.9.5/api/evennia.utils.idmapper.tests.html b/docs/0.9.5/api/evennia.utils.idmapper.tests.html
index ce2da73681..2d5fd8904a 100644
--- a/docs/0.9.5/api/evennia.utils.idmapper.tests.html
+++ b/docs/0.9.5/api/evennia.utils.idmapper.tests.html
@@ -406,7 +406,6 @@ object the first time, the query is executed.
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 “(”. It 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 not completely closing all inlinefunc blocks, will lead
-to the entire string remaining unparsed.
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, template (regex, str) – Regex to match against strings and a
-template with markers {arg1}, {arg2}, etc for replacement using the
-standard .format method.
diff --git a/docs/0.9.5/api/evennia.utils.logger.html b/docs/0.9.5/api/evennia.utils.logger.html
index 79a9df4a9d..b692ed8dd4 100644
--- a/docs/0.9.5/api/evennia.utils.logger.html
+++ b/docs/0.9.5/api/evennia.utils.logger.html
@@ -319,7 +319,7 @@ to preserve a continuous chat history for channel log files.
Convenience method for accessing our _file attribute’s seek method,
-which is used in tail_log_function.
-:param *args: Same args as file.seek
-:param **kwargs: Same kwargs as file.seek
Convenience method for accessing our _file attribute’s readlines method,
-which is used in tail_log_function.
-:param *args: same args as file.readlines
-:param **kwargs: same kwargs as file.readlines
+which is used in tail_log_function.
-
Returns
-
lines (list) – lines from our _file attribute.
+
Parameters
+
+
*args – same args as file.readlines
+
**kwargs – same kwargs as file.readlines
+
+
+
Returns
+
lines (list) – lines from our _file attribute.
@@ -365,6 +375,36 @@ on new lines following datetime info.
+
Stores the current value using .handler.save_handler(self.key, value, **kwargs)
-where kwargs are a combination of those passed into this function and the
-ones specified by the OptionHandler.
+where kwargs are a combination of those passed into this function and
+the ones specified by the OptionHandler.
Keyword Arguments
any (any) – Not used by default. These are passed in from self.set
@@ -901,7 +899,6 @@ entries are processed.
-
diff --git a/docs/0.9.5/api/evennia.utils.picklefield.html b/docs/0.9.5/api/evennia.utils.picklefield.html
index 9bbaa4b1f1..7276eb1389 100644
--- a/docs/0.9.5/api/evennia.utils.picklefield.html
+++ b/docs/0.9.5/api/evennia.utils.picklefield.html
@@ -251,7 +251,6 @@ This is used by the serialization framework.
-
diff --git a/docs/0.9.5/api/evennia.utils.search.html b/docs/0.9.5/api/evennia.utils.search.html
index 753955b7c7..26cc400d1c 100644
--- a/docs/0.9.5/api/evennia.utils.search.html
+++ b/docs/0.9.5/api/evennia.utils.search.html
@@ -147,7 +147,7 @@ one of the arguments must be given to do a search.
Parameters
-
sender (Object or Account, optional) – Get messages sent by a particular account or object
+
sender (Object, Account or Script, optional) – Get messages sent by a particular sender.
receiver (Object, Account or Channel, optional) – Get messages
received by a certain account,object or channel
freetext (str) – Search for a text string in a message. NOTE:
@@ -159,7 +159,7 @@ always gives only one match.
Returns
-
messages (list or Msg) – A list of message matches or a single match if dbref was given.
-
diff --git a/docs/0.9.5/api/evennia.utils.test_resources.html b/docs/0.9.5/api/evennia.utils.test_resources.html
index 9d68fc0b65..0e271914d6 100644
--- a/docs/0.9.5/api/evennia.utils.test_resources.html
+++ b/docs/0.9.5/api/evennia.utils.test_resources.html
@@ -70,7 +70,6 @@ should directly give the module pathname to unload.
...# test code using foo.GLOBALTHING, now set to 'mockval'
-
Notes
This allows for mocking constants global to the module, since
otherwise those would not be mocked (since a module is only
loaded once).
@@ -214,7 +213,6 @@ It helps ensure your tests are run with your own objects.
Safely clean all whitespace at the left of a paragraph.
Parameters
text (str) – The text to dedent.
-
baseline_index (int or None, optional) – Which row to use as a ‘base’
+
baseline_index (int, optional) – Which row to use as a ‘base’
for the indentation. Lines will be dedented to this level but
no further. If None, indent so as to completely deindent the
least indented text.
+
indent (int, optional) – If given, force all lines to this indent.
+This bypasses baseline_index.
Returns
@@ -231,8 +233,8 @@ Defaults to client’s default width.
This pretty-formats an iterable list as string output, adding an optional
alternative separator to the second to last entry. If addquote
is True, the outgoing strings will be surrounded by quotes.
@@ -249,16 +251,16 @@ values with double quotes.
Returns
-
liststr (str) – The list represented as a string.
+
str – The list represented as a string.
Examples
-
# no endsep:
- [1,2,3]->'1, 2, 3'
-# with endsep=='and':
- [1,2,3]->'1, 2 and 3'
-# with addquote and endsep
- [1,2,3]->'"1", "2" and "3"'
+
>>> list_to_string([1,2,3],endsep='')
+'1, 2, 3'
+>>> list_to_string([1,2,3],ensdep='and')
+'1, 2, and 3'
+>>> list_to_string([1,2,3],endsep='and',addquote=True)
+'"1", "2", and "3"'
@@ -282,16 +284,49 @@ values with double quotes.
Returns
-
liststr (str) – The list represented as a string.
+
str – The list represented as a string.
Examples
-
# no endsep:
- [1,2,3]->'1, 2, 3'
-# with endsep=='and':
- [1,2,3]->'1, 2 and 3'
-# with addquote and endsep
- [1,2,3]->'"1", "2" and "3"'
+
>>> list_to_string([1,2,3],endsep='')
+'1, 2, 3'
+>>> list_to_string([1,2,3],ensdep='and')
+'1, 2, and 3'
+>>> list_to_string([1,2,3],endsep='and',addquote=True)
+'"1", "2", and "3"'
+
This pretty-formats an iterable list as string output, adding an optional
+alternative separator to the second to last entry. If addquote
+is True, the outgoing strings will be surrounded by quotes.
+
+
Parameters
+
+
initer (any) – Usually an iterable to print. Each element must be possible to
+present with a string. Note that if this is a generator, it will be
+consumed by this operation.
+
endsep (str, optional) – If set, the last item separator will
+be replaced with this value.
+
addquote (bool, optional) – This will surround all outgoing
+values with double quotes.
+
+
+
Returns
+
str – The list represented as a string.
+
+
+
Examples
+
>>> list_to_string([1,2,3],endsep='')
+'1, 2, 3'
+>>> list_to_string([1,2,3],ensdep='and')
+'1, 2, and 3'
+>>> list_to_string([1,2,3],endsep='and',addquote=True)
+'"1", "2", and "3"'
@@ -550,10 +585,8 @@ be found, the protocol flag is reset to utf-8. In any case, returns bytes.
-
-
Note
+
Notes
If text is already bytes, return it as is.
-
@@ -574,10 +607,8 @@ falling back to settings.ENCODINGS.
decoded_text (str) – The decoded text.
-
-
Note
+
Notes
If text is already str, return it as is.
-
@@ -603,9 +634,10 @@ distance from parent.
Parameters
-
obj (any) – Object to analyze. This may be either an instance
-or a class.
-
parent (any) – Can be either instance, class or python path to class.
+
obj (any) – Object to analyze. This may be either an instance or
+a class.
+
parent (any) – Can be either an instance, a class or the python
+path to the class.
Returns
@@ -613,10 +645,8 @@ or a class.
Notes
-
What differs this function from e.g. isinstance() is that obj
-may be both an instance and a class, and parent may be an
-instance, a class, or the python path to a class (counting from
-the evennia root directory).
+
What differentiates this function from Python’s isinstance() is the
+flexibility in the types allowed for the object and parent being compared.
@@ -639,10 +669,7 @@ any results if called from inside the game.
shortcut to having to use the full backend name.
Parameters
-
-
name (str) – One of ‘sqlite3’, ‘mysql’, ‘postgresql’
-
'oracle'. (or) –
-
+
name (str) – One of ‘sqlite3’, ‘mysql’, ‘postgresql’ or ‘oracle’.
Returns
uses (bool) – If the given database is used or not.
@@ -660,7 +687,7 @@ shortcut to having to use the full backend name.
timedelay (int or float) – The delay in seconds.
callback (callable) – Will be called as callback(*args, **kwargs)
after timedelay seconds.
-
args (any) – Will be used as arguments to callback.
+
*args – Will be used as arguments to callback
Keyword Arguments
@@ -679,8 +706,7 @@ persistent is False by default.
-
-
Note
+
Notes
The task handler (evennia.scripts.taskhandler.TASK_HANDLER) will
be called for persistent or non-persistent tasks.
If persistent is set to True, the callback, its arguments
@@ -694,7 +720,63 @@ If persistent is set to True the delay function will return an int
which is the task’s id itended for use with TASK_HANDLER’s do_task
and remove methods.
All persistent tasks whose time delays have passed will be called on server startup.
callback (callable) – This will be called with *args, **kwargs every
+interval seconds. This must be possible to pickle regardless
+of if persistent is set or not!
+
persistent (bool, optional) – If ticker survives a server reload.
+
idstring (str, optional) – Separates multiple tickers. This is useful
+mainly if wanting to set up multiple repeats for the same
+interval/callback but with different args/kwargs.
+
stop (bool, optional) – If set, use the given parameters to _stop_ a running
+ticker instead of creating a new one.
+
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 – 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
+created ticker. Store this and pass into unrepat() in order to to stop
+this ticker later. Returns None if stop=True.
+
+
Raises
+
KeyError – If trying to stop a ticker that was not found.
This is used to stop a ticker previously started with repeat.
+
+
Parameters
+
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
+directly.
+
+
Returns
+
bool –
+
+
True if a ticker was stopped, False if not (for example because no
matching ticker was found or it was already stopped).
+
+
+
+
+
@@ -758,7 +840,7 @@ some checks for runtime libraries.
Return a class from a module, given the module’s path. This is
+
Return a class from a module, given the class’ full python path. This is
primarily used to convert db_typeclass_path:s to classes.
Parameters
@@ -1039,15 +1122,11 @@ the value, the more exact a match is required).
Returns
-
suggestions (list) –
-
-
Suggestions from vocabulary with a
similarity-rating that higher than or equal to cutoff.
+
suggestions (list) – Suggestions from vocabulary with a
+similarity-rating that higher than or equal to cutoff.
Could be empty if there are no matches.
-
-
-
@@ -1078,47 +1157,126 @@ array) instead of strings.
Note: evennia.utils.evtable is more powerful than this, but this function
-can be useful when the number of columns and rows are unknown and must be
-calculated on the fly.
-
-
Args.
-
table (list): A list of lists to represent columns in the
table: [[val,val,val,…], [val,val,val,…], …], where
+
Format a 2D array of strings into a multi-column table.
+
+
Parameters
+
+
table (list) – A list of lists to represent columns in the
+table: [[val,val,val,…], [val,val,val,…], …], where
each val will be placed on a separate row in the
column. All columns must have the same number of rows (some
-positions may be empty though).
+positions may be empty though).
+
extra_space (int, optional) – Sets how much minimum extra
+padding (in characters) should be left between columns.
+
-
extra_space (int, optional): Sets how much minimum extra
padding (in characters) should be left between columns.
-
-
-
-
-
-
Returns
-
table (list) –
-
-
A list of lists representing the rows to print
out one by one.
-
-
-
+
Returns
+
list – A list of lists representing the rows to print out one by one.
Notes
The function formats the columns to be as wide as the widest member
of each column.
-
Example
-
ftable=format_table([[...],[...],...])
+
evennia.utils.evtable is more powerful than this, but this
+function can be useful when the number of columns and rows are
+unknown and must be calculated on the fly.
+
Examples:
+
ftable=format_table([[1,2,3],[4,5,6]])
+string=""forir,rowinenumarate(ftable):ifir==0:# make first row white
- string+="\\n|w"+""join(row)+"|n"
+ string+="\n|w"+"".join(row)+"|n"else:
- string+="\\n"+"".join(row)
+ string+="\n"+"".join(row)print(string)
Get a value in an interval as a percentage of its position
+in that interval. This also understands negative numbers.
+
+
Parameters
+
+
value (number) – This should be a value minval<=value<=maxval.
+
minval (number or None) – Smallest value in interval. This could be None
+for an open interval (then return will always be 100%)
+
maxval (number or None) – Biggest value in interval. This could be None
+for an open interval (then return will always be 100%)
+
formatted (str, optional) – This is a string that should
+accept one formatting tag. This will receive the
+current value as a percentage. If None, the
+raw float will be returned instead.
+
+
+
Returns
+
str or float – The formatted value or the raw percentage as a float.
+
+
+
Notes
+
We try to handle a weird interval gracefully.
+
+
If either maxval or minval is None (open interval), we (aribtrarily) assume 100%.
+
If minval > maxval, we return 0%.
+
If minval == maxval == value we are looking at a single value match and return 100%.
+
If minval == maxval != value we return 0%.
+
If value not in [minval..maxval], we set value to the closest
+boundary, so the result will be 0% or 100%, respectively.
This helper function makes a ‘grid’ output, where it distributes the given
+string-elements as evenly as possible to fill out the given width.
+will not work well if the variation of length is very big!
+
+
Parameters
+
+
elements (iterable) – A 1D list of string elements to put in the grid.
+
width (int, optional) – The width of the grid area to fill.
+
sep (str, optional) – The extra separator to put between words. If
+set to the empty string, words may run into each other.
+
verbatim_elements (list, optional) – This is a list of indices pointing to
+specific items in the elements list. An element at this index will
+not be included in the calculation of the slot sizes. It will still
+be inserted into the grid at the correct position and may be surrounded
+by padding unless filling the entire line. This is useful for embedding
+decorations in the grid, such as horizontal bars.
+
ignore_ansi (bool, optional) – Ignore ansi markups when calculating white spacing.
+
+
+
Returns
+
list – The grid as a list of ready-formatted rows. We return it
+like this to make it easier to insert decorations between rows, such
+as horizontal bars.
@@ -1135,8 +1293,7 @@ Server by trying to access a PID file.
Examples
-
This can be used to determine if we are in a subprocess by
-something like:
+
This can be used to determine if we are in a subprocess by
self_pid=os.getpid()server_pid,portal_pid=get_evennia_pids()is_subprocess=self_pidnotin(server_pid,portal_pid)
@@ -1293,16 +1450,12 @@ on errors.
Returns
-
processed_result (Object or None) –
-
-
This is always a single result
or None. If None, any error reporting/handling should
+
processed_result (Object or None) – This is always a single result
+or None. If None, any error reporting/handling should
already have happened. The returned object is of the type we are
checking multimatches for (e.g. Objects or Commands)
-
-
-
@@ -1357,27 +1510,55 @@ of the game directory.
List available typeclasses from all available modules.
Parameters
-
parent (str, optional) – If given, only return typeclasses inheriting (at any distance)
-from this parent.
+
parent (str, optional) – If given, only return typeclasses inheriting
+(at any distance) from this parent.
Returns
-
typeclasses (dict) – On the form {“typeclass.path”: typeclass, …}
+
dict – On the form {“typeclass.path”: typeclass, …}
Notes
-
This will dynamicall retrieve all abstract django models inheriting at any distance
-from the TypedObject base (aka a Typeclass) so it will work fine with any custom
+
This will dynamically retrieve all abstract django models inheriting at
+any distance from the TypedObject base (aka a Typeclass) so it will
+work fine with any custom classes being added.
List available cmdsets from all available modules.
+
+
Parameters
+
parent (str, optional) – If given, only return cmdsets inheriting (at
+any distance) from this parent.
+
+
Returns
+
dict – On the form {“cmdset.path”: cmdset, …}
+
+
+
Notes
+
This will dynamically retrieve all abstract django models inheriting at
+any distance from the CmdSet base so it will work fine with any custom
classes being added.
Decorator to make a method pausable with yield(seconds) and able to ask for
-user-input with response=yield(question). For the question-asking to
-work, ‘caller’ must the name of an argument or kwarg to the decorated
-function.
-
Example:
+
Decorator to make a method pausable with yield(seconds)
+and able to ask for user-input with response=yield(question).
+For the question-asking to work, one of the args or kwargs to the
+decorated function must be named ‘caller’.
+
+
Raises
+
+
ValueError – If asking an interactive question but the decorated
+function has no arg or kwarg named ‘caller’.
+
ValueError – If passing non int/float to yield using for pausing.
+
+
+
+
Examples
@interactivedefmyfunc(caller):caller.msg("This is a test")
@@ -1392,7 +1573,57 @@ function.
Notes
-
This turns the method into a generator!
+
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,
diff --git a/docs/0.9.5/api/evennia.utils.validatorfuncs.html b/docs/0.9.5/api/evennia.utils.validatorfuncs.html
index 36bc66b7c3..c1966a2255 100644
--- a/docs/0.9.5/api/evennia.utils.validatorfuncs.html
+++ b/docs/0.9.5/api/evennia.utils.validatorfuncs.html
@@ -66,7 +66,7 @@ inputer’s timezone. Always returns a result in UTC.
account (AccountDB) – The Account performing this lookup. Unless from_tz is provided,
the account’s timezone option will be used.
from_tz (pytz.timezone) – An instance of a pytz timezone object from the
-user. If not provided, tries to use the timezone option of the account.
+user. If not provided, tries to use the timezone option of account.
If neither one is provided, defaults to UTC.
@@ -86,8 +86,9 @@ If neither one is provided, defaults to UTC.
Parameters
-
entry (string) – This is a string from user-input. The intended format is, for example: “5d 2w 90s” for
-‘five days, two weeks, and ninety seconds.’ Invalid sections are ignored.
+
entry (string) – This is a string from user-input. The intended format is, for example:
+“5d 2w 90s” for ‘five days, two weeks, and ninety seconds.’ Invalid sections are
+ignored.
option_key (str) – Name to display this query as.
@@ -120,14 +121,16 @@ If neither one is provided, defaults to UTC.
Simplest check in computer logic, right? This will take user input to flick the switch on or off
-:param entry: A value such as True, On, Enabled, Disabled, False, 0, or 1.
-:type entry: str
-:param option_key: What kind of Boolean we are setting. What Option is this for?
-:type option_key: str
+
Simplest check in computer logic, right? This will take user input to flick the switch on or off
-
Returns
-
Boolean
+
Parameters
+
+
entry (str) – A value such as True, On, Enabled, Disabled, False, 0, or 1.
+
option_key (str) – What kind of Boolean we are setting. What Option is this for?
+
+
+
Returns
+
Boolean
@@ -204,7 +207,6 @@ If neither one is provided, defaults to UTC.
This sub-package holds the web presence of Evennia, using normal
-Django to relate the database contents to web pages. Also the basic
-webclient and the website are defined in here (the webserver itself is
-found under the server package).
+
This sub-package holds the web presence of Evennia, using normal Django to
+relate the database contents to web pages.
File that determines what each URL points to. This uses Python regular expressions.
+This is the starting point when a user enters an URL.
+
+
The URL is matched with a regex, tying it to a given view. Note that this central url.py
+file includes url.py from all the various web-components found in views/ so the search
+space is much larger than what is shown here.
+
The view (a Python function or class is executed)
+
The view uses a template (a HTML file which may contain template markers for dynamically
+modifying its contents; the locations of such templates are given by
+settings.TEMPLATES[0][‘DIRS’]) and which may in turn may include static
+assets (CSS, images etc).
+
The view ‘renders’ the template into a finished HTML page, replacing all
+dynamic content as appropriate.
This file contains the generic, assorted views that don’t fall under one of the other applications.
-Views are django’s way of processing e.g. html templates on the fly.
This is a basic example of a Django class-based view, which are functionally
-very similar to Evennia Commands but differ in structure. Commands are used
-to interface with users using a terminal client. Views are used to interface
-with users using a web browser.
-
To use a class-based view, you need to have written a template in HTML, and
-then you write a view like this to tell Django what values to display on it.
-
While there are simpler ways of writing views using plain functions (and
-Evennia currently contains a few examples of them), just like Commands,
-writing views as classes provides you with more flexibility– you can extend
-classes and change things to suit your needs rather than having to copy and
-paste entire code blocks over and over. Django also comes with many default
-views for displaying things, all of them implemented as classes.
This is a common Django method. Think of this as the website
-equivalent of the Evennia Command.func() method.
-
If you just want to display a static page with no customization, you
-don’t need to define this method– just create a view, define
-template_name and you’re done.
-
The only catch here is that if you extend or overwrite this method,
-you’ll always want to make sure you call the parent method to get a
-context object. It’s just a dict, but it comes prepopulated with all
-sorts of background data intended for display on the page.
-
You can do whatever you want to it, but it must be returned at the end
-of this method.
-
-
Keyword Arguments
-
any (any) – Passed through.
-
-
Returns
-
context (dict) – Dictionary of data you want to display on the page.
Django views typically work with classes called “models.” Evennia objects
-are an enhancement upon these Django models and are called “typeclasses.”
-But Django itself has no idea what a “typeclass” is.
-
For the sake of mitigating confusion, any view class with this in its
-inheritance list will be modified to work with Evennia Typeclass objects or
-Django models interchangeably.
Any view you write that deals with displaying, updating or deleting a
-specific object will want to inherit from this. It provides the mechanisms
-by which to retrieve the object and make sure the user requesting it has
-permissions to actually do things to it.
Any view you write that deals with creating a specific object will want to
-inherit from this. It provides the mechanisms by which to make sure the user
-requesting creation of an object is authenticated, and provides a sane
-default title for the page.
Any view you write that deals with deleting a specific object will want to
-inherit from this. It provides the mechanisms by which to make sure the user
-requesting deletion of an object is authenticated, and that they have
-permissions to delete the requested object.
Any view you write that deals with updating a specific object will want to
-inherit from this. It provides the mechanisms by which to make sure the user
-requesting editing of an object is authenticated, and that they have
-permissions to edit the requested object.
-
This functions slightly different from default Django UpdateViews in that
-it does not update core model fields, only object attributes!
Can be overridden to return any URL you want to redirect the user to
-after the object is successfully updated, but by default it goes to the
-object detail page so the user can see their changes reflected.
Updates object attributes based on values submitted.
-
This is run when the form is submitted and the data on it is deemed
-valid– all values are within expected ranges, all strings contain
-valid characters and lengths, etc.
-
This method is only called if all values for the fields submitted
-passed form validation, so at this point we can assume the data is
-validated and sanitized.
This view provides a mechanism by which a logged-in player can view a list
-of all other characters.
-
This view requires authentication by default as a nominal effort to prevent
-human stalkers and automated bots/scrapers from harvesting data on your users.
This method will override the Django get_queryset method to return a
-list of all characters (filtered/sorted) instead of just those limited
-to the account.
-
-
Returns
-
queryset (QuerySet) – Django queryset for use in the given view.
-
diff --git a/docs/0.9.5/genindex.html b/docs/0.9.5/genindex.html
index d538c51987..80b0d772e0 100644
--- a/docs/0.9.5/genindex.html
+++ b/docs/0.9.5/genindex.html
@@ -65,6 +65,7 @@
| V
| W
| X
+ | Y