diff --git a/docs/0.9.5/.buildinfo b/docs/0.9.5/.buildinfo
deleted file mode 100644
index d470be673a..0000000000
--- a/docs/0.9.5/.buildinfo
+++ /dev/null
@@ -1,4 +0,0 @@
-# Sphinx build info version 1
-# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
-config: b2570a0a5088051190d8ba488ef54bb2
-tags: 645f666f9bcd5a90fca523b33c5a78b7
diff --git a/docs/0.9.5/.nojekyll b/docs/0.9.5/.nojekyll
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/docs/0.9.5/A-voice-operated-elevator-using-events.html b/docs/0.9.5/A-voice-operated-elevator-using-events.html
deleted file mode 100644
index 0b416338a5..0000000000
--- a/docs/0.9.5/A-voice-operated-elevator-using-events.html
+++ /dev/null
@@ -1,540 +0,0 @@
-
-
-
-
-
-
-
-
- A voice operated elevator using events — Evennia 0.9.5 documentation
-
-
-
-
-
-
-
-
-
-
-
-
-
-
This tutorial will walk you through the steps to create a voice-operated elevator, using the in-
-game Python
-system.
-This tutorial assumes the in-game Python system is installed in your game. If it isn’t, you can
-follow the installation steps given in the documentation on in-game
-Python, and
-come back on this tutorial once the system is installed. You do not need to read the entire
-documentation, it’s a good reference, but not the easiest way to learn about it. Hence these
-tutorials.
-
The in-game Python system allows to run code on individual objects in some situations. You don’t
-have to modify the source code to add these features, past the installation. The entire system
-makes it easy to add specific features to some objects, but not all.
-
-
What will we try to do?
-
-
In this tutorial, we are going to create a simple voice-operated elevator. In terms of features, we
-will:
Let’s summarize what we want to achieve first. We would like to create a room that will represent
-the inside of our elevator. In this room, a character could just say “1”, “2” or “3”, and the
-elevator will start moving. The doors will close and open on the new floor (the exits leading in
-and out of the elevator will be modified).
-
We will work on basic features first, and then will adjust some, showing you how easy and powerfully
-independent actions can be configured through the in-game Python system.
We’ll create an elevator right in our room (generally called “Limbo”, of ID 2). You could easily
-adapt the following instructions if you already have some rooms and exits, of course, just remember
-to check the IDs.
-
-
Note: the in-game Python system uses IDs for a lot of things. While it is not mandatory, it is
-good practice to know the IDs you have for your callbacks, because it will make manipulation much
-quicker. There are other ways to identify objects, but as they depend on many factors, IDs are
-usually the safest path in our callbacks.
-
-
Let’s go into limbo (#2) to add our elevator. We’ll add it to the north. To create this room,
-in-game you could type:
-
tunnel n = Inside of an elevator
-
-
-
The game should respond by telling you:
-
Created room Inside of an elevator(#3) of type typeclasses.rooms.Room.
-Created Exit from Limbo to Inside of an elevator: north(#4) (n).
-Created Exit back from Inside of an elevator to Limbo: south(#5) (s).
-
-
-
Note the given IDs:
-
-
#2 is limbo, the first room the system created.
-
#3 is our room inside of an elevator.
-
#4 is the north exit from Limbo to our elevator.
-
#5 is the south exit from an elevator to Limbo.
-
-
Keep these IDs somewhere for the demonstration. You will shortly see why they are important.
-
-
Why have we created exits to our elevator and back to Limbo? Isn’t the elevator supposed to move?
-
-
It is. But we need to have exits that will represent the way inside the elevator and out. What we
-will do, at every floor, will be to change these exits so they become connected to the right room.
-You’ll see this process a bit later.
-
We have two more rooms to create: our floor 2 and 3. This time, we’ll use dig, because we don’t
-need exits leading there, not yet anyway.
-
dig The second floor
-dig The third floor
-
-
-
Evennia should answer with:
-
Created room The second floor(#6) of type typeclasses.rooms.Room.
-Created room The third floor(#7) of type typeclasses.rooms.Room.
-
Let’s go to the elevator (you could use tel#3 if you have the same IDs I have).
-
This is our elevator room. It looks a bit empty, feel free to add a prettier description or other
-things to decorate it a bit.
-
But what we want now is to be able to say “1”, “2” or “3” and have the elevator move in that
-direction.
-
If you have read the previous tutorial about adding dialogues in events, you
-may remember what we need to do. If not, here’s a summary: we need to run some code when somebody
-speaks in the room. So we need to create a callback (the callback will contain our lines of code).
-We just need to know on which event this should be set. You can enter callhere to see the
-possible events in this room.
-
In the table, you should see the “say” event, which is called when somebody says something in the
-room. So we’ll need to add a callback to this event. Don’t worry if you’re a bit lost, just follow
-the following steps, the way they connect together will become more obvious.
-
call/add here = say 1, 2, 3
-
-
-
-
We need to add a callback. A callback contains the code that will be executed at a given time.
-So we use the call/add command and switch.
-
here is our object, the room in which we are.
-
An equal sign.
-
The name of the event to which the callback should be connected. Here, the event is “say”.
-Meaning this callback will be executed every time somebody says something in the room.
-
But we add an event parameter to indicate the keywords said in the room that should execute our
-callback. Otherwise, our callback would be called every time somebody speaks, no matter what. Here
-we limit, indicating our callback should be executed only if the spoken message contains “1”, “2” or
-“3”.
-
-
An editor should open, inviting you to enter the Python code that should be executed. The first
-thing to remember is to read the text provided (it can contain important information) and, most of
-all, the list of variables that are available in this callback:
This is important, in order to know what variables we can use in our callback out-of-the-box. Let’s
-write a single line to be sure our callback is called when we expect it to:
-
character.msg("You just said {}.".format(message))
-
-
-
You can paste this line in-game, then type the :wq command to exit the editor and save your
-modifications.
-
Let’s check. Try to say “hello” in the room. You should see the standard message, but nothing
-more. Now try to say “1”. Below the standard message, you should see:
-
You just said 1.
-
-
-
You can try it. Our callback is only called when we say “1”, “2” or “3”. Which is just what we
-want.
-
Let’s go back in our code editor and add something more useful.
-
call/edit here = say
-
-
-
-
Notice that we used the “edit” switch this time, since the callback exists, we just want to edit
-it.
-
-
The editor opens again. Let’s empty it first:
-
:DD
-
-
-
And turn off automatic indentation, which will help us:
-
:=
-
-
-
-
Auto-indentation is an interesting feature of the code editor, but we’d better not use it at this
-point, it will make copy/pasting more complicated.
So here’s the time to truly code our callback in-game. Here’s a little reminder:
-
-
We have all the IDs of our three rooms and two exits.
-
When we say “1”, “2” or “3”, the elevator should move to the right room, that is change the
-exits. Remember, we already have the exits, we just need to change their location and destination.
-
-
It’s a good idea to try to write this callback yourself, but don’t feel bad about checking the
-solution right now. Here’s a possible code that you could paste in the code editor:
-
# First let's have some constants
-ELEVATOR=get(id=3)
-FLOORS={
- "1":get(id=2),
- "2":get(id=6),
- "3":get(id=7),
-}
-TO_EXIT=get(id=4)
-BACK_EXIT=get(id=5)
-
-# Now we check that the elevator isn't already at this floor
-floor=FLOORS.get(message)
-iffloorisNone:
- character.msg("Which floor do you want?")
-elifTO_EXIT.locationisfloor:
- character.msg("The elevator already is at this floor.")
-else:
- # 'floor' contains the new room where the elevator should be
- room.msg_contents("The doors of the elevator close with a clank.")
- TO_EXIT.location=floor
- BACK_EXIT.destination=floor
- room.msg_contents("The doors of the elevator open to {floor}.",
- mapping=dict(floor=floor))
-
-
-
Let’s review this longer callback:
-
-
We first obtain the objects of both exits and our three floors. We use the get() eventfunc,
-which is a shortcut to obtaining objects. We usually use it to retrieve specific objects with an
-ID. We put the floors in a dictionary. The keys of the dictionary are the floor number (as str),
-the values are room objects.
-
Remember, the message variable contains the message spoken in the room. So either “1”, “2”, or
-“3”. We still need to check it, however, because if the character says something like “1 2” in the
-room, our callback will be executed. Let’s be sure what she says is a floor number.
-
We then check if the elevator is already at this floor. Notice that we use TO_EXIT.location.
-TO_EXIT contains our “north” exit, leading inside of our elevator. Therefore, its location will
-be the room where the elevator currently is.
-
If the floor is a different one, have the elevator “move”, changing just the location and
-destination of both exits.
-
-
The BACK_EXIT (that is “north”) should change its location. The elevator shouldn’t be
-accessible through our old floor.
-
The TO_EXIT (that is “south”, the exit leading out of the elevator) should have a different
-destination. When we go out of the elevator, we should find ourselves in the new floor, not the old
-one.
-
-
-
-
Feel free to expand on this example, changing messages, making further checks. Usage and practice
-are keys.
-
You can quit the editor as usual with :wq and test it out.
Let’s improve our callback. One thing that’s worth adding would be a pause: for the time being,
-when we say the floor number in the elevator, the doors close and open right away. It would be
-better to have a pause of several seconds. More logical.
-
This is a great opportunity to learn about chained events. Chained events are very useful to create
-pauses. Contrary to the events we have seen so far, chained events aren’t called automatically.
-They must be called by you, and can be called after some time.
-
-
Chained events always have the name “chain_X”. Usually, X is a number, but you can give the
-chained event a more explicit name.
-
In our original callback, we will call our chained events in, say, 15 seconds.
-
We’ll also have to make sure the elevator isn’t already moving.
-
-
Other than that, a chained event can be connected to a callback as usual. We’ll create a chained
-event in our elevator, that will only contain the code necessary to open the doors to the new floor.
-
call/add here = chain_1
-
-
-
The callback is added to the “chain_1” event, an event that will not be automatically called by the
-system when something happens. Inside this event, you can paste the code to open the doors at the
-new floor. You can notice a few differences:
-
TO_EXIT.location=floor
-TO_EXIT.destination=ELEVATOR
-BACK_EXIT.location=ELEVATOR
-BACK_EXIT.destination=floor
-room.msg_contents("The doors of the elevator open to {floor}.",
- mapping=dict(floor=floor))
-
-
-
Paste this code into the editor, then use :wq to save and quit the editor.
-
Now let’s edit our callback in the “say” event. We’ll have to change it a bit:
-
-
The callback will have to check the elevator isn’t already moving.
-
It must change the exits when the elevator move.
-
It has to call the “chain_1” event we have defined. It should call it 15 seconds later.
-
-
Let’s see the code in our callback.
-
call/edit here = say
-
-
-
Remove the current code and disable auto-indentation again:
-
:DD
-:=
-
-
-
And you can paste instead the following code. Notice the differences with our first attempt:
-
# First let's have some constants
-ELEVATOR=get(id=3)
-FLOORS={
- "1":get(id=2),
- "2":get(id=6),
- "3":get(id=7),
-}
-TO_EXIT=get(id=4)
-BACK_EXIT=get(id=5)
-
-# Now we check that the elevator isn't already at this floor
-floor=FLOORS.get(message)
-iffloorisNone:
- character.msg("Which floor do you want?")
-elifBACK_EXIT.locationisNone:
- character.msg("The elevator is between floors.")
-elifTO_EXIT.locationisfloor:
- character.msg("The elevator already is at this floor.")
-else:
- # 'floor' contains the new room where the elevator should be
- room.msg_contents("The doors of the elevator close with a clank.")
- TO_EXIT.location=None
- BACK_EXIT.location=None
- call_event(room,"chain_1",15)
-
-
-
What changed?
-
-
We added a little test to make sure the elevator wasn’t already moving. If it is, the
-BACK_EXIT.location (the “south” exit leading out of the elevator) should be None. We’ll remove
-the exit while the elevator is moving.
-
When the doors close, we set both exits’ location to None. Which “removes” them from their
-room but doesn’t destroy them. The exits still exist but they don’t connect anything. If you say
-“2” in the elevator and look around while the elevator is moving, you won’t see any exits.
-
Instead of opening the doors immediately, we call call_event. We give it the object containing
-the event to be called (here, our elevator), the name of the event to be called (here, “chain_1”)
-and the number of seconds from now when the event should be called (here, 15).
-
The chain_1 callback we have created contains the code to “re-open” the elevator doors. That
-is, besides displaying a message, it reset the exits’ location and destination.
-
-
If you try to say “3” in the elevator, you should see the doors closing. Look around you and you
-won’t see any exit. Then, 15 seconds later, the doors should open, and you can leave the elevator
-to go to the third floor. While the elevator is moving, the exit leading to it will be
-inaccessible.
-
-
Note: we don’t define the variables again in our chained event, we just call them. When we
-execute call_event, a copy of our current variables is placed in the database. These variables
-will be restored and accessible again when the chained event is called.
-
-
You can use the call/tasks command to see the tasks waiting to be executed. For instance, say “2”
-in the room, notice the doors closing, and then type the call/tasks command. You will see a task
-in the elevator, waiting to call the chain_1 event.
Here’s another nice little feature of events: you can modify the message of a single exit without
-altering the others. In this case, when someone goes north into our elevator, we’d like to see
-something like: “someone walks into the elevator.” Something similar for the back exit would be
-great too.
-
Inside of the elevator, you can look at the available events on the exit leading outside (south).
-
call south
-
-
-
You should see two interesting rows in this table:
So we can change the message others see when a character leaves, by editing the “msg_leave” event.
-Let’s do that:
-
call/add south = msg_leave
-
-
-
Take the time to read the help. It gives you all the information you should need. We’ll need to
-change the “message” variable, and use custom mapping (between braces) to alter the message. We’re
-given an example, let’s use it. In the code editor, you can paste the following line:
-
message="{character} walks out of the elevator."
-
-
-
Again, save and quit the editor by entering :wq. You can create a new character to see it leave.
This is a crude way to force our beggar out of the elevator, but it allows us to test. You should
-see:
-
A beggar(#8) walks out of the elevator.
-
-
-
Great! Let’s do the same thing for the exit leading inside of the elevator. Follow the beggar,
-then edit “msg_leave” of “north”:
-
call/add north = msg_leave
-
-
-
message="{character} walks into the elevator."
-
-
-
Again, you can force our beggar to move and see the message we have just set. This modification
-applies to these two exits, obviously: the custom message won’t be used for other exits. Since we
-use the same exits for every floor, this will be available no matter at what floor the elevator is,
-which is pretty neat!
Q: what happens if the game reloads or shuts down while a task is waiting to happen?
-
A: if your game reloads while a task is in pause (like our elevator between floors), when the
-game is accessible again, the task will be called (if necessary, with a new time difference to take
-into account the reload). If the server shuts down, obviously, the task will not be called, but
-will be stored and executed when the server is up again.
-
Q: can I use all kinds of variables in my callback? Whether chained or not?
-
A: you can use every variable type you like in your original callback. However, if you
-execute call_event, since your variables are stored in the database, they will need to respect the
-constraints on persistent attributes. A callback will not be stored in this way, for instance.
-This variable will not be available in your chained event.
-
Q: when you say I can call my chained events something else than “chain_1”, “chain_2” and
-such, what is the naming convention?
-
A: chained events have names beginning by “chain_”. This is useful for you and for the
-system. But after the underscore, you can give a more useful name, like “chain_open_doors” in our
-case.
-
Q: do I have to pause several seconds to call a chained event?
-
A: no, you can call it right away. Just leave the third parameter of call_event out (it
-will default to 0, meaning the chained event will be called right away). This will not create a
-task.
-
Q: can I have chained events calling themselves?
-
A: you can. There’s no limitation. Just be careful, a callback that calls itself,
-particularly without delay, might be a good recipe for an infinite loop. However, in some cases, it
-is useful to have chained events calling themselves, to do the same repeated action every X seconds
-for instance.
-
Q: what if I need several elevators, do I need to copy/paste these callbacks each time?
-
A: not advisable. There are definitely better ways to handle this situation. One of them is
-to consider adding the code in the source itself. Another possibility is to call chained events
-with the expected behavior, which makes porting code very easy. This side of chained events will be
-shown in the next tutorial.
Building up to Evennia 1.0 and beyond, it’s time to comb through the Evennia API for old cruft. This
-whitepage is for anyone interested to contribute with their views on what part of the API needs
-refactoring, cleanup or clarification (or extension!)
-
Note that this is not a forum. To keep things clean, each opinion text should ideally present a
-clear argument or lay out a suggestion. Asking for clarification and any side-discussions should be
-held in chat or forum.
This is how to enter an opinion. Use any markdown needed but stay within your section. Also remember
-to copy your text to the clipboard before saving since if someone else edited the wiki in the
-meantime you’ll have to start over.
I don’t agree with removing explicit keywords as suggested by [Johnny on Aug 29 below](API-
-refactoring#reduce-usage-of-optionalpositional-arguments-aug-29-2019). Overriding such a method can
-still be done by get(self,**kwargs) if so desired, making the kwargs explicit helps IMO
-readability of the API. If just giving a generic **kwargs, one must read the docstring or even the
-code to see which keywords are valid.
-
On the other hand, I think it makes sense to as a standard offer an extra **kwargs at the end of
-arg-lists for common methods that are expected to be over-ridden. This make the API more flexible by
-hinting to the dev that they could expand their own over-ridden implementation with their own
-keyword arguments if so desired.
Many classes have methods requiring lengthy positional argument lists, which are tedious and error-
-prone to extend and override especially in cases where not all arguments are even required. It would
-be useful if arguments were reserved for required inputs and anything else relegated to kwargs for
-easier passthrough on extension.
All users (real people) that starts a game Session on Evennia are doing so through an
-object called Account. The Account object has no in-game representation, it represents a unique
-game account. In order to actually get on the game the Account must puppet an Object
-(normally a Character).
-
Exactly how many Sessions can interact with an Account and its Puppets at once is determined by
-Evennia’s MULTISESSION_MODE setting.
-
Apart from storing login information and other account-specific data, the Account object is what is
-chatting on Channels. It is also a good place to store Permissions to be
-consistent between different in-game characters as well as configuration options. The Account
-object also has its own CmdSet, the AccountCmdSet.
-
Logged into default evennia, you can use the ooc command to leave your current
-character and go into OOC mode. You are quite limited in this mode, basically it works
-like a simple chat program. It acts as a staging area for switching between Characters (if your
-game supports that) or as a safety mode if your Character gets deleted. Use ic to attempt to
-(re)puppet a Character.
-
Note that the Account object can have, and often does have, a different set of
-Permissions from the Character they control. Normally you should put your
-permissions on the Account level - this will overrule permissions set on the Character level. For
-the permissions of the Character to come into play the default quell command can be used. This
-allows for exploring the game using a different permission set (but you can’t escalate your
-permissions this way - for hierarchical permissions like Builder, Admin etc, the lower of the
-permissions on the Character/Account will always be used).
You will usually not want more than one Account typeclass for all new accounts (but you could in
-principle create a system that changes an account’s typeclass dynamically).
-
An Evennia Account is, per definition, a Python class that includes evennia.DefaultAccount among
-its parents. In mygame/typeclasses/accounts.py there is an empty class ready for you to modify.
-Evennia defaults to using this (it inherits directly from DefaultAccount).
-
Here’s an example of modifying the default Account class in code:
-
# in mygame/typeclasses/accounts.py
-
- fromevenniaimportDefaultAccount
-
- classAccount(DefaultAccount):# [...]
-
- at_account_creation(self):"this is called only once, when account is first created"
- self.db.real_name=None# this is set later self.db.real_address = None #
-"
- self.db.config_1=True# default config self.db.config_2 = False # "
- self.db.config_3=1# "
-
- # ... whatever else our game needs to know ``` Reload the server with `reload`.
-
-
-
-
… However, if you use examine*self (the asterisk makes you examine your Account object rather
-than your Character), you won’t see your new Attributes yet. This is because at_account_creation
-is only called the very first time the Account is called and your Account object already exists
-(any new Accounts that connect will see them though). To update yourself you need to make sure to
-re-fire the hook on all the Accounts you have already created. Here is an example of how to do this
-using py:
If you wanted Evennia to default to a completely different Account class located elsewhere, you
-must point Evennia to it. Add BASE_ACCOUNT_TYPECLASS to your settings file, and give the python
-path to your custom class as its value. By default this points to typeclasses.accounts.Account,
-the empty template we used above.
Beyond those properties assigned to all typeclassed objects (see Typeclasses), the
-Account also has the following custom properties:
-
-
user - a unique link to a User Django object, representing the logged-in user.
-
obj - an alias for character.
-
name - an alias for user.username
-
sessions - an instance of
-ObjectSessionHandler
-managing all connected Sessions (physical connections) this object listens to (Note: In older
-versions of Evennia, this was a list). The so-called session-id (used in many places) is found
-as
-a property sessid on each Session instance.
-
is_superuser (bool: True/False) - if this account is a superuser.
-
-
Special handlers:
-
-
cmdset - This holds all the current Commands of this Account. By default these are
-the commands found in the cmdset defined by settings.CMDSET_ACCOUNT.
-
nicks - This stores and handles Nicks, in the same way as nicks it works on Objects.
-For Accounts, nicks are primarily used to store custom aliases for
-Channels.
-
-
Selection of special methods (see evennia.DefaultAccount for details):
-
-
get_puppet - get a currently puppeted object connected to the Account and a given session id, if
-any.
-
puppet_object - connect a session to a puppetable Object.
-
unpuppet_object - disconnect a session from a puppetable Object.
-
msg - send text to the Account
-
execute_cmd - runs a command as if this Account did it.
Evennia leverages Django which is a web development framework.
-Huge professional websites are made in Django and there is extensive documentation (and books) on it
-. You are encouraged to at least look at the Django basic tutorials. Here we will just give a brief
-introduction for how things hang together, to get you started.
-
We assume you have installed and set up Evennia to run. A webserver and website comes out of the
-box. You can get to that by entering http://localhost:4001 in your web browser - you should see a
-welcome page with some game statistics and a link to the web client. Let us add a new page that you
-can get to by going to http://localhost:4001/story.
A django “view” is a normal Python function that django calls to render the HTML page you will see
-in the web browser. Here we will just have it spit back the raw html, but Django can do all sorts of
-cool stuff with the page in the view, like adding dynamic content or change it on the fly. Open
-mygame/web folder and add a new module there named story.py (you could also put it in its own
-folder if you wanted to be neat. Don’t forget to add an empty __init__.py file if you do, to tell
-Python you can import from the new folder). Here’s how it looks:
-
# in mygame/web/story.py
-
-fromdjango.shortcutsimportrender
-
-defstorypage(request):
- returnrender(request,"story.html")
-
-
-
This view takes advantage of a shortcut provided to use by Django, render. This shortcut gives the
-template some information from the request, for instance, the game name, and then renders it.
We need to find a place where Evennia (and Django) looks for html files (called templates in
-Django parlance). You can specify such places in your settings (see the TEMPLATES variable in
-default_settings.py for more info), but here we’ll use an existing one. Go to
-mygame/template/overrides/website/ and create a page story.html there.
-
This is not a HTML tutorial, so we’ll go simple:
-
{% extends "base.html" %}
-{% block content %}
-<divclass="row">
- <divclass="col">
- <h1>A story about a tree</h1>
- <p>
- This is a story about a tree, a classic tale ...
- </p>
- </div>
-</div>
-{% endblock %}
-
-
-
Since we’ve used the render shortcut, Django will allow us to extend our base styles easily.
-
If you’d rather not take advantage of Evennia’s base styles, you can do something like this instead:
-
<html>
- <body>
- <h1>A story about a tree</h1>
- <p>
- This is a story about a tree, a classic tale ...
- </body>
-</html>
-
When you enter the address http://localhost:4001/story in your web browser, Django will parse that
-field to figure out which page you want to go to. You tell it which patterns are relevant in the
-file
-mygame/web/urls.py.
-Open it now.
-
Django looks for the variable urlpatterns in this file. You want to add your new pattern to the
-custom_patterns list we have prepared - that is then merged with the default urlpatterns. Here’s
-how it could look:
That is, we import our story view module from where we created it earlier and then create an url
-instance. The first argument to url is the pattern of the url we want to find ("story") (this is
-a regular expression if you are familiar with those) and then our view function we want to direct
-to.
-
That should be it. Reload Evennia and you should be able to browse to your new story page!
Before doing this tutorial you will probably want to read the intro in
-Basic Web tutorial. Reading the three first parts of the
-Django tutorial might help as well.
-
This tutorial will provide a step-by-step process to installing a wiki on your website.
-Fortunately, you don’t have to create the features manually, since it has been done by others, and
-we can integrate their work quite easily with Django. I have decided to focus on
-the Django-wiki.
-
-
Note: this article has been updated for Evennia 0.9. If you’re not yet using this version, be
-careful, as the django wiki doesn’t support Python 2 anymore. (Remove this note when enough time
-has passed.)
-
-
The Django-wiki offers a lot of features associated with
-wikis, is
-actively maintained (at this time, anyway), and isn’t too difficult to install in Evennia. You can
-see a demonstration of Django-wiki here.
You should begin by shutting down the Evennia server if it is running. We will run migrations and
-alter the virtual environment just a bit. Open a terminal and activate your Python environment, the
-one you use to run the evennia command.
Note: this will install the last version of Django wiki. Version >0.4 doesn’t support Python 2, so
-install wiki 0.3 if you haven’t updated to Python 3 yet.
-
-
It might take some time, the Django-wiki having some dependencies.
You will need to add a few settings to have the wiki app on your website. Open your
-server/conf/settings.py file and add the following at the bottom (but before importing
-secret_settings). Here’s what you’ll find in my own setting file (add the whole Django-wiki
-section):
-
r"""
-Evennia settings file.
-
-...
-
-"""
-
-# Use the defaults from Evennia unless explicitly overridden
-fromevennia.settings_defaultimport*
-
-######################################################################
-# Evennia base server config
-######################################################################
-
-# This is the name of your game. Make it catchy!
-SERVERNAME="demowiki"
-
-######################################################################
-# Django-wiki settings
-######################################################################
-INSTALLED_APPS+=(
- 'django.contrib.humanize.apps.HumanizeConfig',
- 'django_nyt.apps.DjangoNytConfig',
- 'mptt',
- 'sorl.thumbnail',
- 'wiki.apps.WikiConfig',
- 'wiki.plugins.attachments.apps.AttachmentsConfig',
- 'wiki.plugins.notifications.apps.NotificationsConfig',
- 'wiki.plugins.images.apps.ImagesConfig',
- 'wiki.plugins.macros.apps.MacrosConfig',
-)
-
-# Disable wiki handling of login/signup
-WIKI_ACCOUNT_HANDLING=False
-WIKI_ACCOUNT_SIGNUP_ALLOWED=False
-
-######################################################################
-# Settings given in secret_settings.py override those in this file.
-######################################################################
-try:
- fromserver.conf.secret_settingsimport*
-exceptImportError:
- print("secret_settings.py file not found or failed to import.")
-
Next we need to add two URLs in our web/urls.py file. Open it and compare the following output:
-you will need to add two URLs in custom_patterns and add one import line:
-
fromdjango.conf.urlsimporturl,include
-fromdjango.urlsimportpath# NEW!
-
-# default evenni a patterns
-fromevennia.web.urlsimporturlpatterns
-
-# eventual custom patterns
-custom_patterns=[
- # url(r'/desired/url/', view, name='example'),
- url('notifications/',include('django_nyt.urls')),# NEW!
- url('wiki/',include('wiki.urls')),# NEW!
-]
-
-# this is required by Django.
-urlpatterns=custom_patterns+urlpatterns
-
-
-
You will probably need to copy line 2, 10, and 11. Be sure to place them correctly, as shown in
-the example above.
It’s time to run the new migrations. The wiki app adds a few tables in our database. We’ll need to
-run:
-
evennia migrate
-
-
-
And that’s it, you can start the server. If you go to http://localhost:4001/wiki , you should see
-the wiki. Use your account’s username and password to connect to it. That’s how simple it is.
A wiki can be a great collaborative tool, but who can see it? Who can modify it? Django-wiki comes
-with a privilege system centered around four values per wiki page. The owner of an article can
-always read and write in it (which is somewhat logical). The group of the article defines who can
-read and who can write, if the user seeing the page belongs to this group. The topic of groups in
-wiki pages will not be discussed here. A last setting determines which other user (that is, these
-who aren’t in the groups, and aren’t the article’s owner) can read and write. Each article has
-these four settings (group read, group write, other read, other write). Depending on your purpose,
-it might not be a good default choice, particularly if you have to remind every builder to keep the
-pages private. Fortunately, Django-wiki gives us additional settings to customize who can read, and
-who can write, a specific article.
-
These settings must be placed, as usual, in your server/conf/settings.py file. They take a
-function as argument, said function (or callback) will be called with the article and the user.
-Remember, a Django user, for us, is an account. So we could check lockstrings on them if needed.
-Here is a default setting to restrict the wiki: only builders can write in it, but anyone (including
-non-logged in users) can read it. The superuser has some additional privileges.
-
# In server/conf/settings.py
-# ...
-
-defis_superuser(article,user):
- """Return True if user is a superuser, False otherwise."""
- returnnotuser.is_anonymous()anduser.is_superuser
-
-defis_builder(article,user):
- """Return True if user is a builder, False otherwise."""
- returnnotuser.is_anonymous()anduser.locks.check_lockstring(user,"perm(Builders)")
-
-defis_anyone(article,user):
- """Return True even if the user is anonymous."""
- returnTrue
-
-# Who can create new groups and users from the wiki?
-WIKI_CAN_ADMIN=is_superuser
-# Who can change owner and group membership?
-WIKI_CAN_ASSIGN=is_superuser
-# Who can change group membership?
-WIKI_CAN_ASSIGN_OWNER=is_superuser
-# Who can change read/write access to groups or others?
-WIKI_CAN_CHANGE_PERMISSIONS=is_superuser
-# Who can soft-delete an article?
-WIKI_CAN_DELETE=is_builder
-# Who can lock an article and permanently delete it?
-WIKI_CAN_MODERATE=is_superuser
-# Who can edit articles?
-WIKI_CAN_WRITE=is_builder
-# Who can read articles?
-WIKI_CAN_READ=is_anyone
-
-
-
Here, we have created three functions: one to return True if the user is the superuser, one to
-return True if the user is a builder, one to return True no matter what (this includes if the
-user is anonymous, E.G. if it’s not logged-in). We then change settings to allow either the
-superuser or
-each builder to moderate, read, write, delete, and more. You can, of course, add more functions,
-adapting them to your need. This is just a demonstration.
-
Providing the WIKI_CAN*... settings will bypass the original permission system. The superuser
-could change permissions of an article, but still, only builders would be able to write it. If you
-need something more custom, you will have to expand on the functions you use.
Unfortunately, Django wiki doesn’t provide a clear and clean entry point to read and write articles
-from Evennia and it doesn’t seem to be a very high priority. If you really need to keep Django wiki
-and to create and manage wiki pages from your code, you can do so, but this article won’t elaborate,
-as this is somewhat more technical.
-
However, it is a good opportunity to present a small project that has been created more recently:
-evennia-wiki has been created to provide a simple
-wiki, more tailored to Evennia and easier to connect. It doesn’t, as yet, provide as many options
-as does Django wiki, but it’s perfectly usable:
-
-
Pages have an inherent and much-easier to understand hierarchy based on URLs.
-
Article permissions are connected to Evennia groups and are much easier to accommodate specific
-requirements.
-
Articles can easily be created, read or updated from the Evennia code itself.
-
Markdown is fully-supported with a default integration to Bootstrap to look good on an Evennia
-website. Tables and table of contents are supported as well as wiki links.
-
The process to override wiki templates makes full use of the template_overrides directory.
-
-
However evennia-wiki doesn’t yet support:
-
-
Images in markdown and the uploading schema. If images are important to you, please consider
-contributing to this new project.
-
Modifying permissions on a per page/setting basis.
-
Moving pages to new locations.
-
Viewing page history.
-
-
Considering the list of features in Django wiki, obviously other things could be added to the list.
-However, these features may be the most important and useful. Additional ones might not be that
-necessary. If you’re interested in supporting this little project, you are more than welcome to
-contribute to it. Thanks!
This is a quick first-time tutorial expanding on the Commands documentation.
-
Let’s assume you have just downloaded Evennia, installed it and created your game folder (let’s call
-it just mygame here). Now you want to try to add a new command. This is the fastest way to do it.
Open mygame/commands/command.py in a text editor. This is just one place commands could be
-placed but you get it setup from the onset as an easy place to start. It also already contains some
-example code.
-
Create a new class in command.py inheriting from default_cmds.MuxCommand. Let’s call it
-CmdEcho in this example.
-
Set the class variable key to a good command name, like echo.
-
Give your class a useful docstring. A docstring is the string at the very top of a class or
-function/method. The docstring at the top of the command class is read by Evennia to become the help
-entry for the Command (see
-Command Auto-help).
-
Define a class method func(self) that echoes your input back to you.
-
-
Below is an example how this all could look for the echo command:
-
# file mygame/commands/command.py
- #[...]
- fromevenniaimportdefault_cmds
- classCmdEcho(default_cmds.MuxCommand):
- """
- Simple command example
-
- Usage:
- echo [text]
-
- This command simply echoes text back to the caller.
- """
-
- key="echo"
-
- deffunc(self):
- "This actually does things"
- ifnotself.args:
- self.caller.msg("You didn't enter anything!")
- else:
- self.caller.msg("You gave the string: '%s'"%self.args)
-
The command is not available to use until it is part of a Command Set. In this
-example we will go the easiest route and add it to the default Character commandset that already
-exists.
-
-
Edit mygame/commands/default_cmdsets.py
-
Import your new command with fromcommands.commandimportCmdEcho.
-
Add a line self.add(CmdEcho()) to CharacterCmdSet, in the at_cmdset_creation method (the
-template tells you where).
-
-
This is approximately how it should look at this point:
-
# file mygame/commands/default_cmdsets.py
- #[...]
- fromcommands.commandimportCmdEcho
- #[...]
- classCharacterCmdSet(default_cmds.CharacterCmdSet):
-
- key="DefaultCharacter"
-
- defat_cmdset_creation(self):
-
- # this first adds all default commands
- super().at_cmdset_creation()
-
- # all commands added after this point will extend or
- # overwrite the default commands.
- self.add(CmdEcho())
-
-
-
Next, run the @reload command. You should now be able to use your new echo command from inside
-the game. Use helpecho to see the documentation for the command.
-
If you have trouble, make sure to check the log for error messages (probably due to syntax errors in
-your command definition).
-
-
Note: Typing echotest will also work. It will be handled as the command echo directly followed
-by
-its argument test (which will end up in self.args).Tochangethisbehavior,youcanaddthearg_regexpropertyalongsidekey,help_category` etc. See the arg_regex
-documentation for more info.
-
-
If you want to overload existing default commands (such as look or get), just add your new
-command with the same key as the old one - it will then replace it. Just remember that you must use
-@reload to see any changes.
-
See Commands for many more details and possibilities when defining Commands and using
-Cmdsets in various ways.
Adding your Command to the CharacterCmdSet is just one easy exapmple. The cmdset system is very
-generic. You can create your own cmdsets (let’s say in a module mycmdsets.py) and add them to
-objects as you please (how to control their merging is described in detail in the Command Set
-documentation).
Now you just need to add this to an object. To test things (as superuser) you can do
-
@py self.cmdset.add("mycmdsets.MyCmdSet")
-
-
-
This will add this cmdset (along with its echo command) to yourself so you can test it. Note that
-you cannot add a single Command to an object on its own, it must be part of a CommandSet in order to
-do so.
-
The Command you added is not there permanently at this point. If you do a @reload the merger will
-be gone. You could add the permanent=True keyword to the cmdset.add call. This will however
-only make the new merged cmdset permanent on that single object. Often you want all objects of
-this particular class to have this cmdset.
-
To make sure all new created objects get your new merged set, put the cmdset.add call in your
-custom Typeclasses’ at_object_creation method:
-
# e.g. in mygame/typeclasses/objects.py
-
- fromevenniaimportDefaultObject
- classMyObject(DefaultObject):
-
- defat_object_creation(self):
- "called when the object is first created"
- self.cmdset.add("mycmdset.MyCmdSet",permanent=True)
-
-
-
All new objects of this typeclass will now start with this cmdset and it will survive a @reload.
-
Note: An important caveat with this is that at_object_creation is only called once, when the
-object is first created. This means that if you already have existing objects in your databases
-using that typeclass, they will not have been initiated the same way. There are many ways to update
-them; since it’s a one-time update you can usually just simply loop through them. As superuser, try
-the following:
-
@py from typeclasses.objects import MyObject; [o.cmdset.add("mycmdset.MyCmdSet") for o in
-
-
-
MyObject.objects.all()]
-
This goes through all objects in your database having the right typeclass, adding the new cmdset to
-each. The good news is that you only have to do this if you want to post-add cmdsets. If you just
-want to add a new command, you can simply add that command to the cmdset’s at_cmdset_creation
-and @reload to make the Command immediately available.
Evennia uses settings variables to know where to look for its default command sets. These are
-normally not changed unless you want to re-organize your game folder in some way. For example, the
-default character cmdset defaults to being defined as
When you create a new Evennia game (with for example evennia--initmygame) Evennia will
-automatically create empty child classes Object, Character, Room and Exit respectively. They
-are found mygame/typeclasses/objects.py, mygame/typeclasses/rooms.py etc.
-
-
Technically these are all Typeclassed, which can be ignored for now. In
-mygame/typeclasses are also base typeclasses for out-of-character things, notably
-Channels, Accounts and Scripts. We don’t cover those in
-this tutorial.
-
-
For your own game you will most likely want to expand on these very simple beginnings. It’s normal
-to want your Characters to have various attributes, for example. Maybe Rooms should hold extra
-information or even all Objects in your game should have properties not included in basic Evennia.
The default build commands of a new Evennia game is set up to use the Room, Exit and Character
-classes found in the same-named modules under mygame/typeclasses/. By default these are empty and
-just implements the default parents from the Evennia library (DefaultRoometc). Just add the
-changes you want to these classes and run @reload to add your new functionality.
Say you want to create a new “Heavy” object-type that characters should not have the ability to pick
-up.
-
-
Edit mygame/typeclasses/objects.py (you could also create a new module there, named something
-like heavy.py, that’s up to how you want to organize things).
-
Create a new class inheriting at any distance from DefaultObject. It could look something like
-this:
-
-
# end of file mygame/typeclasses/objects.py
- fromevenniaimportDefaultObject
-
- classHeavy(DefaultObject):
- "Heavy object"
- defat_object_creation(self):
- "Called whenever a new object is created"
- # lock the object down by default
- self.locks.add("get:false()")
- # the default "get" command looks for this Attribute in order
- # to return a customized error message (we just happen to know
- # this, you'd have to look at the code of the 'get' command to
- # find out).
- self.db.get_err_msg="This is too heavy to pick up."
-
-
-
-
Once you are done, log into the game with a build-capable account and do @create/droprock:objects.Heavy to drop a new heavy “rock” object in your location. Next try to pick it up
-(@quell yourself first if you are a superuser). If you get errors, look at your log files where
-you will find the traceback. The most common error is that you have some sort of syntax error in
-your class.
-
-
Note that the Locks and Attribute which are set in the typeclass could just
-as well have been set using commands in-game, so this is a very simple example.
The at_object_creation is only called once, when the object is first created. This makes it ideal
-for database-bound things like Attributes. But sometimes you want to create temporary
-properties (things that are not to be stored in the database but still always exist every time the
-object is created). Such properties can be initialized in the at_init method on the object.
-at_init is called every time the object is loaded into memory.
-
-
Note: It’s usually pointless and wasteful to assign database data in at_init, since this will
-hit the database with the same value over and over. Put those in at_object_creation instead.
-
-
You are wise to use ndb (non-database Attributes) to store these non-persistent properties, since
-ndb-properties are protected against being cached out in various ways and also allows you to list
-them using various in-game tools:
Note: As mentioned in the Typeclasses documentation, at_init replaces the use of
-the standard __init__ method of typeclasses due to how the latter may be called in situations
-other than you’d expect. So use at_init where you would normally use __init__.
If you already have some Heavy objects created and you add a new Attribute in
-at_object_creation, you will find that those existing objects will not have this Attribute. This
-is not so strange, since at_object_creation is only called once, it will not be called again just
-because you update it. You need to update existing objects manually.
-
If the number of objects is limited, you can use @typeclass/force/reloadobjectname to force a
-re-load of the at_object_creation method (only) on the object. This case is common enough that
-there is an alias @updateobjectname you can use to get the same effect. If there are multiple
-objects you can use @py to loop over the objects you need:
Warning: This information is presented as a convenience, using another webserver than Evennia’s
-own is not directly supported and you are on your own if you want to do so. Evennia’s webserver
-works out of the box without any extra configuration and also runs in-process making sure to avoid
-caching race conditions. The browser web client will most likely not work (at least not without
-tweaking) on a third-party web server.
-
One reason for wanting to use an external webserver like Apache would be to act as a proxy in
-front of the Evennia webserver. Getting this working with TLS (encryption) requires some extra work
-covered at the end of this page.
-
Note that the Apache instructions below might be outdated. If something is not working right, or you
-use Evennia with a different server, please let us know. Also, if there is a particular Linux distro
-you would like covered, please let us know.
After mod_wsgi is installed, copy the evennia/web/utils/evennia_wsgi_apache.conf file to your
-apache2 vhosts/sites folder. On Debian/Ubuntu, this is /etc/apache2/sites-enabled/. Make your
-modifications after copying the file there.
-
Read the comments and change the paths to point to the appropriate locations within your setup.
With any luck, you’ll be able to point your browser at your domain or subdomain that you set up in
-your vhost and see the nifty default Evennia webpage. If not, read the hopefully informative error
-message and work from there. Questions may be directed to our Evennia Community
-site.
If your mod_wsgi is set up to run on daemon mode (as will be the case by default on Debian and
-Ubuntu), you may tell mod_wsgi to reload by using the touch command on
-evennia/game/web/utils/apache_wsgi.conf. When mod_wsgi sees that the file modification time has
-changed, it will force a code reload. Any modifications to the code will not be propagated to the
-live instance of your site until reloaded.
-
If you are not running in daemon mode or want to force the issue, simply restart or reload apache2
-to apply your changes.
If you get strange (and usually uninformative) Permissiondenied errors from Apache, make sure
-that your evennia directory is located in a place the webserver may actually access. For example,
-some Linux distributions may default to very restrictive access permissions on a user’s /home
-directory.
-
One user commented that they had to add the following to their Apache config to get things to work.
-Not confirmed, but worth trying if there are trouble.
-
<Directory "/home/<yourname>/evennia/game/web">
- Options +ExecCGI
- Allow from all
-</Directory>
-
Below are steps on running Evennia using a front-end proxy (Apache HTTP), mod_proxy_http,
-mod_proxy_wstunnel, and mod_ssl. mod_proxy_http and mod_proxy_wstunnel will simply be
-referred to as
-mod_proxy below.
Ubuntu/Debian - Apache HTTP Server and mod_ssljkl are installed together in the apache2
-package and available in the
-standard package repositories for Ubuntu and Debian. mod_ssl needs to be enabled after
-installation:
There is a slight trick in setting up Evennia so websocket traffic is handled correctly by the
-proxy. You must set the WEBSOCKET_CLIENT_URL setting in your mymud/server/conf/settings.py file:
The setting above is what the client’s browser will actually use. Note the use of wss:// is
-because our client will be communicating over an encrypted connection (“wss” indicates websocket
-over SSL/TLS). Also, especially note the additional path /ws at the end of the URL. This is how
-Apache HTTP Server identifies that a particular request should be proxied to Evennia’s websocket
-port but this should be applicable also to other types of proxies (like nginx).
Arx - After the Reckoning is a big and very popular
-Evennia-based game. Arx is heavily roleplaying-centric, relying on game
-masters to drive the story. Technically it’s maybe best described as “a MUSH, but with more coded
-systems”. In August of 2018, the game’s developer, Tehom, generously released the source code of
-Arx on github. This is a treasure-trove for developers wanting
-to pick ideas or even get a starting game to build on. These instructions are based on the Arx-code
-released as of Aug 12, 2018.
-
If you are not familiar with what Evennia is, you can read
-an introduction here.
-
It’s not too hard to run Arx from the sources (of course you’ll start with an empty database) but
-since part of Arx has grown organically, it doesn’t follow standard Evennia paradigms everywhere.
-This page covers one take on installing and setting things up while making your new Arx-based game
-better match with the vanilla Evennia install.
Firstly, set aside a folder/directory on your drive for everything to follow.
-
You need to start by installing Evennia by following most of the Getting
-Started
-Instructions for your OS. The difference is that you need to gitclonehttps://github.com/TehomCD/evennia.git instead of Evennia’s repo because Arx uses TehomCD’s older
-Evennia 0.8 fork, notably still using Python2. This detail is
-important if referring to newer Evennia documentation.
-
If you are new to Evennia it’s highly recommended that you run through the
-instructions in full - including initializing and starting a new empty game and connecting to it.
-That way you can be sure Evennia works correctly as a base line. If you have trouble, make sure to
-read the Troubleshooting instructions for your
-operating system. You can also drop into our
-forums, join #evennia on irc.freenode.net
-or chat from the linked Discord Server.
-
After installing you should have a virtualenv running and you should have the following file
-structure in your set-aside folder:
-
vienv/
-evennia/
-mygame/
-
-
-
-
Here mygame is the empty game you created during the Evennia install, with evennia--init. Go to
-that and run evenniastop to make sure your empty game is not running. We’ll instead let Evenna
-run Arx, so in principle you could erase mygame - but it could also be good to have a clean game
-to compare to.
Arx has split evennia’s normal settings into base_settings.py and production_settings.py. It
-also has its own solution for managing ‘secret’ parts of the settings file. We’ll keep most of Arx
-way but remove the secret-handling and replace it with the normal Evennia method.
-
Cd into myarx/server/conf/ and open the file settings.py in a text editor. The top part (within
-"""...""") is just help text. Wipe everything underneath that and make it look like this instead
-(don’t forget to save):
-
frombase_settingsimport*
-
-TELNET_PORTS=[4000]
-SERVERNAME="MyArx"
-GAME_SLOGAN="The cool game"
-
-try:
- fromserver.conf.secret_settingsimport*
-exceptImportError:
- print("secret_settings.py file not found or failed to import.")
-
-
-
-
Note: Indents and capitalization matter in Python. Make indents 4 spaces (not tabs) for your own
-sanity. If you want a starter on Python in Evennia, [you can look here](Python-basic-
-introduction).
-
-
This will import Arx’ base settings and override them with the Evennia-default telnet port and give
-the game a name. The slogan changes the sub-text shown under the name of your game in the website
-header. You can tweak these to your own liking later.
-
Next, create a new, empty file secret_settings.py in the same location as the settings.py file.
-This can just contain the following:
Replace the long random string with random ASCII characters of your own. The secret key should not
-be shared.
-
Next, open myarx/server/conf/base_settings.py in your text editor. We want to remove/comment out
-all mentions of the decouple package, which Evennia doesn’t use (we use private_settings.py to
-hide away settings that should not be shared).
-
Comment out fromdecoupleimportconfig by adding a # to the start of the line: #fromdecoupleimportconfig. Then search for config( in the file and comment out all lines where this is used.
-Many of these are specific to the server environment where the original Arx runs, so is not that
-relevant to us.
Arx has some further dependencies beyond vanilla Evennia. Start by cd:ing to the root of your
-myarx folder.
-
-
If you run Linux or Mac: Edit myarx/requirements.txt and comment out the line
-pypiwin32==219 - it’s only needed on Windows and will give an error on other platforms.
-
-
Make sure your virtualenv is active, then run
-
pip install -r requirements.txt
-
-
-
The needed Python packages will be installed for you.
This creates the database and will step through all database migrations needed.
-
evennia start
-
-
-
If all goes well Evennia will now start up, running Arx! You can connect to it on localhost (or
-127.0.0.1 if your platform doesn’t alias localhost), port 4000 using a Telnet client.
-Alternatively, you can use your web browser to browse to http://localhost:4001 to see the game’s
-website and get to the web client.
-
When you log in you’ll get the standard Evennia greeting (since the database is empty), but you can
-try help to see that it’s indeed Arx that is running.
The first time you start Evennia after creating the database with the evenniamigrate step above,
-it should create a few starting objects for you - your superuser account, which it will prompt you
-to enter, a starting room (Limbo), and a character object for you. If for some reason this does not
-occur, you may have to follow the steps below. For the first time Superuser login you may have to
-run steps 7-8 and 10 to create and connect to your in-came Character.
-
-
Login to the game website with your Superuser account.
-
Press the Admin button to get into the (Django-) Admin Interface.
-
Navigate to the Accounts section.
-
Add a new Account named for the new staffer. Use a place holder password and dummy e-mail
-address.
-
Flag account as Staff and apply the Admin permission group (This assumes you have already set
-up an Admin Group in Django).
-
Add Tags named player and developer.
-
Log into the game using the web client (or a third-party telnet client) using your superuser
-account. Move to where you want the new staffer character to appear.
-
In the game client, run @create/drop<staffername>:typeclasses.characters.Character, where
-<staffername> is usually the same name you used for the Staffer account you created in the
-Admin earlier (if you are creating a Character for your superuser, use your superuser account
-name).
-This creates a new in-game Character and places it in your current location.
-
Have the new Admin player log into the game.
-
Have the new Admin puppet the character with @icStafferName.
-
Have the new Admin change their password - @password<oldpassword>=<newpassword>.
-
-
Now that you have a Character and an Account object, there’s a few additional things you may need to
-do in order for some commands to function properly. You can either execute these as in-game commands
-while @ic (controlling your character object).
Those steps will give you a ‘RosterEntry’, ‘PlayerOrNpc’, and ‘AssetOwner’ objects. RosterEntry
-explicitly connects a character and account object together, even while offline, and contains
-additional information about a character’s current presence in game (such as which ‘roster’ they’re
-in, if you choose to use an active roster of characters). PlayerOrNpc are more character extensions,
-as well as support for npcs with no in-game presence and just represented by a name which can be
-offscreen members of a character’s family. It also allows for membership in Organizations.
-AssetOwner holds information about a character or organization’s money and resources.
If for some reason you cannot use the Windows Subsystem for Linux (which would use instructions
-identical to the ones above), it’s possible to get Evennia running under Anaconda for Windows. The
-process is a little bit trickier.
Replace the SSH git clone links below with your own github forks.
-If you don’t plan to change Evennia at all, you can use the
-evennia/evennia.git repo instead of a forked one.
…and do the first run. You need winpty because Windows does not have a TTY/PTY
-by default, and so the Python console input commands (used for prompts on first
-run) will fail and you will end up in an unhappy place. Future runs, you should
-not need winpty.
-
winpty …/evennia/bin/windows/evennia.bat start
-
Once this is done, you should have your Evennia server running Arxcode up
-on localhost at port 4000, and the webserver at http://localhost:4001/
Most program code operates synchronously. This means that each statement in your code gets
-processed and finishes before the next can begin. This makes for easy-to-understand code. It is also
-a requirement in many cases - a subsequent piece of code often depend on something calculated or
-defined in a previous statement.
-
Consider this piece of code in a traditional Python program:
When run, this will print "beforecall...", after which the long_running_function gets to work
-for however long time. Only once that is done, the system prints "aftercall...". Easy and
-logical to follow. Most of Evennia work in this way and often it’s important that commands get
-executed in the same strict order they were coded.
-
Evennia, via Twisted, is a single-process multi-user server. In simple terms this means that it
-swiftly switches between dealing with player input so quickly that each player feels like they do
-things at the same time. This is a clever illusion however: If one user, say, runs a command
-containing that long_running_function, all other players are effectively forced to wait until it
-finishes.
-
Now, it should be said that on a modern computer system this is rarely an issue. Very few commands
-run so long that other users notice it. And as mentioned, most of the time you want to enforce
-all commands to occur in strict sequence.
-
When delays do become noticeable and you don’t care in which order the command actually completes,
-you can run it asynchronously. This makes use of the run_async() function in
-src/utils/utils.py:
-
run_async(function,*args,**kwargs)
-
-
-
Where function will be called asynchronously with *args and **kwargs. Example:
Now, when running this you will find that the program will not wait around for
-long_running_function to finish. In fact you will see "beforecall..." and "aftercall..."
-printed out right away. The long-running function will run in the background and you (and other
-users) can go on as normal.
A complication with using asynchronous calls is what to do with the result from that call. What if
-long_running_function returns a value that you need? It makes no real sense to put any lines of
-code after the call to try to deal with the result from long_running_function above - as we saw
-the "aftercall..." got printed long before long_running_function was finished, making that
-line quite pointless for processing any data from the function. Instead one has to use callbacks.
-
utils.run_async takes reserved kwargs that won’t be passed into the long-running function:
-
-
at_return(r) (the callback) is called when the asynchronous function (long_running_function
-above) finishes successfully. The argument r will then be the return value of that function (or
-None).
-
defat_return(r):
- print(r)
-
-
-
-
at_return_kwargs - an optional dictionary that will be fed as keyword arguments to the
-at_return callback.
-
at_err(e) (the errback) is called if the asynchronous function fails and raises an exception.
-This exception is passed to the errback wrapped in a Failure object e. If you do not supply an
-errback of your own, Evennia will automatically add one that silently writes errors to the evennia
-log. An example of an errback is found below:
-
-
defat_err(e):
- print("There was an error:",str(e))
-
-
-
-
at_err_kwargs - an optional dictionary that will be fed as keyword arguments to the at_err
-errback.
-
-
An example of making an asynchronous call from inside a Command definition:
-
fromevenniaimportutils,Command
-
- classCmdAsync(Command):
-
- key="asynccommand"
-
- deffunc(self):
-
- deflong_running_function():
- #[... lots of time-consuming code ...]
- returnfinal_value
-
- defat_return_function(r):
- self.caller.msg("The final value is %s"%r)
-
- defat_err_function(e):
- self.caller.msg("There was an error: %s"%e)
-
- # do the async call, setting all callbacks
- utils.run_async(long_running_function,at_return=at_return_function,
-at_err=at_err_function)
-
-
-
That’s it - from here on we can forget about long_running_function and go on with what else need
-to be done. Whenever it finishes, the at_return_function function will be called and the final
-value will
-pop up for us to see. If not we will see an error message.
The delay function is a much simpler sibling to run_async. It is in fact just a way to delay the
-execution of a command until a future time. This is equivalent to something like time.sleep()
-except delay is asynchronous while sleep would lock the entire server for the duration of the
-sleep.
-
fromevennia.utilsimportdelay
-
- # [...]
- # e.g. inside a Command, where `self.caller` is available
- defcallback(obj):
- obj.msg("Returning!")
- delay(10,callback,self.caller)
-
-
-
This will delay the execution of the callback for 10 seconds. This function is explored much more in
-the Command Duration Tutorial.
-
You can also try the following snippet just see how it works:
As of Evennia 0.9, the @interactivedecorator
-is available. This makes any function or method possible to ‘pause’ and/or await player input
-in an interactive way.
-
fromevennia.utilsimportinteractive
-
- @interactive
- defmyfunc(caller):
-
- whileTrue:
- caller.msg("Getting ready to wait ...")
- yield(5)
- caller.msg("Now 5 seconds have passed.")
-
- response=yield("Do you want to wait another 5 secs?")
-
- ifresponse.lower()notin("yes","y"):
- break
-
-
-
The @interactive decorator gives the function the ability to pause. The use
-of yield(seconds) will do just that - it will asynchronously pause for the
-number of seconds given before continuing. This is technically equivalent to
-using call_async with a callback that continues after 5 secs. But the code
-with @interactive is a little easier to follow.
-
Within the @interactive function, the response=yield("question") question
-allows you to ask the user for input. You can then process the input, just like
-you would if you used the Python input function. There is one caveat to this
-functionality though - it will only work if the function/method has an
-argument named exactly caller. This is because internally Evennia will look
-for the caller argument and treat that as the source of input.
-
All of this makes the @interactive decorator very useful. But it comes with a
-few caveats. Notably, decorating a function/method with @interactive turns it
-into a Python generator. The most
-common issue is that you cannot use return<value> from a generator (just an
-empty return works). To return a value from a function/method you have decorated
-with @interactive, you must instead use a special Twisted function
-twisted.internet.defer.returnValue. Evennia also makes this function
-conveniently available from evennia.utils:
-
fromevennia.utilsimportinteractive,returnValue
-
- @interactive
- defmyfunc():
-
- # ...
- result=10
-
- # this must be used instead of `return result`
- returnValue(result)
-
-
Overall, be careful with choosing when to use asynchronous calls. It is mainly useful for large
-administration operations that have no direct influence on the game world (imports and backup
-operations come to mind). Since there is no telling exactly when an asynchronous call actually ends,
-using them for in-game commands is to potentially invite confusion and inconsistencies (and very
-hard-to-reproduce bugs).
-
The very first synchronous example above is not really correct in the case of Twisted, which is
-inherently an asynchronous server. Notably you might find that you will not see the first beforecall... text being printed out right away. Instead all texts could end up being delayed until
-after the long-running process finishes. So all commands will retain their relative order as
-expected, but they may appear with delays or in groups.
Technically, run_async is just a very thin and simplified wrapper around a
-Twisted Deferred object; the
-wrapper sets
-up a default errback also if none is supplied. If you know what you are doing there is nothing
-stopping you from bypassing the utility function, building a more sophisticated callback chain after
-your own liking.
When performing actions in Evennia it is often important that you store data for later. If you write
-a menu system, you have to keep track of the current location in the menu tree so that the player
-can give correct subsequent commands. If you are writing a combat system, you might have a
-combattant’s next roll get easier dependent on if their opponent failed. Your characters will
-probably need to store roleplaying-attributes like strength and agility. And so on.
-
Typeclassed game entities (Accounts, Objects,
-Scripts and Channels) always have Attributes associated with them.
-Attributes are used to store any type of data ‘on’ such entities. This is different from storing
-data in properties already defined on entities (such as key or location) - these have very
-specific names and require very specific types of data (for example you couldn’t assign a python
-list to the key property no matter how hard you tried). Attributes come into play when you
-want to assign arbitrary data to arbitrary names.
-
Attributes are not secure by default and any player may be able to change them unless you
-prevent this behavior.
To save persistent data on a Typeclassed object you normally use the db (DataBase) operator. Let’s
-try to save some data to a Rose (an Object):
-
# saving
- rose.db.has_thorns=True
- # getting it back
- is_ouch=rose.db.has_thorns
-
-
-
-
This looks like any normal Python assignment, but that db makes sure that an Attribute is
-created behind the scenes and is stored in the database. Your rose will continue to have thorns
-throughout the life of the server now, until you deliberately remove them.
-
To be sure to save non-persistently, i.e. to make sure NOT to create a database entry, you use
-ndb (NonDataBase). It works in the same way:
-
# saving
- rose.ndb.has_thorns=True
- # getting it back
- is_ouch=rose.ndb.has_thorns
-
-
-
Technically, ndb has nothing to do with Attributes, despite how similar they look. No
-Attribute object is created behind the scenes when using ndb. In fact the database is not
-invoked at all since we are not interested in persistence. There is however an important reason to
-use ndb to store data rather than to just store variables direct on entities - ndb-stored data
-is tracked by the server and will not be purged in various cache-cleanup operations Evennia may do
-while it runs. Data stored on ndb (as well as db) will also be easily listed by example the
-@examine command.
-
You can also del properties on db and ndb as normal. This will for example delete an
-Attribute:
-
delrose.db.has_thorns
-
-
-
Both db and ndb defaults to offering an all property on themselves. This returns all
-associated attributes or non-persistent properties.
The .db and .ndb properties are very convenient but if you don’t know the name of the Attribute
-beforehand they cannot be used. Behind the scenes .db actually accesses the AttributeHandler
-which sits on typeclassed entities as the .attributes property. .ndb does the same for the
-.nattributes property.
-
The handlers have normal access methods that allow you to manage and retrieve Attributes and
-NAttributes:
-
-
has('attrname') - this checks if the object has an Attribute with this key. This is equivalent
-to doing obj.db.attrname.
-
get(...) - this retrieves the given Attribute. Normally the value property of the Attribute is
-returned, but the method takes keywords for returning the Attribute object itself. By supplying an
-accessing_object to the call one can also make sure to check permissions before modifying
-anything.
-
add(...) - this adds a new Attribute to the object. An optional lockstring can be
-supplied here to restrict future access and also the call itself may be checked against locks.
-
remove(...) - Remove the given Attribute. This can optionally be made to check for permission
-before performing the deletion. - clear(...) - removes all Attributes from object.
-
all(...) - returns all Attributes (of the given category) attached to this object.
-
-
See this section for more about locking down Attribute
-access and editing. The Nattribute offers no concept of access control.
-
Some examples:
-
importevennia
- obj=evennia.search_object("MyObject")
-
- obj.attributes.add("test","testvalue")
- print(obj.db.test)# prints "testvalue"
- print(obj.attributes.get("test"))# "
- print(obj.attributes.all())# prints [<AttributeObject>]
- obj.attributes.remove("test")
-
An Attribute object is stored in the database. It has the following properties:
-
-
key - the name of the Attribute. When doing e.g. obj.db.attrname=value, this property is set
-to attrname.
-
value - this is the value of the Attribute. This value can be anything which can be pickled -
-objects, lists, numbers or what have you (see
-this section for more info). In the
-example
-obj.db.attrname=value, the value is stored here.
-
category - this is an optional property that is set to None for most Attributes. Setting this
-allows to use Attributes for different functionality. This is usually not needed unless you want
-to use Attributes for very different functionality (Nicks is an example of using
-Attributes
-in this way). To modify this property you need to use the Attribute
-Handler.
-
strvalue - this is a separate value field that only accepts strings. This severely limits the
-data possible to store, but allows for easier database lookups. This property is usually not used
-except when re-using Attributes for some other purpose (Nicks use it). It is only
-accessible via the Attribute Handler.
-
-
There are also two special properties:
-
-
attrtype - this is used internally by Evennia to separate Nicks, from Attributes (Nicks
-use Attributes behind the scenes).
-
model - this is a natural-key describing the model this Attribute is attached to. This is on
-the form appname.modelclass, like objects.objectdb. It is used by the Attribute and
-NickHandler to quickly sort matches in the database. Neither this nor attrtype should normally
-need to be modified.
-
-
Non-database attributes have no equivalence to category nor strvalue, attrtype or model.
So persistent data means that your data will survive a server reboot, whereas with
-non-persistent data it will not …
-
… So why would you ever want to use non-persistent data? The answer is, you don’t have to. Most of
-the time you really want to save as much as you possibly can. Non-persistent data is potentially
-useful in a few situations though.
-
-
You are worried about database performance. Since Evennia caches Attributes very aggressively,
-this is not an issue unless you are reading and writing to your Attribute very often (like many
-times per second). Reading from an already cached Attribute is as fast as reading any Python
-property. But even then this is not likely something to worry about: Apart from Evennia’s own
-caching, modern database systems themselves also cache data very efficiently for speed. Our
-default
-database even runs completely in RAM if possible, alleviating much of the need to write to disk
-during heavy loads.
-
A more valid reason for using non-persistent data is if you want to lose your state when logging
-off. Maybe you are storing throw-away data that are re-initialized at server startup. Maybe you
-are implementing some caching of your own. Or maybe you are testing a buggy Script that
-does potentially harmful stuff to your character object. With non-persistent storage you can be
-sure
-that whatever is messed up, it’s nothing a server reboot can’t clear up.
-
NAttributes have no restrictions at all on what they can store (see next section), since they
-don’t need to worry about being saved to the database - they work very well for temporary storage.
-
You want to implement a fully or partly non-persistent world. Who are we to argue with your
-grand vision!
None of the following affects NAttributes, which does not invoke the database at all. There are no
-restrictions to what can be stored in a NAttribute.
-
-
The database doesn’t know anything about Python objects, so Evennia must serialize Attribute
-values into a string representation in order to store it to the database. This is done using the
-pickle module of Python (the only exception is if you use the strattr keyword of the
-AttributeHandler to save to the strvalue field of the Attribute. In that case you can only save
-strings which will not be pickled).
-
It’s important to note that when you access the data in an Attribute you are always de-serializing
-it from the database representation every time. This is because we allow for storing
-database-entities in Attributes too. If we cached it as its Python form, we might end up with
-situations where the database entity was deleted since we last accessed the Attribute.
-De-serializing data with a database-entity in it means querying the database for that object and
-making sure it still exists (otherwise it will be set to None). Performance-wise this is usually
-not a big deal. But if you are accessing the Attribute as part of some big loop or doing a large
-amount of reads/writes you should first extract it to a temporary variable, operate on that and
-then save the result back to the Attribute. If you are storing a more complex structure like a
-dict or a list you should make sure to “disconnect” it from the database before looping over it,
-as mentioned in the Retrieving Mutable Objects section
-below.
With a single object, we mean anything that is not iterable, like numbers, strings or custom class
-instances without the __iter__ method.
-
-
You can generally store any non-iterable Python entity that can be
-pickled.
-
Single database objects/typeclasses can be stored as any other in the Attribute. These can
-normally not be pickled, but Evennia will behind the scenes convert them to an internal
-representation using their classname, database-id and creation-date with a microsecond precision,
-guaranteeing you get the same object back when you access the Attribute later.
-
If you hide a database object inside a non-iterable custom class (like stored as a variable
-inside it), Evennia will not know it’s there and won’t convert it safely. Storing classes with
-such hidden database objects is not supported and will lead to errors!
-
-
# Examples of valid single-value attribute data:
-obj.db.test1=23
-obj.db.test1=False
-# a database object (will be stored as an internal representation)
-obj.db.test2=myobj
-
-# example of an invalid, "hidden" dbobject
-classInvalid(object):
- def__init__(self,dbobj):
- # no way for Evennia to know this is a dbobj
- self.dbobj=dbobj
-invalid=Invalid(myobj)
-obj.db.invalid=invalid# will cause error!
-
This means storing objects in a collection of some kind and are examples of iterables, pickle-able
-entities you can loop over in a for-loop. Attribute-saving supports the following iterables:
Nestings of any combinations of the above, like lists in dicts or an OrderedDict of tuples, each
-containing dicts, etc.
-
All other iterables (i.e. entities with the __iter__ method) will be converted to a list.
-Since you can use any combination of the above iterables, this is generally not much of a
-limitation.
-
-
Any entity listed in the Single object section above can be
-stored in the iterable.
-
-
As mentioned in the previous section, database entities (aka typeclasses) are not possible to
-pickle. So when storing an iterable, Evennia must recursively traverse the iterable and all its
-nested sub-iterables in order to find eventual database objects to convert. This is a very fast
-process but for efficiency you may want to avoid too deeply nested structures if you can.
-
-
# examples of valid iterables to store
-obj.db.test3=[obj1,45,obj2,67]
-# a dictionary
-obj.db.test4={'str':34,'dex':56,'agi':22,'int':77}
-# a mixed dictionary/list
-obj.db.test5={'members':[obj1,obj2,obj3],'enemies':[obj4,obj5]}
-# a tuple with a list in it
-obj.db.test6=(1,3,4,8,["test","test2"],9)
-# a set
-obj.db.test7=set([1,2,3,4,5])
-# in-situ manipulation
-obj.db.test8=[1,2,{"test":1}]
-obj.db.test8[0]=4
-obj.db.test8[2]["test"]=5
-# test8 is now [4,2,{"test":5}]
-
A side effect of the way Evennia stores Attributes is that mutable iterables (iterables that can
-be modified in-place after they were created, which is everything except tuples) are handled by
-custom objects called _SaverList, _SaverDict etc. These _Saver... classes behave just like the
-normal variant except that they are aware of the database and saves to it whenever new data gets
-assigned to them. This is what allows you to do things like self.db.mylist[7]=val and be sure
-that the new version of list is saved. Without this you would have to load the list into a temporary
-variable, change it and then re-assign it to the Attribute in order for it to save.
-
There is however an important thing to remember. If you retrieve your mutable iterable into another
-variable, e.g. mylist2=obj.db.mylist, your new variable (mylist2) will still be a
-_SaverList. This means it will continue to save itself to the database whenever it is updated!
-
obj.db.mylist=[1,2,3,4]
- mylist=obj.db.mylist
- mylist[3]=5# this will also update database
- print(mylist)# this is now [1,2,3,5]
- print(obj.db.mylist)# this is also [1,2,3,5]
-
-
-
To “disconnect” your extracted mutable variable from the database you simply need to convert the
-_Saver... iterable to a normal Python structure. So to convert a _SaverList, you use the
-list() function, for a _SaverDict you use dict() and so on.
-
obj.db.mylist=[1,2,3,4]
- mylist=list(obj.db.mylist)# convert to normal list
- mylist[3]=5
- print(mylist)# this is now [1,2,3,5]
- print(obj.db.mylist)# this is still [1,2,3,4]
-
-
-
A further problem comes with nested mutables, like a dict containing lists of dicts or something
-like that. Each of these nested mutables would be _Saver* structures connected to the database and
-disconnecting the outermost one of them would not disconnect those nested within. To make really
-sure you disonnect a nested structure entirely from the database, Evennia provides a special
-function evennia.utils.dbserialize.deserialize:
The result of this operation will be a structure only consisting of normal Python mutables (list
-instead of _SaverList and so on).
-
Remember, this is only valid for mutable iterables.
-Immutable objects (strings, numbers, tuples etc) are
-already disconnected from the database from the onset.
-
obj.db.mytup=(1,2,[3,4])
- obj.db.mytup[0]=5# this fails since tuples are immutable
-
- # this works but will NOT update database since outermost is a tuple
- obj.db.mytup[2][1]=5
- print(obj.db.mytup[2][1])# this still returns 4, not 5
-
- mytup1=obj.db.mytup# mytup1 is already disconnected from database since outermost
- # iterable is a tuple, so we can edit the internal list as we want
- # without affecting the database.
-
-
-
-
Attributes will fetch data fresh from the database whenever you read them, so
-if you are performing big operations on a mutable Attribute property (such as looping over a list
-or dict) you should make sure to “disconnect” the Attribute’s value first and operate on this
-rather than on the Attribute. You can gain dramatic speed improvements to big loops this
-way.
Attributes are normally not locked down by default, but you can easily change that for individual
-Attributes (like those that may be game-sensitive in games with user-level building).
-
First you need to set a lock string on your Attribute. Lock strings are specified Locks.
-The relevant lock types are
-
-
attrread - limits who may read the value of the Attribute
-
attredit - limits who may set/change this Attribute
-
-
You cannot use the db handler to modify Attribute object (such as setting a lock on them) - The
-db handler will return the Attribute’s value, not the Attribute object itself. Instead you use
-the AttributeHandler and set it to return the object instead of the value:
Note the return_obj keyword which makes sure to return the Attribute object so its LockHandler
-could be accessed.
-
A lock is no good if nothing checks it – and by default Evennia does not check locks on Attributes.
-You have to add a check to your commands/code wherever it fits (such as before setting an
-Attribute).
-
# in some command code where we want to limit
- # setting of a given attribute name on an object
- attr=obj.attributes.get(attrname,
- return_obj=True,
- accessing_obj=caller,
- default=None,
- default_access=False)
- ifnotattr:
- caller.msg("You cannot edit that Attribute!")
- return
- # edit the Attribute here
-
-
-
The same keywords are available to use with obj.attributes.set() and obj.attributes.remove(),
-those will check for the attredit lock type.
Whether due to abuse, blatant breaking of your rules, or some other reason, you will eventually find
-no other recourse but to kick out a particularly troublesome player. The default command set has
-admin tools to handle this, primarily ban, unban, and boot.
Say we have a troublesome player “YouSuck” - this is a person that refuses common courtesy - an
-abusive
-and spammy account that is clearly created by some bored internet hooligan only to cause grief. You
-have tried to be nice. Now you just want this troll gone.
The easiest recourse is to block the account YouSuck from ever connecting again.
-
ban YouSuck
-
-
-
This will lock the name YouSuck (as well as ‘yousuck’ and any other capitalization combination), and
-next time they try to log in with this name the server will not let them!
-
You can also give a reason so you remember later why this was a good thing (the banned account will
-never see this)
-
ban YouSuck:This is just a troll.
-
-
-
If you are sure this is just a spam account, you might even consider deleting the player account
-outright:
-
account/delete YouSuck
-
-
-
Generally, banning the name is the easier and safer way to stop the use of an account – if you
-change your mind you can always remove the block later whereas a deletion is permanent.
Just because you block YouSuck’s name might not mean the trolling human behind that account gives
-up. They can just create a new account YouSuckMore and be back at it. One way to make things harder
-for them is to tell the server to not allow connections from their particular IP address.
-
First, when the offending account is online, check which IP address they use. This you can do with
-the who command, which will show you something like this:
-
Account Name On for Idle Room Cmds Host
- YouSuckMore 01:12 2m 22 212 237.333.0.223
-
-
-
The “Host” bit is the IP address from which the account is connecting. Use this to define the ban
-instead of the name:
-
ban 237.333.0.223
-
-
-
This will stop YouSuckMore connecting from their computer. Note however that IP address might change
-easily - either due to how the player’s Internet Service Provider operates or by the user simply
-changing computers. You can make a more general ban by putting asterisks * as wildcards for the
-groups of three digits in the address. So if you figure out that !YouSuckMore mainly connects from
-237.333.0.223, 237.333.0.225, and 237.333.0.256 (only changes in their subnet), it might be an idea
-to put down a ban like this to include any number in that subnet:
-
ban 237.333.0.*
-
-
-
You should combine the IP ban with a name-ban too of course, so the account YouSuckMore is truly
-locked regardless of where they connect from.
-
Be careful with too general IP bans however (more asterisks above). If you are unlucky you could be
-blocking out innocent players who just happen to connect from the same subnet as the offender.
YouSuck is not really noticing all this banning yet though - and won’t until having logged out and
-trying to log back in again. Let’s help the troll along.
-
boot YouSuck
-
-
-
Good riddance. You can give a reason for booting too (to be echoed to the player before getting
-kicked out).
Below are other useful commands for dealing with annoying players.
-
-
who – (as admin) Find the IP of a account. Note that one account can be connected to from
-multiple IPs depending on what you allow in your settings.
-
examine/account thomas – Get all details about an account. You can also use *thomas to get
-the account. If not given, you will get the Object thomas if it exists in the same location, which
-is not what you want in this case.
-
boot thomas – Boot all sessions of the given account name.
-
boot 23 – Boot one specific client session/IP by its unique id.
-
ban – List all bans (listed with ids)
-
ban thomas – Ban the user with the given account name
-
ban/ip 134.233.2.111 – Ban by IP
-
ban/ip 134.233.2.* – Widen IP ban
-
ban/ip 134.233.*.* – Even wider IP ban
-
unban 34 – Remove ban with id #34
-
cboot mychannel = thomas – Boot a subscriber from a channel you control
-
clock mychannel = control:perm(Admin);listen:all();send:all() – Fine control of access to
-your channel using lock definitions.
-
-
Locking a specific command (like page) is accomplished like so:
-
-
Examine the source of the command. The default page command class has the lock
-string “cmd:not pperm(page_banned)”. This means that unless the player has the ‘permission’
-“page_banned” they can use this command. You can assign any lock string to allow finer customization
-in your commands. You might look for the value of an Attribute or Tag, your
-current location etc.
-
perm/account thomas = page_banned – Give the account the ‘permission’ which causes (in this
-case) the lock to fail.
-
-
-
perm/del/account thomas = page_banned – Remove the given permission
-
tel thomas = jail – Teleport a player to a specified location or #dbref
-
type thomas = FlowerPot – Turn an annoying player into a flower pot (assuming you have a
-FlowerPot typeclass ready)
-
userpassword thomas = fooBarFoo – Change a user’s password
-
account/delete thomas – Delete a player account (not recommended, use ban instead)
-
server – Show server statistics, such as CPU load, memory usage, and how many objects are
-cached
-
time – Gives server uptime, runtime, etc
-
reload – Reloads the server without disconnecting anyone
-
reset – Restarts the server, kicking all connections
-
shutdown – Stops the server cold without it auto-starting again
-
py – Executes raw Python code, allows for direct inspection of the database and account
-objects on the fly. For advanced users.
-
-
Useful Tip:evenniachangepassword<username> entered into the command prompt will reset the
-password of any account, including the superuser or admin accounts. This is a feature of Django.
For an introduction and motivation to using batch processors, see here. This
-page describes the Batch-code processor. The Batch-command one is covered [here](Batch-Command-
-Processor).
The batch-code processor is a superuser-only function, invoked by
-
> @batchcode path.to.batchcodefile
-
-
-
Where path.to.batchcodefile is the path to a batch-code file. Such a file should have a name
-ending in “.py” (but you shouldn’t include that in the path). The path is given like a python path
-relative to a folder you define to hold your batch files, set by BATCH_IMPORT_PATH in your
-settings. Default folder is (assuming your game is called “mygame”) mygame/world/. So if you want
-to run the example batch file in mygame/world/batch_code.py, you could simply use
-
> @batchcode batch_code
-
-
-
This will try to run through the entire batch file in one go. For more gradual, interactive
-control you can use the /interactive switch. The switch /debug will put the processor in
-debug mode. Read below for more info.
A batch-code file is a normal Python file. The difference is that since the batch processor loads
-and executes the file rather than importing it, you can reliably update the file, then call it
-again, over and over and see your changes without needing to @reload the server. This makes for
-easy testing. In the batch-code file you have also access to the following global variables:
-
-
caller - This is a reference to the object running the batchprocessor.
-
DEBUG - This is a boolean that lets you determine if this file is currently being run in debug-
-mode or not. See below how this can be useful.
-
-
Running a plain Python file through the processor will just execute the file from beginning to end.
-If you want to get more control over the execution you can use the processor’s interactive mode.
-This runs certain code blocks on their own, rerunning only that part until you are happy with it. In
-order to do this you need to add special markers to your file to divide it up into smaller chunks.
-These take the form of comments, so the file remains valid Python.
-
Here are the rules of syntax of the batch-code *.py file.
-
-
#CODE as the first on a line marks the start of a code block. It will last until the beginning
-of another marker or the end of the file. Code blocks contain functional python code. Each #CODE
-block will be run in complete isolation from other parts of the file, so make sure it’s self-
-contained.
-
#HEADER as the first on a line marks the start of a header block. It lasts until the next
-marker or the end of the file. This is intended to hold imports and variables you will need for all
-other blocks .All python code defined in a header block will always be inserted at the top of every
-#CODE blocks in the file. You may have more than one #HEADER block, but that is equivalent to
-having one big one. Note that you can’t exchange data between code blocks, so editing a header-
-variable in one code block won’t affect that variable in any other code block!
-
#INSERTpath.to.file will insert another batchcode (Python) file at that position.
-
A # that is not starting a #HEADER, #CODE or #INSERT instruction is considered a comment.
-
Inside a block, normal Python syntax rules apply. For the sake of indentation, each block acts as
-a separate python module.
-
-
Below is a version of the example file found in evennia/contrib/tutorial_examples/.
-
#
- # This is an example batch-code build file for Evennia.
- #
-
- #HEADER
-
- # This will be included in all other #CODE blocks
-
- fromevenniaimportcreate_object,search_object
- fromevennia.contrib.tutorial_examplesimportred_button
- fromtypeclasses.objectsimportObject
-
- limbo=search_object('Limbo')[0]
-
-
- #CODE
-
- red_button=create_object(red_button.RedButton,key="Red button",
- location=limbo,aliases=["button"])
-
- # caller points to the one running the script
- caller.msg("A red button was created.")
-
- # importing more code from another batch-code file
- #INSERT batch_code_insert
-
- #CODE
-
- table=create_object(Object,key="Blue Table",location=limbo)
- chair=create_object(Object,key="Blue Chair",location=limbo)
-
- string="A %s and %s were created."
- ifDEBUG:
- table.delete()
- chair.delete()
- string+=" Since debug was active, " \
- "they were deleted again."
- caller.msg(string%(table,chair))
-
-
-
This uses Evennia’s Python API to create three objects in sequence.
The batch script will run to the end and tell you it completed. You will also get messages that the
-button and the two pieces of furniture were created. Look around and you should see the button
-there. But you won’t see any chair nor a table! This is because we ran this with the /debug
-switch, which is directly visible as DEBUG==True inside the script. In the above example we
-handled this state by deleting the chair and table again.
-
The debug mode is intended to be used when you test out a batchscript. Maybe you are looking for
-bugs in your code or try to see if things behave as they should. Running the script over and over
-would then create an ever-growing stack of chairs and tables, all with the same name. You would have
-to go back and painstakingly delete them later.
Interactive mode works very similar to the [batch-command processor counterpart](Batch-Command-
-Processor). It allows you more step-wise control over how the batch file is executed. This is useful
-for debugging or for picking and choosing only particular blocks to run. Use @batchcode with the
-/interactive flag to enter interactive mode.
01/02: red_button = create_object(red_button.RedButton, [...] (hh for help)
-
-
-
This shows that you are on the first #CODE block, the first of only two commands in this batch
-file. Observe that the block has not actually been executed at this point!
-
To take a look at the full code snippet you are about to run, use ll (a batch-processor version of
-look).
-
fromevennia.utilsimportcreate,search
- fromevennia.contrib.tutorial_examplesimportred_button
- fromtypeclasses.objectsimportObject
-
- limbo=search.objects(caller,'Limbo',global_search=True)[0]
-
- red_button=create.create_object(red_button.RedButton,key="Red button",
- location=limbo,aliases=["button"])
-
- # caller points to the one running the script
- caller.msg("A red button was created.")
-
-
-
Compare with the example code given earlier. Notice how the content of #HEADER has been pasted at
-the top of the #CODE block. Use pp to actually execute this block (this will create the button
-and give you a message). Use nn (next) to go to the next command. Use hh for a list of commands.
-
If there are tracebacks, fix them in the batch file, then use rr to reload the file. You will
-still be at the same code block and can rerun it easily with pp as needed. This makes for a simple
-debug cycle. It also allows you to rerun individual troublesome blocks - as mentioned, in a large
-batch file this can be very useful (don’t forget the /debug mode either).
-
Use nn and bb (next and back) to step through the file; e.g. nn12 will jump 12 steps forward
-(without processing any blocks in between). All normal commands of Evennia should work too while
-working in interactive mode.
Or rather the lack of it. There is a reason only superusers are allowed to run the batch-code
-processor by default. The code-processor runs without any Evennia security checks and allows
-full access to Python. If an untrusted party could run the code-processor they could execute
-arbitrary python code on your machine, which is potentially a very dangerous thing. If you want to
-allow other users to access the batch-code processor you should make sure to run Evennia as a
-separate and very limited-access user on your machine (i.e. in a ‘jail’). By comparison, the batch-
-command processor is much safer since the user running it is still ‘inside’ the game and can’t
-really do anything outside what the game commands allow them to.
Global variables won’t work in code batch files, each block is executed as stand-alone environments.
-#HEADER blocks are literally pasted on top of each #CODE block so updating some header-variable
-in your block will not make that change available in another block. Whereas a python execution
-limitation, allowing this would also lead to very hard-to-debug code when using the interactive mode
-
-
this would be a classical example of “spaghetti code”.
-
-
The main practical issue with this is when building e.g. a room in one code block and later want to
-connect that room with a room you built in the current block. There are two ways to do this:
-
-
Perform a database search for the name of the room you created (since you cannot know in advance
-which dbref it got assigned). The problem is that a name may not be unique (you may have a lot of “A
-dark forest” rooms). There is an easy way to handle this though - use Tags or Aliases. You
-can assign any number of tags and/or aliases to any object. Make sure that one of those tags or
-aliases is unique to the room (like “room56”) and you will henceforth be able to always uniquely
-search and find it later.
-
Use the caller global property as an inter-block storage. For example, you could have a
-dictionary of room references in an ndb:
-
#HEADER
-ifcaller.ndb.all_roomsisNone:
- caller.ndb.all_rooms={}
-
-#CODE
-# create and store the castle
-castle=create_object("rooms.Room",key="Castle")
-caller.ndb.all_rooms["castle"]=castle
-
-#CODE
-# in another node we want to access the castle
-castle=caller.ndb.all_rooms.get("castle")
-
-
-
-
-
Note how we check in #HEADER if caller.ndb.all_rooms doesn’t already exist before creating the
-dict. Remember that #HEADER is copied in front of every #CODE block. Without that if statement
-we’d be wiping the dict every block!
-
-
-
Don’t treat a batchcode file like any Python file¶
-
Despite being a valid Python file, a batchcode file should only be run by the batchcode processor.
-You should not do things like define Typeclasses or Commands in them, or import them into other
-code. Importing a module in Python will execute base level of the module, which in the case of your
-average batchcode file could mean creating a lot of new objects every time.
-
-
-
Don’t let code rely on the batch-file’s real file path¶
-
When you import things into your batchcode file, don’t use relative imports but always import with
-paths starting from the root of your game directory or evennia library. Code that relies on the
-batch file’s “actual” location will fail. Batch code files are read as text and the strings
-executed. When the code runs it has no knowledge of what file those strings where once a part of.
For an introduction and motivation to using batch processors, see here. This
-page describes the Batch-command processor. The Batch-code one is covered [here](Batch-Code-
-Processor).
The batch-command processor is a superuser-only function, invoked by
-
> @batchcommand path.to.batchcmdfile
-
-
-
Where path.to.batchcmdfile is the path to a batch-command file with the “.ev” file ending.
-This path is given like a python path relative to a folder you define to hold your batch files, set
-with BATCH_IMPORT_PATH in your settings. Default folder is (assuming your game is in the mygame
-folder) mygame/world. So if you want to run the example batch file in
-mygame/world/batch_cmds.ev, you could use
-
> @batchcommand batch_cmds
-
-
-
A batch-command file contains a list of Evennia in-game commands separated by comments. The
-processor will run the batch file from beginning to end. Note that it will not stop if commands in
-it fail (there is no universal way for the processor to know what a failure looks like for all
-different commands). So keep a close watch on the output, or use Interactive mode (see below) to
-run the file in a more controlled, gradual manner.
The batch file is a simple plain-text file containing Evennia commands. Just like you would write
-them in-game, except you have more freedom with line breaks.
-
Here are the rules of syntax of an *.ev file. You’ll find it’s really, really simple:
-
-
All lines having the # (hash)-symbol as the first one on the line are considered comments.
-All non-comment lines are treated as a command and/or their arguments.
-
Comment lines have an actual function – they mark the end of the previous command definition.
-So never put two commands directly after one another in the file - separate them with a comment, or
-the second of the two will be considered an argument to the first one. Besides, using plenty of
-comments is good practice anyway.
-
A line that starts with the word #INSERT is a comment line but also signifies a special
-instruction. The syntax is #INSERT<path.batchfile> and tries to import a given batch-cmd file
-into this one. The inserted batch file (file ending .ev) will run normally from the point of the
-#INSERT instruction.
-
Extra whitespace in a command definition is ignored. - A completely empty line translates in to
-a line break in texts. Two empty lines thus means a new paragraph (this is obviously only relevant
-for commands accepting such formatting, such as the @desc command).
-
The very last command in the file is not required to end with a comment.
-
You cannot nest another @batchcommand statement into your batch file. If you want to link many
-batch-files together, use the #INSERT batch instruction instead. You also cannot launch the
-@batchcode command from your batch file, the two batch processors are not compatible.
-
-
Below is a version of the example file found in evennia/contrib/tutorial_examples/batch_cmds.ev.
-
#
- # This is an example batch build file for Evennia.
- #
-
- # This creates a red button
- @create button:tutorial_examples.red_button.RedButton
- # (This comment ends input for @create)
- # Next command. Let's create something.
- @set button/desc =
- This is a large red button. Now and then
- it flashes in an evil, yet strangely tantalizing way.
-
- A big sign sits next to it. It says:
-
-
- -----------
-
- Press me!
-
- -----------
-
-
- ... It really begs to be pressed! You
- know you want to!
-
- # This inserts the commands from another batch-cmd file named
- # batch_insert_file.ev.
- #INSERT examples.batch_insert_file
-
-
- # (This ends the @set command). Note that single line breaks
- # and extra whitespace in the argument are ignored. Empty lines
- # translate into line breaks in the output.
- # Now let's place the button where it belongs (let's say limbo #2 is
- # the evil lair in our example)
- @teleport #2
- # (This comments ends the @teleport command.)
- # Now we drop it so others can see it.
- # The very last command in the file needs not be ended with #.
- drop button
-
A button will be created, described and dropped in Limbo. All commands will be executed by the user
-calling the command.
-
-
Note that if you interact with the button, you might find that its description changes, loosing
-your custom-set description above. This is just the way this particular object works.
Interactive mode allows you to more step-wise control over how the batch file is executed. This is
-useful for debugging and also if you have a large batch file and is only updating a small part of it
-– running the entire file again would be a waste of time (and in the case of @create-ing objects
-you would to end up with multiple copies of same-named objects, for example). Use @batchcommand
-with the /interactive flag to enter interactive mode.
01/04: @create button:tutorial_examples.red_button.RedButton (hh for help)
-
-
-
This shows that you are on the @create command, the first out of only four commands in this batch
-file. Observe that the command @create has not been actually processed at this point!
-
To take a look at the full command you are about to run, use ll (a batch-processor version of
-look). Use pp to actually process the current command (this will actually @create the button)
-– and make sure it worked as planned. Use nn (next) to go to the next command. Use hh for a
-list of commands.
-
If there are errors, fix them in the batch file, then use rr to reload the file. You will still be
-at the same command and can rerun it easily with pp as needed. This makes for a simple debug
-cycle. It also allows you to rerun individual troublesome commands - as mentioned, in a large batch
-file this can be very useful. Do note that in many cases, commands depend on the previous ones (e.g.
-if @create in the example above had failed, the following commands would have had nothing to
-operate on).
-
Use nn and bb (next and back) to step through the file; e.g. nn12 will jump 12 steps forward
-(without processing any command in between). All normal commands of Evennia should work too while
-working in interactive mode.
The batch-command processor is great for automating smaller builds or for testing new commands and
-objects repeatedly without having to write so much. There are several caveats you have to be aware
-of when using the batch-command processor for building larger, complex worlds though.
-
The main issue is that when you run a batch-command script you (you, as in your superuser
-character) are actually moving around in the game creating and building rooms in sequence, just as
-if you had been entering those commands manually, one by one. You have to take this into account
-when creating the file, so that you can ‘walk’ (or teleport) to the right places in order.
-
This also means there are several pitfalls when designing and adding certain types of objects. Here
-are some examples:
-
-
Rooms that change your Command Set: Imagine that you build a ‘dark’ room, which
-severely limits the cmdsets of those entering it (maybe you have to find the light switch to
-proceed). In your batch script you would create this room, then teleport to it - and promptly be
-shifted into the dark state where none of your normal build commands work …
-
Auto-teleportation: Rooms that automatically teleport those that enter them to another place
-(like a trap room, for example). You would be teleported away too.
-
Mobiles: If you add aggressive mobs, they might attack you, drawing you into combat. If they
-have AI they might even follow you around when building - or they might move away from you before
-you’ve had time to finish describing and equipping them!
-
-
The solution to all these is to plan ahead. Make sure that superusers are never affected by whatever
-effects are in play. Add an on/off switch to objects and make sure it’s always set to off upon
-creation. It’s all doable, one just needs to keep it in mind.
The fact that you build as ‘yourself’ can also be considered an advantage however, should you ever
-decide to change the default command to allow others than superusers to call the processor. Since
-normal access-checks are still performed, a malevolent builder with access to the processor should
-not be able to do all that much damage (this is the main drawback of the Batch Code
-Processor)
-
-
GNU Emacs users might find it interesting to use emacs’
-evennia mode. This is an Emacs major mode found in evennia/utils/evennia-mode.el. It offers
-correct syntax highlighting and indentation with <tab> when editing .ev files in Emacs. See the
-header of that file for installation instructions.
-
VIM users can use amfl’s vim-evennia
-mode instead, see its readme for install instructions.
Building a game world is a lot of work, especially when starting out. Rooms should be created,
-descriptions have to be written, objects must be detailed and placed in their proper places. In many
-traditional MUD setups you had to do all this online, line by line, over a telnet session.
-
Evennia already moves away from much of this by shifting the main coding work to external Python
-modules. But also building would be helped if one could do some or all of it externally. Enter
-Evennia’s batch processors (there are two of them). The processors allows you, as a game admin, to
-build your game completely offline in normal text files (batch files) that the processors
-understands. Then, when you are ready, you use the processors to read it all into Evennia (and into
-the database) in one go.
-
You can of course still build completely online should you want to - this is certainly the easiest
-way to go when learning and for small build projects. But for major building work, the advantages of
-using the batch-processors are many:
-
-
It’s hard to compete with the comfort of a modern desktop text editor; Compared to a traditional
-MUD line input, you can get much better overview and many more features. Also, accidentally pressing
-Return won’t immediately commit things to the database.
-
You might run external spell checkers on your batch files. In the case of one of the batch-
-processors (the one that deals with Python code), you could also run external debuggers and code
-analyzers on your file to catch problems before feeding it to Evennia.
-
The batch files (as long as you keep them) are records of your work. They make a natural starting
-point for quickly re-building your world should you ever decide to start over.
-
If you are an Evennia developer, using a batch file is a fast way to setup a test-game after
-having reset the database.
-
The batch files might come in useful should you ever decide to distribute all or part of your
-world to others.
-
-
There are two batch processors, the Batch-command processor and the Batch-code processor. The
-first one is the simpler of the two. It doesn’t require any programming knowledge - you basically
-just list in-game commands in a text file. The code-processor on the other hand is much more
-powerful but also more complex - it lets you use Evennia’s API to code your world in full-fledged
-Python code.
As mentioned, both the processors take text files as input and then proceed to process them. As long
-as you stick to the standard ASCII character set (which means
-the normal English characters, basically) you should not have to worry much about this section.
-
Many languages however use characters outside the simple ASCII table. Common examples are various
-apostrophes and umlauts but also completely different symbols like those of the greek or cyrillic
-alphabets.
-
First, we should make it clear that Evennia itself handles international characters just fine. It
-(and Django) uses unicode strings internally.
-
The problem is that when reading a text file like the batchfile, we need to know how to decode the
-byte-data stored therein to universal unicode. That means we need an encoding (a mapping) for how
-the file stores its data. There are many, many byte-encodings used around the world, with opaque
-names such as Latin-1, ISO-8859-3 or ARMSCII-8 to pick just a few examples. Problem is that
-it’s practially impossible to determine which encoding was used to save a file just by looking at it
-(it’s just a bunch of bytes!). You have to know.
-
With this little introduction it should be clear that Evennia can’t guess but has to assume an
-encoding when trying to load a batchfile. The text editor and Evennia must speak the same “language”
-so to speak. Evennia will by default first try the international UTF-8 encoding, but you can have
-Evennia try any sequence of different encodings by customizing the ENCODINGS list in your settings
-file. Evennia will use the first encoding in the list that do not raise any errors. Only if none
-work will the server give up and return an error message.
-
You can often change the text editor encoding (this depends on your editor though), otherwise you
-need to add the editor’s encoding to Evennia’s ENCODINGS list. If you are unsure, write a test
-file with lots of non-ASCII letters in the editor of your choice, then import to make sure it works
-as it should.
-
More help with encodings can be found in the entry Text Encodings and also in the
-Wikipedia article here.
-
A footnote for the batch-code processor: Just because Evennia can parse your file and your
-fancy special characters, doesn’t mean that Python allows their use. Python syntax only allows
-international characters inside strings. In all other source code only ASCII set characters are
-allowed.
Evennia’s new default web page uses a framework called Bootstrap. This
-framework is in use across the internet - you’ll probably start to recognize its influence once you
-learn some of the common design patterns. This switch is great for web developers, perhaps like
-yourself, because instead of wondering about setting up different grid systems or what custom class
-another designer used, we have a base, a bootstrap, to work from. Bootstrap is responsive by
-default, and comes with some default styles that Evennia has lightly overrode to keep some of the
-same colors and styles you’re used to from the previous design.
-
For your reading pleasure, a brief overview of Bootstrap follows. For more in-depth info, please
-read the documentation.
The container is meant to hold all your page content. Bootstrap provides two types: fixed-width and
-full-width.
-Fixed-width containers take up a certain max-width of the page - they’re useful for limiting the
-width on Desktop or Tablet platforms, instead of making the content span the width of the page.
-
<div class="container">
- <!--- Your content here -->
-</div>
-
-
-
Full width containers take up the maximum width available to them - they’ll span across a wide-
-screen desktop or a smaller screen phone, edge-to-edge.
-
<div class="container-fluid">
- <!--- This content will span the whole page -->
-</div>
-
-
-
The second part of the layout system is the grid.
-This is the bread-and-butter of the layout of Bootstrap - it allows you to change the size of
-elements depending on the size of the screen, without writing any media queries. We’ll briefly go
-over it - to learn more, please read the docs or look at the source code for Evennia’s home page in
-your browser.
-
-
Important! Grid elements should be in a .container or .container-fluid. This will center the
-contents of your site.
-
-
Bootstrap’s grid system allows you to create rows and columns by applying classes based on
-breakpoints. The default breakpoints are extra small, small, medium, large, and extra-large. If
-you’d like to know more about these breakpoints, please take a look at the documentation for
-them.
-
To use the grid system, first create a container for your content, then add your rows and columns
-like so:
This layout would create three equal-width columns.
-
To specify your sizes - for instance, Evennia’s default site has three columns on desktop and
-tablet, but reflows to single-column on smaller screens. Try it out!
Bootstrap also provides a huge amount of utilities, as well as styling and content elements. To
-learn more about them, please read the Bootstrap docs or read one of our other web tutorials.
Bootstrap provides many utilities and components you can use when customizing Evennia’s web
-presence. We’ll go over a few examples here that you might find useful.
Bootstrap provides base styles for your site. These can be customized through CSS, but the default
-styles are intended to provide a consistent, clean look for sites.
Bootstrap provides classes to easily add responsive margin and padding. Most of the time, you might
-like to add margins or padding through CSS itself - however these classes are used in the default
-Evennia site. Take a look at the docs to
-learn more.
Cards provide a container for other elements
-that stands out from the rest of the page. The “Accounts”, “Recently Connected”, and “Database
-Stats” on the default webpage are all in cards. Cards provide quite a bit of formatting options -
-the following is a simple example, but read the documentation or look at the site’s source for more.
Jumbotrons are useful for featuring an
-image or tagline for your game. They can flow with the rest of your content or take up the full
-width of the page - Evennia’s base site uses the former.
Forms are highly customizable with Bootstrap.
-For a more in-depth look at how to use forms and their styles in your own Evennia site, please read
-over the web character gen tutorial.
There are strictly speaking two types of users in Evennia, the super user and everyone else. The
-superuser is the first user you create, object #1. This is the all-powerful server-owner account.
-Technically the superuser not only has access to everything, it bypasses the permission checks
-entirely. This makes the superuser impossible to lock out, but makes it unsuitable to actually play-
-test the game’s locks and restrictions with (see @quell below). Usually there is no need to have
-but one superuser.
Whereas permissions can be used for anything, those put in settings.PERMISSION_HIERARCHY will have
-a ranking relative each other as well. We refer to these types of permissions as hierarchical
-permissions. When building locks to check these permissions, the perm()lock function is
-used. By default Evennia creates the following hierarchy (spelled exactly like this):
-
-
Developers basically have the same access as superusers except that they do not sidestep
-the Permission system. Assign only to really trusted server-admin staff since this level gives
-access both to server reload/shutdown functionality as well as (and this may be more critical) gives
-access to the all-powerful @py command that allows the execution of arbitrary Python code on the
-command line.
-
Admins can do everything except affecting the server functions themselves. So an Admin
-couldn’t reload or shutdown the server for example. They also cannot execute arbitrary Python code
-on the console or import files from the hard drive.
-
Builders - have all the build commands, but cannot affect other accounts or mess with the
-server.
-
Helpers are almost like a normal Player, but they can also add help files to the database.
-
Players is the default group that new players end up in. A new player have permission to use
-tells and to use and create new channels.
-
-
A user having a certain level of permission automatically have access to locks specifying access of
-a lower level.
-
To assign a new permission from inside the game, you need to be able to use the @perm command.
-This is an Developer-level command, but it could in principle be made lower-access since it only
-allows assignments equal or lower to your current level (so you cannot use it to escalate your own
-permission level). So, assuming you yourself have Developer access (or is superuser), you assign
-a new account “Tommy” to your core staff with the command
-
@perm/account Tommy = Developer
-
-
-
or
-
@perm *Tommy = Developer
-
-
-
We use a switch or the *name format to make sure to put the permission on the Account and not on
-any eventual Character that may also be named “Tommy”. This is usually what you want since the
-Account will then remain an Developer regardless of which Character they are currently controlling.
-To limit permission to a per-Character level you should instead use quelling (see below). Normally
-permissions can be any string, but for these special hierarchical permissions you can also use
-plural (“Developer” and “Developers” both grant the same powers).
When developing it can be useful to check just how things would look had your permission-level been
-lower. For this you can use quelling. Normally, when you puppet a Character you are using your
-Account-level permission. So even if your Character only has Accounts level permissions, your
-Developer-level Account will take precedence. With the @quell command you can change so that the
-Character’s permission takes precedence instead:
-
@quell
-
-
-
This will allow you to test out the game using the current Character’s permission level. A developer
-or builder can thus in principle maintain several test characters, all using different permission
-levels. Note that you cannot escalate your permissions this way; If the Character happens to have a
-higher permission level than the Account, the Account’s (lower) permission will still be used.
The default command definitions coming with Evennia
-follows a style similar to that of MUX, so the
-commands should be familiar if you used any such code bases before.
-
-
Throughout the larger documentation you may come across commands prefixed
-with @. This is just an optional marker used in some places to make a
-command stand out. Evennia defaults to ignoring the use of @ in front of
-your command (so entering dig is the same as entering @dig).
-
-
The default commands have the following style (where [...] marks optional parts):
-
command[/switch/switch...] [arguments ...]
-
-
-
A switch is a special, optional flag to the command to make it behave differently. It is always
-put directly after the command name, and begins with a forward slash (/). The arguments are one
-or more inputs to the commands. It’s common to use an equal sign (=) when assigning something to
-an object.
-
Below are some examples of commands you can try when logged in to the game. Use help<command> for
-learning more about each command and their detailed options.
If you just installed Evennia, your very first player account is called user #1, also known as the
-superuser or god user. This user is very powerful, so powerful that it will override many game
-restrictions such as locks. This can be useful, but it also hides some functionality that you might
-want to test.
-
To temporarily step down from your superuser position you can use the quell command in-game:
-
quell
-
-
-
This will make you start using the permission of your current character’s level instead of your
-superuser level. If you didn’t change any settings your game Character should have an Developer
-level permission - high as can be without bypassing locks like the superuser does. This will work
-fine for the examples on this page. Use unquell to get back to superuser status again afterwards.
Basic objects can be anything – swords, flowers and non-player characters. They are created using
-the create command:
-
create box
-
-
-
This created a new ‘box’ (of the default object type) in your inventory. Use the command inventory
-(or i) to see it. Now, ‘box’ is a rather short name, let’s rename it and tack on a few aliases.
-
name box = very large box;box;very;crate
-
-
-
We now renamed the box to very large box (and this is what we will see when looking at it), but we
-will also recognize it by any of the other names we give - like crate or simply box as before.
-We could have given these aliases directly after the name in the create command, this is true for
-all creation commands - you can always tag on a list of ;-separated aliases to the name of your
-new object. If you had wanted to not change the name itself, but to only add aliases, you could have
-used the alias command.
-
We are currently carrying the box. Let’s drop it (there is also a short cut to create and drop in
-one go by using the /drop switch, for example create/dropbox).
-
drop box
-
-
-
Hey presto - there it is on the ground, in all its normality.
-
examine box
-
-
-
This will show some technical details about the box object. For now we will ignore what this
-information means.
-
Try to look at the box to see the (default) description.
-
look box
-You see nothing special.
-
-
-
The description you get is not very exciting. Let’s add some flavor.
-
describe box = This is a large and very heavy box.
-
-
-
If you try the get command we will pick up the box. So far so good, but if we really want this to
-be a large and heavy box, people should not be able to run off with it that easily. To prevent
-this we need to lock it down. This is done by assigning a Lock to it. Make sure the box was
-dropped in the room, then try this:
-
lock box = get:false()
-
-
-
Locks represent a rather big topic, but for now that will do what we want. This will lock
-the box so noone can lift it. The exception is superusers, they override all locks and will pick it
-up anyway. Make sure you are quelling your superuser powers and try to get the box now:
-
> get box
-You can't get that.
-
-
-
Think thís default error message looks dull? The get command looks for an Attribute
-named get_err_msg for returning a nicer error message (we just happen to know this, you would need
-to peek into the
-code for
-the get command to find out.). You set attributes using the set command:
-
set box/get_err_msg = It's way too heavy for you to lift.
-
-
-
Try to get it now and you should see a nicer error message echoed back to you. To see what this
-message string is in the future, you can use ‘examine.’
-
examine box/get_err_msg
-
-
-
Examine will return the value of attributes, including color codes. examinehere/desc would return
-the raw description of your current room (including color codes), so that you can copy-and-paste to
-set its description to something else.
-
You create new Commands (or modify existing ones) in Python outside the game. See the Adding
-Commands tutorial for help with creating your first own Command.
Scripts are powerful out-of-character objects useful for many “under the hood” things.
-One of their optional abilities is to do things on a timer. To try out a first script, let’s put one
-on ourselves. There is an example script in evennia/contrib/tutorial_examples/bodyfunctions.py
-that is called BodyFunctions. To add this to us we will use the script command:
(note that you don’t have to give the full path as long as you are pointing to a place inside the
-contrib directory, it’s one of the places Evennia looks for Scripts). Wait a while and you will
-notice yourself starting making random observations.
-
script self
-
-
-
This will show details about scripts on yourself (also examine works). You will see how long it is
-until it “fires” next. Don’t be alarmed if nothing happens when the countdown reaches zero - this
-particular script has a randomizer to determine if it will say something or not. So you will not see
-output every time it fires.
-
When you are tired of your character’s “insights”, kill the script with
You create your own scripts in Python, outside the game; the path you give to script is literally
-the Python path to your script file. The Scripts page explains more details.
If we get back to the box we made, there is only so much fun you can do with it at this point. It’s
-just a dumb generic object. If you renamed it to stone and changed its description noone would be
-the wiser. However, with the combined use of custom Typeclasses, Scripts
-and object-based Commands, you could expand it and other items to be as unique, complex
-and interactive as you want.
-
Let’s take an example. So far we have only created objects that use the default object typeclass
-named simply Object. Let’s create an object that is a little more interesting. Under
-evennia/contrib/tutorial_examples there is a module red_button.py. It contains the enigmatic
-RedButton typeclass.
We import the RedButton python class the same way you would import it in Python except Evennia makes
-sure to look inevennia/contrib/ so you don’t have to write the full path every time. There you go
-
-
one red button.
-
-
The RedButton is an example object intended to show off a few of Evennia’s features. You will find
-that the Typeclass and Commands controlling it are inside
-evennia/contrib/tutorial_examples/.
-
If you wait for a while (make sure you dropped it!) the button will blink invitingly. Why don’t you
-try to push it …? Surely a big red button is meant to be pushed. You know you want to.
The main command for shaping the game world is dig. For example, if you are standing in Limbo you
-can dig a route to your new house location like this:
-
dig house = large red door;door;in,to the outside;out
-
-
-
This will create a new room named ‘house’. Spaces at the start/end of names and aliases are ignored
-so you could put more air if you wanted. This call will directly create an exit from your current
-location named ‘large red door’ and a corresponding exit named ‘to the outside’ in the house room
-leading back to Limbo. We also define a few aliases to those exits, so people don’t have to write
-the full thing all the time.
-
If you wanted to use normal compass directions (north, west, southwest etc), you could do that with
-dig too. But Evennia also has a limited version of dig that helps for compass directions (and
-also up/down and in/out). It’s called tunnel:
-
tunnel sw = cliff
-
-
-
This will create a new room “cliff” with an exit “southwest” leading there and a path “northeast”
-leading back from the cliff to your current location.
-
You can create new exits from where you are using the open command:
-
open north;n = house
-
-
-
This opens an exit north (with an alias n) to the previously created room house.
-
If you have many rooms named house you will get a list of matches and have to select which one you
-want to link to. You can also give its database (#dbref) number, which is unique to every object.
-This can be found with the examine command or by looking at the latest constructions with
-objects.
-
Follow the north exit to your ‘house’ or teleport to it:
-
north
-
-
-
or:
-
teleport house
-
-
-
To manually open an exit back to Limbo (if you didn’t do so with the dig command):
Knowing the #dbref of the box (#8 in this example), you can grab the box and get it back here
-without actually yourself going to house first:
-
teleport #8 = here
-
-
-
(You can usually use here to refer to your current location. To refer to yourself you can use
-self or me). The box should now be back in Limbo with you.
-
We are getting tired of the box. Let’s destroy it.
-
destroy box
-
-
-
You can destroy many objects in one go by giving a comma-separated list of objects (or their
-#dbrefs, if they are not in the same location) to the command.
After this brief introduction to building you may be ready to see a more fleshed-out example.
-Evennia comes with a tutorial world for you to explore.
-
First you need to switch back to superuser by using the unquell command. Next, place yourself in
-Limbo and run the following command:
-
batchcommand tutorial_world.build
-
-
-
This will take a while (be patient and don’t re-run the command). You will see all the commands used
-to build the world scroll by as the world is built for you.
-
You will end up with a new exit from Limbo named tutorial. Apart from being a little solo-
-adventure in its own right, the tutorial world is a good source for learning Evennia building (and
-coding).
-
Read the batch
-file to see
-exactly how it’s built, step by step. See also more info about the tutorial world [here](Tutorial-
-World-Introduction).
This page was adapted from the article “Building a Giant Mech in Evennia” by Griatch, published in
-Imaginary Realities Volume 6, issue 1, 2014. The original article is no longer available online,
-this is a version adopted to be compatible with the latest Evennia.
Let us create a functioning giant mech using the Python MUD-creation system Evennia. Everyone likes
-a giant mech, right? Start in-game as a character with build privileges (or the superuser).
-
@create/drop Giant Mech ; mech
-
-
-
Boom. We created a Giant Mech Object and dropped it in the room. We also gave it an alias mech.
-Let’s describe it.
-
@desc mech = This is a huge mech. It has missiles and stuff.
-
-
-
Next we define who can “puppet” the mech object.
-
@lock mech = puppet:all()
-
-
-
This makes it so that everyone can control the mech. More mechs to the people! (Note that whereas
-Evennia’s default commands may look vaguely MUX-like, you can change the syntax to look like
-whatever interface style you prefer.)
-
Before we continue, let’s make a brief detour. Evennia is very flexible about its objects and even
-more flexible about using and adding commands to those objects. Here are some ground rules well
-worth remembering for the remainder of this article:
-
-
The Account represents the real person logging in and has no game-world existence.
-
Any Object can be puppeted by an Account (with proper permissions).
Any Object can be inside another (except if it creates a loop).
-
Any Object can store custom sets of commands on it. Those commands can:
-
-
be made available to the puppeteer (Account),
-
be made available to anyone in the same location as the Object, and
-
be made available to anyone “inside” the Object
-
Also Accounts can store commands on themselves. Account commands are always available unless
-commands on a puppeted Object explicitly override them.
-
-
-
-
In Evennia, using the @ic command will allow you to puppet a given Object (assuming you have
-puppet-access to do so). As mentioned above, the bog-standard Character class is in fact like any
-Object: it is auto-puppeted when logging in and just has a command set on it containing the normal
-in-game commands, like look, inventory, get and so on.
-
@ic mech
-
-
-
You just jumped out of your Character and are now the mech! If people look at you in-game, they
-will look at a mech. The problem at this point is that the mech Object has no commands of its own.
-The usual things like look, inventory and get sat on the Character object, remember? So at the
-moment the mech is not quite as cool as it could be.
-
@ic <Your old Character>
-
-
-
You just jumped back to puppeting your normal, mundane Character again. All is well.
-
-
(But, you ask, where did that @ic command come from, if the mech had no commands on it? The
-answer is that it came from the Account’s command set. This is important. Without the Account being
-the one with the @ic command, we would not have been able to get back out of our mech again.)
Let us make the mech a little more interesting. In our favorite text editor, we will create some new
-mech-suitable commands. In Evennia, commands are defined as Python classes.
-
# in a new file mygame/commands/mechcommands.py
-
-fromevenniaimportCommand
-
-classCmdShoot(Command):
- """
- Firing the mech’s gun
-
- Usage:
- shoot [target]
-
- This will fire your mech’s main gun. If no
- target is given, you will shoot in the air.
- """
- key="shoot"
- aliases=["fire","fire!"]
-
- deffunc(self):
- "This actually does the shooting"
-
- caller=self.caller
- location=caller.location
-
- ifnotself.args:
- # no argument given to command - shoot in the air
- message="BOOM! The mech fires its gun in the air!"
- location.msg_contents(message)
- return
-
- # we have an argument, search for target
- target=caller.search(self.args.strip())
- iftarget:
- message="BOOM! The mech fires its gun at %s"%target.key
- location.msg_contents(message)
-
-classCmdLaunch(Command):
- # make your own 'launch'-command here as an exercise!
- # (it's very similar to the 'shoot' command above).
-
-
-
-
This is saved as a normal Python module (let’s call it mechcommands.py), in a place Evennia looks
-for such modules (mygame/commands/). This command will trigger when the player gives the command
-“shoot”, “fire,” or even “fire!” with an exclamation mark. The mech can shoot in the air or at a
-target if you give one. In a real game the gun would probably be given a chance to hit and give
-damage to the target, but this is enough for now.
-
We also make a second command for launching missiles (CmdLaunch). To save
-space we won’t describe it here; it looks the same except it returns a text
-about the missiles being fired and has different key and aliases. We leave
-that up to you to create as an exercise. You could have it print “WOOSH! The
-mech launches missiles against !”, for example.
-
Now we shove our commands into a command set. A Command Set (CmdSet) is a container
-holding any number of commands. The command set is what we will store on the mech.
-
# in the same file mygame/commands/mechcommands.py
-
-fromevenniaimportCmdSet
-fromevenniaimportdefault_cmds
-
-classMechCmdSet(CmdSet):
- """
- This allows mechs to do do mech stuff.
- """
- key="mechcmdset"
-
- defat_cmdset_creation(self):
- "Called once, when cmdset is first created"
- self.add(CmdShoot())
- self.add(CmdLaunch())
-
-
-
This simply groups all the commands we want. We add our new shoot/launch commands. Let’s head back
-into the game. For testing we will manually attach our new CmdSet to the mech.
This is a little Python snippet (run from the command line as an admin) that searches for the mech
-in our current location and attaches our new MechCmdSet to it. What we add is actually the Python
-path to our cmdset class. Evennia will import and initialize it behind the scenes.
-
@ic mech
-
-
-
We are back as the mech! Let’s do some shooting!
-
fire!
-BOOM! The mech fires its gun in the air!
-
-
-
There we go, one functioning mech. Try your own launch command and see that it works too. We can
-not only walk around as the mech — since the CharacterCmdSet is included in our MechCmdSet, the mech
-can also do everything a Character could do, like look around, pick up stuff, and have an inventory.
-We could now shoot the gun at a target or try the missile launch command. Once you have your own
-mech, what else do you need?
-
-
Note: You’ll find that the mech’s commands are available to you by just standing in the same
-location (not just by puppeting it). We’ll solve this with a lock in the next section.
What we’ve done so far is just to make a normal Object, describe it and put some commands on it.
-This is great for testing. The way we added it, the MechCmdSet will even go away if we reload the
-server. Now we want to make the mech an actual object “type” so we can create mechs without those
-extra steps. For this we need to create a new Typeclass.
-
A Typeclass is a near-normal Python class that stores its existence to the database
-behind the scenes. A Typeclass is created in a normal Python source file:
-
# in the new file mygame/typeclasses/mech.py
-
-fromtypeclasses.objectsimportObject
-fromcommands.mechcommandsimportMechCmdSet
-fromevenniaimportdefault_cmds
-
-classMech(Object):
- """
- This typeclass describes an armed Mech.
- """
- defat_object_creation(self):
- "This is called only when object is first created"
- self.cmdset.add_default(default_cmds.CharacterCmdSet)
- self.cmdset.add(MechCmdSet,permanent=True)
- self.locks.add("puppet:all();call:false()")
- self.db.desc="This is a huge mech. It has missiles and stuff."
-
-
-
For convenience we include the full contents of the default CharacterCmdSet in there. This will
-make a Character’s normal commands available to the mech. We also add the mech-commands from before,
-making sure they are stored persistently in the database. The locks specify that anyone can puppet
-the meck and no-one can “call” the mech’s Commands from ‘outside’ it - you have to puppet it to be
-able to shoot.
-
That’s it. When Objects of this type are created, they will always start out with the mech’s command
-set and the correct lock. We set a default description, but you would probably change this with
-@desc to individualize your mechs as you build them.
-
Back in the game, just exit the old mech (@ic back to your old character) then do
-
@create/drop The Bigger Mech ; bigmech : mech.Mech
-
-
-
We create a new, bigger mech with an alias bigmech. Note how we give the python-path to our
-Typeclass at the end — this tells Evennia to create the new object based on that class (we don’t
-have to give the full path in our game dir typeclasses.mech.Mech because Evennia knows to look in
-the typeclasses folder already). A shining new mech will appear in the room! Just use
To expand on this you could add more commands to the mech and remove others. Maybe the mech
-shouldn’t work just like a Character after all. Maybe it makes loud noises every time it passes from
-room to room. Maybe it cannot pick up things without crushing them. Maybe it needs fuel, ammo and
-repairs. Maybe you’ll lock it down so it can only be puppeted by emo teenagers.
-
Having you puppet the mech-object directly is also just one way to implement a giant mech in
-Evennia.
-
For example, you could instead picture a mech as a “vehicle” that you “enter” as your normal
-Character (since any Object can move inside another). In that case the “insides” of the mech Object
-could be the “cockpit”. The cockpit would have the MechCommandSet stored on itself and all the
-shooting goodness would be made available to you only when you enter it.
-
And of course you could put more guns on it. And make it fly.
This contrib allows you to write custom and easy to use building menus. As the name implies, these
-menus are most useful for building things, that is, your builders might appreciate them, although
-you can use them for your players as well.
-
Building menus are somewhat similar to EvMenu although they don’t use the same system at all and
-are intended to make building easier. They replicate what other engines refer to as “building
-editors”, which allow to you to build in a menu instead of having to enter a lot of complex
-commands. Builders might appreciate this simplicity, and if the code that was used to create them
-is simple as well, coders could find this contrib useful.
Before diving in, there are some things to point out:
-
-
Building menus work on an object. This object will be edited by manipulations in the menu. So
-you can create a menu to add/edit a room, an exit, a character and so on.
-
Building menus are arranged in layers of choices. A choice gives access to an option or to a sub-
-menu. Choices are linked to commands (usually very short). For instance, in the example shown
-below, to edit the room key, after opening the building menu, you can type k. That will lead you
-to the key choice where you can enter a new key for the room. Then you can enter @ to leave this
-choice and go back to the entire menu. (All of this can be changed).
-
To open the menu, you will need something like a command. This contrib offers a basic command for
-demonstration, but we will override it in this example, using the same code with more flexibility.
Let’s begin by adding a new command. You could add or edit the following file (there’s no trick
-here, feel free to organize the code differently):
-
# file: commands/building.py
-fromevennia.contrib.building_menuimportBuildingMenu
-fromcommands.commandimportCommand
-
-classEditCmd(Command):
-
- """
- Editing command.
-
- Usage:
- @edit [object]
-
- Open a building menu to edit the specified object. This menu allows to
- specific information about this object.
-
- Examples:
- @edit here
- @edit self
- @edit #142
-
- """
-
- key="@edit"
- locks="cmd:id(1) or perm(Builders)"
- help_category="Building"
-
- deffunc(self):
- ifnotself.args.strip():
- self.msg("|rYou should provide an argument to this function: the object to edit.|n")
- return
-
- obj=self.caller.search(self.args.strip(),global_search=True)
- ifnotobj:
- return
-
- ifobj.typename=="Room":
- Menu=RoomBuildingMenu
- else:
- self.msg("|rThe object {} cannot be
-edited.|n".format(obj.get_display_name(self.caller)))
- return
-
- menu=Menu(self.caller,obj)
- menu.open()
-
-
-
This command is rather simple in itself:
-
-
It has a key @edit and a lock to only allow builders to use it.
-
In its func method, it begins by checking the arguments, returning an error if no argument is
-specified.
-
It then searches for the given argument. We search globally. The search method used in this
-way will return the found object or None. It will also send the error message to the caller if
-necessary.
-
Assuming we have found an object, we check the object typename. This will be used later when
-we want to display several building menus. For the time being, we only handle Room. If the
-caller specified something else, we’ll display an error.
-
Assuming this object is a Room, we have defined a Menu object containing the class of our
-building menu. We build this class (creating an instance), giving it the caller and the object to
-edit.
-
We then open the building menu, using the open method.
-
-
The end might sound a bit surprising at first glance. But the process is still very simple: we
-create an instance of our building menu and call its open method. Nothing more.
-
-
Where is our building menu?
-
-
If you go ahead and add this command and test it, you’ll get an error. We haven’t defined
-RoomBuildingMenu yet.
-
To add this command, edit commands/default_cmdsets.py. Import our command, adding an import line
-at the top of the file:
-
"""
-...
-"""
-
-fromevenniaimportdefault_cmds
-
-# The following line is to be added
-fromcommands.buildingimportEditCmd
-
-
-
And in the class below (CharacterCmdSet), add the last line of this code:
-
classCharacterCmdSet(default_cmds.CharacterCmdSet):
- """
- The `CharacterCmdSet` contains general in-game commands like `look`,
- `get`, etc available on in-game Character objects. It is merged with
- the `AccountCmdSet` when an Account puppets a Character.
- """
- key="DefaultCharacter"
-
- defat_cmdset_creation(self):
- """
- Populates the cmdset
- """
- super().at_cmdset_creation()
- #
- # any commands you add below will overload the default ones.
- #
- self.add(EditCmd())
-
So far, we can’t use our building menu. Our @edit command will throw an error. We have to define
-the RoomBuildingMenu class. Open the commands/building.py file and add to the end of the file:
-
# ... at the end of commands/building.py
-# Our building menu
-
-classRoomBuildingMenu(BuildingMenu):
-
- """
- Building menu to edit a room.
-
- For the time being, we have only one choice: key, to edit the room key.
-
- """
-
- definit(self,room):
- self.add_choice("key","k",attr="key")
-
-
-
Save these changes, reload your game. You can now use the @edit command. Here’s what we get
-(notice that the commands we enter into the game are prefixed with >, though this prefix will
-probably not appear in your MUD client):
-
> look
-Limbo(#2)
-Welcome to your new Evennia-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 @batchcommand tutorial_world.build.
-
-> @edit here
-Building menu: Limbo
-
- [K]ey: Limbo
- [Q]uit the menu
-
-> q
-Closing the building menu.
-
-> @edit here
-Building menu: Limbo
-
- [K]ey: Limbo
- [Q]uit the menu
-
-> k
--------------------------------------------------------------------------------
-key for Limbo(#2)
-
-You can change this value simply by entering it.
-
-Use @ to go back to the main menu.
-
-Current value: Limbo
-
-> A beautiful meadow
--------------------------------------------------------------------------------
-
-key for A beautiful meadow(#2)
-
-You can change this value simply by entering it.
-
-Use @ to go back to the main menu.
-
-Current value: A beautiful meadow
-
-> @
-Building menu: A beautiful meadow
-
- [K]ey: A beautiful meadow
- [Q]uit the menu
-
-> q
-
-Closing the building menu.
-
-> look
-A beautiful meadow(#2)
-Welcome to your new Evennia-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 @batchcommand tutorial_world.build.
-
-
-
Before diving into the code, let’s examine what we have:
-
-
When we use the @edithere command, a building menu for this room appears.
-
This menu has two choices:
-
-
Enter k to edit the room key. You will go into a choice where you can simply type the key
-room key (the way we have done here). You can use @ to go back to the menu.
-
You can use q to quit the menu.
-
-
-
-
We then check, with the look command, that the menu has modified this room key. So by adding a
-class, with a method and a single line of code within, we’ve added a menu with two choices.
classRoomBuildingMenu(BuildingMenu):
-
- """
- Building menu to edit a room.
-
- For the time being, we have only one choice: key, to edit the room key.
-
- """
-
- definit(self,room):
- self.add_choice("key","k",attr="key")
-
-
-
-
We first create a class inheriting from BuildingMenu. This is usually the case when we want to
-create a building menu with this contrib.
-
In this class, we override the init method, which is called when the menu opens.
-
In this init method, we call add_choice. This takes several arguments, but we’ve defined only
-three here:
-
-
The choice name. This is mandatory and will be used by the building menu to know how to
-display this choice.
-
The command key to access this choice. We’ve given a simple "k". Menu commands usually are
-pretty short (that’s part of the reason building menus are appreciated by builders). You can also
-specify additional aliases, but we’ll see that later.
-
We’ve added a keyword argument, attr. This tells the building menu that when we are in this
-choice, the text we enter goes into this attribute name. It’s called attr, but it could be a room
-attribute or a typeclass persistent or non-persistent attribute (we’ll see other examples as well).
-
-
-
-
-
We’ve added the menu choice for key here, why is another menu choice defined for quit?
-
-
Our building menu creates a choice at the end of our choice list if it’s a top-level menu (sub-menus
-don’t have this feature). You can, however, override it to provide a different “quit” message or to
-perform some actions.
-
I encourage you to play with this code. As simple as it is, it offers some functionalities already.
This somewhat long section explains how to customize building menus. There are different ways
-depending on what you would like to achieve. We’ll go from specific to more advanced here.
In the previous example, we’ve used add_choice. This is one of three methods you can use to add
-choices. The other two are to handle more generic actions:
-
-
add_choice_edit: this is called to add a choice which points to the EvEditor. It is used to
-edit a description in most cases, although you could edit other things. We’ll see an example
-shortly. add_choice_edit uses most of the add_choice keyword arguments we’ll see, but usually
-we specify only two (sometimes three):
-
-
The choice title as usual.
-
The choice key (command key) as usual.
-
Optionally, the attribute of the object to edit, with the attr keyword argument. By
-default, attr contains db.desc. It means that this persistent data attribute will be edited by
-the EvEditor. You can change that to whatever you want though.
-
-
-
add_choice_quit: this allows to add a choice to quit the editor. Most advisable! If you don’t
-do it, the building menu will do it automatically, except if you really tell it not to. Again, you
-can specify the title and key of this menu. You can also call a function when this menu closes.
-
-
So here’s a more complete example (you can replace your RoomBuildingMenu class in
-commands/building.py to see it):
-
classRoomBuildingMenu(BuildingMenu):
-
- """
- Building menu to edit a room.
- """
-
- definit(self,room):
- self.add_choice("key","k",attr="key")
- self.add_choice_edit("description","d")
- self.add_choice_quit("quit this editor","q")
-
-
-
So far, our building menu class is still thin… and yet we already have some interesting feature.
-See for yourself the following MUD client output (again, the commands are prefixed with > to
-distinguish them):
-
> @reload
-
-> @edit here
-Building menu: A beautiful meadow
-
- [K]ey: A beautiful meadow
- [D]escription:
- Welcome to your new Evennia-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 @batchcommand tutorial_world.build.
- [Q]uit this editor
-
-> d
-
-----------Line Editor [editor]----------------------------------------------------
-01| Welcome to your new |wEvennia|n-based game! Visit http://www.evennia.com if you need
-02| help, want to contribute, report issues or just join the community.
-03| As Account #1 you can create a demo/tutorial area with |w@batchcommand tutorial_world.build|n.
-
-> :DD
-
-----------[l:03 w:034 c:0247]------------(:h for help)----------------------------
-Cleared 3 lines from buffer.
-
-> This is a beautiful meadow. But so beautiful I can't describe it.
-
-01| This is a beautiful meadow. But so beautiful I can't describe it.
-
-> :wq
-Building menu: A beautiful meadow
-
- [K]ey: A beautiful meadow
- [D]escription:
- This is a beautiful meadow. But so beautiful I can't describe it.
- [Q]uit this editor
-
-> q
-Closing the building menu.
-
-> look
-A beautiful meadow(#2)
-This is a beautiful meadow. But so beautiful I can't describe it.
-
-
-
So by using the d shortcut in our building menu, an EvEditor opens. You can use the EvEditor
-commands (like we did here, :DD to remove all, :wq to save and quit). When you quit the editor,
-the description is saved (here, in room.db.desc) and you go back to the building menu.
-
Notice that the choice to quit has changed too, which is due to our adding add_choice_quit. In
-most cases, you will probably not use this method, since the quit menu is added automatically.
add_choice and the two methods add_choice_edit and add_choice_quit take a lot of optional
-arguments to make customization easier. Some of these options might not apply to add_choice_edit
-or add_choice_quit however.
-
Below are the options of add_choice, specify them as arguments:
-
-
The first positional, mandatory argument is the choice title, as we have seen. This will
-influence how the choice appears in the menu.
-
The second positional, mandatory argument is the command key to access to this menu. It is best
-to use keyword arguments for the other arguments.
-
The aliases keyword argument can contain a list of aliases that can be used to access to this
-menu. For instance: add_choice(...,aliases=['t'])
-
The attr keyword argument contains the attribute to edit when this choice is selected. It’s a
-string, it has to be the name, from the object (specified in the menu constructor) to reach this
-attribute. For instance, a attr of "key" will try to find obj.key to read and write the
-attribute. You can specify more complex attribute names, for instance, attr="db.desc" to set the
-desc persistent attribute, or attr="ndb.something" so use a non-persistent data attribute on the
-object.
-
The text keyword argument is used to change the text that will be displayed when the menu choice
-is selected. Menu choices provide a default text that you can change. Since this is a long text,
-it’s useful to use multi-line strings (see an example below).
-
The glance keyword argument is used to specify how to display the current information while in
-the menu, when the choice hasn’t been opened. If you examine the previous examples, you will see
-that the current (key or db.desc) was shown in the menu, next to the command key. This is
-useful for seeing at a glance the current value (hence the name). Again, menu choices will provide
-a default glance if you don’t specify one.
-
The on_enter keyword argument allows to add a callback to use when the menu choice is opened.
-This is more advanced, but sometimes useful.
-
The on_nomatch keyword argument is called when, once in the menu, the caller enters some text
-that doesn’t match any command (including the @ command). By default, this will edit the
-specified attr.
-
The on_leave keyword argument allows to specify a callback used when the caller leaves the menu
-choice. This can be useful for cleanup as well.
-
-
These are a lot of possibilities, and most of the time you won’t need them all. Here is a short
-example using some of these arguments (again, replace the RoomBuildingMenu class in
-commands/building.py with the following code to see it working):
-
classRoomBuildingMenu(BuildingMenu):
-
- """
- Building menu to edit a room.
-
- For the time being, we have only one choice: key, to edit the room key.
-
- """
-
- definit(self,room):
- self.add_choice("title",key="t",attr="key",glance="{obj.key}",text="""
- -------------------------------------------------------------------------------
- Editing the title of {{obj.key}}(#{{obj.id}})
-
- You can change the title simply by entering it.
- Use |y{back}|n to go back to the main menu.
-
- Current title: |c{{obj.key}}|n
- """.format(back="|n or |y".join(self.keys_go_back)))
- self.add_choice_edit("description","d")
-
The most surprising part is no doubt the text. We use the multi-line syntax (with """).
-Excessive spaces will be removed from the left for each line automatically. We specify some
-information between braces… sometimes using double braces. What might be a bit odd:
-
-
{back} is a direct format argument we’ll use (see the .format specifiers).
-
{{obj...}}referstotheobjectbeingedited.Weusetwobraces,because.format` will remove
-them.
-
-
In glance, we also use {obj.key} to indicate we want to show the room’s key.
The keyword arguments of add_choice are often strings (type str). But each of these arguments
-can also be a function. This allows for a lot of customization, since we define the callbacks that
-will be executed to achieve such and such an operation.
-
To demonstrate, we will try to add a new feature. Our building menu for rooms isn’t that bad, but
-it would be great to be able to edit exits too. So we can add a new menu choice below
-description… but how to actually edit exits? Exits are not just an attribute to set: exits are
-objects (of type Exit by default) which stands between two rooms (object of type Room). So how
-can we show that?
-
First let’s add a couple of exits in limbo, so we have something to work with:
-
@tunneln
-@tunnels
-
-
-
This should create two new rooms, exits leading to them from limbo and back to limbo.
-
>look
-Abeautifulmeadow(#2)
-Thisisabeautifulmeadow.ButsobeautifulIcan't describe it.
-Exits:north(#4) and south(#7)
-
-
-
We can access room exits with the exits property:
-
>@pyhere.exits
-[<Exit:north>,<Exit:south>]
-
-
-
So what we need is to display this list in our building menu… and to allow to edit it would be
-great. Perhaps even add new exits?
-
First of all, let’s write a function to display the glance on existing exits. Here’s the code,
-it’s explained below:
-
classRoomBuildingMenu(BuildingMenu):
-
- """
- Building menu to edit a room.
-
- """
-
- definit(self,room):
- self.add_choice("title",key="t",attr="key",glance="{obj.key}",text="""
- -------------------------------------------------------------------------------
- Editing the title of {{obj.key}}(#{{obj.id}})
-
- You can change the title simply by entering it.
- Use |y{back}|n to go back to the main menu.
-
- Current title: |c{{obj.key}}|n
- """.format(back="|n or |y".join(self.keys_go_back)))
- self.add_choice_edit("description","d")
- self.add_choice("exits","e",glance=glance_exits,attr="exits")
-
-
-# Menu functions
-defglance_exits(room):
- """Show the room exits."""
- ifroom.exits:
- glance=""
- forexitinroom.exits:
- glance+="\n |y{exit}|n".format(exit=exit.key)
-
- returnglance
-
- return"\n |gNo exit yet|n"
-
-
-
When the building menu opens, it displays each choice to the caller. A choice is displayed with its
-title (rendered a bit nicely to show the key as well) and the glance. In the case of the exits
-choice, the glance is a function, so the building menu calls this function giving it the object
-being edited (the room here). The function should return the text to see.
-
>@edithere
-Buildingmenu:Abeautifulmeadow
-
- [T]itle:Abeautifulmeadow
- [D]escription:
- Thisisabeautifulmeadow.ButsobeautifulIcan't describe it.
- [E]xits:
- north
- south
- [Q]uitthemenu
-
->q
-Closingtheeditor.
-
-
-
-
How do I know the parameters of the function to give?
-
-
The function you give can accept a lot of different parameters. This allows for a flexible approach
-but might seem complicated at first. Basically, your function can accept any parameter, and the
-building menu will send only the parameter based on their names. If your function defines an
-argument named caller for instance (like deffunc(caller): ), then the building menu knows that
-the first argument should contain the caller of the building menu. Here are the arguments, you
-don’t have to specify them (if you do, they need to have the same name):
-
-
menu: if your function defines an argument named menu, it will contain the building menu
-itself.
-
choice: if your function defines an argument named choice, it will contain the Choice object
-representing this menu choice.
-
string: if your function defines an argument named string, it will contain the user input to
-reach this menu choice. This is not very useful, except on nomatch callbacks which we’ll see
-later.
-
obj: if your function defines an argument named obj, it will contain the building menu edited
-object.
-
caller: if your function defines an argument named caller, it will contain the caller of the
-building menu.
-
Anything else: any other argument will contain the object being edited by the building menu.
-
-
So in our case:
-
defglance_exits(room):
-
-
-
The only argument we need is room. It’s not present in the list of possible arguments, so the
-editing object of the building menu (the room, here) is given.
-
-
Why is it useful to get the menu or choice object?
-
-
Most of the time, you will not need these arguments. In very rare cases, you will use them to get
-specific data (like the default attribute that was set). This tutorial will not elaborate on these
-possibilities. Just know that they exist.
-
We should also define a text callback, so that we can enter our menu to see the room exits. We’ll
-see how to edit them in the next section but this is a good opportunity to show a more complete
-callback. To see it in action, as usual, replace the class and functions in commands/building.py:
-
# Our building menu
-
-classRoomBuildingMenu(BuildingMenu):
-
- """
- Building menu to edit a room.
-
- """
-
- definit(self,room):
- self.add_choice("title",key="t",attr="key",glance="{obj.key}",text="""
- -------------------------------------------------------------------------------
- Editing the title of {{obj.key}}(#{{obj.id}})
-
- You can change the title simply by entering it.
- Use |y{back}|n to go back to the main menu.
-
- Current title: |c{{obj.key}}|n
- """.format(back="|n or |y".join(self.keys_go_back)))
- self.add_choice_edit("description","d")
- self.add_choice("exits","e",glance=glance_exits,attr="exits",text=text_exits)
-
-
-# Menu functions
-defglance_exits(room):
- """Show the room exits."""
- ifroom.exits:
- glance=""
- forexitinroom.exits:
- glance+="\n |y{exit}|n".format(exit=exit.key)
-
- returnglance
-
- return"\n |gNo exit yet|n"
-
-deftext_exits(caller,room):
- """Show the room exits in the choice itself."""
- text="-"*79
- text+="\n\nRoom exits:"
- text+="\n Use |y@c|n to create a new exit."
- text+="\n\nExisting exits:"
- ifroom.exits:
- forexitinroom.exits:
- text+="\n |y@e {exit}|n".format(exit=exit.key)
- ifexit.aliases.all():
- text+=" (|y{aliases}|n)".format(aliases="|n, |y".join(
- aliasforaliasinexit.aliases.all()))
- ifexit.destination:
- text+=" toward {destination}".format(destination=exit.get_display_name(caller))
- else:
- text+="\n\n |gNo exit has yet been defined.|n"
-
- returntext
-
-
-
Look at the second callback in particular. It takes an additional argument, the caller (remember,
-the argument names are important, their order is not relevant). This is useful for displaying
-destination of exits accurately. Here is a demonstration of this menu:
-
>@edithere
-Buildingmenu:Abeautifulmeadow
-
- [T]itle:Abeautifulmeadow
- [D]escription:
- Thisisabeautifulmeadow.ButsobeautifulIcan't describe it.
- [E]xits:
- north
- south
- [Q]uitthemenu
-
->e
--------------------------------------------------------------------------------
-
-Roomexits:
- Use@ctocreateanewexit.
-
-Existingexits:
- @enorth(n)towardnorth(#4)
- @esouth(s)towardsouth(#7)
-
->@
-Buildingmenu:Abeautifulmeadow
-
- [T]itle:Abeautifulmeadow
- [D]escription:
- Thisisabeautifulmeadow.ButsobeautifulIcan't describe it.
- [E]xits:
- north
- south
- [Q]uitthemenu
-
->q
-Closingthebuildingmenu.
-
-
-
Using callbacks allows a great flexibility. We’ll now see how to handle sub-menus.
A menu is relatively flat: it has a root (where you see all the menu choices) and individual choices
-you can go to using the menu choice keys. Once in a choice you can type some input or go back to
-the root menu by entering the return command (usually @).
-
Why shouldn’t individual exits have their own menu though? Say, you edit an exit and can change its
-key, description or aliases… perhaps even destination? Why ever not? It would make building much
-easier!
-
The building menu system offers two ways to do that. The first is nested keys: nested keys allow to
-go beyond just one menu/choice, to have menus with more layers. Using them is quick but might feel
-a bit counter-intuitive at first. Another option is to create a different menu class and redirect
-from the first to the second. This option might require more lines but is more explicit and can be
-re-used for multiple menus. Adopt one of them depending of your taste.
So far, we’ve only used menu keys with one letter. We can add more, of course, but menu keys in
-their simple shape are just command keys. Press “e” to go to the “exits” choice.
-
But menu keys can be nested. Nested keys allow to add choices with sub-menus. For instance, type
-“e” to go to the “exits” choice, and then you can type “c” to open a menu to create a new exit, or
-“d” to open a menu to delete an exit. The first menu would have the “e.c” key (first e, then c),
-the second menu would have key as “e.d”.
-
That’s more advanced and, if the following code doesn’t sound very friendly to you, try the next
-section which provides a different approach of the same problem.
-
So we would like to edit exits. That is, you can type “e” to go into the choice of exits, then
-enter @e followed by the exit name to edit it… which will open another menu. In this sub-menu
-you could change the exit key or description.
This needs a bit of code and a bit of explanation. So here we go… the code first, the
-explanations next!
-
# ... from commands/building.py
-# Our building menu
-
-classRoomBuildingMenu(BuildingMenu):
-
- """
- Building menu to edit a room.
-
- For the time being, we have only one choice: key, to edit the room key.
-
- """
-
- definit(self,room):
- self.add_choice("title",key="t",attr="key",glance="{obj.key}",text="""
- -------------------------------------------------------------------------------
- Editing the title of {{obj.key}}(#{{obj.id}})
-
- You can change the title simply by entering it.
- Use |y{back}|n to go back to the main menu.
-
- Current title: |c{{obj.key}}|n
- """.format(back="|n or |y".join(self.keys_go_back)))
- self.add_choice_edit("description","d")
- self.add_choice("exits","e",glance=glance_exits,text=text_exits,
-on_nomatch=nomatch_exits)
-
- # Exit sub-menu
- self.add_choice("exit","e.*",text=text_single_exit,on_nomatch=nomatch_single_exit)
-
-
-
-# Menu functions
-defglance_exits(room):
- """Show the room exits."""
- ifroom.exits:
- glance=""
- forexitinroom.exits:
- glance+="\n |y{exit}|n".format(exit=exit.key)
-
- returnglance
-
- return"\n |gNo exit yet|n"
-
-deftext_exits(caller,room):
- """Show the room exits in the choice itself."""
- text="-"*79
- text+="\n\nRoom exits:"
- text+="\n Use |y@c|n to create a new exit."
- text+="\n\nExisting exits:"
- ifroom.exits:
- forexitinroom.exits:
- text+="\n |y@e {exit}|n".format(exit=exit.key)
- ifexit.aliases.all():
- text+=" (|y{aliases}|n)".format(aliases="|n, |y".join(
- aliasforaliasinexit.aliases.all()))
- ifexit.destination:
- text+=" toward {destination}".format(destination=exit.get_display_name(caller))
- else:
- text+="\n\n |gNo exit has yet been defined.|n"
-
- returntext
-
-defnomatch_exits(menu,caller,room,string):
- """
- The user typed something in the list of exits. Maybe an exit name?
- """
- string=string[3:]
- exit=caller.search(string,candidates=room.exits)
- ifexitisNone:
- return
-
- # Open a sub-menu, using nested keys
- caller.msg("Editing: {}".format(exit.key))
- menu.move(exit)
- returnFalse
-
-# Exit sub-menu
-deftext_single_exit(menu,caller):
- """Show the text to edit single exits."""
- exit=menu.keys[1]
- ifexitisNone:
- return""
-
- return"""
- Exit {exit}:
-
- Enter the exit key to change it, or |y@|n to go back.
-
- New exit key:
- """.format(exit=exit.key)
-
-defnomatch_single_exit(menu,caller,room,string):
- """The user entered something in the exit sub-menu. Replace the exit key."""
- # exit is the second key element: keys should contain ['e', <Exit object>]
- exit=menu.keys[1]
- ifexitisNone:
- caller.msg("|rCannot find the exit.|n")
- menu.move(back=True)
- returnFalse
-
- exit.key=string
- returnTrue
-
-
-
-
That’s a lot of code! And we only handle editing the exit key!
-
-
That’s why at some point you might want to write a real sub-menu, instead of using simple nested
-keys. But you might need both to build pretty menus too!
-
-
The first thing new is in our menu class. After creating a on_nomatch callback for the exits
-menu (that shouldn’t be a surprised), we need to add a nested key. We give this menu a key of
-"e.*". That’s a bit odd! “e” is our key to the exits menu, . is the separator to indicate a
-nested menu, and * means anything. So basically, we create a nested menu that is contains within
-the exits menu and anything. We’ll see what this “anything” is in practice.
-
The glance_exits and text_exits are basically the same.
-
The nomatch_exits is short but interesting. It’s called when we enter some text in the “exits”
-menu (that is, in the list of exits). We have said that the user should enter @e followed by the
-exit name to edit it. So in the nomatch_exits callbac, we check for that input. If the entered
-text begins by @e, we try to find the exit in the room. If we do…
-
We call the menu.move method. That’s where things get a bit complicated with nested menus: we
-need to use menu.move to change from layer to layer. Here, we are in the choice of exits (the
-exits menu, of key “e”). We need to go down one layer to edit an exit. So we call menu.move and
-give it an exit object. The menu system remembers what position the user is based on the keys she
-has entered: when the user opens the menu, there is no key. If she selects the exits choice, the
-menu key being “e”, the position of the user is ["e"] (a list with the menu keys). If we call
-menu.move, whatever we give to this method will be appended to the list of keys, so that the user
-position becomes ["e",<Exitobject>].
-
In the menu class, we have defined the menu “e.*”, meaning “the menu contained in the exits
-choice plus anything”. The “anything” here is an exit: we have called menu.move(exit), so the
-"e.*" menu choice is chosen.
-
In this menu, the text is set to a callback. There is also a on_nomatch callback that is
-called whenever the user enters some text. If so, we change the exit name.
-
-
Using menu.move like this is a bit confusing at first. Sometimes it’s useful. In this case, if
-we want a more complex menu for exits, it makes sense to use a real sub-menu, not nested keys like
-this. But sometimes, you will find yourself in a situation where you don’t need a full menu to
-handle a choice.
The best way to handle individual exits is to create two separate classes:
-
-
One for the room menu.
-
One for the individual exit menu.
-
-
The first one will have to redirect on the second. This might be more intuitive and flexible,
-depending on what you want to achieve. So let’s build two menus:
-
# Still in commands/building.py, replace the menu class and functions by...
-# Our building menus
-
-classRoomBuildingMenu(BuildingMenu):
-
- """
- Building menu to edit a room.
- """
-
- definit(self,room):
- self.add_choice("title",key="t",attr="key",glance="{obj.key}",text="""
- -------------------------------------------------------------------------------
- Editing the title of {{obj.key}}(#{{obj.id}})
-
- You can change the title simply by entering it.
- Use |y{back}|n to go back to the main menu.
-
- Current title: |c{{obj.key}}|n
- """.format(back="|n or |y".join(self.keys_go_back)))
- self.add_choice_edit("description","d")
- self.add_choice("exits","e",glance=glance_exits,text=text_exits,
-on_nomatch=nomatch_exits)
-
-
-# Menu functions
-defglance_exits(room):
- """Show the room exits."""
- ifroom.exits:
- glance=""
- forexitinroom.exits:
- glance+="\n |y{exit}|n".format(exit=exit.key)
-
- returnglance
-
- return"\n |gNo exit yet|n"
-
-deftext_exits(caller,room):
- """Show the room exits in the choice itself."""
- text="-"*79
- text+="\n\nRoom exits:"
- text+="\n Use |y@c|n to create a new exit."
- text+="\n\nExisting exits:"
- ifroom.exits:
- forexitinroom.exits:
- text+="\n |y@e {exit}|n".format(exit=exit.key)
- ifexit.aliases.all():
- text+=" (|y{aliases}|n)".format(aliases="|n, |y".join(
- aliasforaliasinexit.aliases.all()))
- ifexit.destination:
- text+=" toward {destination}".format(destination=exit.get_display_name(caller))
- else:
- text+="\n\n |gNo exit has yet been defined.|n"
-
- returntext
-
-defnomatch_exits(menu,caller,room,string):
- """
- The user typed something in the list of exits. Maybe an exit name?
- """
- string=string[3:]
- exit=caller.search(string,candidates=room.exits)
- ifexitisNone:
- return
-
- # Open a sub-menu, using nested keys
- caller.msg("Editing: {}".format(exit.key))
- menu.open_submenu("commands.building.ExitBuildingMenu",exit,parent_keys=["e"])
- returnFalse
-
-classExitBuildingMenu(BuildingMenu):
-
- """
- Building menu to edit an exit.
-
- """
-
- definit(self,exit):
- self.add_choice("key",key="k",attr="key",glance="{obj.key}")
- self.add_choice_edit("description","d")
-
-
-
The code might be much easier to read. But before detailing it, let’s see how it behaves in the
-game:
-
> @edit here
-Building menu: A beautiful meadow
-
- [T]itle: A beautiful meadow
- [D]escription:
- This is a beautiful meadow. But so beautiful I can't describe it.
- [E]xits:
- door
- south
- [Q]uit the menu
-
-> e
--------------------------------------------------------------------------------
-
-Room exits:
- Use @c to create a new exit.
-
-Existing exits:
- @e door (n) toward door(#4)
- @e south (s) toward south(#7)
-
-Editing: door
-
-> @e door
-Building menu: door
-
- [K]ey: door
- [D]escription:
- None
-
-> k
--------------------------------------------------------------------------------
-key for door(#4)
-
-You can change this value simply by entering it.
-
-Use @ to go back to the main menu.
-
-Current value: door
-
-> north
-
--------------------------------------------------------------------------------
-key for north(#4)
-
-You can change this value simply by entering it.
-
-Use @ to go back to the main menu.
-
-Current value: north
-
-> @
-Building menu: north
-
- [K]ey: north
- [D]escription:
- None
-
-> d
-----------Line Editor [editor]----------------------------------------------------
-01| None
-----------[l:01 w:001 c:0004]------------(:h for help)----------------------------
-
-> :DD
-Cleared 1 lines from buffer.
-
-> This is the northern exit. Cool huh?
-01| This is the northern exit. Cool huh?
-
-> :wq
-Building menu: north
- [K]ey: north
- [D]escription:
- This is the northern exit. Cool huh?
-
-> @
--------------------------------------------------------------------------------
-Room exits:
- Use @c to create a new exit.
-
-Existing exits:
- @e north (n) toward north(#4)
- @e south (s) toward south(#7)
-
-> @
-Building menu: A beautiful meadow
-
- [T]itle: A beautiful meadow
- [D]escription:
- This is a beautiful meadow. But so beautiful I can't describe it.
- [E]xits:
- north
- south
- [Q]uit the menu
-
-> q
-Closing the building menu.
-
-> look
-A beautiful meadow(#2)
-This is a beautiful meadow. But so beautiful I can't describe it.
-Exits: north(#4) and south(#7)
-> @py here.exits[0]
->>> here.exits[0]
-north
-> @py here.exits[0].db.desc
->>> here.exits[0].db.desc
-This is the northern exit. Cool huh?
-
-
-
Very simply, we created two menus and bridged them together. This needs much less callbacks. There
-is only one line in the nomatch_exits to add:
We have to call open_submenu on the menu object (which opens, as its name implies, a sub menu)
-with three arguments:
-
-
The path of the menu class to create. It’s the Python class leading to the menu (notice the
-dots).
-
The object that will be edited by the menu. Here, it’s our exit, so we give it to the sub-menu.
-
The keys of the parent to open when the sub-menu closes. Basically, when we’re in the root of the
-sub-menu and press @, we’ll open the parent menu, with the parent keys. So we specify ["e"],
-since the parent menus is the “exits” choice.
-
-
And that’s it. The new class will be automatically created. As you can see, we have to create a
-on_nomatch callback to open the sub-menu, but once opened, it automatically close whenever needed.
There are some options that can be set on any menu class. These options allow for greater
-customization. They are class attributes (see the example below), so just set them in the class
-body:
-
-
keys_go_back (default to ["@"]): the keys to use to go back in the menu hierarchy, from choice
-to root menu, from sub-menu to parent-menu. By default, only a @ is used. You can change this
-key for one menu or all of them. You can define multiple return commands if you want.
-
sep_keys (default "."): this is the separator for nested keys. There is no real need to
-redefine it except if you really need the dot as a key, and need nested keys in your menu.
-
joker_key (default to "*"): used for nested keys to indicate “any key”. Again, you shouldn’t
-need to change it unless you want to be able to use the @*@ in a command key, and also need nested
-keys in your menu.
-
min_shortcut (default to 1): although we didn’t see it here, one can create a menu choice
-without giving it a key. If so, the menu system will try to “guess” the key. This option allows to
-change the minimum length of any key for security reasons.
-
-
To set one of them just do so in your menu class(es):
Building menus mean to save you time and create a rich yet simple interface. But they can be
-complicated to learn and require reading the source code to find out how to do such and such a
-thing. This documentation, however long, is an attempt at describing this system, but chances are
-you’ll still have questions about it after reading it, especially if you try to push this system to
-a great extent. Do not hesitate to read the documentation of this contrib, it’s meant to be
-exhaustive but user-friendly.
This page gives an overview of the supported SQL databases as well as instructions on install:
-
-
SQLite3 (default)
-
PostgreSQL
-
MySQL / MariaDB
-
-
Since Evennia uses Django, most of our notes are based off of what we
-know from the community and their documentation. While the information below may be useful, you can
-always find the most up-to-date and “correct” information at Django’s Notes about supported
-Databases page.
SQLite3 is a light weight single-file database. It is our default database
-and Evennia will set this up for you automatically if you give no other options. SQLite stores the
-database in a single file (mygame/server/evennia.db3). This means it’s very easy to reset this
-database - just delete (or move) that evennia.db3 file and run evenniamigrate again! No server
-process is needed and the administrative overhead and resource consumption is tiny. It is also very
-fast since it’s run in-memory. For the vast majority of Evennia installs it will probably be all
-that’s ever needed.
-
SQLite will generally be much faster than MySQL/PostgreSQL but its performance comes with two
-drawbacks:
-
-
SQLite ignores length constraints by design; it is possible
-to store very large strings and numbers in fields that technically should not accept them. This is
-not something you will notice; your game will read and write them and function normally, but this
-can create some data migration problems requiring careful thought if you do need to change
-databases later.
-
SQLite can scale well to storage of millions of objects, but if you end up with a thundering herd
-of users trying to access your MUD and web site at the same time, or you find yourself writing long-
-running functions to update large numbers of objects on a live game, either will yield errors and
-interference. SQLite does not work reliably with multiple concurrent threads or processes accessing
-its records. This has to do with file-locking clashes of the database file. So for a production
-server making heavy use of process- or thread pools (or when using a third-party webserver like
-Apache), a proper database is a more appropriate choice.
This is installed and configured as part of Evennia. The database file is created as
-mygame/server/evennia.db3 when you run
-
evennia migrate
-
-
-
without changing any database options. An optional requirement is the sqlite3 client program -
-this is required if you want to inspect the database data manually. A shortcut for using it with the
-evennia database is evenniadbshell. Linux users should look for the sqlite3 package for their
-distro while Mac/Windows should get the sqlite-tools package from this
-page.
-
To inspect the default Evennia database (once it’s been created), go to your game dir and do
-
sqlite3 server/evennia.db3
- # or
- evennia dbshell
-
-
-
This will bring you into the sqlite command line. Use .help for instructions and .quit to exit.
-See here for a cheat-sheet of commands.
PostgreSQL is an open-source database engine, recommended by Django.
-While not as fast as SQLite for normal usage, it will scale better than SQLite, especially if your
-game has an very large database and/or extensive web presence through a separate server process.
First, install the posgresql server. Version 9.6 is tested with Evennia. Packages are readily
-available for all distributions. You need to also get the psql client (this is called postgresql-client on debian-derived systems). Windows/Mac users can find what they need on the postgresql
-download page. You should be setting up a password for your
-database-superuser (always called postgres) when you install.
-
For interaction with Evennia you need to also install psycopg2 to your Evennia install (pipinstallpsycopg2-binary in your virtualenv). This acts as the python bridge to the database server.
-
Next, start the postgres client:
-
psql -U postgres --password
-
-
-
-
:warning: Warning: With the --password argument, Postgres should prompt you for a password.
-If it won’t, replace that with -pyourpassword instead. Do not use the -p argument unless you
-have to since the resulting command, and your password, will be logged in the shell history.
-
-
This will open a console to the postgres service using the psql client.
-
On the psql command line:
-
CREATEUSERevenniaWITHPASSWORD'somepassword';
-CREATEDATABASEevennia;
-
--- Postgres-specific optimizations
--- https://docs.djangoproject.com/en/dev/ref/databases/#optimizing-postgresql-s-configuration
-ALTERROLEevenniaSETclient_encodingTO'utf8';
-ALTERROLEevenniaSETdefault_transaction_isolationTO'read committed';
-ALTERROLEevenniaSETtimezoneTO'UTC';
-
-GRANTALLPRIVILEGESONDATABASEevenniaTOevennia;
--- Other useful commands:
--- \l (list all databases and permissions)
--- \q (exit)
-
We create a database user ‘evennia’ and a new database named evennia (you can call them whatever
-you want though). We then grant the ‘evennia’ user full privileges to the new database so it can
-read/write etc to it.
-If you in the future wanted to completely wipe the database, an easy way to do is to log in as the
-postgres superuser again, then do DROPDATABASEevennia;, then CREATE and GRANT steps above
-again to recreate the database and grant privileges.
MySQL is a commonly used proprietary database system, on par with
-PostgreSQL. There is an open-source alternative called MariaDB that mimics
-all functionality and command syntax of the former. So this section covers both.
First, install and setup MariaDB or MySQL for your specific server. Linux users should look for the
-mysql-server or mariadb-server packages for their respective distributions. Windows/Mac users
-will find what they need from the MySQL downloads or MariaDB
-downloads pages. You also need the respective database clients
-(mysql, mariadb-client), so you can setup the database itself. When you install the server you
-should usually be asked to set up the database root user and password.
-
You will finally also need a Python interface to allow Evennia to talk to the database. Django
-recommends the mysqlclient one. Install this into the evennia virtualenv with pipinstallmysqlclient.
-
Start the database client (this is named the same for both mysql and mariadb):
-
mysql -u root -p
-
-
-
You should get to enter your database root password (set this up when you installed the database
-server).
-
Inside the database client interface:
-
CREATEUSER'evennia'@'localhost'IDENTIFIEDBY'somepassword';
-CREATEDATABASEevennia;
-ALTERDATABASE`evennia`CHARACTERSETutf8;-- note that it's `evennia` with back-ticks, not
-quotes!
-GRANTALLPRIVILEGESONevennia.*TO'evennia'@'localhost';
-FLUSHPRIVILEGES;
--- use 'exit' to quit client
-
Above we created a new local user and database (we called both ‘evennia’ here, you can name them
-what you prefer). We set the character set to utf8 to avoid an issue with prefix character length
-that can pop up on some installs otherwise. Next we grant the ‘evennia’ user all privileges on the
-evennia database and make sure the privileges are applied. Exiting the client brings us back to
-the normal terminal/console.
-
-
Note: If you are not using MySQL for anything else you might consider granting the ‘evennia’ user
-full privileges with GRANTALLPRIVILEGESON*.*TO'evennia'@'localhost';. If you do, it means
-you can use evenniadbshell later to connect to mysql, drop your database and re-create it as a
-way of easy reset. Without this extra privilege you will be able to drop the database but not re-
-create it without first switching to the database-root user.
To tell Evennia to use your new database you need to edit mygame/server/conf/settings.py (or
-secret_settings.py if you don’t want your db info passed around on git repositories).
-
-
Note: The Django documentation suggests using an external db.cnf or other external conf-
-formatted file. Evennia users have however found that this leads to problems (see e.g. issue
-#1184). To avoid trouble we recommend you simply put the configuration in
-your settings as below.
-
-
#
- # MySQL Database Configuration
- #
- DATABASES={
- 'default':{
- 'ENGINE':'django.db.backends.mysql',
- 'NAME':'evennia',
- 'USER':'evennia',
- 'PASSWORD':'somepassword',
- 'HOST':'localhost',# or an IP Address that your DB is hosted on
- 'PORT':'',# use default port
- }
- }
-
-
-
Change this to fit your database setup. Next, run:
-
evennia migrate
-
-
-
to populate your database. Should you ever want to inspect the database directly you can from now on
-also use
-
evennia dbshell
-
-
-
as a shortcut to get into the postgres command line for the right database and user.
-
With the database setup you should now be able to start start Evennia normally with your new
-database.
No testing has been performed with Oracle, but it is also supported through Django. There are
-community maintained drivers for MS SQL and possibly a few
-others. If you try other databases out, consider expanding this page with instructions.
This grid tries to gather info about different MU clients when used with Evennia.
-If you want to report a problem, update an entry or add a client, make a
-new documentation issue for it. Everyone’s encouraged to report their findings.
This FAQ page is for users to share their solutions to coding problems. Keep it brief and link to
-the docs if you can rather than too lengthy explanations. Don’t forget to check if an answer already
-exists before answering - maybe you can clarify that answer rather than to make a new Q&A section.
Q: The #dbref of a database object is ever-increasing. Evennia doesn’t allow you to change or
-reuse them. Will not a big/old game run out of dbref integers eventually?
-
A: No. For example, the default sqlite3 database’s max dbref is 2**64. If you created 10000
-objects every second every minute and every day of the year it would take ~60 million years for you
-to run out of dbref numbers. That’s a database of 140 TeraBytes, if every row was empty. If you are
-still using Evennia at that point and has this concern, get back to us and we can discuss adding
-dbref reuse then.
Q: How does one remove (not replace) e.g. the default getCommand from the
-Character Command Set?
-
A: Go to mygame/commands/default_cmdsets.py. Find the CharacterCmdSet class. It has one
-method named at_cmdset_creation. At the end of that method, add the following line:
-self.remove(default_cmds.CmdGet()). See the Adding Commands Tutorial
-for more info.
-
-
-
Preventing character from moving based on a condition¶
-
Q: How does one keep a character from using any exit, if they meet a certain condition? (I.E. in
-combat, immobilized, etc.)
-
A: The at_before_move hook is called by Evennia just before performing any move. If it returns
-False, the move is aborted. Let’s say we want to check for an Attributecantmove.
-Add the following code to the Character class:
-
defat_before_move(self,destination):
- "Called just before trying to move"
- ifself.db.cantmove:# replace with condition you want to test
- self.msg("Something is preventing you from moving!")
- returnFalse
- returnTrue
-
-
-
-
-
Reference initiating object in an EvMenu command.¶
-
Q: An object has a Command on it starts up an EvMenu instance. How do I capture a reference to
-that object for use in the menu?
-
A: When an EvMenu is started, the menu object is stored as caller.ndb._menutree.
-This is a good place to store menu-specific things since it will clean itself up when the menu
-closes. When initiating the menu, any additional keywords you give will be available for you as
-properties on this menu object:
-
classMyObjectCommand(Command):
- # A Command stored on an object (the object is always accessible from
- # the Command as self.obj)
- deffunc(self):
- # add the object as the stored_obj menu property
- EvMenu(caller,...,stored_obj=self.obj)
-
-
-
-
Inside the menu you can now access the object through caller.ndb._menutree.stored_obj.
Q: How do I add colors to the names of Evennia channels?
-
A: The Channel typeclass’ channel_prefix method decides what is shown at the beginning of a
-channel send. Edit mygame/typeclasses/channels.py (and then @reload):
-
# define our custom color names
-CHANNEL_COLORS={'public':'|015Public|n',
- 'newbie':'|550N|n|551e|n|552w|n|553b|n|554i|n|555e|n',
- 'staff':'|010S|n|020t|n|030a|n|040f|n|050f|n'}
-
-# Add to the Channel class
- # ...
- defchannel_prefix(self,msg,emit=False):
- prefix_string=""
- ifself.keyinCOLORS:
- prefix_string="[%s] "%CHANNEL_COLORS.get(self.key.lower())
- else:
- prefix_string="[%s] "%self.key.capitalize()
- returnprefix_string
-
-
-
Additional hint: To make colors easier to change from one place you could instead put the
-CHANNEL_COLORS dict in your settings file and import it as fromdjango.conf.settingsimportCHANNEL_COLORS.
Q: I want certain commands to turn off in a given room. They should still work normally for
-staff.
-
A: This is done using a custom cmdset on a room locked with the ‘call’ lock type. Only
-if this lock is passed will the commands on the room be made available to an object inside it. Here
-is an example of a room where certain commands are disabled for non-staff:
-
# in mygame/typeclasses/rooms.py
-
-fromevenniaimportdefault_commands,CmdSet
-
-classCmdBlocking(default_commands.MuxCommand):
- # block commands give, get, inventory and drop
- key="give"
- aliases=["get","inventory","drop"]
- deffunc(self):
- self.caller.msg("You cannot do that in this room.")
-
-classBlockingCmdSet(CmdSet):
- key="blocking_cmdset"
- # default commands have prio 0
- priority=1
- defat_cmdset_creation(self):
- self.add(CmdBlocking())
-
-classBlockingRoom(Room):
- defat_object_creation(self):
- self.cmdset.add(BlockingCmdSet,permanent=True)
- # only share commands with players in the room that
- # are NOT Builders or higher
- self.locks.add("call:not perm(Builders)")
-
-
-
After @reload, make some BlockingRooms (or switch a room to it with @typeclass). Entering one
-will now replace the given commands for anyone that does not have the Builders or higher
-permission. Note that the ‘call’ lock is special in that even the superuser will be affected by it
-(otherwise superusers would always see other player’s cmdsets and a game would be unplayable for
-superusers).
Q: I want a command to be available only based on a condition. For example I want the “werewolf”
-command to only be available on a full moon, from midnight to three in-game time.
-
A: This is easiest accomplished by putting the “werewolf” command on the Character as normal,
-but to lock it with the “cmd” type lock. Only if the “cmd” lock type is passed will the
-command be available.
-
# in mygame/commands/command.py
-
-fromevenniaimportCommand
-
-classCmdWerewolf(Command):
- key="werewolf"
- # lock full moon, between 00:00 (midnight) and 03:00.
- locks="cmd:is_full_moon(0, 3)"
- deffunc(self):
- # ...
-
# in mygame/server/conf/lockfuncs.py
-
-defis_full_moon(accessing_obj,accessed_obj,
- starthour,endhour,*args,**kwargs):
- # calculate if the moon is full here and
- # if current game time is between starthour and endhour
- # return True or False
-
-
-
-
After a @reload, the werewolf command will be available only at the right time, that is when the
-is_full_moon lock function returns True.
Q: I have a development server running Evennia. Can I have the server update its code-base when
-I reload?
-
A: Having a development server that pulls updated code whenever you reload it can be really
-useful if you have limited shell access to your server, or want to have it done automatically. If
-you have your project in a configured Git environment, it’s a matter of automatically calling gitpull when you reload. And that’s pretty straightforward:
-
In /server/conf/at_server_startstop.py:
-
importsubprocess
-
-# ... other hooks ...
-
-defat_server_reload_stop():
- """
- This is called only time the server stops before a reload.
- """
- print("Pulling from the game repository...")
- process=subprocess.call(["git","pull"],shell=False)
-
-
-
That’s all. We call subprocess to execute a shell command (that code works on Windows and Linux,
-assuming the current directory is your game directory, which is probably the case when you run
-Evennia). call waits for the process to complete, because otherwise, Evennia would reload on
-partially-modified code, which would be problematic.
-
Now, when you enter @reload on your development server, the game repository is updated from the
-configured remote repository (Github, for instance). Your development cycle could resemble
-something like:
-
-
Coding on the local machine.
-
Testing modifications.
-
Committing once, twice or more (being sure the code is still working, unittests are pretty useful
-here).
-
When the time comes, login to the development server and run @reload.
-
-
The reloading might take one or two additional seconds, since Evennia will pull from your remote Git
-repository. But it will reload on it and you will have your modifications ready, without needing
-connecting to your server using SSH or something similar.
Q: How can I change the default exit messages to something like “XXX leaves east” or “XXX
-arrives from the west”?
-
A: the default exit messages are stored in two hooks, namely announce_move_from and
-announce_move_to, on the Character typeclass (if what you want to change is the message other
-characters will see when a character exits).
-
These two hooks provide some useful features to easily update the message to be displayed. They
-take both the default message and mapping as argument. You can easily call the parent hook with
-these information:
-
-
The message represents the string of characters sent to characters in the room when a character
-leaves.
-
The mapping is a dictionary containing additional mappings (you will probably not need it for
-simple customization).
-
-
It is advisable to look in the code of both
-hooks, and read the
-hooks’ documentation. The explanations on how to quickly update the message are shown below:
-
# In typeclasses/characters.py
-"""
-Characters
-
-"""
-fromevenniaimportDefaultCharacter
-
-classCharacter(DefaultCharacter):
- """
- The default character class.
-
- ...
- """
-
- defannounce_move_from(self,destination,msg=None,mapping=None):
- """
- Called if the move is to be announced. This is
- called while we are still standing in the old
- location.
-
- Args:
- destination (Object): The place we are going to.
- msg (str, optional): a replacement message.
- mapping (dict, optional): additional mapping objects.
-
- You can override this method and call its parent with a
- message to simply change the default message. In the string,
- you can use the following as mappings (between braces):
- object: the object which is moving.
- exit: the exit from which the object is moving (if found).
- origin: the location of the object before the move.
- destination: the location of the object after moving.
-
- """
- super().announce_move_from(destination,msg="{object} leaves {exit}.")
-
- defannounce_move_to(self,source_location,msg=None,mapping=None):
- """
- Called after the move if the move was not quiet. At this point
- we are standing in the new location.
-
- Args:
- source_location (Object): The place we came from
- msg (str, optional): the replacement message if location.
- mapping (dict, optional): additional mapping objects.
-
- You can override this method and call its parent with a
- message to simply change the default message. In the string,
- you can use the following as mappings (between braces):
- object: the object which is moving.
- exit: the exit from which the object is moving (if found).
- origin: the location of the object before the move.
- destination: the location of the object after moving.
-
- """
- super().announce_move_to(source_location,msg="{object} arrives from the {exit}.")
-
-
-
We override both hooks, but call the parent hook to display a different message. If you read the
-provided docstrings, you will better understand why and how we use mappings (information between
-braces). You can provide additional mappings as well, if you want to set a verb to move, for
-instance, or other, extra information.
Do a @reload and all default commands will now use your new tweaked parent class. A copy of the
-MuxCommand class is also found commented-out in the mygame/commands/command.py file.
Q: If a user has already logged out of an Evennia account, their IP is no longer visible to
-staff that wants to ban-by-ip (instead of the user) with @ban/ip?
-
A: One approach is to write the IP from the last session onto the “account” account object.
Adding timestamp for login time and appending to a list to keep the last N login IP addresses and
-timestamps is possible, also. Additionally, if you don’t want the list to grow beyond a
-do_not_exceed length, conditionally pop a value after you’ve added it, if the length has grown too
-long.
-
NOTE: You’ll need to add importtime to generate the login timestamp.
-
defat_post_login(self,session=None,**kwargs):
- super().at_post_login(session=session,**kwargs)
- do_not_exceed=24# Keep the last two dozen entries
- session=self.sessions.all()[-1]# Most recent session
- ifnotself.db.lastsite:
- self.db.lastsite=[]
- self.db.lastsite.insert(0,(session.address,int(time.time())))
- iflen(self.db.lastsite)>do_not_exceed:
- self.db.lastsite.pop()
-
-
-
This only stores the data. You may want to interface the @ban command or make a menu-driven viewer
-for staff to browse the list and display how long ago the login occurred.
A: The reason for this is because certain non-latin characters are visually much wider than
-their len() suggests. There is little Evennia can (reliably) do about this. If you are using such
-characters, you need to make sure to use a suitable mono-spaced font where are width are equal. You
-can set this in your web client and need to recommend it for telnet-client users. See this
-discussion where some suitable fonts are suggested.
Evennia allows for a lot of freedom when designing your game - but to code efficiently you still
-need to adopt some best practices as well as find a good place to start to learn.
Evennia is developed using Python. Even if you are more of a designer than a coder, it is wise to
-learn how to read and understand basic Python code. If you are new to Python, or need a refresher,
-take a look at our two-part Python introduction.
When new to Evennia it can be hard to find things or figure out what is available. Evennia offers a
-special interactive python shell that allows you to experiment and try out things. It’s recommended
-to use ipython for this since the vanilla python prompt is very limited. Here
-are some simple commands to get started:
-
# [open a new console/terminal]
-# [activate your evennia virtualenv in this console/terminal]
-pip install ipython # [only needed the first time]
-cd mygame
-evennia shell
-
-
-
This will open an Evennia-aware python shell (using ipython). From within this shell, try
-
import evennia
-evennia.<TAB>
-
-
-
That is, enter evennia. and press the <TAB> key. This will show you all the resources made
-available at the top level of Evennia’s “flat API”. See the flat API page for more
-info on how to explore it efficiently.
-
You can complement your exploration by peeking at the sections of the much more detailed
-Developer Central. The Tutorials section also contains a growing collection
-of system- or implementation-specific help.
Evennia works by importing your own modules and running them as part of the server. Whereas Evennia
-should just gracefully tell you what errors it finds, it can nevertheless be a good idea for you to
-check your code for simple syntax errors before you load it into the running server. There are
-many python syntax checkers out there. A fast and easy one is
-pyflakes, a more verbose one is
-pylint. You can also check so that your code looks up to snuff using
-pep8. Even with a syntax checker you will not be able to catch
-every possible problem - some bugs or problems will only appear when you actually run the code. But
-using such a checker can be a good start to weed out the simple problems.
Before you start coding away at your dream game, take a look at our Game Planning
-page. It might hopefully help you avoid some common pitfalls and time sinks.
-
-
-
Code in your game folder, not in the evennia/ repository¶
-
As part of the Evennia setup you will create a game folder to host your game code. This is your
-home. You should never need to modify anything in the evennia library (anything you download
-from us, really). You import useful functionality from here and if you see code you like, copy&paste
-it out into your game folder and edit it there.
-
If you find that Evennia doesn’t support some functionality you need, make a
-Feature Request about it. Same goes for bugs. If you add features or fix bugs
-yourself, please consider Contributing your changes upstream!
Python is very good at reporting when and where things go wrong. A traceback shows everything you
-need to know about crashing code. The text can be pretty long, but you usually are only interested
-in the last bit, where it says what the error is and at which module and line number it happened -
-armed with this info you can resolve most problems.
-
Evennia will usually not show the full traceback in-game though. Instead the server outputs errors
-to the terminal/console from which you started Evennia in the first place. If you want more to show
-in-game you can add IN_GAME_ERRORS=True to your settings file. This will echo most (but not all)
-tracebacks both in-game as well as to the terminal/console. This is a potential security problem
-though, so don’t keep this active when your game goes into production.
-
-
A common confusing error is finding that objects in-game are suddenly of the type DefaultObject
-rather than your custom typeclass. This happens when you introduce a critical Syntax error to the
-module holding your custom class. Since such a module is not valid Python, Evennia can’t load it at
-all. Instead of crashing, Evennia will then print the full traceback to the terminal/console and
-temporarily fall back to the safe DefaultObject until you fix the problem and reload.
Some people find reading documentation extremely dull and shun it out of principle. That’s your
-call, but reading docs really does help you, promise! Evennia’s documentation is pretty thorough
-and knowing what is possible can often give you a lot of new cool game ideas. That said, if you
-can’t find the answer in the docs, don’t be shy to ask questions! The
-discussion group and the
-irc chat are also there for you.
Evennia comes with many utilities to help with common coding tasks. Most are accessible directly
-from the flat API, otherwise you can find them in the evennia/utils/ folder.
A common thing to do is to search for objects. There it’s easiest to use the search method defined
-on all objects. This will search for objects in the same location and inside the self object:
-
obj=self.search(objname)
-
-
-
The most common time one needs to do this is inside a command body. obj=self.caller.search(objname) will search inside the caller’s (typically, the character that typed
-the command) .contents (their “inventory”) and .location (their “room”).
-
Give the keyword global_search=True to extend search to encompass entire database. Aliases will
-also be matched by this search. You will find multiple examples of this functionality in the default
-command set.
-
If you need to search for objects in a code module you can use the functions in
-evennia.utils.search. You can access these as shortcuts evennia.search_*.
Apart from the in-game build commands (@create etc), you can also build all of Evennia’s game
-entities directly in code (for example when defining new create commands).
Normally you can use Python print statements to see output to the terminal/log. The print
-statement should only be used for debugging though. For producion output, use the logger which
-will create proper logs either to terminal or to file.
-
fromevenniaimportlogger
- #
- logger.log_err("This is an Error!")
- logger.log_warn("This is a Warning!")
- logger.log_info("This is normal information")
- logger.log_dep("This feature is deprecated")
-
-
-
There is a special log-message type, log_trace() that is intended to be called from inside a
-traceback - this can be very useful for relaying the traceback message back to log without having it
-kill the server.
-
try:
- # [some code that may fail...]
- exceptException:
- logger.log_trace("This text will show beneath the traceback itself.")
-
-
-
The log_file logger, finally, is a very useful logger for outputting arbitrary log messages. This
-is a heavily optimized asynchronous log mechanism using
-threads to avoid overhead. You should be
-able to use it for very heavy custom logging without fearing disk-write delays.
-
logger.log_file(message,filename="mylog.log")
-
-
-
If not an absolute path is given, the log file will appear in the mygame/server/logs/ directory.
-If the file already exists, it will be appended to. Timestamps on the same format as the normal
-Evennia logs will be automatically added to each entry. If a filename is not specified, output will
-be written to a file game/logs/game.log.
Evennia tracks the current server time. You can access this time via the evennia.gametime
-shortcut:
-
fromevenniaimportgametime
-
-# all the functions below return times in seconds).
-
-# total running time of the server
-runtime=gametime.runtime()
-# time since latest hard reboot (not including reloads)
-uptime=gametime.uptime()
-# server epoch (its start time)
-server_epoch=gametime.server_epoch()
-
-# in-game epoch (this can be set by `settings.TIME_GAME_EPOCH`.
-# If not, the server epoch is used.
-game_epoch=gametime.game_epoch()
-# in-game time passed since time started running
-gametime=gametime.gametime()
-# in-game time plus game epoch (i.e. the current in-game
-# time stamp)
-gametime=gametime.gametime(absolute=True)
-# reset the game time (back to game epoch)
-gametime.reset_gametime()
-
-
-
-
The setting TIME_FACTOR determines how fast/slow in-game time runs compared to the real world. The
-setting TIME_GAME_EPOCH sets the starting game epoch (in seconds). The functions from the
-gametime module all return their times in seconds. You can convert this to whatever units of time
-you desire for your game. You can use the @time command to view the server time info.
-
You can also schedule things to happen at specific in-game times using the
-gametime.schedule function:
This function takes a number of seconds as input (e.g. from the gametime module above) and
-converts it to a nice text output in days, hours etc. It’s useful when you want to show how old
-something is. It converts to four different styles of output using the style keyword:
-
-
style 0 - 5d:45m:12s (standard colon output)
-
style 1 - 5d (shows only the longest time unit)
-
style 2 - 5days,45minutes (full format, ignores seconds)
-
style 3 - 5days,45minutes,12seconds (full format, with seconds)
fromevenniaimportutils
-
-def_callback(obj,text):
- obj.msg(text)
-
-# wait 10 seconds before sending "Echo!" to obj (which we assume is defined)
-deferred=utils.delay(10,_callback,obj,"Echo!",persistent=False)
-
-# code here will run immediately, not waiting for the delay to fire!
-
-
-
-
This creates an asynchronous delayed call. It will fire the given callback function after the given
-number of seconds. This is a very light wrapper over a Twisted
-Deferred. Normally this is run
-non-persistently, which means that if the server is @reloaded before the delay is over, the
-callback will never run (the server forgets it). If setting persistent to True, the delay will be
-stored in the database and survive a @reload - but for this to work it is susceptible to the same
-limitations incurred when saving to an Attribute.
-
The deferred return object can usually be ignored, but calling its .cancel() method will abort
-the delay prematurely.
-
utils.delay is the lightest form of delayed call in Evennia. For other way to create time-bound
-tasks, see the TickerHandler and Scripts.
-
-
Note that many delayed effects can be achieved without any need for an active timer. For example
-if you have a trait that should recover a point every 5 seconds you might just need its value when
-it’s needed, but checking the current time and calculating on the fly what value it should have.
This useful function takes two arguments - an object to check and a parent. It returns True if
-object inherits from parent at any distance (as opposed to Python’s in-built is_instance() that
-will only catch immediate dependence). This function also accepts as input any combination of
-classes, instances or python-paths-to-classes.
-
Note that Python code should usually work with duck
-typing. But in Evennia’s case it can sometimes be useful
-to check if an object inherits from a given Typeclass as a way of identification. Say
-for example that we have a typeclass Animal. This has a subclass Felines which in turn has a
-subclass HouseCat. Maybe there are a bunch of other animal types too, like horses and dogs. Using
-inherits_from will allow you to check for all animals in one go:
-
fromevenniaimportutils
- if(utils.inherits_from(obj,"typeclasses.objects.animals.Animal"):
- obj.msg("The bouncer stops you in the door. He says: 'No talking animals allowed.'")
-
In a text game, you are naturally doing a lot of work shuffling text back and forth. Here is a non-
-complete selection of text utilities found in evennia/utils/utils.py (shortcut evennia.utils).
-If nothing else it can be good to look here before starting to develop a solution of your own.
This function will crop a very long line, adding a suffix to show the line actually continues. This
-can be useful in listings when showing multiple lines would mess up things.
-
intxt="This is a long text that we want to crop."
- outtxt=crop(intxt,width=19,suffix="[...]")
- # outtxt is now "This is a long text[...]"
-
This solves what may at first glance appear to be a trivial problem with text - removing
-indentations. It is used to shift entire paragraphs to the left, without disturbing any further
-formatting they may have. A common case for this is when using Python triple-quoted strings in code
-
-
they will retain whichever indentation they have in the code, and to make easily-readable source
-code one usually don’t want to shift the string to the left edge.
-
-
#python code is entered at a given indentation
- intxt="""
- This is an example text that will end
- up with a lot of whitespace on the left.
- It also has indentations of
- its own."""
- outtxt=dedent(intxt)
- # outtxt will now retain all internal indentation
- # but be shifted all the way to the left.
-
-
-
Normally you do the dedent in the display code (this is for example how the help system homogenizes
-help entries).
Evennia supplies two utility functions for converting text to the correct
-encodings. to_str() and to_bytes(). Unless you are adding a custom protocol and
-need to send byte-data over the wire, to_str is the only one you’ll need.
-
The difference from Python’s in-built str() and bytes() operators are that
-the Evennia ones makes use of the ENCODINGS setting and will try very hard to
-never raise a traceback but instead echo errors through logging. See
-here for more info.
The EvTable class (evennia/utils/evtable.py) can be used
-to create correctly formatted text tables. There is also
-EvForm (evennia/utils/evform.py). This reads a fixed-format
-text template from a file in order to create any level of sophisticated ascii layout. Both evtable
-and evform have lots of options and inputs so see the header of each module for help.
-
The third-party PrettyTable module is also included in
-Evennia. PrettyTable is considered deprecated in favor of EvTable since PrettyTable cannot handle
-ANSI colour. PrettyTable can be found in evennia/utils/prettytable/. See its homepage above for
-instructions.
Some types of games want to limit how often a command can be run. If a
-character casts the spell Firestorm, you might not want them to spam that
-command over and over. Or in an advanced combat system, a massive swing may
-offer a chance of lots of damage at the cost of not being able to re-do it for
-a while. Such effects are called cooldowns.
-
This page exemplifies a very resource-efficient way to do cooldowns. A more
-‘active’ way is to use asynchronous delays as in the command duration
-tutorial, the two might be useful to
-combine if you want to echo some message to the user after the cooldown ends.
This little recipe will limit how often a particular command can be run. Since
-Commands are class instances, and those are cached in memory, a command
-instance will remember things you store on it. So just store the current time
-of execution! Next time the command is run, it just needs to check if it has
-that time stored, and compare it with the current time to see if a desired
-delay has passed.
-
importtime
-fromevenniaimportdefault_cmds
-
-classCmdSpellFirestorm(default_cmds.MuxCommand):
- """
- Spell - Firestorm
-
- Usage:
- cast firestorm <target>
-
- This will unleash a storm of flame. You can only release one
- firestorm every five minutes (assuming you have the mana).
- """
- key="cast firestorm"
- locks="cmd:isFireMage()"
-
- deffunc(self):
- "Implement the spell"
-
- # check cooldown (5 minute cooldown)
- now=time.time()
- ifhasattr(self,"lastcast")and \
- now-self.lastcast<5*60:
- message="You cannot cast this spell again yet."
- self.caller.msg(message)
- return
-
- #[the spell effect is implemented]
-
- # if the spell was successfully cast, store the casting time
- self.lastcast=now
-
-
-
We just check the lastcast flag, and update it if everything works out.
-Simple and very effective since everything is just stored in memory. The
-drawback of this simple scheme is that it’s non-persistent. If you do
-@reload, the cache is cleaned and all such ongoing cooldowns will be
-forgotten. It is also limited only to this one command, other commands cannot
-(easily) check for this value.
This is essentially the same mechanism as the simple one above, except we use
-the database to store the information which means the cooldown will survive a
-server reload/reboot. Since commands themselves have no representation in the
-database, you need to use the caster for the storage.
-
# inside the func() of CmdSpellFirestorm as above
-
- # check cooldown (5 minute cooldown)
-
- now=time.time()
- lastcast=self.caller.db.firestorm_lastcast
-
- iflastcastandnow-lastcast<5*60:
- message="You need to wait before casting this spell again."
- self.caller.msg(message)
- return
-
- #[the spell effect is implemented]
-
- # if the spell was successfully cast, store the casting time
- self.caller.db.firestorm_lastcast=now
-
-
-
Since we are storing as an Attribute, we need to identify the
-variable as firestorm_lastcast so we are sure we get the right one (we’ll
-likely have other skills with cooldowns after all). But this method of
-using cooldowns also has the advantage of working between commands - you can
-for example let all fire-related spells check the same cooldown to make sure
-the casting of Firestorm blocks all fire-related spells for a while. Or, in
-the case of taking that big swing with the sword, this could now block all
-other types of attacks for a while before the warrior can recover.
Before reading this tutorial, if you haven’t done so already, you might want to
-read the documentation on commands to get a basic understanding of
-how commands work in Evennia.
-
In some types of games a command should not start and finish immediately.
-Loading a crossbow might take a bit of time to do - time you don’t have when
-the enemy comes rushing at you. Crafting that armour will not be immediate
-either. For some types of games the very act of moving or changing pose all
-comes with a certain time associated with it.
Evennia allows a shortcut in syntax to create simple pauses in commands. This
-syntax uses the yield keyword. The yield keyword is used in Python to
-create generators, although you don’t need to know what generators are to use
-this syntax. A short example will probably make it clear:
-
classCmdTest(Command):
-
- """
- A test command just to test waiting.
-
- Usage:
- test
-
- """
-
- key="test"
- locks="cmd:all()"
-
- deffunc(self):
- self.msg("Before ten seconds...")
- yield10
- self.msg("Afterwards.")
-
-
-
-
Important: The yield functionality will only work in the func method of
-Commands. It only works because Evennia has especially
-catered for it in Commands. If you want the same functionality elsewhere you
-must use the interactive decorator.
-
-
The important line is the yield10. It tells Evennia to “pause” the command
-and to wait for 10 seconds to execute the rest. If you add this command and
-run it, you’ll see the first message, then, after a pause of ten seconds, the
-next message. You can use yield several times in your command.
-
This syntax will not “freeze” all commands. While the command is “pausing”,
-you can execute other commands (or even call the same command again). And
-other players aren’t frozen either.
-
-
Note: this will not save anything in the database. If you reload the game
-while a command is “paused”, it will not resume after the server has
-reloaded.
The yield syntax is easy to read, easy to understand, easy to use. But it’s not that flexible if
-you want more advanced options. Learning to use alternatives might be much worth it in the end.
-
Below is a simple command example for adding a duration for a command to finish.
-
fromevenniaimportdefault_cmds,utils
-
-classCmdEcho(default_cmds.MuxCommand):
- """
- wait for an echo
-
- Usage:
- echo <string>
-
- Calls and waits for an echo
- """
- key="echo"
- locks="cmd:all()"
-
- deffunc(self):
- """
- This is called at the initial shout.
- """
- self.caller.msg("You shout '%s' and wait for an echo ..."%self.args)
- # this waits non-blocking for 10 seconds, then calls self.echo
- utils.delay(10,self.echo)# call echo after 10 seconds
-
- defecho(self):
- "Called after 10 seconds."
- shout=self.args
- string="You hear an echo: %s ... %s ... %s"
- string=string%(shout.upper(),shout.capitalize(),shout.lower())
- self.caller.msg(string)
-
-
-
Import this new echo command into the default command set and reload the server. You will find that
-it will take 10 seconds before you see your shout coming back. You will also find that this is a
-non-blocking effect; you can issue other commands in the interim and the game will go on as usual.
-The echo will come back to you in its own time.
utils.delay(timedelay,callback,persistent=False,*args,**kwargs) is a useful function. It will
-wait timedelay seconds, then call the callback function, optionally passing to it the arguments
-provided to utils.delay by way of *args and/or **kwargs`.
-
-
Note: The callback argument should be provided with a python path to the desired function, for
-instance my_object.my_function instead of my_object.my_function(). Otherwise my_function would
-get called and run immediately upon attempting to pass it to the delay function.
-If you want to provide arguments for utils.delay to use, when calling your callback function, you
-have to do it separatly, for instance using the utils.delay *args and/or **kwargs, as mentioned
-above.
Looking at it you might think that utils.delay(10,callback) in the code above is just an
-alternative to some more familiar thing like time.sleep(10). This is not the case. If you do
-time.sleep(10) you will in fact freeze the entire server for ten seconds! The utils.delay()is
-a thin wrapper around a Twisted
-Deferred that will delay
-execution until 10 seconds have passed, but will do so asynchronously, without bothering anyone else
-(not even you - you can continue to do stuff normally while it waits to continue).
-
The point to remember here is that the delay() call will not “pause” at that point when it is
-called (the way yield does in the previous section). The lines after the delay() call will
-actually execute right away. What you must do is to tell it which function to call after the time
-has passed (its “callback”). This may sound strange at first, but it is normal practice in
-asynchronous systems. You can also link such calls together as seen below:
-
fromevenniaimportdefault_cmds,utils
-
-classCmdEcho(default_cmds.MuxCommand):
- """
- waits for an echo
-
- Usage:
- echo <string>
-
- Calls and waits for an echo
- """
- key="echo"
- locks="cmd:all()"
-
- deffunc(self):
- "This sets off a chain of delayed calls"
- self.caller.msg("You shout '%s', waiting for an echo ..."%self.args)
-
- # wait 2 seconds before calling self.echo1
- utils.delay(2,self.echo1)
-
- # callback chain, started above
- defecho1(self):
- "First echo"
- self.caller.msg("... %s"%self.args.upper())
- # wait 2 seconds for the next one
- utils.delay(2,self.echo2)
-
- defecho2(self):
- "Second echo"
- self.caller.msg("... %s"%self.args.capitalize())
- # wait another 2 seconds
- utils.delay(2,callback=self.echo3)
-
- defecho3(self):
- "Last echo"
- self.caller.msg("... %s ..."%self.args.lower())
-
-
-
The above version will have the echoes arrive one after another, each separated by a two second
-delay.
As mentioned, a great thing about the delay introduced by yield or utils.delay() is that it does
-not block. It just goes on in the background and you are free to play normally in the interim. In
-some cases this is not what you want however. Some commands should simply “block” other commands
-while they are running. If you are in the process of crafting a helmet you shouldn’t be able to also
-start crafting a shield at the same time, or if you just did a huge power-swing with your weapon you
-should not be able to do it again immediately.
-
The simplest way of implementing blocking is to use the technique covered in the Command
-Cooldown tutorial. In that tutorial we implemented cooldowns by having the
-Command store the current time. Next time the Command was called, we compared the current time to
-the stored time to determine if enough time had passed for a renewed use. This is a very
-efficient, reliable and passive solution. The drawback is that there is nothing to tell the Player
-when enough time has passed unless they keep trying.
-
Here is an example where we will use utils.delay to tell the player when the cooldown has passed:
-
fromevenniaimportutils,default_cmds
-
-classCmdBigSwing(default_cmds.MuxCommand):
- """
- swing your weapon in a big way
-
- Usage:
- swing <target>
-
- Makes a mighty swing. Doing so will make you vulnerable
- to counter-attacks before you can recover.
- """
- key="bigswing"
- locks="cmd:all()"
-
- deffunc(self):
- "Makes the swing"
-
- ifself.caller.ndb.off_balance:
- # we are still off-balance.
- self.caller.msg("You are off balance and need time to recover!")
- return
-
- # [attack/hit code goes here ...]
- self.caller.msg("You swing big! You are off balance now.")
-
- # set the off-balance flag
- self.caller.ndb.off_balance=True
-
- # wait 8 seconds before we can recover. During this time
- # we won't be able to swing again due to the check at the top.
- utils.delay(8,self.recover)
-
- defrecover(self):
- "This will be called after 8 secs"
- delself.caller.ndb.off_balance
- self.caller.msg("You regain your balance.")
-
-
-
Note how, after the cooldown, the user will get a message telling them they are now ready for
-another swing.
-
By storing the off_balance flag on the character (rather than on, say, the Command instance
-itself) it can be accessed by other Commands too. Other attacks may also not work when you are off
-balance. You could also have an enemy Command check your off_balance status to gain bonuses, to
-take another example.
One can imagine that you will want to abort a long-running command before it has a time to finish.
-If you are in the middle of crafting your armor you will probably want to stop doing that when a
-monster enters your smithy.
-
You can implement this in the same way as you do the “blocking” command above, just in reverse.
-Below is an example of a crafting command that can be aborted by starting a fight:
-
fromevenniaimportutils,default_cmds
-
-classCmdCraftArmour(default_cmds.MuxCommand):
- """
- Craft armour
-
- Usage:
- craft <name of armour>
-
- This will craft a suit of armour, assuming you
- have all the components and tools. Doing some
- other action (such as attacking someone) will
- abort the crafting process.
- """
- key="craft"
- locks="cmd:all()"
-
- deffunc(self):
- "starts crafting"
-
- ifself.caller.ndb.is_crafting:
- self.caller.msg("You are already crafting!")
- return
- ifself._is_fighting():
- self.caller.msg("You can't start to craft "
- "in the middle of a fight!")
- return
-
- # [Crafting code, checking of components, skills etc]
-
- # Start crafting
- self.caller.ndb.is_crafting=True
- self.caller.msg("You start crafting ...")
- utils.delay(60,self.step1)
-
- def_is_fighting(self):
- "checks if we are in a fight."
- ifself.caller.ndb.is_fighting:
- delself.caller.ndb.is_crafting
- returnTrue
-
- defstep1(self):
- "first step of armour construction"
- ifself._is_fighting():
- return
- self.msg("You create the first part of the armour.")
- utils.delay(60,callback=self.step2)
-
- defstep2(self):
- "second step of armour construction"
- ifself._is_fighting():
- return
- self.msg("You create the second part of the armour.")
- utils.delay(60,step3)
-
- defstep3(self):
- "last step of armour construction"
- ifself._is_fighting():
- return
-
- # [code for creating the armour object etc]
-
- delself.caller.ndb.is_crafting
- self.msg("You finalize your armour.")
-
-
-# example of a command that aborts crafting
-
-classCmdAttack(default_cmds.MuxCommand):
- """
- attack someone
-
- Usage:
- attack <target>
-
- Try to cause harm to someone. This will abort
- eventual crafting you may be currently doing.
- """
- key="attack"
- aliases=["hit","stab"]
- locks="cmd:all()"
-
- deffunc(self):
- "Implements the command"
-
- self.caller.ndb.is_fighting=True
-
- # [...]
-
-
-
The above code creates a delayed crafting command that will gradually create the armour. If the
-attack command is issued during this process it will set a flag that causes the crafting to be
-quietly canceled next time it tries to update.
In the latter examples above we used .ndb storage. This is fast and easy but it will reset all
-cooldowns/blocks/crafting etc if you reload the server. If you don’t want that you can replace
-.ndb with .db. But even this won’t help because the yield keyword is not persisent and nor is
-the use of delay shown above. To resolve this you can use delay with the persistent=True
-keyword. But wait! Making something persistent will add some extra complications, because now you
-must make sure Evennia can properly store things to the database.
-
Here is the original echo-command reworked to function with persistence:
-
fromevenniaimportdefault_cmds,utils
-
-# this is now in the outermost scope and takes two args!
-defecho(caller,args):
- "Called after 10 seconds."
- shout=args
- string="You hear an echo: %s ... %s ... %s"
- string=string%(shout.upper(),shout.capitalize(),shout.lower())
- caller.msg(string)
-
-classCmdEcho(default_cmds.MuxCommand):
- """
- wait for an echo
-
- Usage:
- echo <string>
-
- Calls and waits for an echo
- """
- key="echo"
- locks="cmd:all()"
-
- deffunc(self):
- """
- This is called at the initial shout.
- """
- self.caller.msg("You shout '%s' and wait for an echo ..."%self.args)
- # this waits non-blocking for 10 seconds, then calls echo(self.caller, self.args)
- utils.delay(10,echo,self.caller,self.args,persistent=True)# changes!
-
-
-
-
Above you notice two changes:
-
-
The callback (echo) was moved out of the class and became its own stand-alone function in the
-outermost scope of the module. It also now takes caller and args as arguments (it doesn’t have
-access to them directly since this is now a stand-alone function).
-
utils.delay specifies the echo function (not self.echo - it’s no longer a method!) and sends
-self.caller and self.args as arguments for it to use. We also set persistent=True.
-
-
The reason for this change is because Evennia needs to pickle the callback into storage and it
-cannot do this correctly when the method sits on the command class. Now this behave the same as the
-first version except if you reload (or even shut down) the server mid-delay it will still fire the
-callback when the server comes back up (it will resume the countdown and ignore the downtime).
A prompt is quite common in MUDs. The prompt display useful details about your character that you
-are likely to want to keep tabs on at all times, such as health, magical power etc. It might also
-show things like in-game time, weather and so on. Many modern MUD clients (including Evennia’s own
-webclient) allows for identifying the prompt and have it appear in a correct location (usually just
-above the input line). Usually it will remain like that until it is explicitly updated.
A prompt is sent using the prompt keyword to the msg() method on objects. The prompt will be
-sent without any line breaks.
-
self.msg(prompt="HP: 5, MP: 2, SP: 8")
-
-
-
You can combine the sending of normal text with the sending (updating of the prompt):
-
self.msg("This is a text",prompt="This is a prompt")
-
-
-
You can update the prompt on demand, this is normally done using OOB-tracking of the relevant
-Attributes (like the character’s health). You could also make sure that attacking commands update
-the prompt when they cause a change in health, for example.
-
Here is a simple example of the prompt sent/updated from a command class:
-
fromevenniaimportCommand
-
- classCmdDiagnose(Command):
- """
- see how hurt your are
-
- Usage:
- diagnose [target]
-
- This will give an estimate of the target's health. Also
- the target's prompt will be updated.
- """
- key="diagnose"
-
- deffunc(self):
- ifnotself.args:
- target=self.caller
- else:
- target=self.search(self.args)
- ifnottarget:
- return
- # try to get health, mana and stamina
- hp=target.db.hp
- mp=target.db.mp
- sp=target.db.sp
-
- ifNonein(hp,mp,sp):
- # Attributes not defined
- self.caller.msg("Not a valid target!")
- return
-
- text="You diagnose %s as having " \
- "%i health, %i mana and %i stamina." \
- %(hp,mp,sp)
- prompt="%i HP, %i MP, %i SP"%(hp,mp,sp)
- self.caller.msg(text,prompt=prompt)
-
The prompt sent as described above uses a standard telnet instruction (the Evennia web client gets a
-special flag). Most MUD telnet clients will understand and allow users to catch this and keep the
-prompt in place until it updates. So in principle you’d not need to update the prompt every
-command.
-
However, with a varying user base it can be unclear which clients are used and which skill level the
-users have. So sending a prompt with every command is a safe catch-all. You don’t need to manually
-go in and edit every command you have though. Instead you edit the base command class for your
-custom commands (like MuxCommand in your mygame/commands/command.py folder) and overload the
-at_post_cmd() hook. This hook is always called after the main func() method of the Command.
If you want to add something small like this to Evennia’s default commands without modifying them
-directly the easiest way is to just wrap those with a multiple inheritance to your own base class:
-
# in (for example) mygame/commands/mycommands.py
-
-fromevenniaimportdefault_cmds
-# our custom MuxCommand with at_post_cmd hook
-fromcommands.commandimportMuxCommand
-
-# overloading the look command
-classCmdLook(default_cmds.CmdLook,MuxCommand):
- pass
-
-
-
The result of this is that the hooks from your custom MuxCommand will be mixed into the default
-CmdLook through multiple inheritance. Next you just add this to your default command set:
Command Sets are intimately linked with Commands and you should be familiar with
-Commands before reading this page. The two pages were split for ease of reading.
-
A Command Set (often referred to as a CmdSet or cmdset) is the basic unit for storing one or more
-Commands. A given Command can go into any number of different command sets. Storing Command
-classes in a command set is the way to make commands available to use in your game.
-
When storing a CmdSet on an object, you will make the commands in that command set available to the
-object. An example is the default command set stored on new Characters. This command set contains
-all the useful commands, from look and inventory to @dig and @reload
-(permissions then limit which players may use them, but that’s a separate
-topic).
-
When an account enters a command, cmdsets from the Account, Character, its location, and elsewhere
-are pulled together into a merge stack. This stack is merged together in a specific order to
-create a single “merged” cmdset, representing the pool of commands available at that very moment.
-
An example would be a Window object that has a cmdset with two commands in it: lookthroughwindow and openwindow. The command set would be visible to players in the room with the window,
-allowing them to use those commands only there. You could imagine all sorts of clever uses of this,
-like a Television object which had multiple commands for looking at it, switching channels and so
-on. The tutorial world included with Evennia showcases a dark room that replaces certain critical
-commands with its own versions because the Character cannot see.
-
If you want a quick start into defining your first commands and using them with command sets, you
-can head over to the Adding Command Tutorial which steps through things
-without the explanations.
A CmdSet is, as most things in Evennia, defined as a Python class inheriting from the correct parent
-(evennia.CmdSet, which is a shortcut to evennia.commands.cmdset.CmdSet). The CmdSet class only
-needs to define one method, called at_cmdset_creation(). All other class parameters are optional,
-but are used for more advanced set manipulation and coding (see the [merge rules](Command-
-Sets#merge-rules) section).
-
# file mygame/commands/mycmdset.py
-
-fromevenniaimportCmdSet
-
-# this is a theoretical custom module with commands we
-# created previously: mygame/commands/mycommands.py
-fromcommandsimportmycommands
-
-classMyCmdSet(CmdSet):
- defat_cmdset_creation(self):
- """
- The only thing this method should need
- to do is to add commands to the set.
- """
- self.add(mycommands.MyCommand1())
- self.add(mycommands.MyCommand2())
- self.add(mycommands.MyCommand3())
-
-
-
The CmdSet’s add() method can also take another CmdSet as input. In this case all the commands
-from that CmdSet will be appended to this one as if you added them line by line:
-
defat_cmdset_creation():
- ...
- self.add(AdditionalCmdSet)# adds all command from this set
- ...
-
-
-
If you added your command to an existing cmdset (like to the default cmdset), that set is already
-loaded into memory. You need to make the server aware of the code changes:
-
@reload
-
-
-
You should now be able to use the command.
-
If you created a new, fresh cmdset, this must be added to an object in order to make the commands
-within available. A simple way to temporarily test a cmdset on yourself is use the @py command to
-execute a python snippet:
An object can only have one “default” cmdset (but can also have none). This is meant as a safe fall-
-back even if all other cmdsets fail or are removed. It is always persistent and will not be affected
-by cmdset.delete(). To remove a default cmdset you must explicitly call cmdset.remove_default().
-
Command sets are often added to an object in its at_object_creation method. For more examples of
-adding commands, read the Step by step tutorial. Generally you can
-customize which command sets are added to your objects by using self.cmdset.add() or
-self.cmdset.add_default().
-
-
Important: Commands are identified uniquely by key or alias (see Commands). If any
-overlap exists, two commands are considered identical. Adding a Command to a command set that
-already has an identical command will replace the previous command. This is very important. You
-must take this behavior into account when attempting to overload any default Evennia commands with
-your own. Otherwise, you may accidentally “hide” your own command in your command set when adding a
-new one that has a matching alias.
There are several extra flags that you can set on CmdSets in order to modify how they work. All are
-optional and will be set to defaults otherwise. Since many of these relate to merging cmdsets,
-you might want to read the [Adding and Merging Command Sets](./Command-Sets.md#adding-and-merging-
-command-sets) section for some of these to make sense.
-
-
key (string) - an identifier for the cmdset. This is optional, but should be unique. It is used
-for display in lists, but also to identify special merging behaviours using the key_mergetype
-dictionary below.
-
mergetype (string) - allows for one of the following string values: “Union”, “Intersect”,
-“Replace”, or “Remove”.
-
priority (int) - This defines the merge order of the merge stack - cmdsets will merge in rising
-order of priority with the highest priority set merging last. During a merger, the commands from the
-set with the higher priority will have precedence (just what happens depends on the merge
-type). If priority is identical, the order in the
-merge stack determines preference. The priority value must be greater or equal to -100. Most in-
-game sets should usually have priorities between 0 and 100. Evennia default sets have priorities
-as follows (these can be changed if you want a different distribution):
-
-
EmptySet: -101 (should be lower than all other sets)
-
SessionCmdSet: -20
-
AccountCmdSet: -10
-
CharacterCmdSet: 0
-
ExitCmdSet: 101 (generally should always be available)
-
ChannelCmdSet: 101 (should usually always be available) - since exits never accept
-arguments, there is no collision between exits named the same as a channel even though the commands
-“collide”.
-
-
-
key_mergetype (dict) - a dict of key:mergetype pairs. This allows this cmdset to merge
-differently with certain named cmdsets. If the cmdset to merge with has a key matching an entry in
-key_mergetype, it will not be merged according to the setting in mergetype but according to the
-mode in this dict. Please note that this is more complex than it may seem due to the merge
-order of command sets. Please review that section
-before using key_mergetype.
-
duplicates (bool/None default None) - this determines what happens when merging same-priority
-cmdsets containing same-key commands together. Thedupicate option will only apply when merging
-the cmdset with this option onto one other cmdset with the same priority. The resulting cmdset will
-not retain this duplicate setting.
-
-
None (default): No duplicates are allowed and the cmdset being merged “onto” the old one
-will take precedence. The result will be unique commands. However, the system will assume this
-value to be True for cmdsets on Objects, to avoid dangerous clashes. This is usually the safe bet.
-
False: Like None except the system will not auto-assume any value for cmdsets defined on
-Objects.
-
True: Same-named, same-prio commands will merge into the same cmdset. This will lead to a
-multimatch error (the user will get a list of possibilities in order to specify which command they
-meant). This is is useful e.g. for on-object cmdsets (example: There is a redbutton and a greenbutton in the room. Both have a pressbutton command, in cmdsets with the same priority. This
-flag makes sure that just writing pressbutton will force the Player to define just which object’s
-command was intended).
-
-
-
no_objs this is a flag for the cmdhandler that builds the set of commands available at every
-moment. It tells the handler not to include cmdsets from objects around the account (nor from rooms
-or inventory) when building the merged set. Exit commands will still be included. This option can
-have three values:
-
-
None (default): Passthrough of any value set explicitly earlier in the merge stack. If never
-set explicitly, this acts as False.
-
True/False: Explicitly turn on/off. If two sets with explicit no_objs are merged,
-priority determines what is used.
-
-
-
no_exits - this is a flag for the cmdhandler that builds the set of commands available at every
-moment. It tells the handler not to include cmdsets from exits. This flag can have three values:
-
-
None (default): Passthrough of any value set explicitly earlier in the merge stack. If
-never set explicitly, this acts as False.
-
True/False: Explicitly turn on/off. If two sets with explicit no_exits are merged,
-priority determines what is used.
-
-
-
no_channels (bool) - this is a flag for the cmdhandler that builds the set of commands available
-at every moment. It tells the handler not to include cmdsets from available in-game channels. This
-flag can have three values:
-
-
None (default): Passthrough of any value set explicitly earlier in the merge stack. If
-never set explicitly, this acts as False.
-
True/False: Explicitly turn on/off. If two sets with explicit no_channels are merged,
-priority determines what is used.
When a user issues a command, it is matched against the [merged](./Command-Sets.md#adding-and-merging-
-command-sets) command sets available to the player at the moment. Which those are may change at any
-time (such as when the player walks into the room with the Window object described earlier).
-
The currently valid command sets are collected from the following sources:
-
-
The cmdsets stored on the currently active Session. Default is the empty
-SessionCmdSet with merge priority -20.
-
The cmdsets defined on the Account. Default is the AccountCmdSet with merge priority
--10.
-
All cmdsets on the Character/Object (assuming the Account is currently puppeting such a
-Character/Object). Merge priority 0.
-
The cmdsets of all objects carried by the puppeted Character (checks the call lock). Will not be
-included if no_objs option is active in the merge stack.
-
The cmdsets of the Character’s current location (checks the call lock). Will not be included if
-no_objs option is active in the merge stack.
-
The cmdsets of objects in the current location (checks the call lock). Will not be included if
-no_objs option is active in the merge stack.
-
The cmdsets of Exits in the location. Merge priority +101. Will not be included if no_exits
-orno_objs option is active in the merge stack.
-
The channel cmdset containing commands for posting to all channels the account
-or character is currently connected to. Merge priority +101. Will not be included if no_channels
-option is active in the merge stack.
-
-
Note that an object does not have to share its commands with its surroundings. A Character’s
-cmdsets should not be shared for example, or all other Characters would get multi-match errors just
-by being in the same room. The ability of an object to share its cmdsets is managed by its call
-lock. For example, Character objects defaults to call:false() so that any
-cmdsets on them can only be accessed by themselves, not by other objects around them. Another
-example might be to lock an object with call:inside() to only make their commands available to
-objects inside them, or cmd:holds() to make their commands available only if they are held.
Note: This is an advanced topic. It’s very useful to know about, but you might want to skip it if
-this is your first time learning about commands.
-
CmdSets have the special ability that they can be merged together into new sets. Which of the
-ingoing commands end up in the merged set is defined by the merge rule and the relative
-priorities of the two sets. Removing the latest added set will restore things back to the way it
-was before the addition.
-
CmdSets are non-destructively stored in a stack inside the cmdset handler on the object. This stack
-is parsed to create the “combined” cmdset active at the moment. CmdSets from other sources are also
-included in the merger such as those on objects in the same room (like buttons to press) or those
-introduced by state changes (such as when entering a menu). The cmdsets are all ordered after
-priority and then merged together in reverse order. That is, the higher priority will be merged
-“onto” lower-prio ones. By defining a cmdset with a merge-priority between that of two other sets,
-you will make sure it will be merged in between them.
-The very first cmdset in this stack is called the Default cmdset and is protected from accidental
-deletion. Running obj.cmdset.delete() will never delete the default set. Instead one should add
-new cmdsets on top of the default to “hide” it, as described below. Use the special
-obj.cmdset.delete_default() only if you really know what you are doing.
-
CmdSet merging is an advanced feature useful for implementing powerful game effects. Imagine for
-example a player entering a dark room. You don’t want the player to be able to find everything in
-the room at a glance - maybe you even want them to have a hard time to find stuff in their backpack!
-You can then define a different CmdSet with commands that override the normal ones. While they are
-in the dark room, maybe the look and inv commands now just tell the player they cannot see
-anything! Another example would be to offer special combat commands only when the player is in
-combat. Or when being on a boat. Or when having taken the super power-up. All this can be done on
-the fly by merging command sets.
Basic rule is that command sets are merged in reverse priority order. That is, lower-prio sets are
-merged first and higher prio sets are merged “on top” of them. Think of it like a layered cake with
-the highest priority on top.
-
To further understand how sets merge, we need to define some examples. Let’s call the first command
-set A and the second B. We assume B is the command set already active on our object and
-we will merge A onto B. In code terms this would be done by object.cdmset.add(A).
-Remember, B is already active on object from before.
-
We let the A set have higher priority than B. A priority is simply an integer number. As
-seen in the list above, Evennia’s default cmdsets have priorities in the range -101 to 120. You
-are usually safe to use a priority of 0 or 1 for most game effects.
-
In our examples, both sets contain a number of commands which we’ll identify by numbers, like A1,A2 for set A and B1,B2,B3,B4 for B. So for that example both sets contain commands
-with the same keys (or aliases) “1” and “2” (this could for example be “look” and “get” in the real
-game), whereas commands 3 and 4 are unique to B. To describe a merge between these sets, we
-would write A1,A2+B1,B2,B3,B4=? where ? is a list of commands that depend on which merge
-type A has, and which relative priorities the two sets have. By convention, we read this
-statement as “New command set A is merged onto the old command set B to form ?”.
-
Below are the available merge types and how they work. Names are partly borrowed from Set
-theory.
-
-
Union (default) - The two cmdsets are merged so that as many commands as possible from each
-cmdset ends up in the merged cmdset. Same-key commands are merged by priority.
-
# Union
- A1,A2 + B1,B2,B3,B4 = A1,A2,B3,B4
-
-
-
-
Intersect - Only commands found in both cmdsets (i.e. which have the same keys) end up in
-the merged cmdset, with the higher-priority cmdset replacing the lower one’s commands.
-
# Intersect
- A1,A3,A5 + B1,B2,B4,B5 = A1,A5
-
-
-
-
Replace - The commands of the higher-prio cmdset completely replaces the lower-priority
-cmdset’s commands, regardless of if same-key commands exist or not.
-
# Replace
- A1,A3 + B1,B2,B4,B5 = A1,A3
-
-
-
-
Remove - The high-priority command sets removes same-key commands from the lower-priority
-cmdset. They are not replaced with anything, so this is a sort of filter that prunes the low-prio
-set using the high-prio one as a template.
-
# Remove
- A1,A3 + B1,B2,B3,B4,B5 = B2,B4,B5
-
-
-
-
-
Besides priority and mergetype, a command-set also takes a few other variables to control how
-they merge:
-
-
duplicates (bool) - determines what happens when two sets of equal priority merge. Default is
-that the new set in the merger (i.e. A above) automatically takes precedence. But if
-duplicates is true, the result will be a merger with more than one of each name match. This will
-usually lead to the player receiving a multiple-match error higher up the road, but can be good for
-things like cmdsets on non-player objects in a room, to allow the system to warn that more than one
-‘ball’ in the room has the same ‘kick’ command defined on it and offer a chance to select which
-ball to kick … Allowing duplicates only makes sense for Union and Intersect, the setting is
-ignored for the other mergetypes.
-
key_mergetypes (dict) - allows the cmdset to define a unique mergetype for particular cmdsets,
-identified by their cmdset key. Format is {CmdSetkey:mergetype}. Example:
-{'Myevilcmdset','Replace'} which would make sure for this set to always use ‘Replace’ on the
-cmdset with the key Myevilcmdset only, no matter what the main mergetype is set to.
-
-
-
Warning: The key_mergetypes dictionary can only work on the cmdset we merge onto. When using
-key_mergetypes it is thus important to consider the merge priorities - you must make sure that you
-pick a priority between the cmdset you want to detect and the next higher one, if any. That is, if
-we define a cmdset with a high priority and set it to affect a cmdset that is far down in the merge
-stack, we would not “see” that set when it’s time for us to merge. Example: Merge stack is
-A(prio=-10),B(prio=-5),C(prio=0),D(prio=5). We now merge a cmdset E(prio=10) onto this stack,
-with a key_mergetype={"B":"Replace"}. But priorities dictate that we won’t be merged onto B, we
-will be merged onto E (which is a merger of the lower-prio sets at this point). Since we are merging
-onto E and not B, our key_mergetype directive won’t trigger. To make sure it works we must make
-sure we merge onto B. Setting E’s priority to, say, -4 will make sure to merge it onto B and affect
-it appropriately.
-
-
More advanced cmdset example:
-
fromcommandsimportmycommands
-
-classMyCmdSet(CmdSet):
-
- key="MyCmdSet"
- priority=4
- mergetype="Replace"
- key_mergetypes={'MyOtherCmdSet':'Union'}
-
- defat_cmdset_creation(self):
- """
- The only thing this method should need
- to do is to add commands to the set.
- """
- self.add(mycommands.MyCommand1())
- self.add(mycommands.MyCommand2())
- self.add(mycommands.MyCommand3())
-
It is very important to remember that two commands are compared both by their key properties
-and by their aliases properties. If either keys or one of their aliases match, the two commands
-are considered the same. So consider these two Commands:
-
-
A Command with key “kick” and alias “fight”
-
A Command with key “punch” also with an alias “fight”
-
-
During the cmdset merging (which happens all the time since also things like channel commands and
-exits are merged in), these two commands will be considered identical since they share alias. It
-means only one of them will remain after the merger. Each will also be compared with all other
-commands having any combination of the keys and/or aliases “kick”, “punch” or “fight”.
-
… So avoid duplicate aliases, it will only cause confusion.
Commands are intimately linked to Command Sets and you need to read that page too to
-be familiar with how the command system works. The two pages were split for easy reading.
-
The basic way for users to communicate with the game is through Commands. These can be commands
-directly related to the game world such as look, get, drop and so on, or administrative
-commands such as examine or @dig.
-
The default commands coming with Evennia are ‘MUX-like’ in that they use @
-for admin commands, support things like switches, syntax with the ‘=’ symbol etc, but there is
-nothing that prevents you from implementing a completely different command scheme for your game. You
-can find the default commands in evennia/commands/default. You should not edit these directly -
-they will be updated by the Evennia team as new features are added. Rather you should look to them
-for inspiration and inherit your own designs from them.
-
There are two components to having a command running - the Command class and the Command
-Set (command sets were split into a separate wiki page for ease of reading).
-
-
A Command is a python class containing all the functioning code for what a command does - for
-example, a get command would contain code for picking up objects.
-
A Command Set (often referred to as a CmdSet or cmdset) is like a container for one or more
-Commands. A given Command can go into any number of different command sets. Only by putting the
-command set on a character object you will make all the commands therein available to use by that
-character. You can also store command sets on normal objects if you want users to be able to use the
-object in various ways. Consider a “Tree” object with a cmdset defining the commands climb and
-chop down. Or a “Clock” with a cmdset containing the single command check time.
-
-
This page goes into full detail about how to use Commands. To fully use them you must also read the
-page detailing Command Sets. There is also a step-by-step Adding Command
-Tutorial that will get you started quickly without the extra explanations.
All commands are implemented as normal Python classes inheriting from the base class Command
-(evennia.Command). You will find that this base class is very “bare”. The default commands of
-Evennia actually inherit from a child of Command called MuxCommand - this is the class that
-knows all the mux-like syntax like /switches, splitting by “=” etc. Below we’ll avoid mux-
-specifics and use the base Command class directly.
-
# basic Command definition
- fromevenniaimportCommand
-
- classMyCmd(Command):
- """
- This is the help-text for the command
- """
- key="mycommand"
- defparse(self):
- # parsing the command line here
- deffunc(self):
- # executing the command here
-
-
-
Here is a minimalistic command with no custom parsing:
-
fromevenniaimportCommand
-
- classCmdEcho(Command):
- key="echo"
-
- deffunc(self):
- # echo the caller's input back to the caller
- self.caller.msg("Echo: {}".format(self.args)
-
-
-
-
You define a new command by assigning a few class-global properties on your inherited class and
-overloading one or two hook functions. The full gritty mechanic behind how commands work are found
-towards the end of this page; for now you only need to know that the command handler creates an
-instance of this class and uses that instance whenever you use this command - it also dynamically
-assigns the new command instance a few useful properties that you can assume to always be available.
In Evennia there are three types of objects that may call the command. It is important to be aware
-of this since this will also assign appropriate caller, session, sessid and account
-properties on the command body at runtime. Most often the calling type is Session.
-
-
A Session. This is by far the most common case when a user is entering a command in
-their client.
-
-
caller - this is set to the puppeted Object if such an object exists. If no
-puppet is found, caller is set equal to account. Only if an Account is not found either (such as
-before being logged in) will this be set to the Session object itself.
-
session - a reference to the Session object itself.
-
sessid - sessid.id, a unique integer identifier of the session.
-
account - the Account object connected to this Session. None if not logged in.
-
-
-
An Account. This only happens if account.execute_cmd() was used. No Session
-information can be obtained in this case.
-
-
caller - this is set to the puppeted Object if such an object can be determined (without
-Session info this can only be determined in MULTISESSION_MODE=0 or 1). If no puppet is found,
-this is equal to account.
-
session - None*
-
sessid - None*
-
account - Set to the Account object.
-
-
-
An Object. This only happens if object.execute_cmd() was used (for example by an
-NPC).
-
-
caller - This is set to the calling Object in question.
-
session - None*
-
sessid - None*
-
account - None
-
-
-
-
-
*): There is a way to make the Session available also inside tests run directly on Accounts and
-Objects, and that is to pass it to execute_cmd like so: account.execute_cmd("...",session=<Session>). Doing so will make the .session and .sessid properties available in the
-command.
-
-
-
-
Properties assigned to the command instance at run-time¶
-
Let’s say account Bob with a character BigGuy enters the command look at sword. After the
-system having successfully identified this as the “look” command and determined that BigGuy really
-has access to a command named look, it chugs the look command class out of storage and either
-loads an existing Command instance from cache or creates one. After some more checks it then assigns
-it the following properties:
-
-
caller - The character BigGuy, in this example. This is a reference to the object executing the
-command. The value of this depends on what type of object is calling the command; see the previous
-section.
-
session - the Session Bob uses to connect to the game and control BigGuy (see also
-previous section).
-
sessid - the unique id of self.session, for quick lookup.
cmdstring - the matched key for the command. This would be look in our example.
-
args - this is the rest of the string, except the command name. So if the string entered was
-look at sword, args would be ” at sword”. Note the space kept - Evennia would correctly
-interpret lookatsword too. This is useful for things like /switches that should not use space.
-In the MuxCommand class used for default commands, this space is stripped. Also see the
-arg_regex property if you want to enforce a space to make lookatsword give a command-not-found
-error.
-
obj - the game Object on which this command is defined. This need not be the caller,
-but since look is a common (default) command, this is probably defined directly on BigGuy - so
-obj will point to BigGuy. Otherwise obj could be an Account or any interactive object with
-commands defined on it, like in the example of the “check time” command defined on a “Clock” object.
-
cmdset - this is a reference to the merged CmdSet (see below) from which this command was
-matched. This variable is rarely used, it’s main use is for the [auto-help system](Help-
-System#command-auto-help-system) (Advanced note: the merged cmdset need NOT be the same as
-BigGuy.cmdset. The merged set can be a combination of the cmdsets from other objects in the room,
-for example).
-
raw_string - this is the raw input coming from the user, without stripping any surrounding
-whitespace. The only thing that is stripped is the ending newline marker.
.get_help(caller,cmdset) - Get the help entry for this command. By default the arguments are
-not
-used, but they could be used to implement alternate help-display systems.
-
.client_width() - Shortcut for getting the client’s screen-width. Note that not all clients will
-truthfully report this value - that case the settings.DEFAULT_SCREEN_WIDTH will be returned.
-
.styled_table(*args,**kwargs) - This returns an [EvTable](module-
-evennia.utils.evtable) styled based on the
-session calling this command. The args/kwargs are the same as for EvTable, except styling defaults
-are set.
-
.styled_header, _footer, separator - These will produce styled decorations for
-display to the user. They are useful for creating listings and forms with colors adjustable per-
-user.
Beyond the properties Evennia always assigns to the command at run-time (listed above), your job is
-to define the following class properties:
-
-
key (string) - the identifier for the command, like look. This should (ideally) be unique. A
-key can consist of more than one word, like “press button” or “pull left lever”. Note that both
-key and aliases below determine the identity of a command. So two commands are considered if
-either matches. This is important for merging cmdsets described below.
-
aliases (optional list) - a list of alternate names for the command (["glance","see","l"]).
-Same name rules as for key applies.
-
locks (string) - a lock definition, usually on the form cmd:<lockfuncs>. Locks is a
-rather big topic, so until you learn more about locks, stick to giving the lockstring "cmd:all()"
-to make the command available to everyone (if you don’t provide a lock string, this will be assigned
-for you).
-
help_category (optional string) - setting this helps to structure the auto-help into categories.
-If none is set, this will be set to General.
-
save_for_next (optional boolean). This defaults to False. If True, a copy of this command
-object (along with any changes you have done to it) will be stored by the system and can be accessed
-by the next command by retrieving self.caller.ndb.last_cmd. The next run command will either clear
-or replace the storage.
-
arg_regex (optional raw string): Used to force the parser to limit itself and tell it when the
-command-name ends and arguments begin (such as requiring this to be a space or a /switch). This is
-done with a regular expression. See the arg_regex section for the details.
-
auto_help (optional boolean). Defaults to True. This allows for turning off the auto-help
-system on a per-command basis. This could be useful if you
-either want to write your help entries manually or hide the existence of a command from help’s
-generated list.
-
is_exit (bool) - this marks the command as being used for an in-game exit. This is, by default,
-set by all Exit objects and you should not need to set it manually unless you make your own Exit
-system. It is used for optimization and allows the cmdhandler to easily disregard this command when
-the cmdset has its no_exits flag set.
-
is_channel (bool)- this marks the command as being used for an in-game channel. This is, by
-default, set by all Channel objects and you should not need to set it manually unless you make your
-own Channel system. is used for optimization and allows the cmdhandler to easily disregard this
-command when its cmdset has its no_channels flag set.
-
msg_all_sessions (bool): This affects the behavior of the Command.msg method. If unset
-(default), calling self.msg(text) from the Command will always only send text to the Session that
-actually triggered this Command. If set however, self.msg(text) will send to all Sessions relevant
-to the object this Command sits on. Just which Sessions receives the text depends on the object and
-the server’s MULTISESSION_MODE.
-
-
You should also implement at least two methods, parse() and func() (You could also implement
-perm(), but that’s not needed unless you want to fundamentally change how access checks work).
-
-
at_pre_cmd() is called very first on the command. If this function returns anything that
-evaluates to True the command execution is aborted at this point.
-
parse() is intended to parse the arguments (self.args) of the function. You can do this in any
-way you like, then store the result(s) in variable(s) on the command object itself (i.e. on self).
-To take an example, the default mux-like system uses this method to detect “command switches” and
-store them as a list in self.switches. Since the parsing is usually quite similar inside a command
-scheme you should make parse() as generic as possible and then inherit from it rather than re-
-implementing it over and over. In this way, the default MuxCommand class implements a parse()
-for all child commands to use.
-
func() is called right after parse() and should make use of the pre-parsed input to actually
-do whatever the command is supposed to do. This is the main body of the command. The return value
-from this method will be returned from the execution as a Twisted Deferred.
-
at_post_cmd() is called after func() to handle eventual cleanup.
-
-
Finally, you should always make an informative doc
-string (__doc__) at the top of your
-class. This string is dynamically read by the Help System to create the help entry
-for this command. You should decide on a way to format your help and stick to that.
-
Below is how you define a simple alternative “smile” command:
-
fromevenniaimportCommand
-
-classCmdSmile(Command):
- """
- A smile command
-
- Usage:
- smile [at] [<someone>]
- grin [at] [<someone>]
-
- Smiles to someone in your vicinity or to the room
- in general.
-
- (This initial string (the __doc__ string)
- is also used to auto-generate the help
- for this command)
- """
-
- key="smile"
- aliases=["smile at","grin","grin at"]
- locks="cmd:all()"
- help_category="General"
-
- defparse(self):
- "Very trivial parser"
- self.target=self.args.strip()
-
- deffunc(self):
- "This actually does things"
- caller=self.caller
-
- ifnotself.targetorself.target=="here":
- string=f"{caller.key} smiles"
- else:
- target=caller.search(self.target)
- ifnottarget:
- return
- string=f"{caller.key} smiles at {target.key}"
-
- caller.location.msg_contents(string)
-
-
-
-
The power of having commands as classes and to separate parse() and func()
-lies in the ability to inherit functionality without having to parse every
-command individually. For example, as mentioned the default commands all
-inherit from MuxCommand. MuxCommand implements its own version of parse()
-that understands all the specifics of MUX-like commands. Almost none of the
-default commands thus need to implement parse() at all, but can assume the
-incoming string is already split up and parsed in suitable ways by its parent.
-
Before you can actually use the command in your game, you must now store it
-within a command set. See the Command Sets page.
The command parser is very general and does not require a space to end your command name. This means
-that the alias : to emote can be used like :smiles without modification. It also means
-getstone will get you the stone (unless there is a command specifically named getstone, then
-that will be used). If you want to tell the parser to require a certain separator between the
-command name and its arguments (so that getstone works but getstone gives you a ‘command not
-found’ error) you can do so with the arg_regex property.
-
The arg_regex is a raw regular expression string. The
-regex will be compiled by the system at runtime. This allows you to customize how the part
-immediately following the command name (or alias) must look in order for the parser to match for
-this command. Some examples:
-
-
commandnameargument (arg_regex=r"\s.+"): This forces the parser to require the command name
-to be followed by one or more spaces. Whatever is entered after the space will be treated as an
-argument. However, if you’d forget the space (like a command having no arguments), this would not
-match commandname.
-
commandname or commandnameargument (arg_regex=r"\s.+|$"): This makes both look and
-lookme work but lookme will not.
-
commandname/switchesarguments (arg_regex=r"(?:^(?:\s+|\/).*$)|^$". If you are using
-Evennia’s MuxCommand Command parent, you may wish to use this since it will allow /switches to
-work as well as having or not having a space.
-
-
The arg_regex allows you to customize the behavior of your commands. You can put it in the parent
-class of your command to customize all children of your Commands. However, you can also change the
-base default behavior for all Commands by modifying settings.COMMAND_DEFAULT_ARG_REGEX.
Normally you just use return in one of your Command class’ hook methods to exit that method. That
-will however still fire the other hook methods of the Command in sequence. That’s usually what you
-want but sometimes it may be useful to just abort the command, for example if you find some
-unacceptable input in your parse method. To exit the command this way you can raise
-evennia.InterruptCommand:
-
fromevenniaimportInterruptCommand
-
-classMyCommand(Command):
-
- # ...
-
- defparse(self):
- # ...
- # if this fires, `func()` and `at_post_cmd` will not
- # be called at all
- raiseInterruptCommand()
-
-
Sometimes you want to pause the execution of your command for a little while before continuing -
-maybe you want to simulate a heavy swing taking some time to finish, maybe you want the echo of your
-voice to return to you with an ever-longer delay. Since Evennia is running asynchronously, you
-cannot use time.sleep() in your commands (or anywhere, really). If you do, the entire game will
-be frozen for everyone! So don’t do that. Fortunately, Evennia offers a really quick syntax for
-making pauses in commands.
-
In your func() method, you can use the yield keyword. This is a Python keyword that will freeze
-the current execution of your command and wait for more before processing.
-
-
Note that you cannot just drop yield into any code and expect it to pause. Evennia will only
-pause for you if you yield inside the Command’s func() method. Don’t expect it to work anywhere
-else.
-
-
Here’s an example of a command using a small pause of five seconds between messages:
-
fromevenniaimportCommand
-
-classCmdWait(Command):
- """
- A dummy command to show how to wait
-
- Usage:
- wait
-
- """
-
- key="wait"
- locks="cmd:all()"
- help_category="General"
-
- deffunc(self):
- """Command execution."""
- self.msg("Starting to wait ...")
- yield5
- self.msg("... This shows after 5 seconds. Waiting ...")
- yield2
- self.msg("... And now another 2 seconds have passed.")
-
-
-
The important line is the yield5 and yield2 lines. It will tell Evennia to pause execution
-here and not continue until the number of seconds given has passed.
-
There are two things to remember when using yield in your Command’s func method:
-
-
The paused state produced by the yield is not saved anywhere. So if the server reloads in the
-middle of your command pausing, it will not resume when the server comes back up - the remainder
-of the command will never fire. So be careful that you are not freezing the character or account in
-a way that will not be cleared on reload.
-
If you use yield you may not also use return<values> in your func method. You’ll get an
-error explaining this. This is due to how Python generators work. You can however use a “naked”
-return just fine. Usually there is no need for func to return a value, but if you ever do need
-to mix yield with a final return value in the same func, look at twisted.internet.defer.returnV
-alue.
The yield keyword can also be used to ask for user input. Again you can’t
-use Python’s input in your command, for it would freeze Evennia for
-everyone while waiting for that user to input their text. Inside a Command’s
-func method, the following syntax can also be used:
-
answer=yield("Your question")
-
-
-
Here’s a very simple example:
-
classCmdConfirm(Command):
-
- """
- A dummy command to show confirmation.
-
- Usage:
- confirm
-
- """
-
- key="confirm"
-
- deffunc(self):
- answer=yield("Are you sure you want to go on?")
- ifanswer.strip().lower()in("yes","y"):
- self.msg("Yes!")
- else:
- self.msg("No!")
-
-
-
This time, when the user enters the ‘confirm’ command, she will be asked if she wants to go on.
-Entering ‘yes’ or “y” (regardless of case) will give the first reply, otherwise the second reply
-will show.
-
-
Note again that the yield keyword does not store state. If the game reloads while waiting for
-the user to answer, the user will have to start over. It is not a good idea to use yield for
-important or complex choices, a persistent EvMenu might be more appropriate in this case.
Note: This is an advanced topic. Skip it if this is your first time learning about commands.
-
There are several command-situations that are exceptional in the eyes of the server. What happens if
-the account enters an empty string? What if the ‘command’ given is infact the name of a channel the
-user wants to send a message to? Or if there are multiple command possibilities?
-
Such ‘special cases’ are handled by what’s called system commands. A system command is defined
-in the same way as other commands, except that their name (key) must be set to one reserved by the
-engine (the names are defined at the top of evennia/commands/cmdhandler.py). You can find (unused)
-implementations of the system commands in evennia/commands/default/system_commands.py. Since these
-are not (by default) included in any CmdSet they are not actually used, they are just there for
-show. When the special situation occurs, Evennia will look through all valid CmdSets for your
-custom system command. Only after that will it resort to its own, hard-coded implementation.
-
Here are the exceptional situations that triggers system commands. You can find the command keys
-they use as properties on evennia.syscmdkeys:
-
-
No input (syscmdkeys.CMD_NOINPUT) - the account just pressed return without any input. Default
-is to do nothing, but it can be useful to do something here for certain implementations such as line
-editors that interpret non-commands as text input (an empty line in the editing buffer).
-
Command not found (syscmdkeys.CMD_NOMATCH) - No matching command was found. Default is to
-display the “Huh?” error message.
-
Several matching commands where found (syscmdkeys.CMD_MULTIMATCH) - Default is to show a list of
-matches.
-
User is not allowed to execute the command (syscmdkeys.CMD_NOPERM) - Default is to display the
-“Huh?” error message.
-
Channel (syscmdkeys.CMD_CHANNEL) - This is a Channel name of a channel you are
-subscribing to - Default is to relay the command’s argument to that channel. Such commands are
-created by the Comm system on the fly depending on your subscriptions.
-
New session connection (syscmdkeys.CMD_LOGINSTART). This command name should be put in the
-settings.CMDSET_UNLOGGEDIN. Whenever a new connection is established, this command is always
-called on the server (default is to show the login screen).
-
-
Below is an example of redefining what happens when the account doesn’t provide any input (e.g. just
-presses return). Of course the new system command must be added to a cmdset as well before it will
-work.
-
fromevenniaimportsyscmdkeys,Command
-
- classMyNoInputCommand(Command):
- "Usage: Just press return, I dare you"
- key=syscmdkeys.CMD_NOINPUT
- deffunc(self):
- self.caller.msg("Don't just press return like that, talk to me!")
-
Normally Commands are created as fixed classes and used without modification. There are however
-situations when the exact key, alias or other properties is not possible (or impractical) to pre-
-code (Exits is an example of this).
-
To create a command with a dynamic call signature, first define the command body normally in a class
-(set your key, aliases to default values), then use the following call (assuming the command
-class you created is named MyCommand):
All keyword arguments you give to the Command constructor will be stored as a property on the
-command object. This will overload existing properties defined on the parent class.
-
Normally you would define your class and only overload things like key and aliases at run-time.
-But you could in principle also send method objects (like func) as keyword arguments in order to
-make your command completely customized at run-time.
The functionality of Exit objects in Evennia is not hard-coded in the engine. Instead
-Exits are normal typeclassed objects that auto-create a CmdSet on
-themselves when they load. This cmdset has a single dynamically created Command with the same
-properties (key, aliases and locks) as the Exit object itself. When entering the name of the exit,
-this dynamic exit-command is triggered and (after access checks) moves the Character to the exit’s
-destination.
-Whereas you could customize the Exit object and its command to achieve completely different
-behaviour, you will usually be fine just using the appropriate traverse_* hooks on the Exit
-object. But if you are interested in really changing how things work under the hood, check out
-evennia/objects/objects.py for how the Exit typeclass is set up.
Note: This is an advanced topic that can be skipped when first learning about Commands.
-
A Command class sitting on an object is instantiated once and then re-used. So if you run a command
-from object1 over and over you are in fact running the same command instance over and over (if you
-run the same command but sitting on object2 however, it will be a different instance). This is
-usually not something you’ll notice, since every time the Command-instance is used, all the relevant
-properties on it will be overwritten. But armed with this knowledge you can implement some of the
-more exotic command mechanism out there, like the command having a ‘memory’ of what you last entered
-so that you can back-reference the previous arguments etc.
-
-
Note: On a server reload, all Commands are rebuilt and memory is flushed.
Commands can also be created and added to a cmdset on the fly. Creating a class instance with a
-keyword argument, will assign that keyword argument as a property on this paricular command:
This will start the MyCommand with myvar and foo set as properties (accessable as self.myvar
-and self.foo). How they are used is up to the Command. Remember however the discussion from the
-previous section - since the Command instance is re-used, those properties will remain on the
-command as long as this cmdset and the object it sits is in memory (i.e. until the next reload).
-Unless myvar and foo are somehow reset when the command runs, they can be modified and that
-change will be remembered for subsequent uses of the command.
Note: This is an advanced topic mainly of interest to server developers.
-
Any time the user sends text to Evennia, the server tries to figure out if the text entered
-corresponds to a known command. This is how the command handler sequence looks for a logged-in user:
-
-
A user enters a string of text and presses enter.
-
The user’s Session determines the text is not some protocol-specific control sequence or OOB
-command, but sends it on to the command handler.
-
Evennia’s command handler analyzes the Session and grabs eventual references to Account and
-eventual puppeted Characters (these will be stored on the command object later). The caller
-property is set appropriately.
-
If input is an empty string, resend command as CMD_NOINPUT. If no such command is found in
-cmdset, ignore.
-
If command.key matches settings.IDLE_COMMAND, update timers but don’t do anything more.
-
The command handler gathers the CmdSets available to caller at this time:
-
-
The caller’s own currently active CmdSet.
-
CmdSets defined on the current account, if caller is a puppeted object.
-
CmdSets defined on the Session itself.
-
The active CmdSets of eventual objects in the same location (if any). This includes commands
-on Exits.
-
Sets of dynamically created System commands representing available
-Communications.
-
-
-
All CmdSets of the same priority are merged together in groups. Grouping avoids order-
-dependent issues of merging multiple same-prio sets onto lower ones.
-
All the grouped CmdSets are merged in reverse priority into one combined CmdSet according to
-each set’s merge rules.
-
Evennia’s command parser takes the merged cmdset and matches each of its commands (using its
-key and aliases) against the beginning of the string entered by caller. This produces a set of
-candidates.
-
The cmd parser next rates the matches by how many characters they have and how many percent
-matches the respective known command. Only if candidates cannot be separated will it return multiple
-matches.
-
-
If multiple matches were returned, resend as CMD_MULTIMATCH. If no such command is found in
-cmdset, return hard-coded list of matches.
-
If no match was found, resend as CMD_NOMATCH. If no such command is found in cmdset, give
-hard-coded error message.
-
-
-
If a single command was found by the parser, the correct command object is plucked out of
-storage. This usually doesn’t mean a re-initialization.
-
It is checked that the caller actually has access to the command by validating the lockstring
-of the command. If not, it is not considered as a suitable match and CMD_NOMATCH is triggered.
-
If the new command is tagged as a channel-command, resend as CMD_CHANNEL. If no such command
-is found in cmdset, use hard-coded implementation.
-
Assign several useful variables to the command instance (see previous sections).
-
Call at_pre_command() on the command instance.
-
Call parse() on the command instance. This is fed the remainder of the string, after the name
-of the command. It’s intended to pre-parse the string into a form useful for the func() method.
-
Call func() on the command instance. This is the functional body of the command, actually
-doing useful things.
The return value of Command.func() is a Twisted
-deferred.
-Evennia does not use this return value at all by default. If you do, you must
-thus do so asynchronously, using callbacks.
-
# in command class func()
- defcallback(ret,caller):
- caller.msg("Returned is %s"%ret)
- deferred=self.execute_command("longrunning")
- deferred.addCallback(callback,self.caller)
-
-
-
This is probably not relevant to any but the most advanced/exotic designs (one might use it to
-create a “nested” command structure for example).
-
The save_for_next class variable can be used to implement state-persistent commands. For example
-it can make a command operate on “it”, where it is determined by what the previous command operated
-on.
Apart from moving around in the game world and talking, players might need other forms of
-communication. This is offered by Evennia’s Comm system. Stock evennia implements a ‘MUX-like’
-system of channels, but there is nothing stopping you from changing things to better suit your
-taste.
-
Comms rely on two main database objects - Msg and Channel. There is also the TempMsg which
-mimics the API of a Msg but has no connection to the database.
The Msg object is the basic unit of communication in Evennia. A message works a little like an
-e-mail; it always has a sender (a Account) and one or more recipients. The recipients
-may be either other Accounts, or a Channel (see below). You can mix recipients to send the message
-to both Channels and Accounts if you like.
-
Once created, a Msg is normally not changed. It is peristently saved in the database. This allows
-for comprehensive logging of communications. This could be useful for allowing senders/receivers to
-have ‘mailboxes’ with the messages they want to keep.
senders - this is a reference to one or many Account or Objects (normally
-Characters) sending the message. This could also be an External Connection such as a message
-coming in over IRC/IMC2 (see below). There is usually only one sender, but the types can also be
-mixed in any combination.
-
receivers - a list of target Accounts, Objects (usually Characters) or
-Channels to send the message to. The types of receivers can be mixed in any combination.
-
header - this is a text field for storing a title or header for the message.
hide_from - this can optionally hold a list of objects, accounts or channels to hide this Msg
-from. This relationship is stored in the database primarily for optimization reasons, allowing for
-quickly post-filter out messages not intended for a given target. There is no in-game methods for
-setting this, it’s intended to be done in code.
-
-
You create new messages in code using evennia.create_message (or
-evennia.utils.create.create_message.)
evennia.comms.models also has TempMsg which mimics the API of Msg but is not connected to the
-database. TempMsgs are used by Evennia for channel messages by default. They can be used for any
-system expecting a Msg but when you don’t actually want to save anything.
Channels are Typeclassed entities, which mean they can be easily extended and their
-functionality modified. To change which channel typeclass Evennia uses, change
-settings.BASE_CHANNEL_TYPECLASS.
-
Channels act as generic distributors of messages. Think of them as “switch boards” redistributing
-Msg or TempMsg objects. Internally they hold a list of “listening” objects and any Msg (or
-TempMsg) sent to the channel will be distributed out to all channel listeners. Channels have
-Locks to limit who may listen and/or send messages through them.
-
The sending of text to a channel is handled by a dynamically created Command that
-always have the same name as the channel. This is created for each channel by the global
-ChannelHandler. The Channel command is added to the Account’s cmdset and normal command locks are
-used to determine which channels are possible to write to. When subscribing to a channel, you can
-then just write the channel name and the text to send.
-
The default ChannelCommand (which can be customized by pointing settings.CHANNEL_COMMAND_CLASS to
-your own command), implements a few convenient features:
-
-
It only sends TempMsg objects. Instead of storing individual entries in the database it instead
-dumps channel output a file log in server/logs/channel_<channelname>.log. This is mainly for
-practical reasons - we find one rarely need to query individual Msg objects at a later date. Just
-stupidly dumping the log to a file also means a lot less database overhead.
-
It adds a /history switch to view the 20 last messages in the channel. These are read from the
-end of the log file. One can also supply a line number to start further back in the file (but always
-20 entries at a time). It’s used like this:
-
> public/history
- > public/history 35
-
-
-
-
-
There are two default channels created in stock Evennia - MudInfo and Public. MudInfo
-receives server-related messages meant for Admins whereas Public is open to everyone to chat on
-(all new accounts are automatically joined to it when logging in, it is useful for asking
-questions). The default channels are defined by the DEFAULT_CHANNELS list (see
-evennia/settings_default.py for more details).
-
You create new channels with evennia.create_channel (or evennia.utils.create.create_channel).
-
In code, messages are sent to a channel using the msg or tempmsg methods of channels:
The argument msgobj can be either a string, a previously constructed Msg or a TempMsg - in the
-latter cases all the following keywords are ignored since the message objects already contains all
-this information. If msgobj is a string, the other keywords are used for creating a new Msg or
-TempMsg on the fly, depending on if persistent is set or not. By default, a TempMsg is emitted
-for channel communication (since the default ChannelCommand instead logs to a file).
-
# assume we have a 'sender' object and a channel named 'mychan'
-
- # manually sending a message to a channel
- mychan.msg("Hello!",senders=[sender])
-
When you first connect to your game you are greeted by Evennia’s default connection screen.
-
==============================================================
- Welcome to Evennia, version Beta-ra4d24e8a3cab+!
-
- If you have an existing account, connect to it by typing:
- connect <username> <password>
- If you need to create an account, type (without the <>'s):
- create <username> <password>
-
- If you have spaces in your username, enclose it in quotes.
- Enter help for more info. look will re-show this screen.
-==============================================================
-
-
-
Effective, but not very exciting. You will most likely want to change this to be more unique for
-your game. This is simple:
Evennia will look into this module and locate all globally defined strings in it. These strings
-are used as the text in your connection screen and are shown to the user at startup. If more than
-one such string/screen is defined in the module, a random screen will be picked from among those
-available.
You can also customize the Commands available to use while the connection screen is
-shown (connect, create etc). These commands are a bit special since when the screen is running
-the account is not yet logged in. A command is made available at the login screen by adding them to
-UnloggedinCmdSet in mygame/commands/default_cmdset.py. See Commands and the
-tutorial section on how to add new commands to a default command set.
One of the advantages of Evennia over traditional MUSH development systems is that Evennia is
-capable of integrating into enterprise level integration environments and source control. Because of
-this, it can also be the subject of automation for additional convenience, allowing a more
-streamlined development environment.
Continuous Integration (CI) is a development
-practice that requires developers to integrate code into a shared repository several times a day.
-Each check-in is then verified by an automated build, allowing teams to detect problems early.
-
For Evennia, continuous integration allows an automated build process to:
-
-
Pull down a latest build from Source Control.
-
Run migrations on the backing SQL database.
-
Automate additional unique tasks for that project.
Templates are fancy objects in TeamCity that allow an administrator to define build steps that are
-shared between one or more build projects. Assigning a VCS Root (Source Control) is unnecessary at
-this stage, primarily you’ll be worrying about the build steps and your default parameters (both
-visible on the tabs to the left.)
In this template, you’ll be outlining the steps necessary to build your specific game. (A number of
-sample scripts are provided under this section below!) Click Build Steps and prepare your general
-flow. For this example, we will be doing a few basic example steps:
We do this to update ports or other information that make your production environment unique
-from your development environment.
-
-
-
Making migrations and migrating the game database.
-
Publishing the game files.
-
Reloading the server.
-
-
For each step we’ll being use the “Command Line Runner” (a fancy name for a shell script executor).
-
-
Create a build step with the name: Transform Configuration
-
For the script add:
-
#!/bin/bash
-# Replaces the game configuration with one
-# appropriate for this deployment.
-
-CONFIG="%system.teamcity.build.checkoutDir%/server/conf/settings.py"
-MYCONF="%system.teamcity.build.checkoutDir%/server/conf/my.cnf"
-
-sed -e 's/TELNET_PORTS = [4000]/TELNET_PORTS = [%game.ports%]/g'"$CONFIG" > "$CONFIG".tmp && mv
-
If you look at the parameters side of the page after saving this script, you’ll notice that some new
-parameters have been populated for you. This is because we’ve included new teamcity configuration
-parameters that are populated when the build itself is ran. When creating projects that inherit this
-template, we’ll be able to fill in or override those parameters for project-specific configuration.
-
-
Go ahead and create another build step called “Make Database Migration”
-
-
If you’re using SQLLite on your game, it will be prudent to change working directory on this
-step to: %game.dir%
-
-
-
In this script include:
-
#!/bin/bash
-# Update the DB migration
-
-LOGDIR="server/logs"
-
-. %evenv.dir%/bin/activate
-
-# Check that the logs directory exists.
-if[ ! -d "$LOGDIR"];then
- # Control will enter here if $LOGDIR doesn't exist.
- mkdir "$LOGDIR"
-fi
-
-evennia makemigrations
-
-
-
-
Create yet another build step, this time named: “Execute Database Migration”:
-
-
If you’re using SQLLite on your game, it will be prudent to change working directory on this
-step to: %game.dir%
-
#!/bin/bash
-# Apply the database migration.
-
-LOGDIR="server/logs"
-
-. %evenv.dir%/bin/activate
-
-# Check that the logs directory exists.
-if[ ! -d "$LOGDIR"];then
- # Control will enter here if $LOGDIR doesn't exist.
- mkdir "$LOGDIR"
-fi
-
-evennia migrate
-
-
-
-
-
-
-
Our next build step is where we actually publish our build. Up until now, all work on game has been
-done in a ‘work’ directory on TeamCity’s build agent. From that directory we will now copy our files
-to where our game actually exists on the local server.
-
-
Create a new build step called “Publish Build”:
-
-
If you’re using SQLLite on your game, be sure to order this step ABOVE the Database Migration
-steps. The build order will matter!
-
#!/bin/bash
-# Publishes the build to the proper build directory.
-
-DIRECTORY="%game.dir%"
-
-if[ ! -d "$DIRECTORY"];then
- # Control will enter here if $DIRECTORY doesn't exist.
- mkdir "$DIRECTORY"
-fi
-
-# Copy all the files.
-cp -ruv %teamcity.build.checkoutDir%/* "$DIRECTORY"
-chmod -R 775"$DIRECTORY"
-
-
-
-
-
-
-
Finally the last script will reload our game for us.
-
-
Create a new script called “Reload Game”:
-
-
The working directory on this build step will be: %game.dir%
-
#!/bin/bash
-# Apply the database migration.
-
-LOGDIR="server/logs"
-PIDDIR="server/server.pid"
-
-. %evenv.dir%/bin/activate
-
-# Check that the logs directory exists.
-if[ ! -d "$LOGDIR"];then
- # Control will enter here if $LOGDIR doesn't exist.
- mkdir "$LOGDIR"
-fi
-
-# Check that the server is running.
-if[ -d "$PIDDIR"];then
- # Control will enter here if the game is running.
- evennia reload
-fi
-
-
-
-
-
-
-
Now the template is ready for use! It would be useful this time to revisit the parameters page and
-set the evenv parameter to the directory where your virtualenv exists: IE “/srv/mush/evenv”.
Now it’s time for the last few steps to set up a CI environment.
-
-
Return to the Evennia Project overview/administration page.
-
Create a new Sub-Project called “Production”
-
-
This will be the category that holds our actual game.
-
-
-
Create a new Build Configuration in Production with the name of your MUSH.
-
-
Base this configuration off of the continuous-integration template we made earlier.
-
-
-
In the build configuration, enter VCS roots and create a new VCS root that points to the
-branch/version control that you are using.
-
Go to the parameters page and fill in the undefined parameters for your specific configuration.
-
If you wish for the CI to run every time a commit is made, go to the VCS triggers and add one for
-“On Every Commit”.
-
-
And you’re done! At this point, you can return to the project overview page and queue a new build
-for your game. If everything was set up correctly, the build will complete successfully. Additional
-build steps could be added or removed at this point, adding some features like Unit Testing or more!
This system is still WIP and many things are bound to change!
-
-
Contributing to the docs is is like contributing to the rest of Evennia: Check out the branch of Evennia
-you want to edit the documentation for. Create your own work-branch, make your changes to files
-in evennia/docs/source/ and make a PR for it!
-
The documentation source files are *.md (Markdown) files found in evennia/docs/source/.
-Markdown files are simple text files that can be edited with a normal text editor. They can also
-contain raw HTML directives (but that is very rarely needed). They use
-the Markdown syntax with MyST extensions.
-
-
Important
-
You do not need to be able to test/build the docs locally to contribute a documentation PR.
-We’ll resolve any issues when we merge and build documentation. If you still want to build
-the docs for yourself, instructions are at the end of this document.
The sources are organized into several rough categories, with only a few administrative documents
-at the root of evennia/docs/source/. The folders are named in singular form since they will
-primarily be accessed as link refs (e.g. Component/Accounts)
-
-
source/Components/ are docs describing separate Evennia building blocks, that is, things
-that you can import and use. This extends and elaborates on what can be found out by reading
-the api docs themselves. Example are documentation for Accounts, Objects and Commands.
-
source/Concepts/ describes how larger-scale features of Evennia hang together - things that
-can’t easily be broken down into one isolated component. This can be general descriptions of
-how Models and Typeclasses interact to the path a message takes from the client to the server
-and back.
-
source/Setup/ holds detailed docs on installing, running and maintaining the Evennia server and
-the infrastructure around it.
-
source/Coding/ has help on how to interact with, use and navigate the Evennia codebase itself.
-This also has non-Evennia-specific help on general development concepts and how to set up a sane
-development environment.
-
source/Contribs/ holds documentation specifically for packages in the evennia/contribs/ folder.
-Any contrib-specific tutorials will be found here instead of in Howtos
-
source/Howtos/ holds docs that describe how to achieve a specific goal, effect or
-result in Evennia. This is often on a tutorial or FAQ form and will refer to the rest of the
-documentation for further reading.
-
-
source/Howtos/Starting/ holds all documents part of the initial tutorial sequence.
-
-
-
-
Other files and folders:
-
-
source/api/ contains the auto-generated API documentation as .rst files. Don’t edit these
-files manually, your changes will be lost. To refer to these files, use api: followed by
-the Python path, for example [rpsystemcontrib](evennia.contrib.rpsystem).
-
source/_templates and source/_static should not be modified unless adding a new doc-page
-feature or changing the look of the HTML documentation.
-
conf.py holds the Sphinx configuration. It should usually not be modified except to update
-the Evennia version on a new branch.
The format used for Evennia’s docs is Markdown (Commonmark). While markdown
-supports a few alternative forms for some of these, we try to stick to the below forms for consistency.
We use # to indicate sections/headings. The more # the more of a sub-heading it is (will get
-smaller and smaller font).
-
-
#Heading
-
##SubHeading
-
###SubSubHeading
-
####SubSubSubHeading
-
-
-
Don’t use the same heading/subheading name more than once in one page. While Markdown
-does not prevent it, it will make it impossible to refer to that heading uniquely.
-The Evennia documentation preparser will detect this and give you an error.
A blockquote will create an indented block. It’s useful for emphasis and is
-added by starting one or more lines with >. For ‘notes’ you can also use
-an explicit Note.
Most links will be to other pages of the documentation or to Evennia’s API docs. Each document
-heading can be referenced. The reference always starts with #. The heading-name is always
-given in lowercase and ignores any non-letters. Spaces in the heading title are replaced with
-a single dash -.
-
As an example, let’s assume the following is the contents of a file Menu-stuff.md:
-
# Menu items
-
-Sometext...
-
-## A yes/no? example
-
-Somemoretext...
-
-
-
-
From inside the same file you can refer to each heading as
It’s fine to not include the .md file ending in the reference. The Evennia doc-build process
-will correct for this (and also insert any needed relative paths in the reference).
The documentation contains auto-generated documentation for all of Evennia’s source code. You
-can direct the reader to the sources by just giving the python-path to the location of the
-resource under the evennia/ repository:
Note that you can’t refer to files in the mygame folder this way. The game folder is generated
-dynamically and is not part of the api docs. Refer to the parent classes in evennia where possible.
These are links to resources outside of the documentation. We also provide some convenient shortcuts.
-
-
[linkname](https://evennia.com) - link to an external website.
-
[linkname](https://github.com/evennia/evennia/blob/master/evennia/objects/objects.py) - this is a shortcut to point to a location in the
-official Evennia repository on Github. Note that you must use / and give the full file name. By
-default this is code in the master branch.
-
[linkname](https://github.com/evennia/evennia/issues/new/choose) - this is a shortcut to the Evennia github issue-creation page.
-
-
-
Note that if you want to refer to code, it’s usually better to link to the API as
-described above.
Urls can get long and if you are using the same url/reference in many places it can get a
-little cluttered. So you can also put the url as a ‘footnote’ at the end of your document.
-You can then refer to it by putting your reference within square brackets []. Here’s an example:
As seen, the Markdown syntax can be pretty sloppy (columns don’t need to line up) as long as you
-include the heading separators and make sure to add the correct number of | on every line.
It’s common to want to mark something to be displayed verbatim - just as written - without any
-Markdown parsing. In running text, this is done using backticks (`), like `verbatim text` becomes
-verbatimtext.
-
If you want to put the verbatim text on its own line, you can do so easily by simply indenting
-it 4 spaces (add empty lines on each side for readability too):
A special ‘verbatim’ case is code examples - we want them to get code-highlighting for readability.
-This is done by using the triple-backticks and specify which language we use:
Markdown is easy to read and use. But while it does most of what we need, there are some things it’s
-not quite as expressive as it needs to be. For this we use extended MyST syntax. This is
-on the form
This kind of note may pop more than doing a >Note:....
-
```{note}
-
-This is some noteworthy content that stretches over more than one line to show how the content indents.
-Also the important/warning notes indents like this.
-
-```
-
-
-
-
Note
-
This is some noteworthy content that stretches over more than one line to show how the content indents.
-Also the important/warning notes indents like this.
This will display an informative sidebar that floats to the side of regular content. This is useful
-for example to remind the reader of some concept relevant to the text.
-
```{sidebar} Things to remember
-
-- There can be bullet lists
-- in here.
-
-Separate sections with
-
-an empty line.
-```
-
-
-
-
Hint: If wanting to make sure to have the next header appear on a row of its own (rather than
-squeezed to the left of the sidebar), one can embed a plain HTML string in the markdown like so:
The regular Markdown Python codeblock is usually enough but for more direct control over the style, one
-can also use the {code-block} directive that takes a set of additional :options::
-
```{code-block} python
-:linenos:
-:emphasize-lines: 1-2,8
-:caption: An example code block
-:name: A full code block example
-
-from evennia import Command
-class CmdEcho(Command):
- """
- Usage: echo <arg>
- """
- key = "echo"
- def func(self):
- self.caller.msg(self.args.strip())
-```
-
Here, :linenos: turns on line-numbers and :emphasize-lines: allows for emphasizing certain lines
-in a different color. The :caption: shows an instructive text and :name: is used to reference
-this
-block through the link that will appear (so it should be unique for a given document).
The source code docstrings will be parsed as Markdown. When writing a module docstring, you can use Markdown formatting,
-including header levels down to 4th level (####SubSubSubHeader). After the module documentation it’s
-a good idea to end with four dashes ----. This will create a visible line between the documentation and the
-class/function docs to follow.
-
All non-private classes, methods and functions must have a Google-style docstring, as per the
-[Evennia coding style guidelines][github:evennia/CODING_STYLE.md]. This will then be correctly formatted
-into pretty api docs.
Evennia leverages Sphinx with the MyST extension, which allows us
-to write our docs in light-weight Markdown (more specifically CommonMark, like on github)
-rather than Sphinx’ normal ReST syntax. The MyST parser allows for some extra syntax to
-make us able to express more complex displays than plain Markdown can.
-
For autodoc-generation generation, we use the sphinx-napoleon
-extension to understand our friendly Google-style docstrings used in classes and functions etc.
The sources in evennia/docs/source/ are built into a documentation using the
-Sphinx static generator system. To do this locally you need to use a
-system with make (Linux/Unix/Mac or Windows-WSL). Lacking
-that, you could in principle also run the sphinx build-commands manually - read
-the evennia/docs/Makefile to see which commands are run by the make-commands
-referred to in this document.
-
You don’t necessarily have to build the docs locally to contribute. Markdown is
-not hard and is very readable on its raw text-form.
-
You can furthermore get a good feel for how things will look using a
-Markdown-viewer like Grip. Editors like ReText or IDE’s like
-PyCharm also have native Markdown previews. Building the docs locally is
-however the only way to make sure the outcome is exactly as you expect. The process
-will also find any mistakes you made, like making a typo in a link.
This is the fastest way to compile and view your changes. It will only build
-the main documentation pages and not the API auto-docs or versions. All is
-done in your terminal/console.
-
-
(Optional, but recommended): Activate a virtualenv with Python 3.7.
-
cd to into the evennia/docs folder.
-
Install the documentation-build requirements:
-
makeinstall
-or
-pipinstall-rrequirements.txt
-
-
-
-
Next, build the html-based documentation (re-run this in the future to build your changes):
-
makequick
-
-
-
-
Note any errors from files you have edited.
-
The html-based documentation will appear in the new
-folder evennia/docs/build/html/.
-
Use a web browser to open file://<path-to-folder>/evennia/docs/build/html/index.html and view
-the docs. Note that you will get errors if clicking a link to the auto-docs, because you didn’t
-build them!
The full documentation includes both the doc pages and the API documentation
-generated from the Evennia source. For this you must install Evennia and
-initialize a new game with a default database (you don’t need to have any server
-running)
-
-
It’s recommended that you use a virtualenv. Install your cloned version of Evennia into
-by pointing to the repo folder (the one containing /docs):
-
pipinstall-eevennia
-
-
-
-
Make sure you are in the parent folder containing your evennia/ repo (so two levels
-up from evennia/docs/).
-
Create a new game folder called exactly gamedir at the same level as your evennia
-repo with
-
evennia--initgamedir
-
-
-
-
Then cd into it and create a new, empty database. You don’t need to start the
-game or do any further changes after this.
-
evenniamigrate
-
-
-
-
This is how the structure should look at this point:
If you for some reason want to use another location of your gamedir/, or want it
-named something else (maybe you already use the name ‘gamedir’ for your development …),
-you can do so by setting the EVGAMEDIR environment variable to the absolute path
-of your alternative game dir. For example:
The full Evennia documentation contains docs from many Evennia
-versions, old and new. This is done by pulling documentation from Evennia’s old release
-branches and building them all so readers can choose which one to view. Only
-specific official Evennia branches will be built, so you can’t use this to
-build your own testing branch.
-
-
All local changes must have been committed to git first, since the versioned
-docs are built by looking at the git tree.
-
To build for local checking, run (mv stands for “multi-version”):
-
makemv-local
-
-
-
-
-
This is as close to the ‘real’ version of the docs as you can get locally. The different versions
-will be found under evennia/docs/build/versions/. During deploy a symlink latest will point
-to the latest version of the docs.
Releasing the official docs requires git-push access the the Evennia gh-pages branch
-on github. So there is no risk of you releasing your local changes accidentally.
-
-
To deploy docs in two steps
-
makemv-local
-makedeploy
-
-
-
-
If you know what you are doing you can also do build + deploy in one step:
Even if you are not keen on working on the server code yourself, just spreading the word is a big
-help - it will help attract more people which leads to more feedback, motivation and interest.
-Consider writing about Evennia on your blog or in your favorite (relevant) forum. Write a review
-somewhere (good or bad, we like feedback either way). Rate it on places like ohloh. Talk
-about it to your friends … that kind of thing.
The best way to support Evennia is to become an Evennia patron. Evennia is a free,
-open-source project and any monetary donations you want to offer are completely voluntary. See it as
-a way of announcing that you appreciate the work done - a tip of the hat! A patron donates a
-(usually small) sum every month to show continued support. If this is not your thing you can also
-show your appreciation via a one-time donation (this is a PayPal link but you don’t need
-PayPal yourself).
Evennia depends heavily on good documentation and we are always looking for extra eyes and hands to
-improve it. Even small things such as fixing typos are a great help!
-
The documentation is a wiki and as long as you have a GitHub account you can edit it. It can be a
-good idea to discuss in the chat or forums if you want to add new pages/tutorials. Otherwise, it
-goes a long way just pointing out wiki errors so we can fix them (in an Issue or just over
-chat/forum).
We always need more eyes and hands on the code. Even if you don’t feel confident with tackling a
-[bug or feature][issues], just correcting typos, adjusting formatting or simply using the thing
-and reporting when stuff doesn’t make sense helps us a lot.
-
The most elegant way to contribute code to Evennia is to use GitHub to create a fork of the
-Evennia repository and make your changes to that. Refer to the [Forking Evennia](Version-
-Control#forking-evennia) version
-control instructions for detailed instructions.
-
Once you have a fork set up, you can not only work on your own game in a separate branch, you can
-also commit your fixes to Evennia itself. Make separate branches for all Evennia additions you do -
-don’t edit your local master or develop branches directly. It will make your life a lot easier.
-If you have a change that you think is suitable for the main Evennia repository, you issue a [Pull
-Request][pullrequest]. This will let Evennia devs know you have stuff to share. Bug fixes should
-generally be done against the master branch of Evennia, while new features/contribs should go into
-the develop branch. If you are unsure, just pick one and we’ll figure it out.
To help with Evennia development it’s recommended to do so using a fork repository as described
-above. But for small, well isolated fixes you are also welcome to submit your suggested Evennia
-fixes/addendums as a [patch][patch].
-
You can include your patch in an Issue or a Mailing list post. Please avoid pasting the full patch
-text directly in your post though, best is to use a site like Pastebin and
-just supply the link.
While Evennia’s core is pretty much game-agnostic, it also has a contrib/ directory. The contrib
-directory contains game systems that are specialized or useful only to certain types of games. Users
-are welcome to contribute to the contrib/ directory. Such contributions should always happen via a
-Forked repository as described above.
-
-
If you are unsure if your idea/code is suitable as a contrib, ask the devs before putting any
-work into it. This can also be a good idea in order to not duplicate efforts. This can also act as
-a check that your implementation idea is sound. We are, for example, unlikely to accept contribs
-that require large modifications of the game directory structure.
-
If your code is intended primarily as an example or shows a concept/principle rather than a
-working system, it is probably not suitable for contrib/. You are instead welcome to use it as
-part of a [new tutorial][tutorials]!
-
The code should ideally be contained within a single Python module. But if the contribution is
-large this may not be practical and it should instead be grouped in its own subdirectory (not as
-loose modules).
-
The contribution should preferably be isolated (only make use of core Evennia) so it can easily be
-dropped into use. If it does depend on other contribs or third-party modules, these must be clearly
-documented and part of the installation instructions.
-
The code itself should follow Evennia’s [Code style guidelines][codestyle].
-
The code must be well documented as described in our documentation style
-guide. Expect that your
-code will be read and should be possible to understand by others. Include comments as well as a
-header in all modules. If a single file, the header should include info about how to include the
-contrib in a game (installation instructions). If stored in a subdirectory, this info should go into
-a new README.md file within that directory.
-
Within reason, your contribution should be designed as genre-agnostic as possible. Limit the
-amount of game-style-specific code. Assume your code will be applied to a very different game than
-you had in mind when creating it.
-
To make the licensing situation clear we assume all contributions are released with the same
-license as Evennia. If this is not possible for some reason, talk to us and we’ll
-handle it on a case-by-case basis.
-
Your contribution must be covered by unit tests. Having unit tests will both help
-make your code more stable and make sure small changes does not break it without it being noticed,
-it will also help us test its functionality and merge it quicker. If your contribution is a single
-module, you can add your unit tests to evennia/contribs/tests.py. If your contribution is bigger
-and in its own sub-directory you could just put the tests in your own tests.py file (Evennia will
-find it automatically).
-
Merging of your code into Evennia is not guaranteed. Be ready to receive feedback and to be asked
-to make corrections or fix bugs. Furthermore, merging a contrib means the Evennia project takes on
-the responsibility of maintaining and supporting it. For various reasons this may be deemed to be
-beyond our manpower. However, if your code were to not be accepted for merger for some reason, we
-will instead add a link to your online repository so people can still find and use your work if they
-want.
This tutorial is moderately difficult in content. You might want to be familiar and at ease with
-some Python concepts (like properties) and possibly Django concepts (like queries), although this
-tutorial will try to walk you through the process and give enough explanations each time. If you
-don’t feel very confident with math, don’t hesitate to pause, go to the example section, which shows
-a tiny map, and try to walk around the code or read the explanation.
-
Evennia doesn’t have a coordinate system by default. Rooms and other objects are linked by location
-and content:
-
-
An object can be in a location, that is, another object. Like an exit in a room.
-
An object can access its content. A room can see what objects uses it as location (that would
-include exits, rooms, characters and so on).
-
-
This system allows for a lot of flexibility and, fortunately, can be extended by other systems.
-Here, I offer you a way to add coordinates to every room in a way most compliant with Evennia
-design. This will also show you how to use coordinates, find rooms around a given point for
-instance.
The first concept might be the most surprising at first glance: we will create coordinates as
-tags.
-
-
Why not attributes, wouldn’t that be easier?
-
-
It would. We could just do something like room.db.x=3. The advantage of using tags is that it
-will be easy and effective to search. Although this might not seem like a huge advantage right now,
-with a database of thousands of rooms, it might make a difference, particularly if you have a lot of
-things based on coordinates.
-
Rather than giving you a step-by-step process, I’ll show you the code. Notice that we use
-properties to easily access and update coordinates. This is a Pythonic approach. Here’s our first
-Room class, that you can modify in typeclasses/rooms.py:
-
# in typeclasses/rooms.py
-
-fromevenniaimportDefaultRoom
-
-classRoom(DefaultRoom):
- """
- Rooms are like any Object, except their location is None
- (which is default). They also use basetype_setup() to
- add locks so they cannot be puppeted or picked up.
- (to change that, use at_object_creation instead)
-
- See examples/object.py for a list of
- properties and methods available on all Objects.
- """
-
- @property
- defx(self):
- """Return the X coordinate or None."""
- x=self.tags.get(category="coordx")
- returnint(x)ifisinstance(x,str)elseNone
-
- @x.setter
- defx(self,x):
- """Change the X coordinate."""
- old=self.tags.get(category="coordx")
- ifoldisnotNone:
- self.tags.remove(old,category="coordx")
- ifxisnotNone:
- self.tags.add(str(x),category="coordx")
-
- @property
- defy(self):
- """Return the Y coordinate or None."""
- y=self.tags.get(category="coordy")
- returnint(y)ifisinstance(y,str)elseNone
-
- @y.setter
- defy(self,y):
- """Change the Y coordinate."""
- old=self.tags.get(category="coordy")
- ifoldisnotNone:
- self.tags.remove(old,category="coordy")
- ifyisnotNone:
- self.tags.add(str(y),category="coordy")
-
- @property
- defz(self):
- """Return the Z coordinate or None."""
- z=self.tags.get(category="coordz")
- returnint(z)ifisinstance(z,str)elseNone
-
- @z.setter
- defz(self,z):
- """Change the Z coordinate."""
- old=self.tags.get(category="coordz")
- ifoldisnotNone:
- self.tags.remove(old,category="coordz")
- ifzisnotNone:
- self.tags.add(str(z),category="coordz")
-
-
-
If you aren’t familiar with the concept of properties in Python, I encourage you to read a good
-tutorial on the subject. This article on Python properties
-is well-explained and should help you understand the idea.
-
Let’s look at our properties for x. First of all is the read property.
-
@property
- defx(self):
- """Return the X coordinate or None."""
- x=self.tags.get(category="coordx")
- returnint(x)ifisinstance(x,str)elseNone
-
-
-
What it does is pretty simple:
-
-
It gets the tag of category "coordx". It’s the tag category where we store our X coordinate.
-The tags.get method will return None if the tag can’t be found.
-
We convert the value to an integer, if it’s a str. Remember that tags can only contain str,
-so we’ll need to convert it.
-
-
-
I thought tags couldn’t contain values?
-
-
Well, technically, they can’t: they’re either here or not. But using tag categories, as we have
-done, we get a tag, knowing only its category. That’s the basic approach to coordinates in this
-tutorial.
-
Now, let’s look at the method that will be called when we wish to set x in our room:
-
@x.setter
- defx(self,x):
- """Change the X coordinate."""
- old=self.tags.get(category="coordx")
- ifoldisnotNone:
- self.tags.remove(old,category="coordx")
- ifxisnotNone:
- self.tags.add(str(x),category="coordx")
-
-
-
-
First, we remove the old X coordinate, if it exists. Otherwise, we’d end up with two tags in our
-room with “coordx” as their category, which wouldn’t do at all.
-
Then we add the new tag, giving it the proper category.
-
-
-
Now what?
-
-
If you add this code and reload your game, once you’re logged in with a character in a room as its
-location, you can play around:
It can help in shaping a truly logical world, in its geography, at least.
-
It can allow to look for specific rooms at given coordinates.
-
It can be good in order to quickly find the rooms around a location.
-
It can even be great in path-finding (finding the shortest path between two rooms).
-
-
So far, our coordinate system can help with 1., but not much else. Here are some methods that we
-could add to the Room typeclass. These methods will just be search methods. Notice that they are
-class methods, since we want to get rooms.
First, a simple one: how to find a room at a given coordinate? Say, what is the room at X=0, Y=0,
-Z=0?
-
classRoom(DefaultRoom):
- # ...
- @classmethod
- defget_room_at(cls,x,y,z):
- """
- Return the room at the given location or None if not found.
-
- Args:
- x (int): the X coord.
- y (int): the Y coord.
- z (int): the Z coord.
-
- Return:
- The room at this location (Room) or None if not found.
-
- """
- rooms=cls.objects.filter(
- db_tags__db_key=str(x),db_tags__db_category="coordx").filter(
- db_tags__db_key=str(y),db_tags__db_category="coordy").filter(
- db_tags__db_key=str(z),db_tags__db_category="coordz")
- ifrooms:
- returnrooms[0]
-
- returnNone
-
-
-
This solution includes a bit of Django
-queries.
-Basically, what we do is reach for the object manager and search for objects with the matching tags.
-Again, don’t spend too much time worrying about the mechanism, the method is quite easy to use:
-
Room.get_room_at(5,2,-3)
-
-
-
Notice that this is a class method: you will call it from Room (the class), not an instance.
-Though you still can:
Here’s another useful method that allows us to look for rooms around a given coordinate. This is
-more advanced search and doing some calculation, beware! Look at the following section if you’re
-lost.
-
frommathimportsqrt
-
-classRoom(DefaultRoom):
-
- # ...
-
- @classmethod
- defget_rooms_around(cls,x,y,z,distance):
- """
- Return the list of rooms around the given coordinates.
-
- This method returns a list of tuples (distance, room) that
- can easily be browsed. This list is sorted by distance (the
- closest room to the specified position is always at the top
- of the list).
-
- Args:
- x (int): the X coord.
- y (int): the Y coord.
- z (int): the Z coord.
- distance (int): the maximum distance to the specified position.
-
- Returns:
- A list of tuples containing the distance to the specified
- position and the room at this distance. Several rooms
- can be at equal distance from the position.
-
- """
- # Performs a quick search to only get rooms in a square
- x_r=list(reversed([str(x-i)foriinrange(0,distance+1)]))
- x_r+=[str(x+i)foriinrange(1,distance+1)]
- y_r=list(reversed([str(y-i)foriinrange(0,distance+1)]))
- y_r+=[str(y+i)foriinrange(1,distance+1)]
- z_r=list(reversed([str(z-i)foriinrange(0,distance+1)]))
- z_r+=[str(z+i)foriinrange(1,distance+1)]
- wide=cls.objects.filter(
- db_tags__db_key__in=x_r,db_tags__db_category="coordx").filter(
- db_tags__db_key__in=y_r,db_tags__db_category="coordy").filter(
- db_tags__db_key__in=z_r,db_tags__db_category="coordz")
-
- # We now need to filter down this list to find out whether
- # these rooms are really close enough, and at what distance
- # In short: we change the square to a circle.
- rooms=[]
- forroominwide:
- x2=int(room.tags.get(category="coordx"))
- y2=int(room.tags.get(category="coordy"))
- z2=int(room.tags.get(category="coordz"))
- distance_to_room=sqrt(
- (x2-x)**2+(y2-y)**2+(z2-z)**2)
- ifdistance_to_room<=distance:
- rooms.append((distance_to_room,room))
-
- # Finally sort the rooms by distance
- rooms.sort(key=lambdatup:tup[0])
- returnrooms
-
-
-
This gets more serious.
-
-
We have specified coordinates as parameters. We determine a broad range using the distance.
-That is, for each coordinate, we create a list of possible matches. See the example below.
-
We then search for the rooms within this broader range. It gives us a square
-around our location. Some rooms are definitely outside the range. Again, see the example below
-to follow the logic.
-
We filter down the list and sort it by distance from the specified coordinates.
-
-
Notice that we only search starting at step 2. Thus, the Django search doesn’t look and cache all
-objects, just a wider range than what would be really necessary. This method returns a circle of
-coordinates around a specified point. Django looks for a square. What wouldn’t fit in the circle
-is removed at step 3, which is the only part that includes systematic calculation. This method is
-optimized to be quick and efficient.
An example might help. Consider this very simple map (a textual description follows):
-
4ABCD
-3EFGH
-2IJKL
-1MNOP
- 1234
-
-
-
The X coordinates are given below. The Y coordinates are given on the left. This is a simple
-square with 16 rooms: 4 on each line, 4 lines of them. All the rooms are identified by letters in
-this example: the first line at the top has rooms A to D, the second E to H, the third I to L and
-the fourth M to P. The bottom-left room, X=1 and Y=1, is M. The upper-right room X=4 and Y=4 is D.
-
So let’s say we want to find all the neighbors, distance 1, from the room J. J is at X=2, Y=2.
-
So we use:
-
Room.get_rooms_around(x=2, y=2, z=0, distance=1)
-# we'll assume a z coordinate of 0 for simplicity
-
-
-
-
First, this method gets all the rooms in a square around J. So it gets E F G, I J K, M N O. If
-you want, draw the square around these coordinates to see what’s happening.
-
Next, we browse over this list and check the real distance between J (X=2, Y=2) and the room.
-The four corners of the square are not in this circle. For instance, the distance between J and M
-is not 1. If you draw a circle of center J and radius 1, you’ll notice that the four corners of our
-square (E, G, M and O) are not in this circle. So we remove them.
-
We sort by distance from J.
-
-
So in the end we might obtain something like this:
-
[
- (0,J),# yes, J is part of this circle after all, with a distance of 0
- (1,F),
- (1,I),
- (1,K),
- (1,N),
-]
-
-
-
You can try with more examples if you want to see this in action.
Note: This is considered an advanced topic and is mostly of interest to users planning to implement
-their own custom client protocol.
-
A PortalSession is the basic data object representing an
-external
-connection to the Evennia Portal – usually a human player running a mud client
-of some kind. The way they connect (the language the player’s client and Evennia use to talk to
-each other) is called the connection Protocol. The most common such protocol for MUD:s is the
-Telnet protocol. All Portal Sessions are stored and managed by the Portal’s sessionhandler.
-
It’s technically sometimes hard to separate the concept of PortalSession from the concept of
-Protocol since both depend heavily on the other (they are often created as the same class). When
-data flows through this part of the system, this is how it goes
-
# In the Portal
-You<->
- Protocol+PortalSession<->
- PortalSessionHandler<->
- (AMP)<->
- ServerSessionHandler<->
- ServerSession<->
- InputFunc
-
-
-
(See the Message Path for the bigger picture of how data flows through Evennia). The
-parts that needs to be customized to make your own custom protocol is the Protocol+PortalSession
-(which translates between data coming in/out over the wire to/from Evennia internal representation)
-as well as the InputFunc (which handles incoming data).
Evennia has a plugin-system that add the protocol as a new “service” to the application.
-
Take a look at evennia/server/portal/portal.py, notably the sections towards the end of that file.
-These are where the various in-built services like telnet, ssh, webclient etc are added to the
-Portal (there is an equivalent but shorter list in evennia/server/server.py).
-
To add a new service of your own (for example your own custom client protocol) to the Portal or
-Server, look at mygame/server/conf/server_services_plugins and portal_services_plugins. By
-default Evennia will look into these modules to find plugins. If you wanted to have it look for more
-modules, you could do the following:
-
# add to the Server
- SERVER_SERVICES_PLUGIN_MODULES.append('server.conf.my_server_plugins')
- # or, if you want to add to the Portal
- PORTAL_SERVICES_PLUGIN_MODULES.append('server.conf.my_portal_plugins')
-
-
-
When adding a new connection you’ll most likely only need to add new things to the
-PORTAL_SERVICES_PLUGIN_MODULES.
-
This module can contain whatever you need to define your protocol, but it must contain a function
-start_plugin_services(app). This is called by the Portal as part of its upstart. The function
-start_plugin_services must contain all startup code the server need. The app argument is a
-reference to the Portal/Server application itself so the custom service can be added to it. The
-function should not return anything.
-
This is how it looks:
-
# mygame/server/conf/portal_services_plugins.py
-
- # here the new Portal Twisted protocol is defined
- classMyOwnFactory(...):
- [...]
-
- # some configs
- MYPROC_ENABLED=True# convenient off-flag to avoid having to edit settings all the time
- MY_PORT=6666
-
- defstart_plugin_services(portal):
- "This is called by the Portal during startup"
- ifnotMYPROC_ENABLED:
- return
- # output to list this with the other services at startup
- print(" myproc: %s"%MY_PORT)
-
- # some setup (simple example)
- factory=MyOwnFactory()
- my_service=internet.TCPServer(MY_PORT,factory)
- # all Evennia services must be uniquely named
- my_service.setName("MyService")
- # add to the main portal application
- portal.services.addService(my_service)
-
-
-
Once the module is defined and targeted in settings, just reload the server and your new
-protocol/services should start with the others.
Writing a stable communication protocol from scratch is not something we’ll cover here, it’s no
-trivial task. The good news is that Twisted offers implementations of many common protocols, ready
-for adapting.
-
Writing a protocol implementation in Twisted usually involves creating a class inheriting from an
-already existing Twisted protocol class and from evennia.server.session.Session (multiple
-inheritance), then overloading the methods that particular protocol uses to link them to the
-Evennia-specific inputs.
-
Here’s a example to show the concept:
-
# In module that we'll later add to the system through PORTAL_SERVICE_PLUGIN_MODULES
-
-# pseudo code
-fromtwisted.somethingimportTwistedClient
-# this class is used both for Portal- and Server Sessions
-fromevennia.server.sessionimportSession
-
-fromevennia.server.portal.portalsessionhandlerimportPORTAL_SESSIONS
-
-classMyCustomClient(TwistedClient,Session):
-
- def__init__(self,*args,**kwargs):
- super().__init__(*args,**kwargs)
- self.sessionhandler=PORTAL_SESSIONS
-
- # these are methods we must know that TwistedClient uses for
- # communication. Name and arguments could vary for different Twisted protocols
- defonOpen(self,*args,**kwargs):
- # let's say this is called when the client first connects
-
- # we need to init the session and connect to the sessionhandler. The .factory
- # is available through the Twisted parents
-
- client_address=self.getClientAddress()# get client address somehow
-
- self.init_session("mycustom_protocol",client_address,self.factory.sessionhandler)
- self.sessionhandler.connect(self)
-
- defonClose(self,reason,*args,**kwargs):
- # called when the client connection is dropped
- # link to the Evennia equivalent
- self.disconnect(reason)
-
- defonMessage(self,indata,*args,**kwargs):
- # called with incoming data
- # convert as needed here
- self.data_in(data=indata)
-
- defsendMessage(self,outdata,*args,**kwargs):
- # called to send data out
- # modify if needed
- super().sendMessage(self,outdata,*args,**kwargs)
-
- # these are Evennia methods. They must all exist and look exactly like this
- # The above twisted-methods call them and vice-versa. This connects the protocol
- # the Evennia internals.
-
- defdisconnect(self,reason=None):
- """
- Called when connection closes.
- This can also be called directly by Evennia when manually closing the connection.
- Do any cleanups here.
- """
- self.sessionhandler.disconnect(self)
-
- defat_login(self):
- """
- Called when this session authenticates by the server (if applicable)
- """
-
- defdata_in(self,**kwargs):
- """
- Data going into the server should go through this method. It
- should pass data into `sessionhandler.data_in`. THis will be called
- by the sessionhandler with the data it gets from the approrpriate
- send_* method found later in this protocol.
- """
- self.sessionhandler.data_in(self,text=kwargs['data'])
-
- defdata_out(self,**kwargs):
- """
- Data going out from the server should go through this method. It should
- hand off to the protocol's send method, whatever it's called.
- """
- # we assume we have a 'text' outputfunc
- self.onMessage(kwargs['text'])
-
- # 'outputfuncs' are defined as `send_<outputfunc_name>`. From in-code, they are called
- # with `msg(outfunc_name=<data>)`.
-
- defsend_text(self,txt,*args,**kwargs):
- """
- Send text, used with e.g. `session.msg(text="foo")`
- """
- # we make use of the
- self.data_out(text=txt)
-
- defsend_default(self,cmdname,*args,**kwargs):
- """
- Handles all outputfuncs without an explicit `send_*` method to handle them.
- """
- self.data_out(**{cmdname:str(args)})
-
-
-
-
The principle here is that the Twisted-specific methods are overridden to redirect inputs/outputs to
-the Evennia-specific methods.
To send data out through this protocol, you’d need to get its Session and then you could e.g.
-
session.msg(text="foo")
-
-
-
The message will pass through the system such that the sessionhandler will dig out the session and
-check if it has a send_text method (it has). It will then pass the “foo” into that method, which
-in our case means sending “foo” across the network.
Just because the protocol is there, does not mean Evennia knows what to do with it. An
-Inputfunc must exist to receive it. In the case of the text input exemplified above,
-Evennia alredy handles this input - it will parse it as a Command name followed by its inputs. So
-handle that you need to simply add a cmdset with commands on your receiving Session (and/or the
-Object/Character it is puppeting). If not you may need to add your own Inputfunc (see the
-Inputfunc page for how to do this.
-
These might not be as clear-cut in all protocols, but the principle is there. These four basic
-components - however they are accessed - links to the Portal Session, which is the actual common
-interface between the different low-level protocols and Evennia.
To take two examples, Evennia supports the telnet protocol as well as webclient, via ajax or
-websockets. You’ll find that whereas telnet is a textbook example of a Twisted protocol as seen
-above, the ajax protocol looks quite different due to how it interacts with the
-webserver through long-polling (comet) style requests. All the necessary parts
-mentioned above are still there, but by necessity implemented in very different
-ways.
By default, Evennia’s default channel commands are inspired by MUX. They all
-begin with “c” followed by the action to perform (like “ccreate” or “cdesc”).
-If this default seems strange to you compared to other Evennia commands that
-rely on switches, you might want to check this tutorial out.
-
This tutorial will also give you insight into the workings of the channel system.
-So it may be useful even if you don’t plan to make the exact changes shown here.
Our mission: change the default channel commands to have a different syntax.
-
This tutorial will do the following changes:
-
-
Remove all the default commands to handle channels.
-
Add a + and - command to join and leave a channel. So, assuming there is
-a public channel on your game (most often the case), you could type +public
-to join it and -public to leave it.
-
Group the commands to manipulate channels under the channel name, after a
-switch. For instance, instead of writing cdescpublic=Mypublicchannel,
-you would write public/descMypublicchannel.
-
-
-
I listed removing the default Evennia commands as a first step in the
-process. Actually, we’ll move it at the very bottom of the list, since we
-still want to use them, we might get it wrong and rely on Evennia commands
-for a while longer.
We’ll do the most simple task at first: create two commands, one to join a
-channel, one to leave.
-
-
Why not have them as switches? public/join and public/leave for instance?
-
-
For security reasons, I will hide channels to which the caller is not
-connected. It means that if the caller is not connected to the “public”
-channel, he won’t be able to use the “public” command. This is somewhat
-standard: if we create an administrator-only channel, we don’t want players to
-try (or even know) the channel command. Again, you could design it a different
-way should you want to.
-
First create a file named comms.py in your commands package. It’s
-a rather logical place, since we’ll write different commands to handle
-communication.
-
Okay, let’s add the first command to join a channel:
-
# in commands/comms.py
-fromevennia.utils.searchimportsearch_channel
-fromcommands.commandimportCommand
-
-classCmdConnect(Command):
- """
- Connect to a channel.
- """
-
- key="+"
- help_category="Comms"
- locks="cmd:not pperm(channel_banned)"
- auto_help=False
-
- deffunc(self):
- """Implement the command"""
- caller=self.caller
- args=self.args
- ifnotargs:
- self.msg("Which channel do you want to connect to?")
- return
-
- channelname=self.args
- channel=search_channel(channelname)
- ifnotchannel:
- return
-
- # Check permissions
- ifnotchannel.access(caller,'listen'):
- self.msg("%s: You are not allowed to listen to this channel."%channel.key)
- return
-
- # If not connected to the channel, try to connect
- ifnotchannel.has_connection(caller):
- ifnotchannel.connect(caller):
- self.msg("%s: You are not allowed to join this channel."%channel.key)
- return
- else:
- self.msg("You now are connected to the %s channel. "%channel.key.lower())
- else:
- self.msg("You already are connected to the %s channel. "%channel.key.lower())
-
-
-
Okay, let’s review this code, but if you’re used to Evennia commands, it shouldn’t be too strange:
-
-
We import search_channel. This is a little helper function that we will use to search for
-channels by name and aliases, found in evennia.utils.search. It’s just more convenient.
-
Our class CmdConnect contains the body of our command to join a channel.
-
Notice the key of this command is simply "+". When you enter +something in the game, it will
-try to find a command key +something. Failing that, it will look at other potential matches.
-Evennia is smart enough to understand that when we type +something, + is the command key and
-something is the command argument. This will, of course, fail if you have a command beginning by
-+ conflicting with the CmdConnect key.
-
We have altered some class attributes, like auto_help. If you want to know what they do and
-why they have changed here, you can check the documentation on commands.
-
In the command body, we begin by extracting the channel name. Remember that this name should be
-in the command arguments (that is, in self.args). Following the same example, if a player enters
-+something, self.args should contain "something". We use search_channel to see if this
-channel exists.
-
We then check the access level of the channel, to see if the caller can listen to it (not
-necessarily use it to speak, mind you, just listen to others speak, as these are two different locks
-on Evennia).
-
Finally, we connect the caller if he’s not already connected to the channel. We use the
-channel’s connect method to do this. Pretty straightforward eh?
-
-
Now we’ll add a command to leave a channel. It’s almost the same, turned upside down:
-
classCmdDisconnect(Command):
- """
- Disconnect from a channel.
- """
-
- key="-"
- help_category="Comms"
- locks="cmd:not pperm(channel_banned)"
- auto_help=False
-
- deffunc(self):
- """Implement the command"""
- caller=self.caller
- args=self.args
- ifnotargs:
- self.msg("Which channel do you want to disconnect from?")
- return
-
- channelname=self.args
- channel=search_channel(channelname)
- ifnotchannel:
- return
-
- # If connected to the channel, try to disconnect
- ifchannel.has_connection(caller):
- ifnotchannel.disconnect(caller):
- self.msg("%s: You are not allowed to disconnect from this channel."%channel.key)
- return
- else:
- self.msg("You stop listening to the %s channel. "%channel.key.lower())
- else:
- self.msg("You are not connected to the %s channel. "%channel.key.lower())
-
-
-
So far, you shouldn’t have trouble following what this command does: it’s
-pretty much the same as the CmdConnect class in logic, though it accomplishes
-the opposite. If you are connected to the channel public you could
-disconnect from it using -public. Remember, you can use channel aliases too
-(+pub and -pub will also work, assuming you have the alias pub on the
-public channel).
-
It’s time to test this code, and to do so, you will need to add these two
-commands. Here is a good time to say it: by default, Evennia connects accounts
-to channels. Some other games (usually with a higher multisession mode) will
-want to connect characters instead of accounts, so that several characters in
-the same account can be connected to various channels. You can definitely add
-these commands either in the AccountCmdSet or CharacterCmdSet, the caller
-will be different and the command will add or remove accounts of characters.
-If you decide to install these commands on the CharacterCmdSet, you might
-have to disconnect your superuser account (account #1) from the channel before
-joining it with your characters, as Evennia tends to subscribe all accounts
-automatically if you don’t tell it otherwise.
-
So here’s an example of how to add these commands into your AccountCmdSet.
-Edit the file commands/default_cmdsets.py to change a few things:
-
# In commands/default_cmdsets.py
-fromevenniaimportdefault_cmds
-fromcommands.commsimportCmdConnect,CmdDisconnect
-
-
-# ... Skip to the AccountCmdSet class ...
-
-classAccountCmdSet(default_cmds.AccountCmdSet):
- """
- This is the cmdset available to the Account at all times. It is
- combined with the `CharacterCmdSet` when the Account puppets a
- Character. It holds game-account-specific commands, channel
- commands, etc.
- """
- key="DefaultAccount"
-
- defat_cmdset_creation(self):
- """
- Populates the cmdset
- """
- super().at_cmdset_creation()
-
- # Channel commands
- self.add(CmdConnect())
- self.add(CmdDisconnect())
-
-
-
Save, reload your game, and you should be able to use +public and -public
-now!
It’s time to dive a little deeper into channel processing. What happens in
-Evennia when a player enters publicHelloeverybody!?
-
Like exits, channels are a particular command that Evennia automatically
-creates and attaches to individual channels. So when you enter publicmessage in your game, Evennia calls the public command.
-
-
But I didn’t add any public command…
-
-
Evennia will just create these commands automatically based on the existing
-channels. The base command is the command we’ll need to edit.
-
-
Why edit it? It works just fine to talk.
-
-
Unfortunately, if we want to add switches to our channel names, we’ll have to
-edit this command. It’s not too hard, however, we’ll just start writing a
-standard command with minor twitches.
# In commands/comms.py
-classChannelCommand(Command):
- """
- {channelkey} channel
-
- {channeldesc}
-
- Usage:
- {lower_channelkey} <message>
- {lower_channelkey}/history [start]
- {lower_channelkey}/me <message>
- {lower_channelkey}/who
-
- Switch:
- history: View 20 previous messages, either from the end or
- from <start> number of messages from the end.
- me: Perform an emote on this channel.
- who: View who is connected to this channel.
-
- Example:
- {lower_channelkey} Hello World!
- {lower_channelkey}/history
- {lower_channelkey}/history 30
- {lower_channelkey}/me grins.
- {lower_channelkey}/who
- """
- # 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=""
-
-
-
There are some differences here compared to most common commands.
-
-
There is something disconcerting in the class docstring. Some information is
-between curly braces. This is a format-style which is only used for channel
-commands. {channelkey} will be replaced by the actual channel key (like
-public). {channeldesc} will be replaced by the channel description (like
-“public channel”). And {lower_channelkey}.
-
We have set is_channel to True in the command class variables. You
-shouldn’t worry too much about that: it just tells Evennia this is a special
-command just for channels.
-
key is a bit misleading because it will be replaced eventually. So we
-could set it to virtually anything.
-
The obj class variable is another one we won’t detail right now.
-
arg_regex is important: the default arg_regex in the channel command will
-forbid to use switches (a slash just after the channel name is not allowed).
-That’s why we enforce it here, we allow any syntax.
-
-
-
What will become of this command?
-
-
Well, when we’ll be through with it, and once we’ll add it as the default
-command to handle channels, Evennia will create one per existing channel. For
-instance, the public channel will receive one command of this class, with key
-set to public and aliases set to the channel aliases (like ['pub']).
-
-
Can I see it work?
-
-
Not just yet, there’s still a lot of code needed.
-
Okay we have the command structure but it’s rather empty.
Reading the comments we see that the channel handler will send the command in a
-strange way: a string with the channel name, a colon and the actual message
-entered by the player. So if the player enters “public hello”, the command
-args will contain "public:hello". You can look at the way the channel name
-and message are parsed, this can be used in a lot of different commands.
-
Next we check if there’s any switch, that is, if the message starts with a
-slash. This would be the case if a player entered public/mejumpsupanddown, for instance. If there is a switch, we save it in self.switch. We
-alter self.args at the end to contain a tuple with two values: the channel
-name, and the message (if a switch was used, notice that the switch will be
-stored in self.switch, not in the second element of self.args).
Finally, let’s see the func method in the command class. It will have to
-handle switches and also the raw message to send if no switch was used.
-
# ...
- deffunc(self):
- """
- Create a new message and send it to channel, using
- the already formatted input.
- """
- channelkey,msg=self.args
- caller=self.caller
- channel=ChannelDB.objects.get_channel(channelkey)
-
- # Check that the channel exists
- ifnotchannel:
- self.msg(_("Channel '%s' not found.")%channelkey)
- return
-
- # Check that the caller is connected
- ifnotchannel.has_connection(caller):
- string="You are not connected to channel '%s'."
- self.msg(string%channelkey)
- return
-
- # Check that the caller has send access
- ifnotchannel.access(caller,'send'):
- string="You are not permitted to send to channel '%s'."
- self.msg(string%channelkey)
- return
-
- # Handle the various switches
- ifself.switch=="me":
- ifnotmsg:
- self.msg("What do you want to do on this channel?")
- else:
- msg="{}{}".format(caller.key,msg)
- channel.msg(msg,online=True)
- elifself.switch:
- self.msg("{}: Invalid switch {}.".format(channel.key,self.switch))
- elifnotmsg:
- self.msg("Say what?")
- else:
- ifcallerinchannel.mutelist:
- self.msg("You currently have %s muted."%channel)
- return
- channel.msg(msg,senders=self.caller,online=True)
-
-
-
-
First of all, we try to get the channel object from the channel name we have
-in the self.args tuple. We use ChannelDB.objects.get_channel this time
-because we know the channel name isn’t an alias (that was part of the deal,
-channelname in the parse method contains a command key).
-
We check that the channel does exist.
-
We then check that the caller is connected to the channel. Remember, if the
-caller isn’t connected, we shouldn’t allow him to use this command (that
-includes the switches on channels).
-
We then check that the caller has access to the channel’s send lock. This
-time, we make sure the caller can send messages to the channel, no matter what
-operation he’s trying to perform.
-
Finally we handle switches. We try only one switch: me. This switch would
-be used if a player entered public/mejumpsupanddown (to do a channel
-emote).
-
We handle the case where the switch is unknown and where there’s no switch
-(the player simply wants to talk on this channel).
-
-
The good news: The code is not too complicated by itself. The bad news is that
-this is just an abridged version of the code. If you want to handle all the
-switches mentioned in the command help, you will have more code to write. This
-is left as an exercise.
It’s almost done, but we need to add a method in this command class that isn’t
-often used. I won’t detail it’s usage too much, just know that Evennia will use
-it and will get angry if you don’t add it. So at the end of your class, just
-add:
-
# ...
- 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)"
-
Contrary to most Evennia commands, we won’t add our ChannelCommand to a
-CmdSet. Instead we need to tell Evennia that it should use the command we
-just created instead of its default channel-command.
-
In your server/conf/settings.py file, add a new setting:
Then you can reload your game. Try to type publichello and public/mejumpsupanddown. Don’t forget to enter helppublic to see if your command has
-truly been added.
That was some adventure! And there’s still things to do! But hopefully, this
-tutorial will have helped you in designing your own channel system. Here are a
-few things to do:
-
-
Add more switches to handle various actions, like changing the description of
-a channel for instance, or listing the connected participants.
-
Remove the default Evennia commands to handle channels.
-
Alter the behavior of the channel system so it better aligns with what you
-want to do.
-
-
As a special bonus, you can find a full, working example of a communication
-system similar to the one I’ve shown you: this is a working example, it
-integrates all switches and does ever some extra checking, but it’s also very
-close from the code I’ve provided here. Notice, however, that this resource is
-external to Evennia and not maintained by anyone but the original author of
-this article.
Sometimes, an error is not trivial to resolve. A few simple print statements is not enough to find
-the cause of the issue. Running a debugger can then be very helpful and save a lot of time.
-Debugging
-means running Evennia under control of a special debugger program. This allows you to stop the
-action at a given point, view the current state and step forward through the program to see how its
-logic works.
-
Evennia natively supports these debuggers:
-
-
Pdb is a part of the Python distribution and
-available out-of-the-box.
-
PuDB is a third-party debugger that has a slightly more
-‘graphical’, curses-based user interface than pdb. It is installed with pipinstallpudb.
To run Evennia with the debugger, follow these steps:
-
-
Find the point in the code where you want to have more insight. Add the following line at that
-point.
-
fromevenniaimportset_trace;set_trace()
-
-
-
-
(Re-)start Evennia in interactive (foreground) mode with evenniaistart. This is important -
-without this step the debugger will not start correctly - it will start in this interactive
-terminal.
-
Perform the steps that will trigger the line where you added the set_trace() call. The debugger
-will start in the terminal from which Evennia was interactively started.
-
-
The evennia.set_trace function takes the following arguments:
Here, debugger is one of pdb, pudb or auto. If auto, use pudb if available, otherwise
-use pdb. The term_size tuple sets the viewport size for pudb only (it’s ignored by pdb).
The debugger is useful in different cases, but to begin with, let’s see it working in a command.
-Add the following test command (which has a range of deliberate errors) and also add it to your
-default cmdset. Then restart Evennia in interactive mode with evenniaistart.
-
# In file commands/command.py
-
-
-classCmdTest(Command):
-
- """
- A test command just to test pdb.
-
- Usage:
- test
-
- """
-
- key="test"
-
- deffunc(self):
- fromevenniaimportset_trace;set_trace()# <--- start of debugger
- obj=self.search(self.args)
- self.msg("You've found {}.".format(obj.get_display_name()))
-
-
-
-
If you type test in your game, everything will freeze. You won’t get any feedback from the game,
-and you won’t be able to enter any command (nor anyone else). It’s because the debugger has started
-in your console, and you will find it here. Below is an example with pdb.
pdb notes where it has stopped execution and, what line is about to be executed (in our case, obj=self.search(self.args)), and ask what you would like to do.
When you have the pdb prompt (Pdb), you can type in different commands to explore the code. The
-first one you should know is list (you can type l for short):
-
(Pdb)l
- 43
- 44key="test"
- 45
- 46deffunc(self):
- 47fromevenniaimportset_trace;set_trace()# <--- start of debugger
- 48->obj=self.search(self.args)
- 49self.msg("You've found {}.".format(obj.get_display_name()))
- 50
- 51# -------------------------------------------------------------
- 52#
- 53# The default commands inherit from
-(Pdb)
-
-
-
Okay, this didn’t do anything spectacular, but when you become more confident with pdb and find
-yourself in lots of different files, you sometimes need to see what’s around in code. Notice that
-there is a little arrow (->) before the line that is about to be executed.
-
This is important: about to be, not has just been. You need to tell pdb to go on (we’ll
-soon see how).
pdb allows you to examine variables (or really, to run any Python instruction). It is very useful
-to know the values of variables at a specific line. To see a variable, just type its name (as if
-you were in the Python interpreter:
That figures, since at this point, we haven’t created the variable yet.
-
-
Examining variable in this way is quite powerful. You can even run Python code and keep on
-executing, which can help to check that your fix is actually working when you have identified an
-error. If you have variable names that will conflict with pdb commands (like a list
-variable), you can prefix your variable with !, to tell pdb that what follows is Python code.
It’s time we asked pdb to execute the current line. To do so, use the next command. You can
-shorten it by just typing n:
-
(Pdb)n
-AttributeError:"'CmdTest' object has no attribute 'search'"
->.../mygame/commands/command.py(79)func()
-->obj=self.search(self.args)
-(Pdb)
-
-
-
Pdb is complaining that you try to call the search method on a command… whereas there’s no
-search method on commands. The character executing the command is in self.caller, so we might
-change our line:
pdb is waiting to execute the same instruction… it provoked an error but it’s ready to try
-again, just in case. We have fixed it in theory, but we need to reload, so we need to enter a
-command. To tell pdb to terminate and keep on running the program, use the continue (or c)
-command:
-
(Pdb)c
-...
-
-
-
You see an error being caught, that’s the error we have fixed… or hope to have. Let’s reload the
-game and try again. You need to run evenniaistart again and then run test to get into the
-command again.
n is useful, but it will avoid stepping inside of functions if it can. But most of the time, when
-we have an error we don’t understand, it’s because we use functions or methods in a way that wasn’t
-intended by the developer of the API. Perhaps using wrong arguments, or calling the function in a
-situation that would cause a bug. When we have a line in the debugger that calls a function or
-method, we can “step” to examine it further. For instance, in the previous example, when pdb was
-about to execute obj=self.caller.search(self.args), we may want to see what happens inside of
-the search method.
-
To do so, use the step (or s) command. This command will show you the definition of the
-function/method and you can then use n as before to see it line-by-line. In our little example,
-stepping through a function or method isn’t that useful, but when you have an impressive set of
-commands, functions and so on, it might really be handy to examine some feature and make sure they
-operate as planned.
PuDB and Pdb share the same commands. The only real difference is how it’s presented. The look
-command is not needed much in pudb since it displays the code directly in its user interface.
-
-
-
Pdb/PuDB command
-
To do what
-
-
-
-
list (or l)
-
List the lines around the point of execution (not needed for pudb, it will show
-
-
this directly).
-
-
-
print (or p)
-
Display one or several variables.
-
-
!
-
Run Python code (using a ! is often optional).
-
-
continue (or c)
-
Continue execution and terminate the debugger for this time.
-
-
next (or n)
-
Execute the current line and goes to the next one.
-
-
step (or s)
-
Step inside of a function or method to examine it.
-
-
<RETURN>
-
Repeat the last command (don’t type n repeatedly, just type it once and then press
The full set of default Evennia commands currently contains 98 commands in 9 source
-files. Our policy for adding default commands is outlined here. The
-Commands documentation explains how Commands work as well as make new or customize
-existing ones. Note that this page is auto-generated. Report problems to the issue
-tracker.
-
-
Note
-
Some game-states adds their own Commands which are not listed here. Examples include editing a text
-with EvEditor, flipping pages in EvMore or using the
-Batch-Processor’s interactive mode.
Evennia allows for exits to have any name. The command “kitchen” is a valid exit name as well as
-“jump out the window” or “north”. An exit actually consists of two parts: an Exit Object
-and an Exit Command stored on said exit object. The command has the same key and aliases
-as the object, which is why you can see the exit in the room and just write its name to traverse it.
-
If you try to enter the name of a non-existing exit, it is thus the same as trying a non-exising
-command; Evennia doesn’t care about the difference:
-
> jump out the window
- Command 'jump out the window' is not available. Type "help" for help.
-
-
-
Many games don’t need this type of freedom however. They define only the cardinal directions as
-valid exit names (Evennia’s @tunnel command also offers this functionality). In this case, the
-error starts to look less logical:
-
> west
- Command 'west' is not available. Maybe you meant "@set" or "@reset"?
-
-
-
Since we for our particular game know that west is an exit direction, it would be better if the
-error message just told us that we couldn’t go there.
To solve this you need to be aware of how to write and add new commands.
-What you need to do is to create new commands for all directions you want to support in your game.
-In this example all we’ll do is echo an error message, but you could certainly consider more
-advanced uses. You add these commands to the default command set. Here is an example of such a set
-of commands:
-
# for example in a file mygame/commands/movecommands.py
-
-fromevenniaimportdefault_cmds
-
-classCmdExitError(default_cmds.MuxCommand):
- "Parent class for all exit-errors."
- locks="cmd:all()"
- arg_regex=r"\s|$"
- auto_help=False
- deffunc(self):
- "returns the error"
- self.caller.msg("You cannot move %s."%self.key)
-
-classCmdExitErrorNorth(CmdExitError):
- key="north"
- aliases=["n"]
-
-classCmdExitErrorEast(CmdExitError):
- key="east"
- aliases=["e"]
-
-classCmdExitErrorSouth(CmdExitError):
- key="south"
- aliases=["s"]
-
-classCmdExitErrorWest(CmdExitError):
- key="west"
- aliases=["w"]
-
-
-
Make sure to add the directional commands (not their parent) to the CharacterCmdSet class in
-mygame/commands/default_cmdsets.py:
After a @reload these commands (assuming you don’t get any errors - check your log) will be
-loaded. What happens henceforth is that if you are in a room with an Exitobject (let’s say it’s
-“north”), the proper Exit-command will overload your error command (also named “north”). But if you
-enter an direction without having a matching exit for it, you will fallback to your default error
-commands:
-
> east
- You cannot move east.
-
-
-
Further expansions by the exit system (including manipulating the way the Exit command itself is
-created) can be done by modifying the Exit typeclass directly.
The anwer is that this would not work and understanding why is important in order to not be
-confused when working with commands and command sets.
-
The reason it doesn’t work is because Evennia’s command system compares commands both
-by key and by aliases. If either of those match, the two commands are considered identical
-as far as cmdset merging system is concerned.
-
So the above example would work fine as long as there were no Exits at all in the room. But what
-happens when we enter a room with an exit “north”? The Exit’s cmdset is merged onto the default one,
-and since there is an alias match, the system determines our CmdExitError to be identical. It is
-thus overloaded by the Exit command (which also correctly defaults to a higher priority). The result
-is that you can go through the north exit normally but none of the error messages for the other
-directions are available since the single error command was completely overloaded by the single
-matching “north” exit-command.
Next tutorial: [adding a voice-operated elevator with events](A-voice-operated-elevator-using-
-events).
-
-
This tutorial will walk you through the steps to create several dialogues with characters, using the
-in-game Python
-system.
-This tutorial assumes the in-game Python system is installed in your game. If it isn’t, you can
-follow the installation steps given in the documentation on in-game
-Python, and
-come back on this tutorial once the system is installed. You do not need to read the entire
-documentation, it’s a good reference, but not the easiest way to learn about it. Hence these
-tutorials.
-
The in-game Python system allows to run code on individual objects in some situations. You don’t
-have to modify the source code to add these features, past the installation. The entire system
-makes it easy to add specific features to some objects, but not all. This is why it can be very
-useful to create a dialogue system taking advantage of the in-game Python system.
-
-
What will we try to do?
-
-
In this tutorial, we are going to create a basic dialogue to have several characters automatically
-respond to specific messages said by others.
This will create a merchant in the room where you currently are. It doesn’t have anything, like a
-description, you can decorate it a bit if you like.
-
As said above, the in-game Python system consists in linking objects with arbitrary code. This code
-will be executed in some circumstances. Here, the circumstance is “when someone says something in
-the same room”, and might be more specific like “when someone says hello”. We’ll decide what code
-to run (we’ll actually type the code in-game). Using the vocabulary of the in-game Python system,
-we’ll create a callback: a callback is just a set of lines of code that will run under some
-conditions.
-
You can have an overview of every “conditions” in which callbacks can be created using the @call
-command (short for @callback). You need to give it an object as argument. Here for instance, we
-could do:
-
@call a merchant
-
-
-
You should see a table with three columns, showing the list of events existing on our newly-created
-merchant. There are quite a lot of them, as it is, althougn no line of code has been set yet. For
-our system, you might be more interested by the line describing the say event:
-
| say | 0 (0) | After another character has said something in |
-| | | the character's room. |
-
-
-
We’ll create a callback on the say event, called when we say “hello” in the merchant’s room:
-
@call/add a merchant = say hello
-
-
-
Before seeing what this command displays, let’s see the command syntax itself:
-
-
@call is the command name, /add is a switch. You can read the help of the command to get the
-help of available switches and a brief overview of syntax.
-
We then enter the object’s name, here “a merchant”. You can enter the ID too (“#3” in my case),
-which is useful to edit the object when you’re not in the same room. You can even enter part of the
-name, as usual.
-
An equal sign, a simple separator.
-
The event’s name. Here, it’s “say”. The available events are displayed when you use @call
-without switch.
-
After a space, we enter the conditions in which this callback should be called. Here, the
-conditions represent what the other character should say. We enter “hello”. Meaning that if
-someone says something containing “hello” in the room, the callback we are now creating will be
-called.
-
-
When you enter this command, you should see something like this:
That’s some list of information. What’s most important to us now is:
-
-
The “say” event is called whenever someone else speaks in the room.
-
We can set callbacks to fire when specific keywords are present in the phrase by putting them as
-additional parameters. Here we have set this parameter to “hello”. We can have several keywords
-separated by a comma (we’ll see this in more details later).
-
We have three default variables we can use in this callback: speaker which contains the
-character who speaks, character which contains the character who’s modified by the in-game Python
-system (here, or merchant), and message which contains the spoken phrase.
-
-
This concept of variables is important. If it makes things more simple to you, think of them as
-parameters in a function: they can be used inside of the function body because they have been set
-when the function was called.
-
This command has opened an editor where we can type our Python code.
character.location.msg_contents("{character} shrugs and says: 'well, yes, hello to you!'",
-mapping=dict(character=character))
-
-
-
Once you have entered this line, you can type :wq to save the editor and quit it.
-
And now if you use the “say” command with a message containing “hello”:
-
Yousay,"Hello sir merchant!"
-amerchant(#3) shrugs and says: 'well, yes, hello to you!'
-
-
-
If you say something that doesn’t contain “hello”, our callback won’t execute.
-
In summary:
-
-
When we say something in the room, using the “say” command, the “say” event of all characters
-(except us) is called.
-
The in-game Python system looks at what we have said, and checks whether one of our callbacks in
-the “say” event contains a keyword that we have spoken.
-
If so, call it, defining the event variables as we have seen.
-
The callback is then executed as normal Python code. Here we have called the msg_contents
-method on the character’s location (probably a room) to display a message to the entire room. We
-have also used mapping to easily display the character’s name. This is not specific to the in-game
-Python system. If you feel overwhelmed by the code we’ve used, just shorten it and use something
-more simple, for instance:
So far, we have only set one line in our callbacks. Which is useful, but we often need more. For
-an entire dialogue, you might want to do a bit more than that.
-
@call/add merchant = say bandit, bandits
-
-
-
And in the editor you can paste the following lines:
-
character.location.msg_contents("{character} says: 'Bandits he?'",
-mapping=dict(character=character))
-character.location.msg_contents("{character} scratches his head, considering.",
-mapping=dict(character=character))
-character.location.msg_contents("{character} whispers: 'Aye, saw some of them, north from here. No
-troubleo' mine, but...'", mapping=dict(character=character))
-speaker.msg("{character} looks at you more
-closely.".format(character=character.get_display_name(speaker)))
-speaker.msg("{character} continues in a low voice: 'Ain't my place to say, but if you need to find
-'em, they'reencampedsomedistanceawayfromtheroad,Iguessnearacaveor
-something.'.".format(character=character.get_display_name(speaker)))
-
-
-
Now try to ask the merchant about bandits:
-
Yousay,"have you seen bandits?"
-amerchant(#3) says: 'Bandits he?'
-amerchant(#3) scratches his head, considering.
-amerchant(#3) whispers: 'Aye, saw some of them, north from here. No trouble o' mine, but...'
-amerchant(#3) looks at you more closely.
-amerchant(#3) continues in a low voice: 'Ain't my place to say, but if you need to find 'em,
-they're encamped some distance away from the road, I guess near a cave or something.'.
-
-
-
Notice here that the first lines of dialogue are spoken to the entire room, but then the merchant is
-talking directly to the speaker, and only the speaker hears it. There’s no real limit to what you
-can do with this.
-
-
You can set a mood system, storing attributes in the NPC itself to tell you in what mood he is,
-which will influence the information he will give… perhaps the accuracy of it as well.
-
You can add random phrases spoken in some context.
-
You can use other actions (you’re not limited to having the merchant say something, you can ask
-him to move, gives you something, attack if you have a combat system, or whatever else).
-
The callbacks are in pure Python, so you can write conditions or loops.
-
You can add in “pauses” between some instructions using chained events. This tutorial won’t
-describe how to do that however. You already have a lot to play with.
Q: can I create several characters who would answer to specific dialogue?
-
A: of course. Te in-game Python system is so powerful because you can set unique code for
-various objects. You can have several characters answering to different things. You can even have
-different characters in the room answering to greetings. All callbacks will be executed one after
-another.
-
Q: can I have two characters answering to the same dialogue in exactly the same way?
-
A: It’s possible but not so easy to do. Usually, event grouping is set in code, and depends
-on different games. However, if it is for some infrequent occurrences, it’s easy to do using
-chained events.
-
Q: is it possible to deploy callbacks on all characters sharing the same prototype?
-
A: not out of the box. This depends on individual settings in code. One can imagine that all
-characters of some type would share some events, but this is game-specific. Rooms of the same zone
-could share the same events as well. It is possible to do but requires modification of the source
-code.
-
Next tutorial: [adding a voice-operated elevator with events](A-voice-operated-elevator-using-
-events).
The game directory is created with evennia--init<name>. In the Evennia documentation we always
-assume it’s called mygame. Apart from the server/ subfolder within, you could reorganize this
-folder if you preferred a different code structure for your game.
server/ - The structure of this folder should not change since Evennia expects it.
-
-
conf/ - All
-server configuration files sits here. The most important file is settings.py.
-
logs/ - Portal log files are stored here (Server is logging to the terminal by default)
-
-
-
typeclasses/ - this folder contains empty templates for overloading default game entities of
-Evennia. Evennia will automatically use the changes in those templates for the game entities it
-creates.
world/ - this is a “miscellaneous” folder holding everything related to the world you are
-building, such as build scripts and rules modules that don’t fit with one of the other folders.
If you cloned the GIT repo following the instructions, you will have a folder named evennia. The
-top level of it contains Python package specific stuff such as a readme file, setup.py etc. It
-also has two subfoldersbin/ and evennia/ (again).
-
The bin/ directory holds OS-specific binaries that will be used when installing Evennia with pip
-as per the Getting started instructions. The library itself is in the evennia
-subfolder. From your code you will access this subfolder simply by importevennia.
-
-
evennia
-
-
__init__.py - The “flat API” of Evennia resides here.
settings_default.py - Root settings of Evennia. Copy settings
-from here to mygame/server/settings.py file.
-
typeclasses/ - Abstract classes for the typeclass storage and database system.
-
utils/ - Various miscellaneous useful coding resources.
-
web/ - Web resources and webserver. Partly copied into game directory on
-initialization.
-
-
-
-
All directories contain files ending in .py. These are Python modules and are the basic units of
-Python code. The roots of directories also have (usually empty) files named __init__.py. These are
-required by Python so as to be able to find and import modules in other directories. When you have
-run Evennia at least once you will find that there will also be .pyc files appearing, these are
-pre-compiled binary versions of the .py files to speed up execution.
-
The root of the evennia folder has an __init__.py file containing the “flat API”.
-This holds shortcuts to various subfolders in the evennia library. It is provided to make it easier
-to find things; it allows you to just import evennia and access things from that rather than
-having to import from their actual locations inside the source tree.
This is a whitepage for free discussion about the wiki docs and refactorings needed.
-
Note that this is not a forum. To keep things clean, each opinion text should ideally present a
-clear argument or lay out a suggestion. Asking for clarification and any side-discussions should be
-held in chat or forum.
This is how to make a discussion entry for the whitepage. Use any markdown formatting you need. Also
-remember to copy your work to the clipboard before saving the page since if someone else edited the
-page since you started, you’ll have to reload and write again.
I think it would be useful for the pages that explain how to use various features of Evennia to
-have explicit and easily visible links to the respective API entry or entries. Some pages do, but
-not all. I imagine this as a single entry at the top of the page […].
It would help me (and probably a couple of others) if there is a way to show the file path where a
-particular thing exists. Maybe up under the ‘last edited’ line we could have a line like:
-evennia/locks/lockhandler.py
-
-
This would help in development to quickly refer to where a resource is located.
Batch Code should have a link in the developer area. It is currently only
-listed in the tutorials section as an afterthought to a tutorial title.
-
-
In regards to the general structure of each wiki page: I’d like to see a table of contents at the
-top of each one, so that it can be quickly navigated and is immediately apparent what sections are
-covered on the page. Similar to the current Getting Started page.
-
-
The structuring of the page should also include a quick reference cheatsheet for certain aspects.
-Such as Tags including a quick reference section at the top that lists an example of every
-available method you can use in a clear and consistent format, along with a comment. Readers
-shouldn’t have to decipher the article to gather such basic information and it should instead be
-available at first glance.
-
Example of a quick reference:
-
Tags
-
# Add a tag.
-obj.tags.add("label")
-
-# Remove a tag.
-obj.tags.remove("label")
-
-# Remove all tags.
-obj.tags.clear()
-
-# Search for a tag. Evennia must be imported first.
-store_result=evennia.search_tag("label")
-
-# Return a list of all tags.
-obj.tags.all()
-
-
-
Aliases
-
# Add an alias.
-obj.aliases.add("label")
-
-ETC...
-
-
-
-
In regards to comment structure, I often find that smushing together lines with comments to be too
-obscure. White space should be used to clearly delineate what information the comment is for. I
-understand that the current format is that a comment references whatever is below it, but newbies
-may not know that until they realize it.
If I want to find information on the correct syntax for is_typeclass(), here’s what I do:
-
-
Pop over to the wiki. Okay, this is a developer functionality. Let’s try that.
-
Ctrl+F on Developer page. No results.
-
Ctrl+F on API page. No results. Ctrl+F on Flat API page. No results
-
Ctrl+F on utils page. No results.
-
Ctrl+F on utils.utils page. No results.
-
Ctrl+F in my IDE. Results.
-
Fortunately, there’s only one result for def is_typeclass. If this was at_look, there would be
-several results, and I’d have to go through each of those individually, and most of them would just
-call return_appearance
-
-
An important part of a refactor, in my opinion, is separating out the “Tutorials” from the
-“Reference” documentation.
An often desired feature in a MUD is to show an in-game map to help navigation. The Static in-game
-map tutorial solves this by creating a static map, meaning the map is pre-
-drawn once and for all - the rooms are then created to match that map. When walking around, parts of
-the static map is then cut out and displayed next to the room description.
-
In this tutorial we’ll instead do it the other way around; We will dynamically draw the map based on
-the relationships we find between already existing rooms.
There are at least two requirements needed for this tutorial to work.
-
-
The structure of your mud has to follow a logical layout. Evennia supports the layout of your
-world to be ‘logically’ impossible with rooms looping to themselves or exits leading to the other
-side of the map. Exits can also be named anything, from “jumping out the window” to “into the fifth
-dimension”. This tutorial assumes you can only move in the cardinal directions (N, E, S and W).
-
Rooms must be connected and linked together for the map to be generated correctly. Vanilla
-Evennia comes with a admin command tunnel that allows a
-user to create rooms in the cardinal directions, but additional work is needed to assure that rooms
-are connected. For example, if you tunneleast and then immediately do tunnelwest you’ll find
-that you have created two completely stand-alone rooms. So care is needed if you want to create a
-“logical” layout. In this tutorial we assume you have such a grid of rooms that we can generate the
-map from.
Before getting into the code, it is beneficial to understand and conceptualize how this is going to
-work. The idea is analogous to a worm that starts at your current position. It chooses a direction
-and ‘walks’ outward from it, mapping its route as it goes. Once it has traveled a pre-set distance
-it stops and starts over in another direction. An important note is that we want a system which is
-easily callable and not too complicated. Therefore we will wrap this entire code into a custom
-Python class (not a typeclass as this doesn’t use any core objects from evennia itself).
-
We are going to create something that displays like this when you type ‘look’:
First we must define the components for displaying the map. For the “worm” to know what symbol to
-draw on the map we will have it check an Attribute on the room it visits called sector_type. For
-this tutorial we understand two symbols - a normal room and the room with us in it. We also define a
-fallback symbol for rooms without said Attribute - that way the map will still work even if we
-didn’t prepare the room correctly. Assuming your game folder is named mygame, we create this code
-in mygame/world/map.py.
-
# in mygame/world/map.py
-
-# the symbol is identified with a key "sector_type" on the
-# Room. Keys None and "you" must always exist.
-SYMBOLS={None:' . ',# for rooms without sector_type Attribute
- 'you':'[@]',
- 'SECT_INSIDE':'[.]'}
-
-
-
Since trying to access an unset Attribute returns None, this means rooms without the sector_type
-Atttribute will show as .. Next we start building the custom class Map. It will hold all
-methods we need.
self.caller is normally your Character object, the one using the map.
-
self.max_width/length determine the max width and length of the map that will be generated. Note
-that it’s important that these variables are set to odd numbers to make sure the display area has
-a center point.
-
self.worm_has_mapped is building off the worm analogy above. This dictionary will store all
-rooms the “worm” has mapped as well as its relative position within the grid. This is the most
-important variable as it acts as a ‘checker’ and ‘address book’ that is able to tell us where the
-worm has been and what it has mapped so far.
-
self.curX/Y are coordinates representing the worm’s current location on the grid.
-
-
Before any sort of mapping can actually be done we need to create an empty display area and do some
-sanity checks on it by using the following methods.
-
# in mygame/world/map.py
-
-classMap(object):
- # [... continued]
-
- defcreate_grid(self):
- # This method simply creates an empty grid/display area
- # with the specified variables from __init__(self):
- board=[]
- forrowinrange(self.max_width):
- board.append([])
- forcolumninrange(self.max_length):
- board[row].append(' ')
- returnboard
-
- defcheck_grid(self):
- # this method simply checks the grid to make sure
- # that both max_l and max_w are odd numbers.
- returnTrueifself.max_length%2!=0orself.max_width%2!=0\
- elseFalse
-
-
-
Before we can set our worm on its way, we need to know some of the computer science behind all this
-called ‘Graph Traversing’. In Pseudo code what we are trying to accomplish is this:
-
# pseudo code
-
-defdraw_room_on_map(room,max_distance):
- self.draw(room)
-
- ifmax_distance==0:
- return
-
- forexitinroom.exits:
- ifself.has_drawn(exit.destination):
- # skip drawing if we already visited the destination
- continue
- else:
- # first time here!
- self.draw_room_on_map(exit.destination,max_distance-1)
-
-
-
The beauty of Python is that our actual code of doing this doesn’t differ much if at all from this
-Pseudo code example.
-
-
max_distance is a variable indicating to our Worm how many rooms AWAY from your current location
-will it map. Obviously the larger the number the more time it will take if your current location has
-many many rooms around you.
-
-
The first hurdle here is what value to use for ‘max_distance’. There is no reason for the worm to
-travel further than what is actually displayed to you. For example, if your current location is
-placed in the center of a display area of size max_length=max_width=9, then the worm need only
-go 4 spaces in either direction:
-
[.][.][.][.][@][.][.][.][.]
- 432101234
-
-
-
The max_distance can be set dynamically based on the size of the display area. As your width/length
-changes it becomes a simple algebraic linear relationship which is simply max_distance=(min(max_width,max_length)-1)/2.
self.draw_room_on_map(self,room,max_distance) - the main method that ties it all together.`
-
-
Now that we know which methods we need, let’s refine our initial __init__(self) to pass some
-conditional statements and set it up to start building the display.
-
#mygame/world/map.py
-
-classMap(object):
-
- def__init__(self,caller,max_width=9,max_length=9):
- self.caller=caller
- self.max_width=max_width
- self.max_length=max_length
- self.worm_has_mapped={}
- self.curX=None
- self.curY=None
-
- ifself.check_grid():
- # we have to store the grid into a variable
- self.grid=self.create_grid()
- # we use the algebraic relationship
- self.draw_room_on_map(caller.location,
- ((min(max_width,max_length)-1)/2)
-
-
-
-
Here we check to see if the parameters for the grid are okay, then we create an empty canvas and map
-our initial location as the first room!
-
As mentioned above, the code for the self.draw_room_on_map() is not much different than the Pseudo
-code. The method is shown below:
-
# in mygame/world/map.py, in the Map class
-
-defdraw_room_on_map(self,room,max_distance):
- self.draw(room)
-
- ifmax_distance==0:
- return
-
- forexitinroom.exits:
- ifexit.namenotin("north","east","west","south"):
- # we only map in the cardinal directions. Mapping up/down would be
- # an interesting learning project for someone who wanted to try it.
- continue
- ifself.has_drawn(exit.destination):
- # we've been to the destination already, skip ahead.
- continue
-
- self.update_pos(room,exit.name.lower())
- self.draw_room_on_map(exit.destination,max_distance-1)
-
-
-
The first thing the “worm” does is to draw your current location in self.draw. Lets define that…
-
#in mygame/word/map.py, in the Map class
-
-defdraw(self,room):
- # draw initial ch location on map first!
- ifroom==self.caller.location:
- self.start_loc_on_grid()
- self.worm_has_mapped[room]=[self.curX,self.curY]
- else:
- # map all other rooms
- self.worm_has_mapped[room]=[self.curX,self.curY]
- # this will use the sector_type Attribute or None if not set.
- self.grid[self.curX][self.curY]=SYMBOLS[room.db.sector_type]
-
-
-
In self.start_loc_on_grid():
-
defmedian(self,num):
- lst=sorted(range(0,num))
- n=len(lst)
- m=n-1
- return(lst[n//2]+lst[m//2])/2.0
-
-defstart_loc_on_grid(self):
- x=self.median(self.max_width)
- y=self.median(self.max_length)
- # x and y are floats by default, can't index lists with float types
- x,y=int(x),int(y)
-
- self.grid[x][y]=SYMBOLS['you']
- self.curX,self.curY=x,y# updating worms current location
-
-
-
After the system has drawn the current map it checks to see if the max_distance is 0 (since this
-is the inital start phase it is not). Now we handle the iteration once we have each individual exit
-in the room. The first thing it does is check if the room the Worm is in has been mapped already…
-lets define that…
If has_drawn returns False that means the worm has found a room that hasn’t been mapped yet. It
-will then ‘move’ there. The self.curX/Y sort of lags behind, so we have to make sure to track the
-position of the worm; we do this in self.update_pos() below.
-
defupdate_pos(self,room,exit_name):
- # this ensures the coordinates stays up to date
- # to where the worm is currently at.
- self.curX,self.curY= \
- self.worm_has_mapped[room][0],self.worm_has_mapped[room][1]
-
- # now we have to actually move the pointer
- # variables depending on which 'exit' it found
- ifexit_name=='east':
- self.curY+=1
- elifexit_name=='west':
- self.curY-=1
- elifexit_name=='north':
- self.curX-=1
- elifexit_name=='south':
- self.curX+=1
-
-
-
Once the system updates the position of the worm it feeds the new room back into the original
-draw_room_on_map() and starts the process all over again…
-
That is essentially the entire thing. The final method is to bring it all together and make a nice
-presentational string out of it using the self.show_map() method.
In order for the map to get triggered we store it on the Room typeclass. If we put it in
-return_appearance we will get the map back every time we look at the room.
-
-
return_appearance is a default Evennia hook available on all objects; it is called e.g. by the
-look command to get the description of something (the room in this case).
-
-
# in mygame/typeclasses/rooms.py
-
-fromevenniaimportDefaultRoom
-fromworld.mapimportMap
-
-classRoom(DefaultRoom):
-
- defreturn_appearance(self,looker):
- # [...]
- string="%s\n"%Map(looker).show_map()
- # Add all the normal stuff like room description,
- # contents, exits etc.
- string+="\n"+super().return_appearance(looker)
- returnstring
-
-
-
Obviously this method of generating maps doesn’t take into account of any doors or exits that are
-hidden… etc… but hopefully it serves as a good base to start with. Like previously mentioned, it
-is very important to have a solid foundation on rooms before implementing this. You can try this on
-vanilla evennia by using @tunnel and essentially you can just create a long straight/edgy non-
-looping rooms that will show on your in-game map.
-
The above example will display the map above the room description. You could also use an
-EvTable to place description and map next to each other. Some other
-things you can do is to have a Command that displays with a larger radius, maybe with a
-legend and other features.
-
Below is the whole map.py for your reference. You need to update your Room typeclass (see above)
-to actually call it. Remember that to see different symbols for a location you also need to set the
-sector_type Attribute on the room to one of the keys in the SYMBOLS dictionary. So in this
-example, to make a room be mapped as [.] you would set the room’s sector_type to
-"SECT_INSIDE". Try it out with @sethere/sector_type="SECT_INSIDE". If you wanted all new
-rooms to have a given sector symbol, you could change the default in the SYMBOLS´dictionarybelow,oryoucouldaddtheAttributeintheRoom'sat_object_creation` method.
-
#mygame/world/map.py
-
-# These are keys set with the Attribute sector_type on the room.
-# The keys None and "you" must always exist.
-SYMBOLS={None:' . ',# for rooms without a sector_type attr
- 'you':'[@]',
- 'SECT_INSIDE':'[.]'}
-
-classMap(object):
-
- def__init__(self,caller,max_width=9,max_length=9):
- self.caller=caller
- self.max_width=max_width
- self.max_length=max_length
- self.worm_has_mapped={}
- self.curX=None
- self.curY=None
-
- ifself.check_grid():
- # we actually have to store the grid into a variable
- self.grid=self.create_grid()
- self.draw_room_on_map(caller.location,
- ((min(max_width,max_length)-1)/2))
-
- defupdate_pos(self,room,exit_name):
- # this ensures the pointer variables always
- # stays up to date to where the worm is currently at.
- self.curX,self.curY= \
- self.worm_has_mapped[room][0],self.worm_has_mapped[room][1]
-
- # now we have to actually move the pointer
- # variables depending on which 'exit' it found
- ifexit_name=='east':
- self.curY+=1
- elifexit_name=='west':
- self.curY-=1
- elifexit_name=='north':
- self.curX-=1
- elifexit_name=='south':
- self.curX+=1
-
- defdraw_room_on_map(self,room,max_distance):
- self.draw(room)
-
- ifmax_distance==0:
- return
-
- forexitinroom.exits:
- ifexit.namenotin("north","east","west","south"):
- # we only map in the cardinal directions. Mapping up/down would be
- # an interesting learning project for someone who wanted to try it.
- continue
- ifself.has_drawn(exit.destination):
- # we've been to the destination already, skip ahead.
- continue
-
- self.update_pos(room,exit.name.lower())
- self.draw_room_on_map(exit.destination,max_distance-1)
-
- defdraw(self,room):
- # draw initial caller location on map first!
- ifroom==self.caller.location:
- self.start_loc_on_grid()
- self.worm_has_mapped[room]=[self.curX,self.curY]
- else:
- # map all other rooms
- self.worm_has_mapped[room]=[self.curX,self.curY]
- # this will use the sector_type Attribute or None if not set.
- self.grid[self.curX][self.curY]=SYMBOLS[room.db.sector_type]
-
- defmedian(self,num):
- lst=sorted(range(0,num))
- n=len(lst)
- m=n-1
- return(lst[n//2]+lst[m//2])/2.0
-
- defstart_loc_on_grid(self):
- x=self.median(self.max_width)
- y=self.median(self.max_length)
- # x and y are floats by default, can't index lists with float types
- x,y=int(x),int(y)
-
- self.grid[x][y]=SYMBOLS['you']
- self.curX,self.curY=x,y# updating worms current location
-
-
- defhas_drawn(self,room):
- returnTrueifroominself.worm_has_mapped.keys()elseFalse
-
-
- defcreate_grid(self):
- # This method simply creates an empty grid
- # with the specified variables from __init__(self):
- board=[]
- forrowinrange(self.max_width):
- board.append([])
- forcolumninrange(self.max_length):
- board[row].append(' ')
- returnboard
-
- defcheck_grid(self):
- # this method simply checks the grid to make sure
- # both max_l and max_w are odd numbers
- returnTrueifself.max_length%2!=0or \
- self.max_width%2!=0elseFalse
-
- defshow_map(self):
- map_string=""
- forrowinself.grid:
- map_string+=" ".join(row)
- map_string+="\n"
-
- returnmap_string
-
The Dynamic map could be expanded with further capabilities. For example, it could mark exits or
-allow NE, SE etc directions as well. It could have colors for different terrain types. One could
-also look into up/down directions and figure out how to display that in a good way.
Evennia offers a powerful in-game line editor in evennia.utils.eveditor.EvEditor. This editor,
-mimicking the well-known VI line editor. It offers line-by-line editing, undo/redo, line deletes,
-search/replace, fill, dedent and more.
caller (Object or Account): The user of the editor.
-
loadfunc (callable, optional): This is a function called when the editor is first started. It
-is called with caller as its only argument. The return value from this function is used as the
-starting text in the editor buffer.
-
savefunc (callable, optional): This is called when the user saves their buffer in the editor is
-called with two arguments, caller and buffer, where buffer is the current buffer.
-
quitfunc (callable, optional): This is called when the user quits the editor. If given, all
-cleanup and exit messages to the user must be handled by this function.
-
key (str, optional): This text will be displayed as an identifier and reminder while editing.
-It has no other mechanical function.
-
persistent (default False): if set to True, the editor will survive a reboot.
If you set the persistent keyword to True when creating the editor, it will remain open even
-when reloading the game. In order to be persistent, an editor needs to have its callback functions
-(loadfunc, savefunc and quitfunc) as top-level functions defined in the module. Since these
-functions will be stored, Python will need to find them.
-
fromevenniaimportCommand
-fromevennia.utilsimporteveditor
-
-defload(caller):
- "get the current value"
- returncaller.attributes.get("test")
-
-defsave(caller,buffer):
- "save the buffer"
- caller.attributes.set("test",buffer)
-
-defquit(caller):
- "Since we define it, we must handle messages"
- caller.msg("Editor exited")
-
-classCmdSetTestAttr(Command):
- """
- Set the "test" Attribute using
- the line editor.
-
- Usage:
- settestattr
-
- """
- key="settestattr"
- deffunc(self):
- "Set up the callbacks and launch the editor"
- key="%s/test"%self.caller
- # launch the editor
- eveditor.EvEditor(self.caller,
- loadfunc=load,savefunc=save,quitfunc=quit,
- key=key,persistent=True)
-
The editor mimics the VIM editor as best as possible. The below is an excerpt of the return from
-the in-editor help command (:h).
-
<txt> - any non-command is appended to the end of the buffer.
- : <l> - view buffer or only line <l>
- :: <l> - view buffer without line numbers or other parsing
- ::: - print a ':' as the only character on the line...
- :h - this help.
-
- :w - save the buffer (don't quit)
- :wq - save buffer and quit
- :q - quit (will be asked to save if buffer was changed)
- :q! - quit without saving, no questions asked
-
- :u - (undo) step backwards in undo history
- :uu - (redo) step forward in undo history
- :UU - reset all changes back to initial state
-
- :dd <l> - delete line <n>
- :dw <l> <w> - delete word or regex <w> in entire buffer or on line <l>
- :DD - clear buffer
-
- :y <l> - yank (copy) line <l> to the copy buffer
- :x <l> - cut line <l> and store it in the copy buffer
- :p <l> - put (paste) previously copied line directly after <l>
- :i <l> <txt> - insert new text <txt> at line <l>. Old line will move down
- :r <l> <txt> - replace line <l> with text <txt>
- :I <l> <txt> - insert text at the beginning of line <l>
- :A <l> <txt> - append text after the end of line <l>
-
- :s <l> <w> <txt> - search/replace word or regex <w> in buffer or on line <l>
-
- :f <l> - flood-fill entire buffer or line <l>
- :fi <l> - indent entire buffer or line <l>
- :fd <l> - de-indent entire buffer or line <l>
-
- :echo - turn echoing of the input on/off (helpful for some clients)
-
- Legend:
- <l> - line numbers, or range lstart:lend, e.g. '3:7'.
- <w> - one word or several enclosed in quotes.
- <txt> - longer string, usually not needed to be enclosed in quotes.
-
The EvEditor is also used to edit some Python code in Evennia. The @py command supports an
-/edit switch that will open the EvEditor in code mode. This mode isn’t significantly different
-from the standard one, except it handles automatic indentation of blocks and a few options to
-control this behavior.
-
-
:< to remove a level of indentation for the future lines.
-
:+ to add a level of indentation for the future lines.
-
:= to disable automatic indentation altogether.
-
-
Automatic indentation is there to make code editing more simple. Python needs correct indentation,
-not as an aesthetic addition, but as a requirement to determine beginning and ending of blocks. The
-EvEditor will try to guess the next level of indentation. If you type a block “if”, for instance,
-the EvEditor will propose you an additional level of indentation at the next line. This feature
-cannot be perfect, however, and sometimes, you will have to use the above options to handle
-indentation.
-
:= can be used to turn automatic indentation off completely. This can be very useful when trying
-to paste several lines of code that are already correctly indented, for instance.
-
To see the EvEditor in code mode, you can use the @py/edit command. Type in your code (on one or
-several lines). You can then use the :w option (save without quitting) and the code you have
-typed will be executed. The :! will do the same thing. Executing code while not closing the
-editor can be useful if you want to test the code you have typed but add new lines after your test.
The EvMenu utility class is located in
-evennia/utils/evmenu.py.
-It allows for easily adding interactive menus to the game; for example to implement Character
-creation, building commands or similar. Below is an example of offering NPC conversation choices:
-
Theguardlooksatyoususpiciously.
-"No one is supposed to be in here ..."
-hesays,ahandonhisweapon.
-_______________________________________________
- 1.Trytobribehim[Cha+10gold]
- 2.Convincehimyouworkhere[Int]
- 3.Appealtohisvanity[Cha]
- 4.Trytoknockhimout[Luck+Dex]
- 5.Trytorunaway[Dex]
-
-
-
-
This is an example of a menu node. Think of a node as a point where the menu stops printing text
-and waits for user to give some input. By jumping to different nodes depending on the input, a menu
-is constructed.
The native way to define an EvMenu is to define Python functions, one per node. It will load all
-those
-functions/nodes either from a module or by being passed a dictionary mapping the node’s names to
-said functions, like {"nodename":<function>,...}. Since you are dealing with raw code, this is
-by
-far the most powerful way - for example you could have dynamic nodes that change content depending
-on game context, time and what you picked before.
For a simpler menu you often don’t need the full flexibility you get from defining each node as a
-Python function. For that, there is the EvMenu templating language. This allows you to define the
-menu
-in a more human-readable string with a simple format. This is then parsed to produce the
-{"nodename":<function>,...} mapping for you, for the EvMenu to use normally. The templating
-language is described in the Menu templating section.
Initializing the menu is done using a call to the evennia.utils.evmenu.EvMenu class. This is the
-most common way to do so - from inside a Command:
-
# in, for example gamedir/commands/command.py
-
-fromevennia.utils.evmenuimportEvMenu
-
-classCmdTestMenu(Command):
-
- key="testcommand"
-
- deffunc(self):
-
- EvMenu(caller,"world.mymenu")
-
-
-
-
When running this command, the menu will start using the menu nodes loaded from
-mygame/world/mymenu.py and use this to build the menu-tree - each function name becomes
-the name of a node in the tree. See next section on how to define menu nodes.
-
Alternatively, you could pass the menu-tree to EvMenu directly:
caller (Object or Account): is a reference to the object using the menu. This object will get a
-new CmdSet assigned to it, for handling the menu.
-
menu_data (str, module or dict): is a module or python path to a module where the global-level
-functions will each be considered to be a menu node. Their names in the module will be the names
-by which they are referred to in the module. Importantly, function names starting with an
-underscore
-_ will be ignored by the loader. Alternatively, this can be a direct mapping
-{"nodename":function,...}.
-
startnode (str): is the name of the menu-node to start the menu at. Changing this means that
-you can jump into a menu tree at different positions depending on circumstance and thus possibly
-re-use menu entries.
-
cmdset_mergetype (str): This is usually one of “Replace” or “Union” (see [CmdSets](Command-
-Sets).
-The first means that the menu is exclusive - the user has no access to any other commands while
-in the menu. The Union mergetype means the menu co-exists with previous commands (and may
-overload
-them, so be careful as to what to name your menu entries in this case).
-
cmdset_priority (int): The priority with which to merge in the menu cmdset. This allows for
-advanced usage.
-
auto_quit, auto_look, auto_help (bool): If either of these are True, the menu
-automatically makes a quit, look or help command available to the user. The main reason why
-you’d want to turn this off is if you want to use the aliases “q”, “l” or “h” for something in
-your
-menu. Nevertheless, at least quit is highly recommend - if False, the menu must itself
-supply
-an “exit node” (a node without any options), or the user will be stuck in the menu until the
-server
-reloads (or eternally if the menu is persistent)!
-
cmd_on_exit (str): This command string will be executed right after the menu has closed down.
-From experience, it’s useful to trigger a “look” command to make sure the user is aware of the
-change of state; but any command can be used. If set to None, no command will be triggered
-after
-exiting the menu.
-
persistent (bool) - if True, the menu will survive a reload (so the user will not be kicked
-out by the reload - make sure they can exit on their own!)
-
startnode_input (str or (str, dict) tuple): Pass an input text or a input text + kwargs to the
-start node as if it was entered on a fictional previous node. This can be very useful in order to
-start a menu differently depending on the Command’s arguments in which it was initialized.
-
session (Session): Useful when calling the menu from an Account in
-MULTISESSION_MODDE higher than 2, to make sure only the right Session sees the menu output.
-
debug (bool): If set, the menudebug command will be made available in the menu. Use it to
-list the current state of the menu and use menudebug<variable> to inspect a specific state
-variable from the list.
-
All other keyword arguments will be available as initial data for the nodes. They will be
-available in all nodes as properties on caller.ndb._menutree (see below). These will also
-survive a @reload if the menu is persistent.
-
-
You don’t need to store the EvMenu instance anywhere - the very act of initializing it will store it
-as caller.ndb._menutree on the caller. This object will be deleted automatically when the menu
-is exited and you can also use it to store your own temporary variables for access throughout the
-menu. Temporary variables you store on a persistent _menutree as it runs will
-not survive a @reload, only those you set as part of the original EvMenu call.
While all of the above forms are okay, it’s recommended to stick to the third and last form since
-it
-gives the most flexibility. The previous forms are mainly there for backwards compatibility with
-existing menus from a time when EvMenu was less able.
caller (Object or Account): The object using the menu - usually a Character but could also be a
-Session or Account depending on where the menu is used.
-
raw_string (str): If this is given, it will be set to the exact text the user entered on the
-previous node (that is, the command entered to get to this node). On the starting-node of the
-menu, this will be an empty string, unless startnode_input was set.
-
kwargs (dict): These extra keyword arguments are extra optional arguments passed to the node
-when the user makes a choice on the previous node. This may include things like status flags
-and details about which exact option was chosen (which can be impossible to determine from
-raw_string alone). Just what is passed in kwargs is up to you when you create the previous
-node.
The text variable is a string or tuple. This text is what will be displayed when the user reaches
-this node. If this is a tuple, then the first element of the tuple will be considered the displayed
-text and the second the help-text to display when the user enters the help command on this node.
-
text=("This is the text to display","This is the help text for this node")
-
-
-
Returning a None text is allowed and simply leads to a node with no text and only options. If the
-help text is not given, the menu will give a generic error message when using help.
The options list describe all the choices available to the user when viewing this node. If
-options is
-returned as None, it means that this node is an Exit node - any text is displayed and then the
-menu immediately exits, running the exit_cmd if given.
-
Otherwise, options should be a list (or tuple) of dictionaries, one for each option. If only one
-option is
-available, a single dictionary can also be returned. This is how it could look:
-
defnode_test(caller,raw_string,**kwargs):
-
- text="A goblin attacks you!"
-
- options=(
- {"key":("Attack","a","att"),
- "desc":"Strike the enemy with all your might",
- "goto":"node_attack"},
- {"key":("Defend","d","def"),
- "desc":"Hold back and defend yourself",
- "goto":(_defend,{"str":10,"enemyname":"Goblin"})})
-
- returntext,options
-
-
-
-
This will produce a menu node looking like this:
-
A goblin attacks you!
-________________________________
-
-Attack: Strike the enemy with all your might
-Defend: Hold back and defend yourself
-
-
The option’s key is what the user should enter in order to choose that option. If given as a
-tuple, the
-first string of that tuple will be what is shown on-screen while the rest are aliases for picking
-that option. In the above example, the user could enter “Attack” (or “attack”, it’s not
-case-sensitive), “a” or “att” in order to attack the goblin. Aliasing is useful for adding custom
-coloring to the choice. The first element of the aliasing tuple should then be the colored version,
-followed by a version without color - since otherwise the user would have to enter the color codes
-to select that choice.
-
Note that the key is optional. If no key is given, it will instead automatically be replaced
-with a running number starting from 1. If removing the key part of each option, the resulting
-menu node would look like this instead:
-
A goblin attacks you!
-________________________________
-
-1: Strike the enemy with all your might
-2: Hold back and defend yourself
-
-
-
-
Whether you want to use a key or rely on numbers is mostly
-a matter of style and the type of menu.
-
EvMenu accepts one important special key given only as "_default". This key is used when a user
-enters something that does not match any other fixed keys. It is particularly useful for getting
-user input:
-
defnode_readuser(caller,raw_string,**kwargs):
- text="Please enter your name"
-
- options={"key":"_default",
- "goto":"node_parse_input"}
-
- returntext,options
-
-
-
-
A "_default" option does not show up in the menu, so the above will just be a node saying
-"Pleaseenteryourname". The name they entered will appear as raw_string in the next node.
This simply contains the description as to what happens when selecting the menu option. For
-"_default" options or if the key is already long or descriptive, it is not strictly needed. But
-usually it’s better to keep the key short and put more detail in desc.
This is the operational part of the option and fires only when the user chooses said option. Here
-are three ways to write it
-
-def_action_two(caller,raw_string,**kwargs):
- # do things ...
- return"calculated_node_to_go_to"
-
-def_action_three(caller,raw_string,**kwargs):
- # do things ...
- return"node_four",{"mode":4}
-
-defnode_select(caller,raw_string,**kwargs):
-
- text=("select one",
- "help - they all do different things ...")
-
- options=({"desc":"Option one",
- "goto":"node_one"},
- {"desc":"Option two",
- "goto":_action_two},
- {"desc":"Option three",
- "goto":(_action_three,{"key":1,"key2":2})}
- )
-
- returntext,options
-
-
-
-
As seen above, goto could just be pointing to a single nodename string - the name of the node to
-go to. When given like this, EvMenu will look for a node named like this and call its associated
-function as
-
nodename(caller,raw_string,**kwargs)
-
-
-
Here, raw_string is always the input the user entered to make that choice and kwargs are the
-same as those kwargs that already entered the current node (they are passed on).
-
Alternatively the goto could point to a “goto-callable”. Such callables are usually defined in the
-same
-module as the menu nodes and given names starting with _ (to avoid being parsed as nodes
-themselves). These callables will be called the same as a node function - callable(caller,raw_string,**kwargs), where raw_string is what the user entered on this node and **kwargs is
-forwarded from the node’s own input.
-
The goto option key could also point to a tuple (callable,kwargs) - this allows for customizing
-the kwargs passed into the goto-callable, for example you could use the same callable but change the
-kwargs passed into it depending on which option was actually chosen.
-
The “goto callable” must either return a string "nodename" or a tuple ("nodename",mykwargs).
-This will lead to the next node being called as either nodename(caller,raw_string,**kwargs) or
-nodename(caller,raw_string,**mykwargs) - so this allows changing (or replacing) the options
-going
-into the next node depending on what option was chosen.
-
There is one important case - if the goto-callable returns None for a nodename, the current
-node will run again, possibly with different kwargs. This makes it very easy to re-use a node over
-and over, for example allowing different options to update some text form being passed and
-manipulated for every iteration.
-
-
The EvMenu also supports the exec option key. This allows for running a callable before the
-goto-callable. This functionality comes from a time before goto could be a callable and is
-deprecated as of Evennia 0.8. Use goto for all functionality where you’d before use exec.
When the menu starts, the EvMenu instance is stored on the caller as caller.ndb._menutree. Through
-this object you can in principle reach the menu’s internal state if you know what you are doing.
-This is also a good place to store temporary, more global variables that may be cumbersome to keep
-passing from node to node via the **kwargs. The _menutree will be deleted automatically when the
-menu closes, meaning you don’t need to worry about cleaning anything up.
-
If you want permanent state storage, it’s instead better to use an Attribute on caller. Remember
-that this will remain after the menu closes though, so you need to handle any needed cleanup
-yourself.
The EvMenu display of nodes, options etc are controlled by a series of formatting methods on the
-EvMenu class. To customize these, simply create a new child class of EvMenu and override as
-needed. Here is an example:
-
fromevennia.utils.evmenuimportEvMenu
-
-classMyEvMenu(EvMenu):
-
- defnodetext_formatter(self,nodetext):
- """
- Format the node text itself.
-
- Args:
- nodetext (str): The full node text (the text describing the node).
-
- Returns:
- nodetext (str): The formatted node text.
-
- """
-
- defhelptext_formatter(self,helptext):
- """
- Format the node's help text
-
- Args:
- helptext (str): The unformatted help text for the node.
-
- Returns:
- helptext (str): The formatted help text.
-
- """
-
- defoptions_formatter(self,optionlist):
- """
- Formats the option block.
-
- Args:
- optionlist (list): List of (key, description) tuples for every
- option related to this node.
- caller (Object, Account or None, optional): The caller of the node.
-
- Returns:
- options (str): The formatted option display.
-
- """
-
- defnode_formatter(self,nodetext,optionstext):
- """
- Formats the entirety of the node.
-
- Args:
- nodetext (str): The node text as returned by `self.nodetext_formatter`.
- optionstext (str): The options display as returned by `self.options_formatter`.
- caller (Object, Account or None, optional): The caller of the node.
-
- Returns:
- node (str): The formatted node to display.
-
- """
-
-
-
-
See evennia/utils/evmenu.py for the details of their default implementations.
The EvMenu is very powerful and flexible. But often your menu is simple enough to
-not require the full power of EvMenu. For this you can use the Evmenu templating language.
-
This is how the templating is used:
-
fromevennia.utils.evmenuimportparse_menu_template,EvMenu
-
-template_string="(will be described below)"
-# this could be empty if you don't need to access any callables
-# in your template
-goto_callables={"mycallable1":function,...}
-
-# generate the menutree
-menutree=parse_menu_template(caller,template_string,goto_callables)
-# a normal EvMenu call
-EvMenu(caller,menutree,...)
-
-
-
-
… So the parse_menu_template is just another way to generate the menutree dict needed by
-EvMenu - after this EvMenu works normally.
-
The good thing with this two-step procedude is that you can mix- and match - if you wanted
-you could insert a normal, fully flexible function-based node-function in the menutree before
-passing
-the whole thing into EvMenu and get the best of both worlds. It also makes it
-easy to substitute base EvMenu with a child class that changes the menu display.
-
… But if you really don’t need any such customization, you can also apply the template in one step
-using
-the template2menu helper:
-
fromevennia.utils.evmenuimporttemplate2menu
-
-template_string="(will be described below)"
-goto_callables={"mycallable1":function,...}
-
-template2menu(caller,template_string,goto_callables,startnode="start",...)
-
-
-
-
In addition to the template-related arguments, template2menu takes all the same **kwargs
-as EvMenu and will parse the template and start the menu for you in one go.
The template is a normal string with a very simple format. Each node begins
-with a marker ##Node<nameofnode>, follwowed by a ##Options separator (the Node and
-Options are
-case-insensitive).
-
template_string="""
-
-## NODE start
-
-<text for the node>
-
-## OPTIONS
-
-# this is a comment. Only line-comments are allowed.
-
-key;alias;alias: description -> goto_str_or_callable
-key;alias;alias: goto_str_or_callable
->pattern: goto_str_or_callable
-
-"""
-
-
-
-
The text after ##NODE defines the name of the node. This must be unique within the
-menu because this is what you use for goto statements. The name could have spaces.
-
The area between ##NODE and ##OPTIONS contains the text of the node. It can have
-normal formatting and will retain intentation.
-
The ##OPTIONS section, until the next ##NODE or the end of the string,
-holds the options, one per line.
-
Option-indenting is ignored but can be useful for readability.
-
The options-section can also have line-comments, marked by starting the line with #.
-
A node without a following ##OPTIONS section indicates an end node, and reaching
-it will print the text and immediately exit the menu (same as for regular EvMenu).
where you can enter next or n to go to the menu node named node2.
-
To skip the description, just add the goto without the ->:
-
next;n: node2
-
-
-
This will create a menu option without any description:
-
next
-
-
-
A special key is >. This acts as a pattern matcher. Between > and the : one
-can fit an optional pattern. This
-pattern will first be parsed with glob-style
-parsing and then
-with regex, and only if
-the player’s input matches either will the option be chosen. An input-matching
-option cannot have a description.
-
# this matches the empty string (just pressing return)
- >: node2
-
- # this matches input starting with 'test' (regex match)
- > ^test.+?: testnode
-
- # this matches any number input (regex match)
- > [0-9]+?: countnode
-
- # this matches everything not covered by previous options
- # (glob-matching, space is stripped without quotes)
- > *: node3
-
-
-
You can have multiple pattern-matchers for a node but remember that options are
-checked in the order they are listed. So make sure to put your pattern-matchers
-in decending order of generality; if you have a ‘catch-all’ pattern,
-it should be put last or those behind it will never be tried.
-
next;n:node2
- back;b:node1
- >:node2
-
-
-
The above would give you the option to write next/back but you can also just press return to move on
-to the next node.
Instead of giving the name of a node to go to, you can also give the name
-of a goto_callable, which in turn returns the name of the node to go to. You
-tell the template it’s a callable by simply adding () at the end.
-
next: Go to node 2 -> goto_node2()
-
-
-
You can also add keyword arguments:
-
back: myfunction(from=foo)
-
-
-
-
Note: ONLY keyword-arguments are supported! Trying to pass a positional
-argument will lead to an error.
-
-
The contents of the kwargs-values will be evaluated by literal_eval so
-you don’t need to add quotes to strings unless they have spaces in them. Numbers
-will be converted correctly, but more complex input structures (like lists or dicts) will
-not - if you want more complex input you should use a full function-based EvMenu
-node instead.
-
The goto-callable is defined just like any Evmenu goto-func. You must always
-use the full form (including **kwargs):
Return None to re-run the current node. Any keyword arguments you specify in
-your template will be passed to your goto-callable in **kwargs. Unlike in
-regular EvMenu nodes you can’t return kwargs to pass it between nodes and other dynamic
-tricks.
-
All goto-callables you use in your menu-template must be added to the
-goto_callable mapping that you pass to parse_menu_template or
-template2menu.
-template_string="""
-
-## NODE start
-
-This is the text of the start node.
-Both ## NODE, ## node or ## Node works. The node-name can have
-spaces.
-
-The text area can have multiple lines, line breaks etc.
-
-## OPTIONS
-
- # here starts the option-defition
- # comments are only allowed from beginning of line.
- # Indenting is not necessary, but good for readability
-
- 1: Option number 1 -> node1
- 2: Option number 2 -> node2
- next: This steps next -> go_back()
- # the -> can be ignored if there is no desc
- back: go_back(from_node=start)
- abort: abort
-
-# ----------------------------------- this is ignored
-
-## NODE node1
-
-Text for Node1. Enter a message!
-<return> to go back.
-
-## options
-
- # Starting the option-line with >
- # allows to perform different actions depending on
- # what is inserted.
-
- # this catches everything starting with foo
- > foo*: handle_foo_message()
-
- # regex are also allowed (this catches number inputs)
- > [0-9]+?: handle_numbers()
-
- # this catches the empty return
- >: start
-
- # this catches everything else
- > *: handle_message(from_node=node1)
-
-# -----------------------------------------
-
-## NODE node2
-
-Text for Node2. Just go back.
-
-## options
-
- >: start
-
-# node abort
-
-This exits the menu since there is no `## options` section.
-
-
-"""
-
-# we assume the callables are defined earlier
-goto_callables={"go_back":go_back_func,
- "handle_foo_message":handle_message,
- "handle_numbers":my_number_handler,
- "handle_message":handle_message2}
-
-# boom - a menu
-template2menu(caller,template_string,goto_callables)
-
-
Yes/No prompt - entering text with limited possible responses
-(this is not using EvMenu but the conceptually similar yet technically unrelated get_input
-helper function accessed as evennia.utils.evmenu.get_input).
Below is an example of a simple branching menu node leading to different other nodes depending on
-choice:
-
# in mygame/world/mychargen.py
-
-defdefine_character(caller):
- text= \
- """
- What aspect of your character do you want
- to change next?
- """
- options=({"desc":"Change the name",
- "goto":"set_name"},
- {"desc":"Change the description",
- "goto":"set_description"})
- returntext,options
-
-EvMenu(caller,"world.mychargen",startnode="define_character")
-
-
-
-
This will result in the following node display:
-
What aspect of your character do you want
-to change next?
-_________________________
-1: Change the name
-2: Change the description
-
-
-
Note that since we didn’t specify the “name” key, EvMenu will let the user enter numbers instead. In
-the following examples we will not include the EvMenu call but just show nodes running inside the
-menu. Also, since EvMenu also takes a dictionary to describe the menu, we could have called it
-like this instead in the example:
-def_is_in_mage_guild(caller,raw_string,**kwargs):
- ifcaller.tags.get('mage',category="guild_member"):
- return"mage_guild_welcome"
- else:
- return"mage_guild_blocked"
-
-defenter_guild:
- text='You say to the mage guard:'
- options({'desc':'I need to get in there.',
- 'goto':_is_in_mage_guild},
- {'desc':'Never mind',
- 'goto':'end_conversation'})
- returntext,options
-
-
-
This simple callable goto will analyse what happens depending on who the caller is. The
-enter_guild node will give you a choice of what to say to the guard. If you try to enter, you will
-end up in different nodes depending on (in this example) if you have the right Tag set on
-yourself or not. Note that since we don’t include any ‘key’s in the option dictionary, you will just
-get to pick between numbers.
Here is an example of passing arguments into the goto callable and use that to influence
-which node it should go to next:
-
-def_set_attribute(caller,raw_string,**kwargs):
- "Get which attribute to modify and set it"
-
- attrname,value=kwargs.get("attr",(None,None))
- next_node=kwargs.get("next_node")
-
- caller.attributes.add(attrname,attrvalue)
-
- returnnext_node
-
-
-defnode_background(caller):
- text= \
- """
- {} experienced a traumatic event
- in their childhood. What was it?
- """.format(caller.key}
-
- options=({"key":"death",
- "desc":"A violent death in the family",
- "goto":(_set_attribute,{"attr":("experienced_violence",True),
- "next_node":"node_violent_background"})},
- {"key":"betrayal",
- "desc":"The betrayal of a trusted grown-up",
- "goto":(_set_attribute,{"attr":("experienced_betrayal",True),
- "next_node":"node_betrayal_background"})})
- returntext,options
-
-
-
This will give the following output:
-
Kovash the magnificent experienced a traumatic event
-in their childhood. What was it?
-____________________________________________________
-death: A violent death in the family
-betrayal: The betrayal of a trusted grown-up
-
-
-
-
Note above how we use the _set_attribute helper function to set the attribute depending on the
-User’s choice. In thie case the helper function doesn’t know anything about what node called it - we
-even tell it which nodename it should return, so the choices leads to different paths in the menu.
-We could also imagine the helper function analyzing what other choices
An example of the menu asking the user for input - any input.
-
-def_set_name(caller,raw_string,**kwargs):
-
- inp=raw_string.strip()
-
- prev_entry=kwargs.get("prev_entry")
-
- ifnotinp:
- # a blank input either means OK or Abort
- ifprev_entry:
- caller.key=prev_entry
- caller.msg("Set name to {}.".format(prev_entry))
- return"node_background"
- else:
- caller.msg("Aborted.")
- return"node_exit"
- else:
- # re-run old node, but pass in the name given
- returnNone,{"prev_entry":inp}
-
-
-defenter_name(caller,raw_string,**kwargs):
-
- # check if we already entered a name before
- prev_entry=kwargs.get("prev_entry")
-
- ifprev_entry:
- text="Current name: {}.\nEnter another name or <return> to accept."
- else:
- text="Enter your character's name or <return> to abort."
-
- options={"key":"_default",
- "goto":(_set_name,{"prev_entry":prev_entry})}
-
- returntext,options
-
-
-
-
This will display as
-
Enteryourcharacter's name or <return> to abort.
-
->Gandalf
-
-Currentname:Gandalf
-Enteranothernameor<return>toaccept.
-
->
-
-SetnametoGandalf.
-
-
-
-
Here we re-use the same node twice for reading the input data from the user. Whatever we enter will
-be caught by the _default option and passed into the helper function. We also pass along whatever
-name we have entered before. This allows us to react correctly on an “empty” input - continue to the
-node named "node_background" if we accept the input or go to an exit node if we presses Return
-without entering anything. By returning None from the helper function we automatically re-run the
-previous node, but updating its ingoing kwargs to tell it to display a different text.
A convenient way to store data is to store it on the caller.ndb._menutree which you can reach from
-every node. The advantage of doing this is that the _menutree NAttribute will be deleted
-automatically when you exit the menu.
-
-def_set_name(caller,raw_string,**kwargs):
-
- caller.ndb._menutree.charactersheet={}
- caller.ndb._menutree.charactersheet['name']=raw_string
- caller.msg("You set your name to {}".format(raw_string)
- return"background"
-
-defnode_set_name(caller):
- text='Enter your name:'
- options={'key':'_default',
- 'goto':_set_name}
-
- returntext,options
-
-...
-
-
-defnode_view_sheet(caller):
- text="Character sheet:\n{}".format(self.ndb._menutree.charactersheet)
-
- options=({"key":"Accept",
- "goto":"finish_chargen"},
- {"key":"Decline",
- "goto":"start_over"})
-
- returntext,options
-
-
-
-
Instead of passing the character sheet along from node to node through the kwargs we instead
-set it up temporarily on caller.ndb._menutree.charactersheet. This makes it easy to reach from
-all nodes. At the end we look at it and, if we accept the character the menu will likely save the
-result to permanent storage and exit.
-
-
One point to remember though is that storage on caller.ndb._menutree is not persistent across
-@reloads. If you are using a persistent menu (using EvMenu(...,persistent=True) you should
-use
-caller.db to store in-menu data like this as well. You must then yourself make sure to clean it
-when the user exits the menu.
Sometimes you want to make a chain of menu nodes one after another, but you don’t want the user to
-be able to continue to the next node until you have verified that what they input in the previous
-node is ok. A common example is a login menu:
-
-def_check_username(caller,raw_string,**kwargs):
- # we assume lookup_username() exists
- ifnotlookup_username(raw_string):
- # re-run current node by returning `None`
- caller.msg("|rUsername not found. Try again.")
- returnNone
- else:
- # username ok - continue to next node
- return"node_password"
-
-
-defnode_username(caller):
- text="Please enter your user name."
- options={"key":"_default",
- "goto":_check_username}
- returntext,options
-
-
-def_check_password(caller,raw_string,**kwargs):
-
- nattempts=kwargs.get("nattempts",0)
- ifnattempts>3:
- caller.msg("Too many failed attempts. Logging out")
- return"node_abort"
- elifnotvalidate_password(raw_string):
- caller.msg("Password error. Try again.")
- returnNone,{"nattempts",nattempts+1}
- else:
- # password accepted
- return"node_login"
-
-defnode_password(caller,raw_string,**kwargs):
- text="Enter your password."
- options={"key":"_default",
- "goto":_check_password}
- returntext,options
-
-
Here the goto-callables will return to the previous node if there is an error. In the case of
-password attempts, this will tick up the nattempts argument that will get passed on from iteration
-to iteration until too many attempts have been made.
You can also define your nodes directly in a dictionary to feed into the EvMenu creator.
-
defmynode(caller):
- # a normal menu node function
- returntext,options
-
-menu_data={"node1":mynode,
- "node2":lambdacaller:(
- "This is the node text",
- ({"key":"lambda node 1",
- "desc":"go to node 1 (mynode)",
- "goto":"node1"},
- {"key":"lambda node 2",
- "desc":"go to thirdnode",
- "goto":"node3"})),
- "node3":lambdacaller,raw_string:(
- # ... etc ) }
-
-# start menu, assuming 'caller' is available from earlier
-EvMenu(caller,menu_data,startnode="node1")
-
-
-
-
The keys of the dictionary become the node identifiers. You can use any callable on the right form
-to describe each node. If you use Python lambda expressions you can make nodes really on the fly.
-If you do, the lambda expression must accept one or two arguments and always return a tuple with two
-elements (the text of the node and its options), same as any menu node function.
-
Creating menus like this is one way to present a menu that changes with the circumstances - you
-could for example remove or add nodes before launching the menu depending on some criteria. The
-drawback is that a lambda expression is much more
-limited than a full
-function - for example you can’t use other Python keywords like if inside the body of the
-lambda.
-
Unless you are dealing with a relatively simple dynamic menu, defining menus with lambda’s is
-probably more work than it’s worth: You can create dynamic menus by instead making each node
-function more clever. See the NPC shop tutorial for an example of this.
This describes two ways for asking for simple questions from the user. Using Python’s input
-will not work in Evennia. input will block the entire server for everyone until that one
-player has entered their text, which is not what you want.
In the func method of your Commands (only) you can use Python’s built-in yield command to
-request input in a similar way to input. It looks like this:
-
result=yield("Please enter your answer:")
-
-
-
This will send “Please enter your answer” to the Command’s self.caller and then pause at that
-point. All other players at the server will be unaffected. Once caller enteres a reply, the code
-execution will continue and you can do stuff with the result. Here is an example:
-
fromevenniaimportCommand
-classCmdTestInput(Command):
- key="test"
- deffunc(self):
- result=yield("Please enter something:")
- self.caller.msg(f"You entered {result}.")
- result2=yield("Now enter something else:")
- self.caller.msg(f"You now entered {result2}.")
-
-
-
Using yield is simple and intuitive, but it will only access input from self.caller and you
-cannot abort or time out the pause until the player has responded. Under the hood, it is actually
-just a wrapper calling get_input described in the following section.
-
-
Important Note: In Python you cannot mix yield and return<value> in the same method. It has
-to do with yield turning the method into a
-generator. A return without an argument works, you
-can just not do return<value>. This is usually not something you need to do in func() anyway,
-but worth keeping in mind.
The evmenu module offers a helper function named get_input. This is wrapped by the yield
-statement which is often easier and more intuitive to use. But get_input offers more flexibility
-and power if you need it. While in the same module as EvMenu, get_input is technically unrelated
-to it. The get_input allows you to ask and receive simple one-line input from the user without
-launching the full power of a menu to do so. To use, call get_input like this:
-
get_input(caller,prompt,callback)
-
-
-
Here caller is the entity that should receive the prompt for input given as prompt. The
-callback is a callable function(caller,prompt,user_input) that you define to handle the answer
-from the user. When run, the caller will see prompt appear on their screens and any text they
-enter will be sent into the callback for whatever processing you want.
-
Below is a fully explained callback and example call:
-
fromevenniaimportCommand
-fromevennia.utils.evmenuimportget_input
-
-defcallback(caller,prompt,user_input):
- """
- This is a callback you define yourself.
-
- Args:
- caller (Account or Object): The one being asked
- for input
- prompt (str): A copy of the current prompt
- user_input (str): The input from the account.
-
- Returns:
- repeat (bool): If not set or False, exit the
- input prompt and clean up. If returning anything
- True, stay in the prompt, which means this callback
- will be called again with the next user input.
- """
- caller.msg(f"When asked '{prompt}', you answered '{user_input}'.")
-
-get_input(caller,"Write something! ",callback)
-
Normally, the get_input function quits after any input, but as seen in the example docs, you could
-return True from the callback to repeat the prompt until you pass whatever check you want.
-
-
Note: You cannot link consecutive questions by putting a new get_input call inside the
-callback. If you want that you should use an EvMenu instead (see the Repeating the same
-node example above). Otherwise you can either peek at the
-implementation of get_input and implement your own mechanism (it’s just using cmdset nesting) or
-you can look at this extension suggested on the mailing
-list.
Below is an example of a Yes/No prompt using the get_input function:
-
defyesno(caller,prompt,result):
- ifresult.lower()in("y","yes","n","no"):
- # do stuff to handle the yes/no answer
- # ...
- # if we return None/False the prompt state
- # will quit after this
- else:
- # the answer is not on the right yes/no form
- caller.msg("Please answer Yes or No. \n{prompt}")
-@# returning True will make sure the prompt state is not exited
- returnTrue
-
-# ask the question
-get_input(caller,"Is Evennia great (Yes/No)?",yesno)
-
The evennia.utils.evmenu.list_node is an advanced decorator for use with EvMenu node functions.
-It is used to quickly create menus for manipulating large numbers of items.
The menu will automatically create an multi-page option listing that one can flip through. One can
-inpect each entry and then select them with prev/next. This is how it is used:
-
fromevennia.utils.evmenuimportlist_node
-
-
-...
-
-_options(caller):
- return['option1','option2',...'option100']
-
-_select(caller,menuchoice,available_choices):
- # analyze choice
- returnnode_matching_the_choice
-
-@list_node(_options,select=_select,pagesize=10)
-defnode_mylist(caller,raw_string,**kwargs):
- ...
-
- # the decorator auto-creates the options; any options
- # returned here would be appended to the auto-options
- returnnode_text,{}
-
-
-
The options argument to list_node is either a list, a generator or a callable returning a list
-of strings for each option that should be displayed in the node.
-
The select is a callable in the example above but could also be the name of a menu node. If a
-callable, the menuchoice argument holds the selection done and available_choices holds all the
-options available. The callable should return the menu to go to depending on the selection (or
-None to rerun the same node). If the name of a menu node, the selection will be passed as
-selection kwarg to that node.
-
The decorated node itself should return text to display in the node. It must return at least an
-empty dictionary for its options. It returning options, those will supplement the options
-auto-created by the list_node decorator.
The EvMenu is implemented using Commands. When you start a new EvMenu, the user of the
-menu will be assigned a CmdSet with the commands they need to navigate the menu.
-This means that if you were to, from inside the menu, assign a new command set to the caller, you
-may override the Menu Cmdset and kill the menu. If you want to assign cmdsets to the caller as part
-of the menu, you should store the cmdset on caller.ndb._menutree and wait to actually assign it
-until the exit node.
When sending a very long text to a user client, it might scroll beyond of the height of the client
-window. The evennia.utils.evmore.EvMore class gives the user the in-game ability to only view one
-page of text at a time. It is usually used via its access function, evmore.msg.
-
The name comes from the famous unix pager utility more which performs just this function.
Where receiver is an Object or a Account. If the text is longer than the
-client’s screen height (as determined by the NAWS handshake or by settings.CLIENT_DEFAULT_HEIGHT)
-the pager will show up, something like this:
-
-
[…]
-aute irure dolor in reprehenderit in voluptate velit
-esse cillum dolore eu fugiat nulla pariatur. Excepteur
-sint occaecat cupidatat non proident, sunt in culpa qui
-officia deserunt mollit anim id est laborum.
-
-
-
(more [1/6] return|back|top|end|abort)
-
-
where the user will be able to hit the return key to move to the next page, or use the suggested
-commands to jump to previous pages, to the top or bottom of the document as well as abort the
-paging.
-
The pager takes several more keyword arguments for controlling the message output. See the
-evmore-API for more info.
The Evennia game index is a list of games built or
-being built with Evennia. Anyone is allowed to add their game to the index
-
-
also if you have just started development and don’t yet accept external
-players. It’s a chance for us to know you are out there and for you to make us
-intrigued about or excited for your upcoming game!
-
-
All we ask is that you check so your game-name does not collide with one
-already in the list - be nice!
This will start the Evennia Connection wizard. From the menu, select to add
-your game to the Evennia Game Index. Follow the prompts and don’t forget to
-save your new settings in the end. Use quit at any time if you change your
-mind.
-
-
The wizard will create a new file mygame/server/conf/connection_settings.py
-with the settings you chose. This is imported from the end of your main
-settings file and will thus override it. You can edit this new file if you
-want, but remember that if you run the wizard again, your changes may get
-over-written.
If you don’t want to use the wizard (maybe because you already have the client installed from an
-earlier version), you can also configure your index entry in your settings file
-(mygame/server/conf/settings.py). Add the following:
-
GAME_INDEX_ENABLED=True
-
-GAME_INDEX_LISTING={
- # required
- 'game_status':'pre-alpha',# pre-alpha, alpha, beta, launched
- 'listing_contact':"dummy@dummy.com",# not publicly shown.
- 'short_description':'Short blurb',
-
- # optional
- 'long_description':
- "Longer description that can use Markdown like *bold*, _italic_"
- "and [linkname](http://link.com). Use \n for line breaks."
- 'telnet_hostname':'dummy.com',
- 'telnet_port':'1234',
- 'web_client_url':'dummy.com/webclient',
- 'game_website':'dummy.com',
- # 'game_name': 'MyGame', # set only if different than settings.SERVERNAME
-}
-
-
-
Of these, the game_status, short_description and listing_contact are
-required. The listing_contact is not publicly visible and is only meant as a
-last resort if we need to get in touch with you over any listing issue/bug (so
-far this has never happened).
-
If game_name is not set, the settings.SERVERNAME will be used. Use empty strings
-('') for optional fields you don’t want to specify at this time.
If you don’t specify neither telnet_hostname+port nor
-web_client_url, the Game index will list your game as Not yet public.
-Non-public games are moved to the bottom of the index since there is no way
-for people to try them out. But it’s a good way to show you are out there, even
-if you are not ready for players yet.
A MUD (originally Multi-User Dungeon, with later variants Multi-User Dimension and Multi-User
-Domain) is a multiplayer real-time virtual world described primarily in text. MUDs combine elements
-of role-playing games, hack and slash, player versus player, interactive fiction and online chat.
-Players can read or view descriptions of rooms, objects, other players, non-player characters, and
-actions performed in the virtual world. Players typically interact with each other and the world by
-typing commands that resemble a natural language. - Wikipedia
-
-
If you are reading this, it’s quite likely you are dreaming of creating and running a text-based
-massively-multiplayer game (MUD/MUX/MUSH etc) of your very own. You
-might just be starting to think about it, or you might have lugged around that perfect game in
-your mind for years … you know just how good it would be, if you could only make it come to
-reality. We know how you feel. That is, after all, why Evennia came to be.
-
Evennia is in principle a MUD-building system: a bare-bones Python codebase and server intended to
-be highly extendable for any style of game. “Bare-bones” in this context means that we try to impose
-as few game-specific things on you as possible. So whereas we for convenience offer basic building
-blocks like objects, characters, rooms, default commands for building and administration etc, we
-don’t prescribe any combat rules, mob AI, races, skills, character classes or other things that will
-be different from game to game anyway. It is possible that we will offer some such systems as
-contributions in the future, but these will in that case all be optional.
-
What we do however, is to provide a solid foundation for all the boring database, networking, and
-behind-the-scenes administration stuff that all online games need whether they like it or not.
-Evennia is fully persistent, that means things you drop on the ground somewhere will still be
-there a dozen server reboots later. Through Django we support a large variety of different database
-systems (a database is created for you automatically if you use the defaults).
-
Using the full power of Python throughout the server offers some distinct advantages. All your
-coding, from object definitions and custom commands to AI scripts and economic systems is done in
-normal Python modules rather than some ad-hoc scripting language. The fact that you script the game
-in the same high-level language that you code it in allows for very powerful and custom game
-implementations indeed.
-
The server ships with a default set of player commands that are similar to the MUX command set. We
-do not aim specifically to be a MUX server, but we had to pick some default to go with (see
-this for more about our original motivations). It’s easy to remove or add commands, or
-to have the command syntax mimic other systems, like Diku, LP, MOO and so on. Or why not create a
-new and better command system of your own design.
Evennia’s demo server can be found at demo.evennia.com. If you prefer to
-connect to the demo via your own telnet client you can do so at silvren.com, port 4280. Here is
-a screenshot.
-
Once you installed Evennia yourself it comes with its own tutorial - this shows off some of the
-possibilities and gives you a small single-player quest to play. The tutorial takes only one
-single in-game command to install as explained here.
Game development is done by the server importing your normal Python modules. Specific server
-features are implemented by overloading hooks that the engine calls appropriately.
-
All game entities are simply Python classes that handle database negotiations behind the scenes
-without you needing to worry.
-
Command sets are stored on individual objects (including characters) to offer unique functionality
-and object-specific commands. Sets can be updated and modified on the fly to expand/limit player
-input options during play.
-
Scripts are used to offer asynchronous/timed execution abilities. Scripts can also be persistent.
-There are easy mechanisms to thread particularly long-running processes and built-in ways to start
-“tickers” for games that wants them.
-
In-game communication channels are modular and can be modified to any functionality, including
-mailing systems and full logging of all messages.
-
Server can be fully rebooted/reloaded without users disconnecting.
-
An Account can freely connect/disconnect from game-objects, offering an easy way to implement
-multi-character systems and puppeting.
-
Each Account can optionally control multiple Characters/Objects at the same time using the same
-login information.
-
Spawning of individual objects via a prototypes-like system.
-
Tagging can be used to implement zones and object groupings.
-
All source code is extensively documented.
-
Unit-testing suite, including tests of default commands and plugins.
Assuming you have Evennia working (see the quick start instructions) and have
-gotten as far as to start the server and connect to it with the client of your choice, here’s what
-you need to know depending on your skills and needs.
-
-
I don’t know (or don’t want to do) any programming - I just want to run a game!¶
-
Evennia comes with a default set of commands for the Python newbies and for those who need to get a
-game running now. Stock Evennia is enough for running a simple ‘Talker’-type game - you can build
-and describe rooms and basic objects, have chat channels, do emotes and other things suitable for a
-social or free-form MU*. Combat, mobs and other game elements are not included, so you’ll have a
-very basic game indeed if you are not willing to do at least some coding.
Evennia’s source code is extensively documented and is viewable online.
-We also have a comprehensive online manual with lots of examples.
-But while Python is
-considered a very easy programming language to get into, you do have a learning curve to climb if
-you are new to programming. You should probably sit down
-with a Python beginner’s tutorial (there are plenty of them on
-the web if you look around) so you at least know what you are seeing. See also our
-link page for some reading suggestions. To efficiently code your dream game in
-Evennia you don’t need to be a Python guru, but you do need to be able to read example code
-containing at least these basic Python features:
Obviously, the more things you feel comfortable with, the easier time you’ll have to find your way.
-With just basic knowledge you should be able to define your own Commands, create custom
-Objects as well as make your world come alive with basic Scripts. You can
-definitely build a whole advanced and customized game from extending Evennia’s examples only.
-
-
-
I know my Python stuff and I am willing to use it!¶
-
Even if you started out as a Python beginner, you will likely get to this point after working on
-your game for a while. With more general knowledge in Python the full power of Evennia opens up for
-you. Apart from modifying commands, objects and scripts, you can develop everything from advanced
-mob AI and economic systems, through sophisticated combat and social mini games, to redefining how
-commands, players, rooms or channels themselves work. Since you code your game by importing normal
-Python modules, there are few limits to what you can accomplish.
-
If you also happen to know some web programming (HTML, CSS, Javascript) there is also a web
-presence (a website and a mud web client) to play around with …
From here you can continue browsing the online documentation to
-find more info about Evennia. Or you can jump into the Tutorials and get your hands
-dirty with code right away. You can also read the developer’s dev blog for many tidbits and snippets about Evennia’s development and
-structure.
-
Some more hints:
-
-
Get engaged in the community. Make an introductory post to our mailing list/forum and get to know people. It’s also
-highly recommended you hop onto our Developer chat
-on IRC. This allows you to chat directly with other developers new and old as well as with the devs
-of Evennia itself. This chat is logged (you can find links on http://www.evennia.com) and can also
-be searched from the same place for discussion topics you are interested in.
-
Read the Game Planning wiki page. It gives some ideas for your work flow and the
-state of mind you should aim for - including cutting down the scope of your game for its first
-release.
-
Do the Tutorial for basic MUSH-like game carefully from
-beginning to end and try to understand what does what. Even if you are not interested in a MUSH for
-your own game, you will end up with a small (very small) game that you can build or learn from.
Evennia represents a learning curve for those who used to code on
-Diku type MUDs. While coding in Python is easy if you
-already know C, the main effort is to get rid of old C programming habits. Trying to code Python the
-way you code C will not only look ugly, it will lead to less optimal and harder to maintain code.
-Reading Evennia example code is a good way to get a feel for how different problems are approached
-in Python.
-
Overall, Python offers an extensive library of resources, safe memory management and excellent
-handling of errors. While Python code does not run as fast as raw C code does, the difference is not
-all that important for a text-based game. The main advantage of Python is an extremely fast
-development cycle with and easy ways to create game systems that would take many times more code and
-be much harder to make stable and maintainable in C.
As mentioned, the main difference between Evennia and a Diku-derived codebase is that Evennia is
-written purely in Python. Since Python is an interpreted language there is no compile stage. It is
-modified and extended by the server loading Python modules at run-time. It also runs on all computer
-platforms Python runs on (which is basically everywhere).
-
Vanilla Diku type engines save their data in custom flat file type storage solutions. By
-contrast, Evennia stores all game data in one of several supported SQL databases. Whereas flat files
-have the advantage of being easier to implement, they (normally) lack many expected safety features
-and ways to effectively extract subsets of the stored data. For example, if the server loses power
-while writing to a flatfile it may become corrupt and the data lost. A proper database solution is
-not susceptible to this - at no point is the data in a state where it cannot be recovered. Databases
-are also highly optimized for querying large data sets efficiently.
Diku expresses the character object referenced normally by:
-
structcharch* then all character-related fields can be accessed by ch->. In Evennia, one must
-pay attention to what object you are using, and when you are accessing another through back-
-handling, that you are accessing the right object. In Diku C, accessing character object is normally
-done by:
-
/* creating pointer of both character and room struct */
-
-void(structcharch*,structroomroom*){
-intdam;
-if(ROOM_FLAGGED(room,ROOM_LAVA)){
-dam=100
-ch->damage_taken=dam
-};
-};
-
-
-
As an example for creating Commands in Evennia via the fromevenniaimportCommand the character
-object that calls the command is denoted by a class property as self.caller. In this example
-self.caller is essentially the ‘object’ that has called the Command, but most of the time it is an
-Account object. For a more familiar Diku feel, create a variable that becomes the account object as:
-
#mygame/commands/command.py
-
-fromevenniaimportCommand
-
-classCmdMyCmd(Command):
- """
- This is a Command Evennia Object
- """
-
- [...]
-
- deffunc(self):
- ch=self.caller
- # then you can access the account object directly by using the familiar ch.
- ch.msg("...")
- account_name=ch.name
- race=ch.db.race
-
-
-
-
As mentioned above, care must be taken what specific object you are working with. If focused on a
-room object and you need to access the account object:
-
#mygame/typeclasses/room.py
-
-fromevenniaimportDefaultRoom
-
-classMyRoom(DefaultRoom):
- [...]
-
- defis_account_object(self,object):
- # a test to see if object is an account
- [...]
-
- defmyMethod(self):
- #self.caller would not make any sense, since self refers to the
- # object of 'DefaultRoom', you must find the character obj first:
- forchinself.contents:
- ifself.is_account_object(ch):
- # now you can access the account object with ch:
- account_name=ch.name
- race=ch.db.race
-
-
-
-
-
Emulating Evennia to Look and Feel Like A Diku/ROM¶
-
To emulate a Diku Mud on Evennia some work has to be done before hand. If there is anything that all
-coders and builders remember from Diku/Rom days is the presence of VNUMs. Essentially all data was
-saved in flat files and indexed by VNUMs for easy access. Evennia has the ability to emulate VNUMS
-to the extent of categorising rooms/mobs/objs/trigger/zones[…] into vnum ranges.
-
Evennia has objects that are called Scripts. As defined, they are the ‘out of game’ instances that
-exist within the mud, but never directly interacted with. Scripts can be used for timers, mob AI,
-and even a stand alone databases.
-
Because of their wonderful structure all mob, room, zone, triggers, etc… data can be saved in
-independently created global scripts.
-
Here is a sample mob file from a Diku Derived flat file.
-
#0
-mob0~
-mob0~
-mob0
-~
- Mob0
-~
-10 0 0 0 0 0 0 0 0 E
-1 20 9 0d0+10 1d2+0
-10 100
-8 8 0
-E
-#1
-Puff dragon fractal~
-Puff~
-Puff the Fractal Dragon is here, contemplating a higher reality.
-~
- Is that some type of differential curve involving some strange, and unknown
-calculus that she seems to be made out of?
-~
-516106 0 0 0 2128 0 0 0 1000 E
-34 9 -10 6d6+340 5d5+5
-340 115600
-8 8 2
-BareHandAttack: 12
-E
-T 95
-
-
-
Each line represents something that the MUD reads in and does something with it. This isn’t easy to
-read, but let’s see if we can emulate this as a dictionary to be stored on a database script created
-in Evennia.
-
First, let’s create a global script that does absolutely nothing and isn’t attached to anything. You
-can either create this directly in-game with the @py command or create it in another file to do some
-checks and balances if for whatever reason the script needs to be created again. Progmatically it
-can be done like so:
Just by creating a simple script object and assigning it a ‘vnums’ attribute as a type dictionary.
-Next we have to create the mob layout…
-
# vnum : mob_data
-
-mob_vnum_1={
- 'key':'puff',
- 'sdesc':'puff the fractal dragon',
- 'ldesc':'Puff the Fractal Dragon is here, ' \
- 'contemplating a higher reality.',
- 'ddesc':' Is that some type of differential curve ' \
- 'involving some strange, and unknown calculus ' \
- 'that she seems to be made out of?',
- [...]
- }
-
-# Then saving it to the data, assuming you have the script obj stored in a variable.
-mob_db.db.vnums[1]=mob_vnum_1
-
-
-
This is a very ‘caveman’ example, but it gets the idea across. You can use the keys in the
-mob_db.vnums to act as the mob vnum while the rest contains the data…
-
Much simpler to read and edit. If you plan on taking this route, you must keep in mind that by
-default evennia ‘looks’ at different properties when using the look command for instance. If you
-create an instance of this mob and make its self.key=1, by default evennia will say
-
Hereis:1
-
You must restructure all default commands so that the mud looks at different properties defined on
-your mob.
This page is adopted from an article originally posted for the MUSH community here on
-musoapbox.net.
-
MUSHes are text multiplayer games traditionally used for
-heavily roleplay-focused game styles. They are often (but not always) utilizing game masters and
-human oversight over code automation. MUSHes are traditionally built on the TinyMUSH-family of game
-servers, like PennMUSH, TinyMUSH, TinyMUX and RhostMUSH. Also their siblings
-MUCK and MOO are
-often mentioned together with MUSH since they all inherit from the same
-TinyMUD base. A major feature is the
-ability to modify and program the game world from inside the game by using a custom scripting
-language. We will refer to this online scripting as softcode here.
-
Evennia works quite differently from a MUSH both in its overall design and under the hood. The same
-things are achievable, just in a different way. Here are some fundamental differences to keep in
-mind if you are coming from the MUSH world.
In MUSH, users tend to code and expand all aspects of the game from inside it using softcode. A MUSH
-can thus be said to be managed solely by Players with different levels of access. Evennia on the
-other hand, differentiates between the role of the Player and the Developer.
-
-
An Evennia Developer works in Python from outside the game, in what MUSH would consider
-“hardcode”. Developers implement larger-scale code changes and can fundamentally change how the game
-works. They then load their changes into the running Evennia server. Such changes will usually not
-drop any connected players.
-
An Evennia Player operates from inside the game. Some staff-level players are likely to double
-as developers. Depending on access level, players can modify and expand the game’s world by digging
-new rooms, creating new objects, alias commands, customize their experience and so on. Trusted staff
-may get access to Python via the @py command, but this would be a security risk for normal Players
-to use. So the Player usually operates by making use of the tools prepared for them by the
-Developer - tools that can be as rigid or flexible as the developer desires.
For a Player, collaborating on a game need not be too different between MUSH and Evennia. The
-building and description of the game world can still happen mostly in-game using build commands,
-using text tags and inline functions to prettify and customize the
-experience. Evennia offers external ways to build a world but those are optional. There is also
-nothing in principle stopping a Developer from offering a softcode-like language to Players if
-that is deemed necessary.
-
For Developers of the game, the difference is larger: Code is mainly written outside the game in
-Python modules rather than in-game on the command line. Python is a very popular and well-supported
-language with tons of documentation and help to be found. The Python standard library is also a
-great help for not having to reinvent the wheel. But that said, while Python is considered one of
-the easier languages to learn and use it is undoubtedly very different from MUSH softcode.
-
While softcode allows collaboration in-game, Evennia’s external coding instead opens up the
-possibility for collaboration using professional version control tools and bug tracking using
-websites like github (or bitbucket for a free private repo). Source code can be written in proper
-text editors and IDEs with refactoring, syntax highlighting and all other conveniences. In short,
-collaborative development of an Evennia game is done in the same way most professional collaborative
-development is done in the world, meaning all the best tools can be used.
Inheritance works differently in Python than in softcode. Evennia has no concept of a “master
-object” that other objects inherit from. There is in fact no reason at all to introduce “virtual
-objects” in the game world - code and data are kept separate from one another.
-
In Python (which is an object oriented
-language) one instead creates classes - these are like blueprints from which you spawn any number
-of object instances. Evennia also adds the extra feature that every instance is persistent in the
-database (this means no SQL is ever needed). To take one example, a unique character in Evennia is
-an instances of the class Character.
-
One parallel to MUSH’s @parent command may be Evennia’s @typeclass command, which changes which
-class an already existing object is an instance of. This way you can literally turn a Character
-into a Flowerpot on the spot.
-
if you are new to object oriented design it’s important to note that all object instances of a class
-does not have to be identical. If they did, all Characters would be named the same. Evennia allows
-to customize individual objects in many different ways. One way is through Attributes, which are
-database-bound properties that can be linked to any object. For example, you could have an Orc
-class that defines all the stuff an Orc should be able to do (probably in turn inheriting from some
-Monster class shared by all monsters). Setting different Attributes on different instances
-(different strength, equipment, looks etc) would make each Orc unique despite all sharing the same
-class.
-
The @spawn command allows one to conveniently choose between different “sets” of Attributes to
-put on each new Orc (like the “warrior” set or “shaman” set) . Such sets can even inherit one
-another which is again somewhat remniscent at least of the effect of @parent and the object-
-based inheritance of MUSH.
-
There are other differences for sure, but that should give some feel for things. Enough with the
-theory. Let’s get down to more practical matters next. To install, see the
-Getting Started instructions.
By default Evennia’s desc command updates your description and that’s it. There is a more feature-
-rich optional “multi-descer” in evennia/contrib/multidesc.py though. This alternative allows for
-managing and combining a multitude of keyed descriptions.
-
To activate the multi-descer, cd to your game folder and into the commands sub-folder. There
-you’ll find the file default_cmdsets.py. In Python lingo all *.py files are called modules.
-Open the module in a text editor. We won’t go into Evennia in-game Commands and Command sets
-further here, but suffice to say Evennia allows you to change which commands (or versions of
-commands) are available to the player from moment to moment depending on circumstance.
-
Add two new lines to the module as seen below:
-
# the file mygame/commands/default_cmdsets.py
-# [...]
-
-fromevennia.contribimportmultidescer# <- added now
-
-classCharacterCmdSet(default_cmds.CharacterCmdSet):
- """
- The CharacterCmdSet contains general in-game commands like look,
- get etc available on in-game Character objects. It is merged with
- the AccountCmdSet when an Account puppets a Character.
- """
- key="DefaultCharacter"
-
- defat_cmdset_creation(self):
- """
- Populates the cmdset
- """
- super().at_cmdset_creation()
- #
- # any commands you add below will overload the default ones.
- #
- self.add(multidescer.CmdMultiDesc())# <- added now
-# [...]
-
-
-
Note that Python cares about indentation, so make sure to indent with the same number of spaces as
-shown above!
-
So what happens above? We import the moduleevennia/contrib/multidescer.py at the top. Once
-imported we can access stuff inside that module using full stop (.). The multidescer is defined as
-a class CmdMultiDesc (we could find this out by opening said module in a text editor). At the
-bottom we create a new instance of this class and add it to the CharacterCmdSet class. For the
-sake of this tutorial we only need to know that CharacterCmdSet contains all commands that should
-be be available to the Character by default.
-
This whole thing will be triggered when the command set is first created, which happens on server
-start. So we need to reload Evennia with @reload - no one will be disconnected by doing this. If
-all went well you should now be able to use desc (or +desc) and find that you have more
-possibilities:
-
> help +desc # get help on the command
-> +desc eyes = His eyes are blue.
-> +desc basic = A big guy.
-> +desc/set basic + + eyes # we add an extra space between
-> look me
-A big guy. His eyes are blue.
-
-
-
If there are errors, a traceback will show in the server log - several lines of text showing
-where the error occurred. Find where the error is by locating the line number related to the
-default_cmdsets.py file (it’s the only one you’ve changed so far). Most likely you mis-spelled
-something or missed the indentation. Fix it and either @reload again or run evenniastart as
-needed.
This use of + was prescribed by the Developer that coded this +desc command. What if the
-Player doesn’t like this syntax though? Do players need to pester the dev to change it? Not
-necessarily. While Evennia does not allow the player to build their own multi-descer on the command
-line, it does allow for re-mapping the command syntax to one they prefer. This is done using the
-nick command.
-
Here’s a nick that changes how to input the command above:
The string on the left will be matched against your input and if matching, it will be replaced with
-the string on the right. The $-type tags will store space-separated arguments and put them into
-the replacement. The nick allows shell-like wildcards, so you
-can use *, ?, [...], [!...] etc to match parts of the input.
-
The same description as before can now be set as
-
> setdesc basic cape footwear attitude
-
-
-
With the nick functionality players can mitigate a lot of syntax dislikes even without the
-developer changing the underlying Python code.
If you are a Developer and are interested in making a more MUSH-like Evennia game, a good start is
-to look into the Evennia Tutorial for a first MUSH-like game.
-That steps through building a simple little game from scratch and helps to acquaint you with the
-various corners of Evennia. There is also the Tutorial for running roleplaying sessions that can be of interest.
-
An important aspect of making things more familiar for Players is adding new and tweaking existing
-commands. How this is done is covered by the Tutorial on adding new commands. You may also find it useful to shop through the evennia/contrib/ folder. The Tutorial
-world is a small single-player quest you can try (it’s not very MUSH-
-like but it does show many Evennia concepts in action). Beyond that there are many more
-tutorials to try out. If you feel you want a more visual overview you can also look at
-Evennia in pictures.
This tutorial will explain how to set up a realtime or play-by-post tabletop style game using a
-fresh Evennia server.
-
The scenario is thus: You and a bunch of friends want to play a tabletop role playing game online.
-One of you will be the game master and you are all okay with playing using written text. You want
-both the ability to role play in real-time (when people happen to be online at the same time) as
-well as the ability for people to post when they can and catch up on what happened since they were
-last online.
-
This is the functionality we will be needing and using:
-
-
The ability to make one of you the GM (game master), with special abilities.
-
A Character sheet that players can create, view and fill in. It can also be locked so only the
-GM can modify it.
-
A dice roller mechanism, for whatever type of dice the RPG rules require.
-
Rooms, to give a sense of location and to compartmentalize play going on- This means both
-Character movements from location to location and GM explicitly moving them around.
-
Channels, for easily sending text to all subscribing accounts, regardless of location.
-
Account-to-Account messaging capability, including sending to multiple recipients
-simultaneously, regardless of location.
-
-
We will find most of these things are already part of vanilla Evennia, but that we can expand on the
-defaults for our particular use-case. Below we will flesh out these components from start to finish.
We will assume you start from scratch. You need Evennia installed, as per the Getting
-Started instructions. Initialize a new game directory with evenniainit<gamedirname>. In this tutorial we assume your game dir is simply named mygame. You can use the
-default database and keep all other settings to default for now. Familiarize yourself with the
-mygame folder before continuing. You might want to browse the [First Steps Coding](First-Steps-
-Coding) tutorial, just to see roughly where things are modified.
Simplest way: Being an admin, just give one account Admins permission using the standard @perm
-command.
-
Better but more work: Make a custom command to set/unset the above, while tweaking the Character
-to show your renewed GM status to the other accounts.
Evennia has the following permission hierarchy out of
-the box: Players, Helpers, Builders, Admins and finally Developers. We could change these but
-then we’d need to update our Default commands to use the changes. We want to keep this simple, so
-instead we map our different roles on top of this permission ladder.
-
-
Players is the permission set on normal players. This is the default for anyone creating a new
-account on the server.
-
Helpers are like Players except they also have the ability to create/edit new help entries.
-This could be granted to players who are willing to help with writing lore or custom logs for
-everyone.
-
Builders is not used in our case since the GM should be the only world-builder.
-
Admins is the permission level the GM should have. Admins can do everything builders can
-(create/describe rooms etc) but also kick accounts, rename them and things like that.
-
Developers-level permission are the server administrators, the ones with the ability to
-restart/shutdown the server as well as changing the permission levels.
-
-
-
The superuser is not part of the hierarchy and actually
-completely bypasses it. We’ll assume server admin(s) will “just” be Developers.
There is no need to remove the basic Players permission when adding the higher permission: the
-highest will be used. Permission level names are not case sensitive. You can also use both plural
-and singular, so “Admins” gives the same powers as “Admin”.
Use of @perm works out of the box, but it’s really the bare minimum. Would it not be nice if other
-accounts could tell at a glance who the GM is? Also, we shouldn’t really need to remember that the
-permission level is called “Admins”. It would be easier if we could just do @gm<account> and
-@notgm<account> and at the same time change something make the new GM status apparent.
-
So let’s make this possible. This is what we’ll do:
-
-
We’ll customize the default Character class. If an object of this class has a particular flag,
-its name will have the string(GM) added to the end.
-
We’ll add a new command, for the server admin to assign the GM-flag properly.
Let’s first start by customizing the Character. We recommend you browse the beginning of the
-Account page to make sure you know how Evennia differentiates between the OOC “Account
-objects” (not to be confused with the Accounts permission, which is just a string specifying your
-access) and the IC “Character objects”.
-
Open mygame/typeclasses/characters.py and modify the default Character class:
-
# in mygame/typeclasses/characters.py
-
-# [...]
-
-classCharacter(DefaultCharacter):
- # [...]
- defget_display_name(self,looker,**kwargs):
- """
- This method customizes how character names are displayed. We assume
- only permissions of types "Developers" and "Admins" require
- special attention.
- """
- name=self.key
- selfaccount=self.account# will be None if we are not puppeted
- lookaccount=looker.account# - " -
-
- ifselfaccountandselfaccount.db.is_gm:
- # A GM. Show name as name(GM)
- name="%s(GM)"%name
-
- iflookaccountand \
- (lookaccount.permissions.get("Developers")orlookaccount.db.is_gm):
- # Developers/GMs see name(#dbref) or name(GM)(#dbref)
- return"%s(#%s)"%(name,self.id)
- else:
- returnname
-
-
-
-
Above, we change how the Character’s name is displayed: If the account controlling this Character is
-a GM, we attach the string (GM) to the Character’s name so everyone can tell who’s the boss. If we
-ourselves are Developers or GM’s we will see database ids attached to Characters names, which can
-help if doing database searches against Characters of exactly the same name. We base the “gm-
-ingness” on having an flag (an Attribute) named is_gm. We’ll make sure new GM’s
-actually get this flag below.
-
-
Extra exercise: This will only show the (GM) text on Characters puppeted by a GM account,
-that is, it will show only to those in the same location. If we wanted it to also pop up in, say,
-who listings and channels, we’d need to make a similar change to the Account typeclass in
-mygame/typeclasses/accounts.py. We leave this as an exercise to the reader.
We will describe in some detail how to create and add an Evennia command here with the
-hope that we don’t need to be as detailed when adding commands in the future. We will build on
-Evennia’s default “mux-like” commands here.
-
Open mygame/commands/command.py and add a new Command class at the bottom:
-
# in mygame/commands/command.py
-
-fromevenniaimportdefault_cmds
-
-# [...]
-
-importevennia
-
-classCmdMakeGM(default_cmds.MuxCommand):
- """
- Change an account's GM status
-
- Usage:
- @gm <account>
- @ungm <account>
-
- """
- # note using the key without @ means both @gm !gm etc will work
- key="gm"
- aliases="ungm"
- locks="cmd:perm(Developers)"
- help_category="RP"
-
- deffunc(self):
- "Implement the command"
- caller=self.caller
-
- ifnotself.args:
- caller.msg("Usage: @gm account or @ungm account")
- return
-
- accountlist=evennia.search_account(self.args)# returns a list
- ifnotaccountlist:
- caller.msg("Could not find account '%s'"%self.args)
- return
- eliflen(accountlist)>1:
- caller.msg("Multiple matches for '%s': %s"%(self.args,accountlist))
- return
- else:
- account=accountlist[0]
-
- ifself.cmdstring=="gm":
- # turn someone into a GM
- ifaccount.permissions.get("Admins"):
- caller.msg("Account %s is already a GM."%account)
- else:
- account.permissions.add("Admins")
- caller.msg("Account %s is now a GM."%account)
- account.msg("You are now a GM (changed by %s)."%caller)
- account.character.db.is_gm=True
- else:
- # @ungm was entered - revoke GM status from someone
- ifnotaccount.permissions.get("Admins"):
- caller.msg("Account %s is not a GM."%account)
- else:
- account.permissions.remove("Admins")
- caller.msg("Account %s is no longer a GM."%account)
- account.msg("You are no longer a GM (changed by %s)."%caller)
- delaccount.character.db.is_gm
-
-
-
-
All the command does is to locate the account target and assign it the Admins permission if we
-used @gm or revoke it if using the @ungm alias. We also set/unset the is_gm Attribute that is
-expected by our new Character.get_display_name method from earlier.
-
-
We could have made this into two separate commands or opted for a syntax like @gm/revoke<accountname>. Instead we examine how this command was called (stored in self.cmdstring) in order
-to act accordingly. Either way works, practicality and coding style decides which to go with.
-
-
To actually make this command available (only to Developers, due to the lock on it), we add it to
-the default Account command set. Open the file mygame/commands/default_cmdsets.py and find the
-AccountCmdSet class:
Finally, issue the @reload command to update the server to your changes. Developer-level players
-(or the superuser) should now have the @gm/@ungm command available.
There are many ways to build a Character sheet in text, from manually pasting strings together to
-more automated ways. Exactly what is the best/easiest way depends on the sheet one tries to create.
-We will here show two examples using the EvTable and EvForm utilities.Later we will create
-Commands to edit and display the output from those utilities.
-
-
Note that due to the limitations of the wiki, no color is used in any of the examples. See the
-text tag documentation for how to add color to the tables and forms.
EvTable is a text-table generator. It helps with displaying text in
-ordered rows and columns. This is an example of using it in code:
-
# this can be tried out in a Python shell like iPython
-
-fromevennia.utilsimportevtable
-
-# we hardcode these for now, we'll get them as input later
-STR,CON,DEX,INT,WIS,CHA=12,13,8,10,9,13
-
-table=evtable.EvTable("Attr","Value",
- table=[
- ["STR","CON","DEX","INT","WIS","CHA"],
- [STR,CON,DEX,INT,WIS,CHA]
- ],align='r',border="incols")
-
-
-
Above, we create a two-column table by supplying the two columns directly. We also tell the table to
-be right-aligned and to use the “incols” border type (borders drawns only in between columns). The
-EvTable class takes a lot of arguments for customizing its look, you can see some of the possible
-keyword arguments here. Once you have the table you
-could also retroactively add new columns and rows to it with table.add_row() and
-table.add_column(): if necessary the table will expand with empty rows/columns to always remain
-rectangular.
This is a minimalistic but effective Character sheet. By combining the table_string with other
-strings one could build up a reasonably full graphical representation of a Character. For more
-advanced layouts we’ll look into EvForm next.
EvForm allows the creation of a two-dimensional “graphic” made by
-text characters. On this surface, one marks and tags rectangular regions (“cells”) to be filled with
-content. This content can be either normal strings or EvTable instances (see the previous section,
-one such instance would be the table variable in that example).
-
In the case of a Character sheet, these cells would be comparable to a line or box where you could
-enter the name of your character or their strength score. EvMenu also easily allows to update the
-content of those fields in code (it use EvTables so you rebuild the table first before re-sending it
-to EvForm).
-
The drawback of EvForm is that its shape is static; if you try to put more text in a region than it
-was sized for, the text will be cropped. Similarly, if you try to put an EvTable instance in a field
-too small for it, the EvTable will do its best to try to resize to fit, but will eventually resort
-to cropping its data or even give an error if too small to fit any data.
-
An EvForm is defined in a Python module. Create a new file mygame/world/charsheetform.py and
-modify it thus:
The #coding statement (which must be put on the very first line to work) tells Python to use the
-utf-8 encoding for the file. Using the FORMCHAR and TABLECHAR we define what single-character we
-want to use to “mark” the regions of the character sheet holding cells and tables respectively.
-Within each block (which must be separated from one another by at least one non-marking character)
-we embed identifiers 1-4 to identify each block. The identifier could be any single character except
-for the FORMCHAR and TABLECHAR
-
-
You can still use FORMCHAR and TABLECHAR elsewhere in your sheet, but not in a way that it
-would identify a cell/table. The smallest identifiable cell/table area is 3 characters wide
-including the identifier (for example x2x).
-
-
Now we will map content to this form.
-
# again, this can be tested in a Python shell
-
-# hard-code this info here, later we'll ask the
-# account for this info. We will re-use the 'table'
-# variable from the EvTable example.
-
-NAME="John, the wise old admin with a chip on his shoulder"
-ADVANTAGES="Language-wiz, Intimidation, Firebreathing"
-DISADVANTAGES="Bad body odor, Poor eyesight, Troubled history"
-
-fromevennia.utilsimportevform
-
-# load the form from the module
-form=evform.EvForm("world/charsheetform.py")
-
-# map the data to the form
-form.map(cells={"1":NAME,"3":ADVANTAGES,"4":DISADVANTAGES},
- tables={"2":table})
-
-
-
We create some RP-sounding input and re-use the table variable from the previous EvTable
-example.
-
-
Note, that if you didn’t want to create the form in a separate module you could also load it
-directly into the EvForm call like this: EvForm(form={"FORMCHAR":"x","TABLECHAR":"c","FORM":formstring}) where FORM specifies the form as a string in the same way as listed in the module
-above. Note however that the very first line of the FORM string is ignored, so start with a \n.
As seen, the texts and tables have been slotted into the text areas and line breaks have been added
-where needed. We chose to just enter the Advantages/Disadvantages as plain strings here, meaning
-long names ended up split between rows. If we wanted more control over the display we could have
-inserted \n line breaks after each line or used a borderless EvTable to display those as well.
We will assume we go with the EvForm example above. We now need to attach this to a Character so
-it can be modified. For this we will modify our Character class a little more:
-
# mygame/typeclasses/character.py
-
-fromevennia.utilsimportevform,evtable
-
-[...]
-
-classCharacter(DefaultCharacter):
- [...]
- defat_object_creation(self):
- "called only once, when object is first created"
- # we will use this to stop account from changing sheet
- self.db.sheet_locked=False
- # we store these so we can build these on demand
- self.db.chardata={"str":0,
- "con":0,
- "dex":0,
- "int":0,
- "wis":0,
- "cha":0,
- "advantages":"",
- "disadvantages":""}
- self.db.charsheet=evform.EvForm("world/charsheetform.py")
- self.update_charsheet()
-
- defupdate_charsheet(self):
- """
- Call this to update the sheet after any of the ingoing data
- has changed.
- """
- data=self.db.chardata
- table=evtable.EvTable("Attr","Value",
- table=[
- ["STR","CON","DEX","INT","WIS","CHA"],
- [data["str"],data["con"],data["dex"],
- data["int"],data["wis"],data["cha"]]],
- align='r',border="incols")
- self.db.charsheet.map(tables={"2":table},
- cells={"1":self.key,
- "3":data["advantages"],
- "4":data["disadvantages"]})
-
-
-
-
Use @reload to make this change available to all newly created Characters. Already existing
-Characters will not have the charsheet defined, since at_object_creation is only called once.
-The easiest to force an existing Character to re-fire its at_object_creation is to use the
-@typeclass command in-game:
We will add a command to edit the sections of our Character sheet. Open
-mygame/commands/command.py.
-
# at the end of mygame/commands/command.py
-
-ALLOWED_ATTRS=("str","con","dex","int","wis","cha")
-ALLOWED_FIELDNAMES=ALLOWED_ATTRS+ \
- ("name","advantages","disadvantages")
-
-def_validate_fieldname(caller,fieldname):
- "Helper function to validate field names."
- iffieldnamenotinALLOWED_FIELDNAMES:
- err="Allowed field names: %s"%(", ".join(ALLOWED_FIELDNAMES))
- caller.msg(err)
- returnFalse
- iffieldnameinALLOWED_ATTRSandnotvalue.isdigit():
- caller.msg("%s must receive a number."%fieldname)
- returnFalse
- returnTrue
-
-classCmdSheet(MuxCommand):
- """
- Edit a field on the character sheet
-
- Usage:
- @sheet field value
-
- Examples:
- @sheet name Ulrik the Warrior
- @sheet dex 12
- @sheet advantages Super strength, Night vision
-
- If given without arguments, will view the current character sheet.
-
- Allowed field names are:
- name,
- str, con, dex, int, wis, cha,
- advantages, disadvantages
-
- """
-
- key="sheet"
- aliases="editsheet"
- locks="cmd: perm(Players)"
- help_category="RP"
-
- deffunc(self):
- caller=self.caller
- ifnotself.argsorlen(self.args)<2:
- # not enough arguments. Display the sheet
- ifsheet:
- caller.msg(caller.db.charsheet)
- else:
- caller.msg("You have no character sheet.")
- return
-
- # if caller.db.sheet_locked:
- caller.msg("Your character sheet is locked.")
- return
-
- # split input by whitespace, once
- fieldname,value=self.args.split(None,1)
- fieldname=fieldname.lower()# ignore case
-
- ifnot_validate_fieldnames(caller,fieldname):
- return
- iffieldname=="name":
- self.key=value
- else:
- caller.chardata[fieldname]=value
- caller.update_charsheet()
- caller.msg("%s was set to %s."%(fieldname,value))
-
-
-
-
Most of this command is error-checking to make sure the right type of data was input. Note how the
-sheet_locked Attribute is checked and will return if not set.
-
This command you import into mygame/commands/default_cmdsets.py and add to the CharacterCmdSet,
-in the same way the @gm command was added to the AccountCmdSet earlier.
Game masters use basically the same input as Players do to edit a character sheet, except they can
-do it on other players than themselves. They are also not stopped by any sheet_locked flags.
-
# continuing in mygame/commands/command.py
-
-classCmdGMsheet(MuxCommand):
- """
- GM-modification of char sheets
-
- Usage:
- @gmsheet character [= fieldname value]
-
- Switches:
- lock - lock the character sheet so the account
- can no longer edit it (GM's still can)
- unlock - unlock character sheet for Account
- editing.
-
- Examples:
- @gmsheet Tom
- @gmsheet Anna = str 12
- @gmsheet/lock Tom
-
- """
- key="gmsheet"
- locks="cmd: perm(Admins)"
- help_category="RP"
-
- deffunc(self):
- caller=self.caller
- ifnotself.args:
- caller.msg("Usage: @gmsheet character [= fieldname value]")
-
- ifself.rhs:
- # rhs (right-hand-side) is set only if a '='
- # was given.
- iflen(self.rhs)<2:
- caller.msg("You must specify both a fieldname and value.")
- return
- fieldname,value=self.rhs.split(None,1)
- fieldname=fieldname.lower()
- ifnot_validate_fieldname(caller,fieldname):
- return
- charname=self.lhs
- else:
- # no '=', so we must be aiming to look at a charsheet
- fieldname,value=None,None
- charname=self.args.strip()
-
- character=caller.search(charname,global_search=True)
- ifnotcharacter:
- return
-
- if"lock"inself.switches:
- ifcharacter.db.sheet_locked:
- caller.msg("The character sheet is already locked.")
- else:
- character.db.sheet_locked=True
- caller.msg("%s can no longer edit their character sheet."%character.key)
- elif"unlock"inself.switches:
- ifnotcharacter.db.sheet_locked:
- caller.msg("The character sheet is already unlocked.")
- else:
- character.db.sheet_locked=False
- caller.msg("%s can now edit their character sheet."%character.key)
-
- iffieldname:
- iffieldname=="name":
- character.key=value
- else:
- character.db.chardata[fieldname]=value
- character.update_charsheet()
- caller.msg("You set %s's %s to %s."%(character.key,fieldname,value)
- else:
- # just display
- caller.msg(character.db.charsheet)
-
-
-
The @gmsheet command takes an additional argument to specify which Character’s character sheet to
-edit. It also takes /lock and /unlock switches to block the Player from tweaking their sheet.
-
Before this can be used, it should be added to the default CharacterCmdSet in the same way as the
-normal @sheet. Due to the lock set on it, this command will only be available to Admins (i.e.
-GMs) or higher permission levels.
Evennia’s contrib folder already comes with a full dice roller. To add it to the game, simply
-import contrib.dice.CmdDice into mygame/commands/default_cmdsets.py and add CmdDice to the
-CharacterCmdset as done with other commands in this tutorial. After a @reload you will be able
-to roll dice using normal RPG-style format:
-
roll2d6+3
-7
-
-
-
Use helpdice to see what syntax is supported or look at evennia/contrib/dice.py to see how it’s
-implemented.
Evennia comes with rooms out of the box, so no extra work needed. A GM will automatically have all
-needed building commands available. A fuller go-through is found in the Building
-tutorial. Here are some useful highlights:
-
-
@digroomname;alias=exit_there;alias,exit_back;alias - this is the basic command for digging
-a new room. You can specify any exit-names and just enter the name of that exit to go there.
-
@tunneldirection=roomname - this is a specialized command that only accepts directions in the
-cardinal directions (n,ne,e,se,s,sw,w,nw) as well as in/out and up/down. It also automatically
-builds “matching” exits back in the opposite direction.
-
@create/dropobjectname - this creates and drops a new simple object in the current location.
-
@descobj - change the look-description of the object.
-
@telobject=location - teleport an object to a named location.
-
@searchobjectname - locate an object in the database.
-
-
-
TODO: Describe how to add a logging room, that logs says and poses to a log file that people can
-access after the fact.
Evennia comes with Channels in-built and they are described fully in the
-documentation. For brevity, here are the relevant commands for normal use:
-
-
@ccreatenew_channel;alias;alias=shortdescription - Creates a new channel.
-
addcomchannel - join an existing channel. Use addcomalias=channel to add a new alias you
-can use to talk to the channel, as many as desired.
-
delcomaliasorchannel - remove an alias from a channel or, if the real channel name is given,
-unsubscribe completely.
-
@channels lists all available channels, including your subscriptions and any aliases you have
-set up for them.
-
-
You can read channel history: if you for example are chatting on the public channel you can do
-public/history to see the 20 last posts to that channel or public/history32 to view twenty
-posts backwards, starting with the 32nd from the end.
The @py command supplied with the default command set of Evennia allows you to execute Python
-commands directly from inside the game. An alias to @py is simply “!”. Access to the @py
-command should be severely restricted. This is no joke - being able to execute arbitrary Python
-code on the server is not something you should entrust to just anybody.
evennia - Evennia’s flat API - through this you can access all of Evennia.
-
-
For accessing other objects in the same room you need to use self.search(name). For objects in
-other locations, use one of the evennia.search_* methods. See [below](./Execute-Python-Code.md#finding-
-objects).
This is an example where we import and test one of Evennia’s utilities found in
-src/utils/utils.py, but also accessible through ev.utils:
-
@py from ev import utils; utils.time_format(33333)
-<<< Done.
-
-
-
Note that we didn’t get any return value, all we where told is that the code finished executing
-without error. This is often the case in more complex pieces of code which has no single obvious
-return value. To see the output from the time_format() function we need to tell the system to
-echo it to us explicitly with self.msg().
-
@py from ev import utils; self.msg(str(utils.time_format(33333)))
-09:15
-<<< Done.
-
-
-
-
Warning: When using the msg function wrap our argument in str() to convert it into a string
-above. This is not strictly necessary for most types of data (Evennia will usually convert to a
-string behind the scenes for you). But for lists and tuples you will be confused by the output
-if you don’t wrap them in str(): only the first item of the iterable will be returned. This is
-because doing msg(text) is actually just a convenience shortcut; the full argument that msg
-accepts is something called an outputfunc on the form (cmdname,(args),{kwargs}) (see the
-message path for more info). Sending a list/tuple confuses Evennia to think you are
-sending such a structure. Converting it to a string however makes it clear it should just be
-displayed as-is.
-
-
If you were to use Python’s standard print, you will see the result in your current stdout (your
-terminal by default, otherwise your log file).
A common use for @py is to explore objects in the database, for debugging and performing specific
-operations that are not covered by a particular command.
-
Locating an object is best done using self.search():
self.search() is by far the most used case, but you can also search other database tables for
-other Evennia entities like scripts or configuration entities. To do this you can use the generic
-search entries found in ev.search_*.
-
@py evennia.search_script("sys_game_time")
-<<< [<src.utils.gametime.GameTime object at 0x852be2c>]
-
-
-
(Note that since this becomes a simple statement, we don’t have to wrap it in self.msg() to get
-the output). You can also use the database model managers directly (accessible through the objects
-properties of database models or as evennia.managers.*). This is a bit more flexible since it
-gives you access to the full range of database search methods defined in each manager.
-
@py evennia.managers.scripts.script_search("sys_game_time")
-<<< [<src.utils.gametime.GameTime object at 0x852be2c>]
-
-
-
The managers are useful for all sorts of database studies.
@py has the advantage of operating inside a running server (sharing the same process), where you
-can test things in real time. Much of this can be done from the outside too though.
-
In a terminal, cd to the top of your game directory (this bit is important since we need access to
-your config file) and run
-
evennia shell
-
-
-
Your default Python interpreter will start up, configured to be able to work with and import all
-modules of your Evennia installation. From here you can explore the database and test-run individual
-modules as desired.
-
It’s recommended that you get a more fully featured Python interpreter like
-iPython. If you use a virtual environment, you can just get it
-with pipinstallipython. IPython allows you to better work over several lines, and also has a lot
-of other editing features, such as tab-completion and __doc__-string reading.
This section gives a brief step-by-step introduction on how to set up Evennia for the first time so
-you can modify and overload the defaults easily. You should only need to do these steps once. It
-also walks through you making your first few tweaks.
-
Before continuing, make sure you have Evennia installed and running by following the Getting
-Started instructions. You should have initialized a new game folder with the
-evennia--initfoldername command. We will in the following assume this folder is called
-“mygame”.
-
It might be a good idea to eye through the brief Coding Introduction too
-(especially the recommendations in the section about the evennia “flat” API and about using evenniashell will help you here and in the future).
-
To follow this tutorial you also need to know the basics of operating your computer’s
-terminal/command line. You also need to have a text editor to edit and create source text files.
-There are plenty of online tutorials on how to use the terminal and plenty of good free text
-editors. We will assume these things are already familiar to you henceforth.
Below are some first things to try with your new custom modules. You can test these to get a feel
-for the system. See also Tutorials for more step-by-step help and special cases.
We will add some simple rpg attributes to our default Character. In the next section we will follow
-up with a new command to view those attributes.
-
-
Edit mygame/typeclasses/characters.py and modify the Character class. The
-at_object_creation method also exists on the DefaultCharacter parent and will overload it. The
-get_abilities method is unique to our version of Character.
-
classCharacter(DefaultCharacter):
- # [...]
- defat_object_creation(self):
- """
- Called only at initial creation. This is a rather silly
- example since ability scores should vary from Character to
- Character and is usually set during some character
- generation step instead.
- """
- #set persistent attributes
- self.db.strength=5
- self.db.agility=4
- self.db.magic=2
-
- defget_abilities(self):
- """
- Simple access method to return ability
- scores as a tuple (str,agi,mag)
- """
- returnself.db.strength,self.db.agility,self.db.magic
-
-
-
-
Reload the server (you will still be connected to the game after doing
-this). Note that if you examine yourself you will not see any new Attributes appear yet. Read
-the next section to understand why.
It’s important to note that the new Attributes we added above will only be stored on
-newly created characters. The reason for this is simple: The at_object_creation method, where we
-added those Attributes, is per definition only called when the object is first created, then never
-again. This is usually a good thing since those Attributes may change over time - calling that hook
-would reset them back to start values. But it also means that your existing character doesn’t have
-them yet. You can see this by calling the get_abilities hook on yourself at this point:
-
# (you have to be superuser to use @py)
-@pyself.get_abilities()
-<<<(None,None,None)
-
-
-
This is easily remedied.
-
@updateself
-
-
-
This will (only) re-run at_object_creation on yourself. You should henceforth be able to get the
-abilities successfully:
-
@pyself.get_abilities()
-<<<(5,4,2)
-
-
-
This is something to keep in mind if you start building your world before your code is stable -
-startup-hooks will not (and should not) automatically run on existing objects - you have to update
-your existing objects manually. Luckily this is a one-time thing and pretty simple to do. If the
-typeclass you want to update is in typeclasses.myclass.MyClass, you can do the following (e.g.
-from evenniashell):
-
fromtypeclasses.myclassimportMyClass
-# loop over all MyClass instances in the database
-# and call .swap_typeclass on them
-forobjinMyClass.objects.all():
- obj.swap_typeclass(MyClass,run_start_hooks="at_object_creation")
-
-
-
Using swap_typeclass to the same typeclass we already have will re-run the creation hooks (this is
-what the @update command does under the hood). From in-game you can do the same with @py:
One may experience errors for a number of reasons. Common beginner errors are spelling mistakes,
-wrong indentations or code omissions leading to a SyntaxError. Let’s say you leave out a colon
-from the end of a class function like so: defat_object_creation(self). The client will reload
-without issue. However, if you look at the terminal/console (i.e. not in-game), you will see
-Evennia complaining (this is called a traceback):
Evennia will still be restarting and following the tutorial, doing @pyself.get_abilities() will
-return the right response (None,None,None). But when attempting to @typeclass/forceself you
-will get this response:
The full error will show in the terminal/console but this is confusing since you did add
-get_abilities before. Note however what the error says - you (self) should be a Character but
-the error talks about DefaultObject. What has happened is that due to your unhandled SyntaxError
-earlier, Evennia could not load the character.py module at all (it’s not valid Python). Rather
-than crashing, Evennia handles this by temporarily falling back to a safe default - DefaultObject
-
-
in order to keep your MUD running. Fix the original SyntaxError and reload the server. Evennia
-will then be able to use your modified Character class again and things should work.
-
-
-
Note: Learning how to interpret an error traceback is a critical skill for anyone learning Python.
-Full tracebacks will appear in the terminal/Console you started Evennia from. The traceback text can
-sometimes be quite long, but you are usually just looking for the last few lines: The description of
-the error and the filename + line number for where the error occurred. In the example above, we see
-it’s a SyntaxError happening at line33 of mygame\typeclasses\characters.py. In this case it
-even points out where on the line it encountered the error (the missing colon). Learn to read
-tracebacks and you’ll be able to resolve the vast majority of common errors easily.
The @py command used above is only available to privileged users. We want any player to be able to
-see their stats. Let’s add a new command to list the abilities we added in the previous
-section.
-
-
Open mygame/commands/command.py. You could in principle put your command anywhere but this
-module has all the imports already set up along with some useful documentation. Make a new class at
-the bottom of this file:
-
classCmdAbilities(BaseCommand):
- """
- List abilities
-
- Usage:
- abilities
-
- Displays a list of your current ability values.
- """
- key="abilities"
- aliases=["abi"]
- lock="cmd:all()"
- help_category="General"
-
- deffunc(self):
- """implements the actual functionality"""
-
- str,agi,mag=self.caller.get_abilities()
- string="STR: %s, AGI: %s, MAG: %s"%(str,agi,mag)
- self.caller.msg(string)
-
-
-
-
Next you edit mygame/commands/default_cmdsets.py and add a new import to it near the top:
-
fromcommands.commandimportCmdAbilities
-
-
-
-
In the CharacterCmdSet class, add the following near the bottom (it says where):
-
self.add(CmdAbilities())
-
-
-
-
Reload the server (noone will be disconnected by doing this).
-
-
You (and anyone else) should now be able to use abilities (or its alias abi) as part of your
-normal commands in-game:
Let’s test to make a new type of object. This example is an “wise stone” object that returns some
-random comment when you look at it, like this:
-
> look stone
-
-A very wise stone
-
-This is a very wise old stone.
-It grumbles and says: 'The world is like a rock of chocolate.'
-
-
-
-
Create a new module in mygame/typeclasses/. Name it wiseobject.py for this example.
-
In the module import the base Object (typeclasses.objects.Object). This is empty by default,
-meaning it is just a proxy for the default evennia.DefaultObject.
-
Make a new class in your module inheriting from Object. Overload hooks on it to add new
-functionality. Here is an example of how the file could look:
-
fromrandomimportchoice
-fromtypeclasses.objectsimportObject
-
-classWiseObject(Object):
- """
- An object speaking when someone looks at it. We
- assume it looks like a stone in this example.
- """
- defat_object_creation(self):
- """Called when object is first created"""
- self.db.wise_texts= \
- ["Stones have feelings too.",
- "To live like a stone is to not have lived at all.",
- "The world is like a rock of chocolate."]
-
- defreturn_appearance(self,looker):
- """
- Called by the look command. We want to return
- a wisdom when we get looked at.
- """
- # first get the base string from the
- # parent's return_appearance.
- string=super().return_appearance(looker)
- wisewords="\n\nIt grumbles and says: '%s'"
- wisewords=wisewords%choice(self.db.wise_texts)
- returnstring+wisewords
-
-
-
-
Check your code for bugs. Tracebacks will appear on your command line or log. If you have a grave
-Syntax Error in your code, the source file itself will fail to load which can cause issues with the
-entire cmdset. If so, fix your bug and reload the server from the command line
-(noone will be disconnected by doing this).
-
Use @create/dropstone:wiseobject.WiseObject to create a talkative stone. If the @create
-command spits out a warning or cannot find the typeclass (it will tell you which paths it searched),
-re-check your code for bugs and that you gave the correct path. The @create command starts looking
-for Typeclasses in mygame/typeclasses/.
-
Use lookstone to test. You will see the default description (“You see nothing special”)
-followed by a random message of stony wisdom. Use @descstone=Thisisawiseoldstone. to make
-it look nicer. See the Builder Docs for more information.
-
-
Note that at_object_creation is only called once, when the stone is first created. If you make
-changes to this method later, already existing stones will not see those changes. As with the
-Character example above you can use @typeclass/force to tell the stone to re-run its
-initialization.
-
The at_object_creation is a special case though. Changing most other aspects of the typeclass does
-not require manual updating like this - you just need to @reload to have all changes applied
-automatically to all existing objects.
There are more Tutorials, including one for building a whole little MUSH-like
-game - that is instructive also if you have no interest in
-MUSHes per se. A good idea is to also get onto the IRC
-chat and the mailing
-list to get in touch with the community and other
-developers.
So you have Evennia up and running. You have a great game idea in mind. Now it’s time to start
-cracking! But where to start? Here are some ideas for a workflow. Note that the suggestions on this
-page are just that - suggestions. Also, they are primarily aimed at a lone hobby designer or a small
-team developing a game in their free time.
-
Below are some minimal steps for getting the first version of a new game world going with players.
-It’s worth to at least make the attempt to do these steps in order even if you are itching to jump
-ahead in the development cycle. On the other hand, you should also make sure to keep your work fun
-for you, or motivation will falter. Making a full game is a lot of work as it is, you’ll need all
-your motivation to make it a reality.
-
Remember that 99.99999% of all great game ideas never lead to a game. Especially not to an online
-game that people can actually play and enjoy. So our first all overshadowing goal is to beat those
-odds and get something out the door! Even if it’s a scaled-down version of your dream game,
-lacking many “must-have” features! It’s better to get it out there and expand on it later than to
-code in isolation forever until you burn out, lose interest or your hard drive crashes.
-
Like is common with online games, getting a game out the door does not mean you are going to be
-“finished” with the game - most MUDs add features gradually over the course of years - it’s often
-part of the fun!
This is what you do before having coded a single line or built a single room. Many prospective game
-developers are very good at parts of this process, namely in defining what their world is “about”:
-The theme, the world concept, cool monsters and so on. It is by all means very important to define
-what is the unique appeal of your game. But it’s unfortunately not enough to make your game a
-reality. To do that you must also have an idea of how to actually map those great ideas onto
-Evennia.
-
A good start is to begin by planning out the basic primitives of the game and what they need to be
-able to do. Below are a far-from-complete list of examples (and for your first version you should
-definitely try for a much shorter list):
These are the behind-the-scenes features that exist in your game, often without being represented by
-a specific in-game object.
-
-
Should your game rules be enforced by coded systems or are you planning for human game masters to
-run and arbitrate rules?
-
What are the actual mechanical game rules? How do you decide if an action succeeds or fails? What
-“rolls” does the game need to be able to do? Do you base your game off an existing system or make up
-your own?
-
Does the flow of time matter in your game - does night and day change? What about seasons? Maybe
-your magic system is affected by the phase of the moon?
-
Do you want changing, global weather? This might need to operate in tandem over a large number of
-rooms.
-
Do you want a game-wide economy or just a simple barter system? Or no formal economy at all?
-
Should characters be able to send mail to each other in-game?
-
Should players be able to post on Bulletin boards?
-
What is the staff hierarchy in your game? What powers do you want your staff to have?
-
What should a Builder be able to build and what commands do they need in order to do that?
Is a simple description enough or should the description be able to change (such as with time, by
-light conditions, weather or season)?
-
Should the room have different statuses? Can it have smells, sounds? Can it be affected by
-dramatic weather, fire or magical effects? If so, how would this affect things in the room? Or are
-these things something admins/game masters should handle manually?
-
Can objects be hidden in the room? Can a person hide in the room? How does the room display this?
Consider the most basic (non-player-controlled) object in your game.
-
-
How numerous are your objects? Do you want large loot-lists or are objects just role playing props
-created on demand?
-
Does the game use money? If so, is each coin a separate object or do you just store a bank account
-value?
-
What about multiple identical objects? Do they form stacks and how are those stacks handled in
-that case?
-
Does an object have weight or volume (so you cannot carry an infinite amount of them)?
-
Can objects be broken? If so, does it have a health value? Is burning it causing the same damage
-as smashing it? Can it be repaired?
-
Is a weapon a specific type of object or are you supposed to be able to fight with a chair too?
-Can you fight with a flower or piece of paper as well?
-
NPCs/mobs are also objects. Should they just stand around or should they have some sort of AI?
-
Are NPCs/mobs differet entities? How is an Orc different from a Kobold, in code - are they the
-same object with different names or completely different types of objects, with custom code?
-
Should there be NPCs giving quests? If so, how would you track quest status and what happens when
-multiple players try to do the same quest? Do you use instances or some other mechanism?
These are the objects controlled directly by Players.
-
-
Can players have more than one Character active at a time or are they allowed to multi-play?
-
How does a Player create their Character? A Character-creation screen? Answering questions?
-Filling in a form?
-
Do you want to use classes (like “Thief”, “Warrior” etc) or some other system, like Skill-based?
-
How do you implement different “classes” or “races”? Are they separate types of objects or do you
-simply load different stats on a basic object depending on what the Player wants?
-
If a Character can hide in a room, what skill will decide if they are detected?
-
What skill allows a Character to wield a weapon and hit? Do they need a special skill to wield a
-chair rather than a sword?
-
Does a Character need a Strength attribute to tell how much they can carry or which objects they
-can smash?
-
What does the skill tree look like? Can a Character gain experience to improve? By killing
-enemies? Solving quests? By roleplaying?
-
etc.
-
-
A MUD’s a lot more involved than you would think and these things hang together in a complex web. It
-can easily become overwhelming and it’s tempting to want all functionality right out of the door.
-Try to identify the basic things that “make” your game and focus only on them for your first
-release. Make a list. Keep future expansions in mind but limit yourself.
This is the actual work of creating the “game” part of your game. Many “game-designer” types tend to
-gloss over this bit and jump directly to World Building. Vice versa, many “game-coder” types
-tend to jump directly to this part without doing the Planning first. Neither way is good and
-will lead to you having to redo all your hard work at least once, probably more.
-
Evennia’s Developer Central tries to help you with this bit of development. We
-also have a slew of Tutorials with worked examples. Evennia tries hard to make this
-part easier for you, but there is no way around the fact that if you want anything but a very basic
-Talker-type game you will have to bite the bullet and code your game (or find a coder willing to
-do it for you).
-
Even if you won’t code anything yourself, as a designer you need to at least understand the basic
-paradigms of Evennia, such as Objects, Commands and Scripts and
-how they hang together. We recommend you go through the [Tutorial World](Tutorial-World-
-Introduction) in detail (as well as glancing at its code) to get at least a feel for what is
-involved behind the scenes. You could also look through the tutorial for building a game from
-scratch.
-
During Coding you look back at the things you wanted during the Planning phase and try to
-implement them. Don’t be shy to update your plans if you find things easier/harder than you thought.
-The earlier you revise problems, the easier they will be to fix.
-
A good idea is to host your code online (publicly or privately) using version control. Not only will
-this make it easy for multiple coders to collaborate (and have a bug-tracker etc), it also means
-your work is backed up at all times. The Version Control tutorial has
-instructions for setting up a sane developer environment with proper version control.
This is an integral part of your Coding. It might seem obvious to experienced coders, but it cannot
-be emphasized enough that you should test things on a small scale before putting your untested
-code into a large game-world. The earlier you test, the easier and cheaper it will be to fix bugs
-and even rework things that didn’t work out the way you thought they would. You might even have to
-go back to the Planning phase if your ideas can’t handle their meet with reality.
-
This means building singular in-game examples. Make one room and one object of each important type
-and test so they work correctly in isolation. Then add more if they are supposed to interact with
-each other in some way. Build a small series of rooms to test how mobs move around … and so on. In
-short, a test-bed for your growing code. It should be done gradually until you have a fully
-functioning (if not guaranteed bug-free) miniature tech demo that shows all the features you want
-in the first release of your game. There does not need to be any game play or even a theme to your
-tests, this is only for you and your co-coders to see. The more testing you do on this small scale,
-the less headaches you will have in the next phase.
Up until this point we’ve only had a few tech-demo objects in the database. This step is the act of
-populating the database with a larger, thematic world. Too many would-be developers jump to this
-stage too soon (skipping the Coding or even Planning stages). What if the rooms you build
-now doesn’t include all the nice weather messages the code grows to support? Or the way you store
-data changes under the hood? Your building work would at best require some rework and at worst you
-would have to redo the whole thing. And whereas Evennia’s typeclass system does allow you to edit
-the properties of existing objects, some hooks are only called at object creation … Suffice to
-say you are in for a lot of unnecessary work if you build stuff en masse without having the
-underlying code systems in some reasonable shape first.
-
So before starting to build, the “game” bit (Coding + Testing) should be more or less
-complete, at least to the level of your initial release.
-
Before starting to build, you should also plan ahead again. Make sure it is clear to yourself and
-your eventual builders just which parts of the world you want for your initial release. Establish
-for everyone which style, quality and level of detail you expect. Your goal should not be to
-complete your entire world in one go. You want just enough to make the game’s “feel” come across.
-You want a minimal but functioning world where the intended game play can be tested and roughly
-balanced. You can always add new areas later.
-
During building you get free and extensive testing of whatever custom build commands and systems you
-have made at this point. Since Building often involves different people than those Coding, you also
-get a chance to hear if some things are hard to understand or non-intuitive. Make sure to respond
-to this feedback.
As mentioned, don’t hold onto your world more than necessary. Get it out there with a huge Alpha
-flag and let people try it! Call upon your alpha-players to try everything - they will find ways
-to break your game in ways that you never could have imagined. In Alpha you might be best off to
-focus on inviting friends and maybe other MUD developers, people who you can pester to give proper
-feedback and bug reports (there will be bugs, there is no way around it). Follow the quick
-instructions for Online Setup to make your game visible online. If you hadn’t
-already, make sure to put up your game on the Evennia game index so
-people know it’s in the works (actually, even pre-alpha games are allowed in the index so don’t be
-shy)!
Once things stabilize in Alpha you can move to Beta and let more people in. Many MUDs are in
-perpetual beta, meaning they are never considered
-“finished”, but just repeat the cycle of Planning, Coding, Testing and Building over and over as new
-features get implemented or Players come with suggestions. As the game designer it is now up to you
-to gradually perfect your vision.
A lot of games use a separate time system we refer to as game time. This runs in parallel to what
-we usually think of as real time. The game time might run at a different speed, use different
-names for its time units or might even use a completely custom calendar. You don’t need to rely on a
-game time system at all. But if you do, Evennia offers basic tools to handle these various
-situations. This tutorial will walk you through these features.
Many games let their in-game time run faster or slower than real time, but still use our normal
-real-world calendar. This is common both for games set in present day as well as for games in
-historical or futuristic settings. Using a standard calendar has some advantages:
-
-
Handling repetitive actions is much easier, since converting from the real time experience to the
-in-game perceived one is easy.
-
The intricacies of the real world calendar, with leap years and months of different length etc are
-automatically handled by the system.
-
-
Evennia’s game time features assume a standard calendar (see the relevant section below for a custom
-calendar).
All is done through the settings. Here are the settings you should use if you want a game time with
-a standard calendar:
-
# in a file settings.py in mygame/server/conf
-# The time factor dictates if the game world runs faster (timefactor>1)
-# or slower (timefactor<1) than the real world.
-TIME_FACTOR=2.0
-
-# The starting point of your game time (the epoch), in seconds.
-# In Python a value of 0 means Jan 1 1970 (use negatives for earlier
-# start date). This will affect the returns from the utils.gametime
-# module.
-TIME_GAME_EPOCH=None
-
-
-
By default, the game time runs twice as fast as the real time. You can set the time factor to be 1
-(the game time would run exactly at the same speed than the real time) or lower (the game time will
-be slower than the real time). Most games choose to have the game time spinning faster (you will
-find some games that have a time factor of 60, meaning the game time runs sixty times as fast as the
-real time, a minute in real time would be an hour in game time).
-
The epoch is a slightly more complex setting. It should contain a number of seconds that would
-indicate the time your game started. As indicated, an epoch of 0 would mean January 1st, 1970. If
-you want to set your time in the future, you just need to find the starting point in seconds. There
-are several ways to do this in Python, this method will show you how to do it in local time:
-
# We're looking for the number of seconds representing
-# January 1st, 2020
-fromdatetimeimportdatetime
-importtime
-start=datetime(2020,1,1)
-time.mktime(start.timetuple())
-
-
-
This should return a huge number - the number of seconds since Jan 1 1970. Copy that directly into
-your settings (editing server/conf/settings.py):
-
# in a file settings.py in mygame/server/conf
-TIME_GAME_EPOCH=1577865600
-
-
-
Reload the game with @reload, and then use the @time command. You should see something like
-this:
The line that is most relevant here is the game time epoch. You see it shown at 2020-01-01. From
-this point forward, the game time keeps increasing. If you keep typing @time, you’ll see the game
-time updated correctly… and going (by default) twice as fast as the real time.
The gametime utility also has a way to schedule game-related events, taking into account your game
-time, and assuming a standard calendar (see below for the same feature with a custom calendar). For
-instance, it can be used to have a specific message every (in-game) day at 6:00 AM showing how the
-sun rises.
-
The function schedule() should be used here. It will create a script with some
-additional features to make sure the script is always executed when the game time matches the given
-parameters.
-
The schedule function takes the following arguments:
-
-
The callback, a function to be called when time is up.
-
The keyword repeat (False by default) to indicate whether this function should be called
-repeatedly.
-
Additional keyword arguments sec, min, hour, day, month and year to describe the time
-to schedule. If the parameter isn’t given, it assumes the current time value of this specific unit.
-
-
Here is a short example for making the sun rise every day:
-
# in a file ingame_time.py in mygame/world/
-
-fromevennia.utilsimportgametime
-fromtypeclasses.roomsimportRoom
-
-defat_sunrise():
- """When the sun rises, display a message in every room."""
- # Browse all rooms
- forroominRoom.objects.all():
- room.msg_contents("The sun rises from the eastern horizon.")
-
-defstart_sunrise_event():
- """Schedule an sunrise event to happen every day at 6 AM."""
- script=gametime.schedule(at_sunrise,repeat=True,hour=6,min=0,sec=0)
- script.key="at sunrise"
-
-
-
If you want to test this function, you can easily do something like:
The script will be created silently. The at_sunrise function will now be called every in-game day
-at 6 AM. You can use the @scripts command to see it. You could stop it using @scripts/stop. If
-we hadn’t set repeat the sun would only have risen once and then never again.
-
We used the @py command here: nothing prevents you from adding the system into your game code.
-Remember to be careful not to add each event at startup, however, otherwise there will be a lot of
-overlapping events scheduled when the sun rises.
-
The schedule function when using repeat set to True works with the higher, non-specified unit.
-In our example, we have specified hour, minute and second. The higher unit we haven’t specified is
-day: schedule assumes we mean “run the callback every day at the specified time”. Therefore, you
-can have an event that runs every hour at HH:30, or every month on the 3rd day.
-
-
A word of caution for repeated scripts on a monthly or yearly basis: due to the variations in the
-real-life calendar you need to be careful when scheduling events for the end of the month or year.
-For example, if you set a script to run every month on the 31st it will run in January but find no
-such day in February, April etc. Similarly, leap years may change the number of days in the year.
Using a custom calendar to handle game time is sometimes needed if you want to place your game in a
-fictional universe. For instance you may want to create the Shire calendar which Tolkien described
-having 12 months, each which 30 days. That would give only 360 days per year (presumably hobbits
-weren’t really fond of the hassle of following the astronomical calendar). Another example would be
-creating a planet in a different solar system with, say, days 29 hours long and months of only 18
-days.
-
Evennia handles custom calendars through an optional contrib module, called custom_gametime.
-Contrary to the normal gametime module described above it is not active by default.
In our first example of the Shire calendar, used by hobbits in books by Tolkien, we don’t really
-need the notion of weeks… but we need the notion of months having 30 days, not 28.
-
The custom calendar is defined by adding the TIME_UNITS setting to your settings file. It’s a
-dictionary containing as keys the name of the units, and as value the number of seconds (the
-smallest unit for us) in this unit. Its keys must be picked among the following: “sec”, “min”,
-“hour”, “day”, “week”, “month” and “year” but you don’t have to include them all. Here is the
-configuration for the Shire calendar:
-
# in a file settings.py in mygame/server/conf
-TIME_UNITS={"sec":1,
- "min":60,
- "hour":60*60,
- "day":60*60*24,
- "month":60*60*24*30,
- "year":60*60*24*30*12}
-
-
-
We give each unit we want as keys. Values represent the number of seconds in that unit. Hour is
-set to 60 * 60 (that is, 3600 seconds per hour). Notice that we don’t specify the week unit in this
-configuration: instead, we skip from days to months directly.
-
In order for this setting to work properly, remember all units have to be multiples of the previous
-units. If you create “day”, it needs to be multiple of hours, for instance.
-
So for our example, our settings may look like this:
-
# in a file settings.py in mygame/server/conf
-# Time factor
-TIME_FACTOR=4
-
-# Game time epoch
-TIME_GAME_EPOCH=0
-
-# Units
-TIME_UNITS={
- "sec":1,
- "min":60,
- "hour":60*60,
- "day":60*60*24,
- "month":60*60*24*30,
- "year":60*60*24*30*12,
-}
-
-
-
Notice we have set a time epoch of 0. Using a custom calendar, we will come up with a nice display
-of time on our own. In our case the game time starts at year 0, month 0, day 0, and at midnight.
-
Note that while we use “month”, “week” etc in the settings, your game may not use those terms in-
-game, instead referring to them as “cycles”, “moons”, “sand falls” etc. This is just a matter of you
-displaying them differently. See next section.
As pointed out earlier, the @time command is meant to be used with a standard calendar, not a
-custom one. We can easily create a new command though. We’ll call it time, as is often the case
-on other MU*. Here’s an example of how we could write it (for the example, you can create a file
-showtime.py in your commands directory and paste this code in it):
-
# in a file mygame/commands/gametime.py
-
-fromevennia.contribimportcustom_gametime
-
-fromcommands.commandimportCommand
-
-classCmdTime(Command):
-
- """
- Display the time.
-
- Syntax:
- time
-
- """
-
- key="time"
- locks="cmd:all()"
-
- deffunc(self):
- """Execute the time command."""
- # Get the absolute game time
- year,month,day,hour,min,sec=custom_gametime.custom_gametime(absolute=True)
- string="We are in year {year}, day {day}, month {month}."
- string+="\nIt's {hour:02}:{min:02}:{sec:02}."
- self.msg(string.format(year=year,month=month,day=day,
- hour=hour,min=min,sec=sec))
-
-
-
Don’t forget to add it in your CharacterCmdSet to see this command:
-
# in mygame/commands/default_cmdset.py
-
-fromcommands.gametimeimportCmdTime# <-- Add
-
-# ...
-
-classCharacterCmdSet(default_cmds.CharacterCmdSet):
- """
- The `CharacterCmdSet` contains general in-game commands like `look`,
- `get`, etc available on in-game Character objects. It is merged with
- the `AccountCmdSet` when an Account puppets a Character.
- """
- key="DefaultCharacter"
-
- defat_cmdset_creation(self):
- """
- Populates the cmdset
- """
- super().at_cmdset_creation()
- # ...
- self.add(CmdTime())# <- Add
-
-
-
Reload your game with the @reload command. You should now see the time command. If you enter
-it, you might see something like:
-
We are in year 0, day 0, month 0.
-It's 00:52:17.
-
-
-
You could display it a bit more prettily with names for months and perhaps even days, if you want.
-And if “months” are called “moons” in your game, this is where you’d add that.
The custom_gametime module also has a way to schedule game-related events, taking into account
-your game time (and your custom calendar). It can be used to have a specific message every day at
-6:00 AM, to show the sun rises, for instance. The custom_gametime.schedule function works in the
-same way as described for the default one above.
This will help you download, install and start Evennia for the first time.
-
-
Note: You don’t need to make anything visible to the ‘net in order to run and
-test out Evennia. Apart from downloading and updating you don’t even need an
-internet connection until you feel ready to share your game with the world.
evenniastart (make sure to make a superuser when asked)
-Evennia should now be running and you can connect to it by pointing a web browser to
-http://localhost:4001 or a MUD telnet client to localhost:4000 (use 127.0.0.1 if your OS does
-not recognize localhost).
-
-
We also release Docker images
-based on master and develop branches.
If you run into any issues during the installation and first start, please
-check out Linux Troubleshooting.
-
For Debian-derived systems (like Ubuntu, Mint etc), start a terminal and
-install the dependencies:
-
sudoapt-getupdate
-sudoapt-getinstallpython3python3-pippython3-devpython3-setuptoolspython3-git
-python3-virtualenvgcc
-
-# If you are using an Ubuntu version that defaults to Python3, like 18.04+, use this instead:
-sudoapt-getupdate
-sudoapt-getinstallpython3.7python3-pippython3.7-devpython3-setuptoolsvirtualenvgcc
-
-
-
-
Note that, the default Python version for your distribution may still not be Python3.7 after this.
-This is ok - we’ll specify exactly which Python to use later.
-You should make sure to not be root after this step, running as root is a
-security risk. Now create a folder where you want to do all your Evennia
-development:
-
mkdirmuddev
-cdmuddev
-
-
-
Next we fetch Evennia itself:
-
gitclonehttps://github.com/evennia/evennia.git
-
-
-
A new folder evennia will appear containing the Evennia library. This only
-contains the source code though, it is not installed yet. To isolate the
-Evennia install and its dependencies from the rest of the system, it is good
-Python practice to install into a virtualenv. If you are unsure about what a
-virtualenv is and why it’s useful, see the Glossary entry on
-virtualenv.
-
Run python-V to see which version of Python your system defaults to.
-
# If your Linux defaults to Python3.7+:
-virtualenvevenv
-
-# If your Linux defaults to Python2 or an older version
-# of Python3, you must instead point to Python3.7+ explicitly:
-virtualenv-p/usr/bin/python3.7evenv
-
-
-
A new folder evenv will appear (we could have called it anything). This
-folder will hold a self-contained setup of Python packages without interfering
-with default Python packages on your system (or the Linux distro lagging behind
-on Python package versions). It will also always use the right version of Python.
-Activate the virtualenv:
-
sourceevenv/bin/activate
-
-
-
The text (evenv) should appear next to your prompt to show that the virtual
-environment is active.
-
-
Remember that you need to activate the virtualenv like this every time you
-start a new terminal to get access to the Python packages (notably the
-important evennia program) we are about to install.
-
-
Next, install Evennia into your active virtualenv. Make sure you are standing
-at the top of your mud directory tree (so you see the evennia/ and evenv/
-folders) and run
cdmygame
-evenniamigrate# (this creates the database)
-evenniastart# (create a superuser when asked. Email is optional.)
-
-
-
-
Server logs are found in mygame/server/logs/. To easily view server logs
-live in the terminal, use evennia-l (exit the log-view with Ctrl-C).
-
-
Your game should now be running! Open a web browser at http://localhost:4001
-or point a telnet client to localhost:4000 and log in with the user you
-created. Check out where to go next.
The Evennia server is a terminal program. Open the terminal e.g. from
-Applications->Utilities->Terminal. Here is an introduction to the Mac
-terminal
-if you are unsure how it works. If you run into any issues during the
-installation, please check out Mac Troubleshooting.
-
-
Python should already be installed but you must make sure it’s a high enough version.
-(This discusses
-how you may upgrade it). Remember that you need Python3.7, not Python2.7!
If you run into issues with installing Twisted later you may need to
-install gcc and the Python headers.
-
-
After this point you should not need sudo or any higher privileges to install anything.
-
Now create a folder where you want to do all your Evennia development:
-
mkdirmuddev
-cdmuddev
-
-
-
Next we fetch Evennia itself:
-
gitclonehttps://github.com/evennia/evennia.git
-
-
-
A new folder evennia will appear containing the Evennia library. This only
-contains the source code though, it is not installed yet. To isolate the
-Evennia install and its dependencies from the rest of the system, it is good
-Python practice to install into a virtualenv. If you are unsure about what a
-virtualenv is and why it’s useful, see the Glossary entry on virtualenv.
-
Run python-V to check which Python your system defaults to.
-
# If your Mac defaults to Python3:
-virtualenvevenv
-
-# If your Mac defaults to Python2 you need to specify the Python3.7 binary explicitly:
-virtualenv-p/path/to/your/python3.7evenv
-
-
-
A new folder evenv will appear (we could have called it anything). This
-folder will hold a self-contained setup of Python packages without interfering
-with default Python packages on your system. Activate the virtualenv:
-
sourceevenv/bin/activate
-
-
-
The text (evenv) should appear next to your prompt to show the virtual
-environment is active.
-
-
Remember that you need to activate the virtualenv like this every time you
-start a new terminal to get access to the Python packages (notably the
-important evennia program) we are about to install.
-
-
Next, install Evennia into your active virtualenv. Make sure you are standing
-at the top of your mud directory tree (so you see the evennia/ and evenv/
-folders) and run
-
pipinstall--upgradepip# Old pip versions may be an issue on Mac.
-pipinstall--upgradesetuptools# Ditto concerning Mac issues.
-pipinstall-eevennia
-
cdmygame
-evenniamigrate# (this creates the database)
-evenniastart# (create a superuser when asked. Email is optional.)
-
-
-
-
Server logs are found in mygame/server/logs/. To easily view server logs
-live in the terminal, use evennia-l (exit the log-view with Ctrl-C).
-
-
Your game should now be running! Open a web browser at http://localhost:4001
-or point a telnet client to localhost:4000 and log in with the user you
-created. Check out where to go next.
If you run into any issues during the installation, please check out
-Windows Troubleshooting.
-
-
If you are running Windows10, consider using the Windows Subsystem for Linux
-(WSL) instead.
-You should then follow the Linux install instructions above.
-
-
The Evennia server itself is a command line program. In the Windows launch
-menu, start All Programs -> Accessories -> command prompt and you will get
-the Windows command line interface. Here is one of many tutorials on using the Windows command
-line
-if you are unfamiliar with it.
-
-
Install Python from the Python homepage. You will
-need to be a
-Windows Administrator to install packages. You want Python version 3.7.0 (latest verified
-version), usually
-the 64-bit version (although it doesn’t matter too much). When installing, make sure
-to check-mark all install options, especially the one about making Python
-available on the path (you may have to scroll to see it). This allows you to
-just write python in any console without first finding where the python
-program actually sits on your hard drive.
-
You need to also get GIT and install it. You
-can use the default install options but when you get asked to “Adjust your PATH
-environment”, you should select the second option “Use Git from the Windows
-Command Prompt”, which gives you more freedom as to where you can use the
-program.
-
Finally you must install the Microsoft Visual C++ compiler for
-Python. Download and run the linked installer and
-install the C++ tools. Keep all the defaults. Allow the install of the “Win10 SDK”, even if you are
-on Win7 (not tested on older Windows versions). If you later have issues with installing Evennia due
-to a failure to build the “Twisted wheels”, this is where you are missing things.
-
You may need the pypiwin32 Python headers. Install
-these only if you have issues.
-
-
You can install Evennia wherever you want. cd to that location and create a
-new folder for all your Evennia development (let’s call it muddev).
-
mkdirmuddev
-cdmuddev
-
-
-
-
Hint: If cd isn’t working you can use pushd instead to force the
-directory change.
-
-
Next we fetch Evennia itself:
-
gitclonehttps://github.com/evennia/evennia.git
-
-
-
A new folder evennia will appear containing the Evennia library. This only
-contains the source code though, it is not installed yet. To isolate the
-Evennia install and its dependencies from the rest of the system, it is good
-Python practice to install into a virtualenv. If you are unsure about what a
-virtualenv is and why it’s useful, see the Glossary entry on virtualenv.
-
In your console, try python-V to see which version of Python your system
-defaults to.
-
pipinstallvirtualenv
-
-# If your setup defaults to Python3.7:
-virtualenvevenv
-
-# If your setup defaults to Python2, specify path to python3.exe explicitly:
-virtualenv-pC:\Python37\python.exeevenv
-
-# If you get an infinite spooling response, press CTRL + C to interrupt and try using:
-python-mvenvevenv
-
-
-
-
A new folder evenv will appear (we could have called it anything). This
-folder will hold a self-contained setup of Python packages without interfering
-with default Python packages on your system. Activate the virtualenv:
-
# If you are using a standard command prompt, you can use the following:
-evenv\scripts\activate.bat
-
-# If you are using a PS Shell, Git Bash, or other, you can use the following:
-.\evenv\scripts\activate
-
-
-
-
The text (evenv) should appear next to your prompt to show the virtual
-environment is active.
-
-
Remember that you need to activate the virtualenv like this every time you
-start a new console window if you want to get access to the Python packages
-(notably the important evennia program) we are about to install.
-
-
Next, install Evennia into your active virtualenv. Make sure you are standing
-at the top of your mud directory tree (so you see the evennia and evenv
-folders when you use the dir command) and run
-
pipinstall-eevennia
-
-
-
For more info about pip, see the Glossary entry on pip. If
-the install failed with any issues, see [Windows Troubleshooting](./Getting-Started.md#windows-
-troubleshooting).
-Next we’ll start our new game, we’ll call it “mygame” here. This creates a new folder where you will
-be
-creating your new game:
-
evennia--initmygame
-
-
-
Your final folder structure should look like this:
cdmygame
-evenniamigrate# (this creates the database)
-evenniastart# (create a superuser when asked. Email is optional.)
-
-
-
-
Server logs are found in mygame/server/logs/. To easily view server logs
-live in the terminal, use evennia-l (exit the log-view with Ctrl-C).
-
-
Your game should now be running! Open a web browser at http://localhost:4001
-or point a telnet client to localhost:4000 and log in with the user you
-created. Check out where to go next.
Welcome to Evennia! Your new game is fully functioning, but empty. If you just
-logged in, stand in the Limbo room and run
-
@batchcommand tutorial_world.build
-
-
-
to build Evennia’s tutorial world - it’s a small solo quest to
-explore. Only run the instructed @batchcommand once. You’ll get a lot of text scrolling by as the
-tutorial is built. Once done, the tutorial exit will have appeared out of Limbo - just write
-tutorial to enter it.
-
Once you get back to Limbo from the tutorial (if you get stuck in the tutorial quest you can do
-@tel#2 to jump to Limbo), a good idea is to learn how to [start, stop and reload](Start-Stop-
-Reload) the Evennia server. You may also want to familiarize yourself with some commonly used terms
-in our Glossary. After that, why not experiment with creating some new items and build
-some new rooms out from Limbo.
-
From here on, you could move on to do one of our introductory tutorials or simply dive
-headlong into Evennia’s comprehensive manual. While
-Evennia has no major game systems out of the box, we do supply a range of optional contribs that
-you can use or borrow from. They range from dice rolling and alternative color schemes to barter and
-combat systems. You can find the growing list of contribs
-here.
If you have issues with installing or starting Evennia for the first time,
-check the section for your operating system below. If you have an issue not
-covered here, please report it
-so it can be fixed or a workaround found!
-
Remember, the server logs are in mygame/server/logs/. To easily view server logs in the terminal,
-you can run evennia-l, or (in the future) start the server with evenniastart-l.
If you get an error when installing Evennia (especially with lines mentioning
-failing to include Python.h) then try sudoapt-getinstallpython3-setuptoolspython3-dev.
-Once installed, run pipinstall-eevennia again.
-
Under some not-updated Linux distributions you may run into errors with a
-too-old setuptools or missing functools. If so, update your environment
-with pipinstall--upgradepipwheelsetuptools. Then try pipinstall-eevennia again.
-
If you get an setup.pynotfound error message while trying to pipinstall, make sure you are
-in the right directory. You should be at the same level of the evenv directory, and the
-evennia git repository. Note that there is an evennia directory inside of the repository too.
-
One user reported a rare issue on Ubuntu 16 is an install error on installing Twisted; Command"pythonsetup.pyegg_info"failedwitherrorcode1in/tmp/pip-build-vnIFTg/twisted/ with errors
-like distutils.errors.DistutilsError:CouldnotfindsuitabledistributionforRequirement.parse('incremental>=16.10.1'). This appears possible to solve by simply updating Ubuntu
-with sudoapt-getupdate&&sudoapt-getdist-upgrade.
-
Users of Fedora (notably Fedora 24) has reported a gcc error saying the directory
-/usr/lib/rpm/redhat/redhat-hardened-cc1 is missing, despite gcc itself being installed. The
-confirmed work-around seems to be to
-install the redhat-rpm-config package with e.g. sudodnfinstallredhat-rpm-config.
-
Some users trying to set up a virtualenv on an NTFS filesystem find that it fails due to issues
-with symlinks not being supported. Answer is to not use NTFS (seriously, why would you do that to
-yourself?)
Mac users have reported a critical MemoryError when trying to start Evennia on Mac with a Python
-version below 2.7.12. If you get this error, update to the latest XCode and Python2 version.
-
Some Mac users have reported not being able to connect to localhost (i.e. your own computer). If
-so, try to connect to 127.0.0.1 instead, which is the same thing. Use port 4000 from mud clients
-and port 4001 from the web browser as usual.
If you installed Python but the python command is not available (even in a new console), then
-you might have missed installing Python on the path. In the Windows Python installer you get a list
-of options for what to install. Most or all options are pre-checked except this one, and you may
-even have to scroll down to see it. Reinstall Python and make sure it’s checked.
-
If your MUD client cannot connect to localhost:4000, try the equivalent 127.0.0.1:4000
-instead. Some MUD clients on Windows does not appear to understand the alias localhost.
-
If you run virtualenvevenv and get a 'virtualenv'isnotrecognizedasaninternalorexternalcommand,operableprogramorbatchfile. error, you can mkdirevenv, cdevenv and then python-mvirtualenv. as a workaround.
-
Some Windows users get an error installing the Twisted ‘wheel’. A wheel is a pre-compiled binary
-package for Python. A common reason for this error is that you are using a 32-bit version of Python,
-but Twisted has not yet uploaded the latest 32-bit wheel. Easiest way to fix this is to install a
-slightly older Twisted version. So if, say, version 18.1 failed, install 18.0 manually with pipinstalltwisted==18.0. Alternatively you could try to get a 64-bit version of Python (uninstall the
-32bit one). If so, you must then deactivate the virtualenv, delete the evenv folder and recreate
-it anew (it will then use the new Python executable).
-
If your server won’t start, with no error messages (and no log files at all when starting from
-scratch), try to start with evenniaipstart instead. If you then see an error about systemcannotfindthepathspecified, it may be that the file evennia/evennia/server/twistd.bat has the wrong
-path to the twistd executable. This file is auto-generated, so try to delete it and then run
-evenniastart to rebuild it and see if it works. If it still doesn’t work you need to open it in a
-text editor like Notepad. It’s just one line containing the path to the twistd.exe executable as
-determined by Evennia. If you installed Twisted in a non-standard location this might be wrong and
-you should update the line to the real location.
-
Some users have reported issues with Windows WSL and anti-virus software during Evennia
-development. Timeout errors and the inability to run evenniaconnections may be due to your anti-
-virus software interfering. Try disabling or changing your anti-virus software settings.
The term ‘account’ refers to the player’s unique account on the game. It is
-represented by the Accounttypeclass and holds things like email, password,
-configuration etc.
-
When a player connects to the game, they connect to their account. The account has no
-representation in the game world. Through their Account they can instead choose to
-puppet one (or more, depending on game mode) Characters in
-the game.
-
In the default multisession mode of Evennia, you immediately start
-puppeting a Character with the same name as your Account when you log in - mimicking how older
-servers used to work.
This usually refers to Django’sAdmin site or database-administration web page
-(link to Django docs). The admin site is
-an automatically generated web interface to the database (it can be customized extensively). It’s
-reachable from the admin link on the default Evennia website you get with your server.
The term Attribute should not be confused with (properties or
-fields. The Attribute represents arbitrary pieces of data that can be attached
-to any typeclassed entity in Evennia. Attributes allows storing new persistent
-data on typeclasses without changing their underlying database schemas. Read more about Attributes
-here.
A Channel refers to an in-game communication channel. It’s an entity that people subscribe to and
-which re-distributes messages between all subscribers. Such subscribers default to being
-Accounts, for out-of-game communication but could also be Objects (usually
-Characters) if one wanted to adopt Channels for things like in-game walkie-
-talkies or phone systems. It is represented by the Channel typeclass. You can read more about the
-comm system here.
The Character is the term we use for the default avatar being puppeted by the
-account in the game world. It is represented by the Character typeclass (which
-is a child of Object). Many developers use children of this class to represent
-monsters and other NPCs. You can read more about it here.
Django is a professional and very popular Python web framework,
-similar to Rails for the Ruby language. It is one of Evennia’s central library dependencies (the
-other one is Twisted). Evennia uses Django for two main things - to map all
-database operations to Python and for structuring our web site.
-
Through Django, we can work with any supported database (SQlite3, Postgres, MySQL …) using generic
-Python instead of database-specific SQL: A database table is represented in Django as a Python class
-(called a model). An Python instance of such a class represents a row in that table.
-
There is usually no need to know the details of Django’s database handling in order to use Evennia -
-it will handle most of the complexity for you under the hood using what we call
-typeclasses. But should you need the power of Django you can always get it.
-Most commonly people want to use “raw” Django when doing more advanced/custom database queries than
-offered by Evennia’s default search functions. One will then need
-to read about Django’s querysets. Querysets are Python method calls on a special form that lets
-you build complex queries. They get converted into optimized SQL queries under the hood, suitable
-for your current database. [Here is our tutorial/explanation of Django queries](Tutorial-Searching-
-For-Objects#queries-in-django).
-
-
By the way, Django (and Evennia) does allow you to fall through and send raw SQL if you really
-want to. It’s highly unlikely to be needed though; the Django database abstraction is very, very
-powerful.
-
-
The other aspect where Evennia uses Django is for web integration. On one end Django gives an
-infrastructure for wiring Python functions (called views) to URLs: the view/function is called
-when a user goes that URL in their browser, enters data into a form etc. The return is the web page
-to show. Django also offers templating with features such as being able to add special markers in
-HTML where it will insert the values of Python variables on the fly (like showing the current player
-count on the web page). [Here is one of our tutorials on wiring up such a web page](Add-a-simple-
-new-web-page). Django also comes with the admin site, which automatically
-maps the database into a form accessible from a web browser.
Contribs are optional and often more game-specific code-snippets contributed by the Evennia community.
-They are distributed with Evennia in the contrib/ folder.
This term is sometimes used to represent the main Evennia library code suite, excluding its
-contrib directory. It can sometimes come up in code reviews, such as
-
-
Evennia is game-agnostic but this feature is for a particular game genre. So it does not belong in
-core. Better make it a contrib.
A field or database field in Evennia refers to a property on a
-typeclass directly linked to an underlying database column. Only a few fixed
-properties per typeclass are database fields but they are often tied to the core functionality of
-that base typeclass (for example Objects store its location as a field). In all
-other cases, attributes are used to add new persistent data to the typeclass.
-Read more about typeclass properties here.
Git is a version control
-tool. It allows us to track the development of the Evennia code by dividing it into units called
-commits. A ‘commit’ is sort of a save-spot - you save the current state of your code and can then
-come back to it later if later changes caused problems. By tracking commits we know what ‘version’
-of the code we are currently using.
-
Evennia’s source code + its source history is jointly called a repository.
-This is centrally stored at our online home on GitHub. Everyone using or
-developing Evennia makes a ‘clone’ of this repository to their own computer - everyone
-automatically gets everything that is online, including all the code history.
-
-
Don’t confuse Git and GitHub. The former is the version control system. The
-latter is a website (run by a company) that allows you to upload source code controlled by Git for
-others to see (among other things).
-
-
Git allows multiple users from around the world to efficiently collaborate on Evennia’s code: People
-can make local commits on their cloned code. The commits they do can then be uploaded to GitHub and
-reviewed by the Evennia lead devs - and if the changes look ok they can be safely merged into the
-central Evennia code - and everyone can pull those changes to update their local copies.
-
Developers using Evennia often uses Git on their own games in the same way - to track their changes
-and to help collaboration with team mates. This is done completely independently of Evennia’s Git
-usage.
-
Common usage (for non-Evennia developers):
-
-
gitclone<github-url> - clone an online repository to your computer. This is what you do when
-you ‘download’ Evennia. You only need to do this once.
-
gitpull (inside local copy of repository) - sync your local repository with what is online.
-
-
-
Full usage of Git is way beyond the scope of this glossary. See Tutorial - version
-control for more info and links to the Git documentation.
This term is used for upgrading the database structure (it’s schema )to a new version. Most often
-this is due to Evennia’s upstream schema changing. When that happens you need to
-migrate that schema to the new version as well. Once you have used git to pull the
-latest changes, just cd into your game dir and run
-
evennia migrate
-
-
-
That should be it (see virtualenv if you get a warning that the evennia
-command is not available). See also Updating your game for more details.
-
-
Technically, migrations are shipped as little Python snippets of code that explains which database
-actions must be taken to upgrade from one version of the schema to the next. When you run the
-command above, those snippets are run in sequence.
This term refers to the MULTISESSION_MODE setting, which has a value of 0 to 3. The mode alters
-how players can connect to the game, such as how many Sessions a player can start with one account
-and how many Characters they can control at the same time. It is described in detail
-here.
Github is where Evennia’s source code and documentation is hosted.
-This online repository of code we also sometimes refer to as upstream.
-
GitHub is a business, offering free hosting to Open-source projects like Evennia. Despite the
-similarity in name, don’t confuse GitHub the website with Git, the versioning
-system. Github hosts Git repositories online and helps with collaboration and
-infrastructure. Git itself is a separate project.
In general Python (and other object-oriented languages), an object is what we call the instance of a class. But one of Evennia’s
-core typeclasses is also called “Object”. To separate these in the docs we
-try to use object to refer to the general term and capitalized Object when we refer to the
-typeclass.
pip comes with Python and is the main tool for installing third-
-party Python packages from the web. Once a python package is installed you can do import<packagename> in your Python code.
-
Common usage:
-
-
pipinstall<package-name> - install the given package along with all its dependencies.
-
pipsearch<name> - search Python’s central package repository PyPi for a
-package of that name.
-
pipinstall--upgrade<package_name> - upgrade a package you already have to the latest version.
-
pipinstall<packagename>==1.5 - install exactly a specific package version.
-
pipinstall<folder> - install a Python package you have downloaded earlier (or cloned using
-git).
-
pipinstall-e<folder> - install a local package by just making a soft link to the folder. This
-means that if the code in <folder> changes, the installed Python package is immediately updated.
-If not using -e, one would need to run pipinstall--upgrade<folder> every time to make the
-changes available when you import this package into your code. Evennia is installed this way.
-
-
For development, pip is usually used together with a virtualenv to install
-all packages and dependencies needed for a project in one, isolated location on the hard drive.
An account can take control and “play as” any Object. When
-doing so, we call this puppeting, (like puppeteering).
-Normally the entity being puppeted is of the Character subclass but it does
-not have to be.
A property is a general term used for properties on any Python object. The term also sometimes
-refers to the property built-in function of Python (read more here). Note the distinction between properties,
-fields and Attributes.
A repository is a version control/git term. It represents a folder containing
-source code plus its versioning history.
-
-
In Git’s case, that history is stored in a hidden folder .git. If you ever feel the need to look
-into this folder you probably already know enough Git to know why.
-
-
The evennia folder you download from us with gitclone is a repository. The code on
-GitHub is often referred to as the ‘online repository’ (or the upstream
-repository). If you put your game dir under version control, that of course becomes a repository as
-well.
When we refer to Scripts, we generally refer to the Scripttypeclass. Scripts are
-the mavericks of Evennia - they are like Objects but without any in-game
-existence. They are useful as custom places to store data but also as building blocks in persistent
-game systems. Since the can be initialized with timing capabilities they can also be used for long-
-time persistent time keeping (for fast updates other types of timers may be better though). Read
-more about Scripts here
A Session is a Python object representing a single client connection to the server. A
-given human player could connect to the game from different clients and each would get a Session
-(even if you did not allow them to actually log in and get access to an
-account).
-
Sessions are nottypeclassed and has no database persistence. But since they
-always exist (also when not logged in), they share some common functionality with typeclasses that
-can be useful for certain game states.
A Tag is a simple label one can attach to one or more objects in the game. Tagging is a
-powerful way to group entities and can also be used to indicate they have particular shared abilities.
-Tags are shared between objects (unlike Attributes).
The Ticker handler runs Evennia’s optional ‘ticker’ system. In other engines, such
-as DIKU, all game events are processed only at specific
-intervals called ‘ticks’. Evennia has no such technical limitation (events are processed whenever
-needed) but using a fixed tick can still be useful for certain types of game systems, like combat.
-Ticker Handler allows you to emulate any number of tick rates (not just one) and subscribe actions
-to be called when those ticks come around.
The typeclass is an Evennia-specific term. A typeclass allows developers to work with
-database-persistent objects as if they were normal Python objects. It makes use of specific
-Django features to link a Python class to a database table. Sometimes we refer to
-such code entities as being typeclassed.
-
Evennia’s main typeclasses are Account, Object,
-Script and Channel. Children of the base class (such as
-Character) will use the same database table as the parent, but can have vastly
-different Python capabilities (and persistent features through Attributes and
-Tags. A typeclass can be coded and treated pretty much like any other Python class
-except it must inherit (at any distance) from one of the base typeclasses. Also, creating a new
-instance of a typeclass will add a new row to the database table to which it is linked.
-
The core typeclasses in the Evennia library are all named DefaultAccount,
-DefaultObject etc. When you initialize your [game dir] you automatically get empty children of
-these, called Account, Object etc that you can start working with.
Twisted is a heavy-duty asynchronous networking engine. It is one
-of Evennia’s two major library dependencies (the other one is Django). Twisted is
-what “runs” Evennia - it handles Evennia’s event loop. Twisted also has the building blocks we need
-to construct network protocols and communicate with the outside world; such as our MUD-custom
-version of Telnet, Telnet+SSL, SSH, webclient-websockets etc. Twisted also runs our integrated web
-server, serving the Django-based website for your game.
The standard virtualenv program comes with Python. It is
-used to isolate all Python packages needed by a given Python project into one folder (we call that
-folder evenv but it could be called anything). A package environment created this way is usually
-referred to as “a virtualenv”. If you ever try to run the evennia program and get an error saying
-something like “the command ‘evennia’ is not available” - it’s probably because your virtualenv is
-not ‘active’ yet (see below).
-
Usage:
-
-
virtualenv<name> - initialize a new virtualenv <name> in a new folder <name> in the current
-location. Called evenv in these docs.
-
virtualenv-ppath/to/alternate/python_executable<name> - create a virtualenv using another
-Python version than default.
-
source<folder_name>/bin/activate(linux/mac) - activate the virtualenv in <folder_name>.
-
<folder_name>\Scripts\activate (windows)
-
deactivate - turn off the currently activated virtualenv.
-
-
A virtualenv is ‘activated’ only for the console/terminal it was started in, but it’s safe to
-activate the same virtualenv many times in different windows if you want. Once activated, all Python
-packages now installed with pip will install to evenv rather than to a global
-location like /usr/local/bin or C:\ProgramFiles.
-
-
Note that if you have root/admin access you could install Evennia globally just fine, without
-using a virtualenv. It’s strongly discouraged and considered bad practice though. Experienced Python
-developers tend to rather create one new virtualenv per project they are working on, to keep the
-varying installs cleanly separated from one another.
-
-
When you execute Python code within this activated virtualenv, only those packages installed
-within will be possible to import into your code. So if you installed a Python package globally on
-your computer, you’ll need to install it again in your virtualenv.
-
-
Virtualenvs only deal with Python programs/packages. Other programs on your computer couldn’t
-care less if your virtualenv is active or not. So you could use git without the virtualenv being
-active, for example.
-
-
When your virtualenv is active you should see your console/terminal prompt change to
-
(evenv) ...
-
-
-
… or whatever name you gave the virtualenv when you initialized it.
-
-
We sometimes say that we are “in” the virtualenv when it’s active. But just to be clear - you
-never have to actually cd into the evenv folder. You can activate it from anywhere and will
-still be considered “in” the virtualenv wherever you go until you deactivate or close the
-console/terminal.
-
-
So, when do I need to activate my virtualenv? If the virtualenv is not active, none of the Python
-packages/programs you installed in it will be available to you. So at a minimum, it needs to be
-activated whenever you want to use the evennia command for any reason.
Grapevine is a new chat network for MU**** games. By
-connecting an in-game channel to the grapevine network, players on your game
-can chat with players in other games, also non-Evennia ones.
To use Grapevine, you first need the pyopenssl module. Install it into your
-Evennia python environment with
-
pip install pyopenssl
-
-
-
To configure Grapevine, you’ll need to activate it in your settings file.
-
GRAPEVINE_ENABLED=True
-
-
-
Next, register an account at https://grapevine.haus. When you have logged in,
-go to your Settings/Profile and to the Games sub menu. Here you register your
-new game by filling in its information. At the end of registration you are going
-to get a ClientID and a ClientSecret. These should not be shared.
-
Open/create the file mygame/server/conf/secret_settings.py and add the following:
You can also customize the Grapevine channels you are allowed to connect to. This
-is added to the GRAPEVINE_CHANNELS setting. You can see which channels are available
-by going to the Grapevine online chat here: https://grapevine.haus/chat.
-
Start/reload Evennia and log in as a privileged user. You should now have a new
-command available: @grapevine2chan. This command is called like this:
Here, the evennia_channel must be the name of an existing Evennia channel and
-grapevine_channel one of the supported channels in GRAPEVINE_CHANNELS.
-
-
At the time of writing, the Grapevine network only has two channels:
-testing and gossip. Evennia defaults to allowing connecting to both. Use
-testing for trying your connection.
You can connect Grapevine to any Evennia channel (so you could connect it to
-the default public channel if you like), but for testing, let’s set up a
-new channel gw.
-
@ccreate gw = This is connected to an gw channel!
-
-
-
You will automatically join the new channel.
-
Next we will create a connection to the Grapevine network.
-
@grapevine2chan gw = gossip
-
-
-
Evennia will now create a new connection and connect it to Grapevine. Connect
-to https://grapevine.haus/chat to check.
-
Write something in the Evennia channel gw and check so a message appears in
-the Grapevine chat. Write a reply in the chat and the grapevine bot should echo
-it to your channel in-game.
-
Your Evennia gamers can now chat with users on external Grapevine channels!
Evennia supports guest logins out of the box. A guest login is an anonymous, low-access account
-and can be useful if you want users to have a chance to try out your game without committing to
-creating a real account.
-
Guest accounts are turned off by default. To activate, add this to your game/settings.py file:
-
GUEST_ENABLED = True
-
-
-
Henceforth users can use connectguest (in the default command set) to login with a guest account.
-You may need to change your Connection Screen to inform them of this
-possibility. Guest accounts work differently from normal accounts - they are automatically deleted
-whenever the user logs off or the server resets (but not during a reload). They are literally re-
-usable throw-away accounts.
-
You can add a few more variables to your settings.py file to customize your guests:
-
-
BASE_GUEST_TYPECLASS - the python-path to the default typeclass for guests.
-Defaults to "typeclasses.accounts.Guest".
-
PERMISSION_GUEST_DEFAULT - permission level for guest accounts. Defaults to "Guests",
-which is the lowest permission level in the hierarchy.
-
GUEST_START_LOCATION - the #dbref to the starting location newly logged-in guests should
-appear at. Defaults to "#2 (Limbo).
-
GUEST_HOME - guest home locations. Defaults to Limbo as well.
-
GUEST_LIST - this is a list holding the possible guest names to use when entering the game. The
-length of this list also sets how many guests may log in at the same time. By default this is a list
-of nine names from "Guest1" to "Guest9".
Making Evennia, HTTPS and Secure Websockets play nicely together¶
-
This we can do by installing a proxy between Evennia and the outgoing ports of your server.
-Essentially,
-Evennia will think it’s only running locally (on localhost, IP 127.0.0.1) - the proxy will
-transparently
-map that to the “real” outgoing ports and handle HTTPS/WSS for us.
Here we will use HAProxy, an open-source proxy that is easy to set up
-and use. We will
-also be using LetsEncrypt, especially the excellent
-helper-program Certbot which pretty much automates the whole
-certificate setup process for us.
-
Before starting you also need the following:
-
-
(optional) The host name of your game (like myawesomegame.com). This is something you must
-previously have purchased from a domain registrar and set up with DNS to point to the IP of your
-server.
-
If you don’t have a domain name or haven’t set it up yet, you must at least know the IP of your
-server. Find this with ifconfig or similar from inside the server. If you use a hosting service
-like DigitalOcean you can also find the droplet’s IP address in the control panel.
-
You must open port 80 in your firewall. This is used by Certbot below to auto-renew certificates.
-So you can’t really run another webserver alongside this setup without tweaking.
-
You must open port 443 (HTTPS) in your firewall.
-
You must open port 4002 (the default Websocket port) in your firewall.
Certificates guarantee that you are you. Easiest is to get this with
-Letsencrypt and the
-Certbot program. Certbot has a lot of install instructions
-for various operating systems. Here’s for Debian/Ubuntu:
-
sudoaptinstallcertbot
-
-
-
Make sure to stop Evennia and that no port-80 using service is running, then
-
sudocertbotcertonly--standalone
-
-
-
You will get some questions you need to answer, such as an email to send certificate errors to and
-the host name (or IP, supposedly) to use with this certificate. After this, the certificates will
-end up in /etc/letsencrypt/live/<your-host-or-ip>/*pem (example from Ubuntu). The critical files
-for our purposes are fullchain.pem and privkey.pem.
-
Certbot sets up a cron-job/systemd job to regularly renew the certificate. To check this works, try
-
sudocertbotrenew--dry-run
-
-
-
The certificate is only valid for 3 months at a time, so make sure this test works (it requires port
-80 to be open). Look up Certbot’s page for more help.
-
We are not quite done. HAProxy expects these two files to be one file.
This will create a new .pem file by concatenating the two files together. The yourhostname.pem
-file (or whatever you named it) is what we will use when the the HAProxy config file (below) asks
-for “your-certificate.pem”.
Make sure to reboot (stop + start) evennia completely:
-
evenniareboot
-
-
-
Finally you start the proxy:
-
sudohaproxy-f/path/to/the/above/config_file.cfg
-
-
-
Make sure you can connect to your game from your browser and that you end up with an https:// page
-and can use the websocket webclient.
-
Once everything works you may want to start the proxy automatically and in the background. Stop the
-proxy with Ctrl-C and uncomment the line #daemon in the config file, then start the proxy again
-
-
it will now start in the bacground.
-
-
You may also want to have the proxy start automatically; this you can do with cron, the inbuilt
-Linux mechanism for running things at specific times.
-
sudocrontab-e
-
-
-
Choose your editor and add a new line at the end of the crontab file that opens:
Before doing this tutorial you will probably want to read the intro in Basic Web tutorial. Reading the three first parts of the Django tutorial might help as well.
-
This tutorial will show you how to access the help system through your website. Both help commands
-and regular help entries will be visible, depending on the logged-in user or an anonymous character.
-
This tutorial will show you how to:
-
-
Create a new page to add to your website.
-
Take advantage of a basic view and basic templates.
-
Access the help system on your website.
-
Identify whether the viewer of this page is logged-in and, if so, to what account.
The first step is to create our new Django app. An app in Django can contain pages and
-mechanisms: your website may contain different apps. Actually, the website provided out-of-the-box
-by Evennia has already three apps: a “webclient” app, to handle the entire webclient, a “website”
-app to contain your basic pages, and a third app provided by Django to create a simple admin
-interface. So we’ll create another app in parallel, giving it a clear name to represent our help
-system.
-
From your game directory, use the following command:
-
evennia startapp help_system
-
-
-
-
Note: calling the app “help” would have been more explicit, but this name is already used by
-Django.
-
-
This will create a directory named help_system at the root of your game directory. It’s a good
-idea to keep things organized and move this directory in the “web” directory of your game. Your
-game directory should look like:
-
mygame/
- ...
- web/
- help_system/
- ...
-
-
-
The “web/help_system” directory contains files created by Django. We’ll use some of them, but if
-you want to learn more about them all, you should read the Django
-tutorial.
-
There is a last thing to be done: your folder has been added, but Django doesn’t know about it, it
-doesn’t know it’s a new app. We need to tell it, and we do so by editing a simple setting. Open
-your “server/conf/settings.py” file and add, or edit, these lines:
-
# Web configuration
-INSTALLED_APPS+=(
- "web.help_system",
-)
-
-
-
You can start Evennia if you want, and go to your website, probably at
-http://localhost:4001 . You won’t see anything different though: we added
-the app but it’s fairly empty.
At this point, our new app contains mostly empty files that you can explore. In order to create
-a page for our help system, we need to add:
-
-
A view, dealing with the logic of our page.
-
A template to display our new page.
-
A new URL pointing to our page.
-
-
-
We could get away by creating just a view and a new URL, but that’s not a recommended way to work
-with your website. Building on templates is so much more convenient.
A view in Django is a simple Python function placed in the “views.py” file in your app. It will
-handle the behavior that is triggered when a user asks for this information by entering a URL (the
-connection between views and URLs will be discussed later).
-
So let’s create our view. You can open the “web/help_system/views.py” file and paste the following
-lines:
Our view handles all code logic. This time, there’s not much: when this function is called, it will
-render the template we will now create. But that’s where we will do most of our work afterward.
The render function called into our view asks the templatehelp_system/index.html. The
-templates of our apps are stored in the app directory, “templates” sub-directory. Django may have
-created the “templates” folder already. If not, create it yourself. In it, create another folder
-“help_system”, and inside of this folder, create a file named “index.html”. Wow, that’s some
-hierarchy. Your directory structure (starting from web) should look like this:
Here’s a little explanation line by line of what this template does:
-
-
It loads the “base.html” template. This describes the basic structure of all your pages, with
-a menu at the top and a footer, and perhaps other information like images and things to be present
-on each page. You can create templates that do not inherit from “base.html”, but you should have a
-good reason for doing so.
-
The “base.html” template defines all the structure of the page. What is left is to override
-some sections of our pages. These sections are called blocks. On line 2, we override the block
-named “blocktitle”, which contains the title of our page.
-
Same thing here, we override the block named “content”, which contains the main content of our
-web page. This block is bigger, so we define it on several lines.
-
This is perfectly normal HTML code to display a level-2 heading.
Last step to add our page: we need to add a URL leading to it… otherwise users won’t be able to
-access it. The URLs of our apps are stored in the app’s directory “urls.py” file.
-
Open the “web/help_system/urls.py” file (you might have to create it) and write in it:
-
# URL patterns for the help_system app
-
-fromdjango.urlsimportinclude,path
-fromweb.help_system.viewsimportindex
-
-urlpatterns=[
- path(r'',index,name="help-index")
-]
-
-
-
We also need to add our app as a namespace holder for URLS. Edit the file “web/urls.py” (you might
-have to create this one too). In it you will find the custom_patterns variable. Replace it with:
When a user will ask for a specific URL on your site, Django will:
-
-
Read the list of custom patterns defined in “web/urls.py”. There’s one pattern here, which
-describes to Django that all URLs beginning by ‘help/’ should be sent to the ‘help_system’ app. The
-‘help/’ part is removed.
-
Then Django will check the “web.help_system/urls.py” file. It contains only one URL, which is
-empty.
-
-
In other words, if the URL is ‘/help/’, then Django will execute our defined view.
You can now reload or start Evennia. Open a tab in your browser and go to
-http://localhost:4001/help_system/ . If everything goes well,
-you should see your new page… which isn’t empty since Evennia uses our “base.html” template. In
-the content of our page, there’s only a heading that reads “help index”. Notice that the title of
-our page is “mygame - Help index” (“mygame” is replaced by the name of your game).
-
From now on, it will be easier to move forward and add features.
Have the help of commands and help entries accessed online.
-
Have various commands and help entries depending on whether the user is logged in or not.
-
-
In terms of pages, we’ll have:
-
-
One to display the list of help topics.
-
One to display the content of a help topic.
-
-
The first one would link to the second.
-
-
Should we create two URLs?
-
-
The answer is… maybe. It depends on what you want to do. We have our help index accessible
-through the “/help_system/” URL. We could have the detail of a help entry accessible through
-“/help_system/desc” (to see the detail of the “desc” command). The problem is that our commands or
-help topics may contain special characters that aren’t to be present in URLs. There are different
-ways around this problem. I have decided to use a GET variable here, which would create URLs like
-this:
-
/help_system?name=desc
-
-
-
If you use this system, you don’t have to add a new URL: GET and POST variables are accessible
-through our requests and we’ll see how soon enough.
One of our requirements is to have a help system tailored to our accounts. If an account with admin
-access logs in, the page should display a lot of commands that aren’t accessible to common users.
-And perhaps even some additional help topics.
-
Fortunately, it’s fairly easy to get the logged in account in our view (remember that we’ll do most
-of our coding there). The request object, passed to our function, contains a user attribute.
-This attribute will always be there: we cannot test whether it’s None or not, for instance. But
-when the request comes from a user that isn’t logged in, the user attribute will contain an
-anonymous Django user. We then can use the is_anonymous method to see whether the user is logged-
-in or not. Last gift by Evennia, if the user is logged in, request.user contains a reference to
-an account object, which will help us a lot in coupling the game and online system.
In this second case, it will select the first character of the account.
-
But what if the user’s not logged in? Again, we have different solutions. One of the most simple
-is to create a character that will behave as our default character for the help system. You can
-create it through your game: connect to it and enter:
-
@charcreate anonymous
-
-
-
The system should answer:
-
Created new character anonymous. Use @ic anonymous to enter the game as this character.
-
-
-
So in our view, we could have something like this:
What we’re going to do is to browse through all commands and help entries, and list all the commands
-that can be seen by this character (either our ‘anonymous’ character, or our logged-in character).
-
The code is longer, but it presents the entire concept in our view. Edit the
-“web/help_system/views.py” file and paste into it:
-
fromdjango.httpimportHttp404
-fromdjango.shortcutsimportrender
-fromevennia.help.modelsimportHelpEntry
-
-fromtypeclasses.charactersimportCharacter
-
-defindex(request):
- """The 'index' view."""
- user=request.user
- ifnotuser.is_anonymous()anduser.character:
- character=user.character
- else:
- character=Character.objects.get(db_key="anonymous")
-
- # Get the categories and topics accessible to this character
- categories,topics=_get_topics(character)
-
- # If we have the 'name' in our GET variable
- topic=request.GET.get("name")
- iftopic:
- iftopicnotintopics:
- raiseHttp404("This help topic doesn't exist.")
-
- topic=topics[topic]
- context={
- "character":character,
- "topic":topic,
- }
- returnrender(request,"help_system/detail.html",context)
- else:
- context={
- "character":character,
- "categories":categories,
- }
- returnrender(request,"help_system/index.html",context)
-
-def_get_topics(character):
- """Return the categories and topics for this character."""
- cmdset=character.cmdset.all()[0]
- commands=cmdset.commands
- entries=[entryforentryinHelpEntry.objects.all()]
- categories={}
- topics={}
-
- # Browse commands
- forcommandincommands:
- ifnotcommand.auto_helpornotcommand.access(character):
- continue
-
- # Create the template for a command
- template={
- "name":command.key,
- "category":command.help_category,
- "content":command.get_help(character,cmdset),
- }
-
- category=command.help_category
- ifcategorynotincategories:
- categories[category]=[]
- categories[category].append(template)
- topics[command.key]=template
-
- # Browse through the help entries
- forentryinentries:
- ifnotentry.access(character,'view',default=True):
- continue
-
- # Create the template for an entry
- template={
- "name":entry.key,
- "category":entry.help_category,
- "content":entry.entrytext,
- }
-
- category=entry.help_category
- ifcategorynotincategories:
- categories[category]=[]
- categories[category].append(template)
- topics[entry.key]=template
-
- # Sort categories
- forentriesincategories.values():
- entries.sort(key=lambdac:c["name"])
-
- categories=list(sorted(categories.items()))
- returncategories,topics
-
-
-
That’s a bit more complicated here, but all in all, it can be divided in small chunks:
-
-
The index function is our view:
-
-
It begins by getting the character as we saw in the previous section.
-
It gets the help topics (commands and help entries) accessible to this character. It’s another
-function that handles that part.
-
If there’s a GET variable “name” in our URL (like “/help?name=drop”), it will retrieve it. If
-it’s not a valid topic’s name, it returns a 404. Otherwise, it renders the template called
-“detail.html”, to display the detail of our topic.
-
If there’s no GET variable “name”, render “index.html”, to display the list of topics.
-
-
-
The _get_topics is a private function. Its sole mission is to retrieve the commands a character
-can execute, and the help entries this same character can see. This code is more Evennia-specific
-than Django-specific, it will not be detailed in this tutorial. Just notice that all help topics
-are stored in a dictionary. This is to simplify our job when displaying them in our templates.
-
-
Notice that, in both cases when we asked to render a template, we passed to render a third
-argument which is the dictionary of variables used in our templates. We can pass variables this
-way, and we will use them in our templates.
This template is definitely more detailed. What it does is:
-
-
Browse through all categories.
-
For all categories, display a level-2 heading with the name of the category.
-
All topics in a category (remember, they can be either commands or help entries) are displayed in
-a table. The trickier part may be that, when the loop is above 5, it will create a new line. The
-table will have 5 columns at the most per row.
-
For every cell in the table, we create a link redirecting to the detail page (see below). The
-URL would look something like “help?name=say”. We use urlencode to ensure special characters are
-properly escaped.
It’s now time to show the detail of a topic (command or help entry). You can create the file
-“web/help_system/templates/help_system/detail.html”. You can paste into it the following code:
Remember to reload or start Evennia, and then go to
-http://localhost:4001/help_system. You should see the list of
-commands and topics accessible by all characters. Try to login (click the “login” link in the menu
-of your website) and go to the same page again. You should now see a more detailed list of commands
-and help entries. Click on one to see its detail.
As always, a tutorial is here to help you feel comfortable adding new features and code by yourself.
-Here are some ideas of things to improve this little feature:
-
-
Links at the bottom of the detail template to go back to the index might be useful.
-
A link in the main menu to link to this page would be great… for the time being you have to
-enter the URL, users won’t guess it’s there.
-
Colors aren’t handled at this point, which isn’t exactly surprising. You could add it though.
-
Linking help entries between one another won’t be simple, but it would be great. For instance, if
-you see a help entry about how to use several commands, it would be great if these commands were
-themselves links to display their details.
An important part of Evennia is the online help system. This allows the players and staff alike to
-learn how to use the game’s commands as well as other information pertinent to the game. The help
-system has many different aspects, from the normal editing of help entries from inside the game, to
-auto-generated help entries during code development using the auto-help system.
This will show a list of help entries, ordered after categories. You will find two sections,
-Command help entries and Other help entries (initially you will only have the first one). You
-can use help to get more info about an entry; you can also give partial matches to get suggestions.
-If you give category names you will only be shown the topics in that category.
A common item that requires help entries are in-game commands. Keeping these entries up-to-date with
-the actual source code functionality can be a chore. Evennia’s commands are therefore auto-
-documenting straight from the sources through its auto-help system. Only commands that you and
-your character can actually currently use are picked up by the auto-help system. That means an admin
-will see a considerably larger amount of help topics than a normal player when using the default
-help command.
-
The auto-help system uses the __doc__ strings of your command classes and formats this to a nice-
-looking help entry. This makes for a very easy way to keep the help updated - just document your
-commands well and updating the help file is just a @reload away. There is no need to manually
-create and maintain help database entries for commands; as long as you keep the docstrings updated
-your help will be dynamically updated for you as well.
-
Example (from a module with command definitions):
-
classCmdMyCmd(Command):
- """
- mycmd - my very own command
-
- Usage:
- mycmd[/switches] <args>
-
- Switches:
- test - test the command
- run - do something else
-
- This is my own command that does this and that.
-
- """
- # [...]
-
- help_category="General"# default
- auto_help=True# default
-
- # [...]
-
-
-
The text at the very top of the command class definition is the class’ __doc__-string and will be
-shown to users looking for help. Try to use a consistent format - all default commands are using the
-structure shown above.
-
You should also supply the help_category class property if you can; this helps to group help
-entries together for people to more easily find them. See the help command in-game to see the
-default categories. If you don’t specify the category, “General” is assumed.
-
If you don’t want your command to be picked up by the auto-help system at all (like if you want to
-write its docs manually using the info in the next section or you use a cmdset that
-has its own help functionality) you can explicitly set auto_help class property to False in your
-command definition.
-
Alternatively, you can keep the advantages of auto-help in commands, but control the display of
-command helps. You can do so by overriding the command’s get_help() method. By default, this
-method will return the class docstring. You could modify it to add custom behavior: the text
-returned by this method will be displayed to the character asking for help in this command.
These are all help entries not involving commands (this is handled automatically by the Command
-Auto-help system). Non-automatic help entries describe how
-your particular game is played - its rules, world descriptions and so on.
-
A help entry consists of four parts:
-
-
The topic. This is the name of the help entry. This is what players search for when they are
-looking for help. The topic can contain spaces and also partial matches will be found.
-
The help category. Examples are Administration, Building, Comms or General. This is an
-overall grouping of similar help topics, used by the engine to give a better overview.
-
The text - the help text itself, of any length.
-
locks - a lock definition. This can be used to limit access to this help entry, maybe
-because it’s staff-only or otherwise meant to be restricted. Help commands check for access_types
-view and edit. An example of a lock string would be view:perm(Builders).
-
-
You can create new help entries in code by using evennia.create_help_entry().
-
fromevenniaimportcreate_help_entry
-entry=create_help_entry("emote",
- "Emoting is important because ...",
- category="Roleplaying",locks="view:all()")
-
-
-
From inside the game those with the right permissions can use the @sethelp command to add and
-modify help entries.
-
> @sethelp/add emote = The emote command is ...
-
-
-
Using @sethelp you can add, delete and append text to existing entries. By default new entries
-will go in the General help category. You can change this using a different form of the @sethelp
-command:
-
> @sethelp/add emote, Roleplaying = Emoting is important because ...
-
-
-
If the category Roleplaying did not already exist, it is created and will appear in the help
-index.
-
You can, finally, define a lock for the help entry by following the category with a lock
-definition:
-
> @sethelp/add emote, Roleplaying, view:all() = Emoting is ...
-
If you cannot find what you are looking for in the online documentation, here’s what to do:
-
-
If you think the documentation is not clear enough and are short on time, fill in our quick little
-online form and let us know - no login required. Maybe the docs need to be improved or a new
-tutorial added! Note that while this form is useful as a suggestion box we cannot answer questions
-or reply to you. Use the discussion group or chat (linked below) if you want feedback.
-
If you have trouble with a missing feature or a problem you think is a bug, go to the
-issue tracker and search to see if has been reported/suggested already. If you can’t find an
-existing entry create a new one.
-
If you need help, want to start a discussion or get some input on something you are working on,
-make a post to the discussions group This is technically a ‘mailing list’, but you don’t
-need to use e-mail; you can post and read all messages just as easily from your browser via the
-online interface.
-
If you want more direct discussions with developers and other users, consider dropping into our
-IRC chat channel #evennia on the Freenode network. Please note however that you have to be
-patient if you don’t get any response immediately; we are all in very different time zones and many
-have busy personal lives. So you might have to hang around for a while - you’ll get noticed
-eventually!
Evennia is a completely non-funded project. It relies on the time donated by its users and
-developers in order to progress.
-
The first and easiest way you as a user can help us out is by taking part in
-community discussions and by giving feedback on what is good or bad. Report bugs you find and features
-you lack to our issue tracker. Just the simple act of letting developers know you are out
-there using their program is worth a lot. Generally mentioning and reviewing Evennia elsewhere is
-also a nice way to spread the word.
-
If you’d like to help develop Evennia more hands-on, here are some ways to get going:
-
-
Look through our online documentation wiki and see if you
-can help improve or expand the documentation (even small things like fixing typos!). You don’t need
-any particular permissions to edit the wiki.
-
Send a message to our discussion group and/or our IRC chat asking about what
-needs doing, along with what your interests and skills are.
-
Take a look at our issue tracker and see if there’s something you feel like taking on.
-here are bugs that need fixes. At any given time there may also be some
-[bounties][issues-bounties] open - these are issues members of the community has put up money to see
-fixed (if you want to put up a bounty yourself you can do so via our page on
-[bountysource][bountysource]).
-
Check out the Contributing page on how to practically contribute with code using
-github.
-
-
… And finally, if you want to help motivate and support development you can also drop some coins
-in the developer’s cup. You can make a donation via PayPal or, even better,
-[become an Evennia patron on Patreon][patreon]! This is a great way to tip your hat and show that you
-appreciate the work done with the server! Finally, if you want to encourage the community to resolve
-a particular
Twitter is an online social networking service that enables
-users to send and read short 280-character messages called “tweets”. Following is a short tutorial
-explaining how to enable users to send tweets from inside Evennia.
You must first have a Twitter account. Log in and register an App at the Twitter Dev
-Site. Make sure you enable access to “write” tweets!
-
To tweet from Evennia you will need both the “API Token” and the “API secret” strings as well as the
-“Access Token” and “Access Secret” strings.
-
Twitter changed their requirements to require a Mobile number on the Twitter account to register new
-apps with write access. If you’re unable to do this, please see this Dev
-post which describes how to get around
-it.
Evennia doesn’t have a tweet command out of the box so you need to write your own little
-Command in order to tweet. If you are unsure about how commands work and how to add
-them, it can be an idea to go through the Adding a Command Tutorial
-before continuing.
-
You can create the command in a separate command module (something like mygame/commands/tweet.py)
-or together with your other custom commands, as you prefer.
-
This is how it can look:
-
importtwitter
-fromevenniaimportCommand
-
-# here you insert your unique App tokens
-# from the Twitter dev site
-TWITTER_API=twitter.Api(consumer_key='api_key',
- consumer_secret='api_secret',
- access_token_key='access_token_key',
- access_token_secret='access_token_secret')
-
-classCmdTweet(Command):
- """
- Tweet a message
-
- Usage:
- tweet <message>
-
- This will send a Twitter tweet to a pre-configured Twitter account.
- A tweet has a maximum length of 280 characters.
- """
-
- key="tweet"
- locks="cmd:pperm(tweet) or pperm(Developers)"
- help_category="Comms"
-
- deffunc(self):
- "This performs the tweet"
-
- caller=self.caller
- tweet=self.args
-
- ifnottweet:
- caller.msg("Usage: tweet <message>")
- return
-
- tlen=len(tweet)
- iftlen>280:
- caller.msg("Your tweet was %i chars long (max 280)."%tlen)
- return
-
- # post the tweet
- TWITTER_API.PostUpdate(tweet)
-
- caller.msg("You tweeted:\n%s"%tweet)
-
-
-
Be sure to substitute your own actual API/Access keys and secrets in the appropriate places.
-
We default to limiting tweet access to players with Developers-level access or to those players
-that have the permission “tweet” (allow individual characters to tweet with @perm/playerplayername=tweet). You may change the lock as you feel is appropriate. Change the overall
-permission to Players if you want everyone to be able to tweet.
-
Now add this command to your default command set (e.g in mygame/commands/defalt_cmdsets.py”) and
-reload the server. From now on those with access can simply use tweet<message> to see the tweet
-posted from the game’s Twitter account.
This shows only a basic tweet setup, other things to do could be:
-
-
Auto-Adding the character name to the tweet
-
More error-checking of postings
-
Changing locks to make tweeting open to more people
-
Echo your tweets to an in-game channel
-
-
Rather than using an explicit command you can set up a Script to send automatic tweets, for example
-to post updated game stats. See the Tweeting Game Stats tutorial for
-help.
Disambiguation: This page is related to using IRC inside an Evennia game. To join the official
-Evennia IRC chat, connect to irc.freenode.net and join #evennia. Alternatively, you can join our
-Discord, which is mirrored to IRC.
-
IRC (Internet Relay Chat) is a long standing
-chat protocol used by many open-source projects for communicating in real time. By connecting one of
-Evennia’s Channels to an IRC channel you can communicate also with people not on
-an mud themselves. You can also use IRC if you are only running your Evennia MUD locally on your
-computer (your game doesn’t need to be open to the public)! All you need is an internet connection.
-For IRC operation you also need twisted.words.
-This is available simply as a package python-twisted-words in many Linux distros, or directly
-downloadable from the link.
You can connect IRC to any Evennia channel (so you could connect it to the default public channel
-if you like), but for testing, let’s set up a new channel irc.
-
@ccreate irc = This is connected to an irc channel!
-
-
-
You will automatically join the new channel.
-
Next we will create a connection to an external IRC network and channel. There are many, many IRC
-nets. Here is a list of some of the biggest
-ones, the one you choose is not really very important unless you want to connect to a particular
-channel (also make sure that the network allows for “bots” to connect).
-
For testing, we choose the Freenode network, irc.freenode.net. We will connect to a test
-channel, let’s call it #myevennia-test (an IRC channel always begins with #). It’s best if you
-pick an obscure channel name that didn’t exist previously - if it didn’t exist it will be created
-for you.
-
-
Don’t connect to #evennia for testing and debugging, that is Evennia’s official chat channel!
-You are welcome to connect your game to #evennia once you have everything working though - it
-can be a good way to get help and ideas. But if you do, please do so with an in-game channel open
-only to your game admins and developers).
-
-
The port needed depends on the network. For Freenode this is 6667.
-
What will happen is that your Evennia server will connect to this IRC channel as a normal user. This
-“user” (or “bot”) needs a name, which you must also supply. Let’s call it “mud-bot”.
-
To test that the bot connects correctly you also want to log onto this channel with a separate,
-third-party IRC client. There are hundreds of such clients available. If you use Firefox, the
-Chatzilla plugin is good and easy. Freenode also offers its own web-based chat page. Once you
-have connected to a network, the command to join is usually /join#channelname (don’t forget the
-#).
Evennia will now create a new IRC bot mud-bot and connect it to the IRC network and the channel
-#myevennia. If you are connected to the IRC channel you will soon see the user mud-bot connect.
-
Write something in the Evennia channel irc.
-
irc Hello, World!
-[irc] Anna: Hello, World!
-
-
-
If you are viewing your IRC channel with a separate IRC client you should see your text appearing
-there, spoken by the bot:
-
mud-bot> [irc] Anna: Hello, World!
-
-
-
Write Hello! in your IRC client window and it will appear in your normal channel, marked with the
-name of the IRC channel you used (#evennia here).
-
[irc] Anna@#myevennia-test: Hello!
-
-
-
Your Evennia gamers can now chat with users on external IRC channels!
The simplest way to create an online roleplaying game (at least from a code perspective) is to
-simply grab a paperback RPG rule book, get a staff of game masters together and start to run scenes
-with whomever logs in. Game masters can roll their dice in front of their computers and tell the
-players the results. This is only one step away from a traditional tabletop game and puts heavy
-demands on the staff - it is unlikely staff will be able to keep up around the clock even if they
-are very dedicated.
-
Many games, even the most roleplay-dedicated, thus tend to allow for players to mediate themselves
-to some extent. A common way to do this is to introduce coded systems - that is, to let the
-computer do some of the heavy lifting. A basic thing is to add an online dice-roller so everyone can
-make rolls and make sure noone is cheating. Somewhere at this level you find the most bare-bones
-roleplaying MUSHes.
-
The advantage of a coded system is that as long as the rules are fair the computer is too - it makes
-no judgement calls and holds no personal grudges (and cannot be accused of holding any). Also, the
-computer doesn’t need to sleep and can always be online regardless of when a player logs on. The
-drawback is that a coded system is not flexible and won’t adapt to the unprogrammed actions human
-players may come up with in role play. For this reason many roleplay-heavy MUDs do a hybrid
-variation - they use coded systems for things like combat and skill progression but leave role play
-to be mostly freeform, overseen by staff game masters.
-
Finally, on the other end of the scale are less- or no-roleplay games, where game mechanics (and
-thus player fairness) is the most important aspect. In such games the only events with in-game value
-are those resulting from code. Such games are very common and include everything from hack-and-slash
-MUDs to various tactical simulations.
-
So your first decision needs to be just what type of system you are aiming for. This page will try
-to give some ideas for how to organize the “coded” part of your system, however big that may be.
We strongly recommend that you code your rule system as stand-alone as possible. That is, don’t
-spread your skill check code, race bonus calculation, die modifiers or what have you all over your
-game.
-
-
Put everything you would need to look up in a rule book into a module in mygame/world. Hide away
-as much as you can. Think of it as a black box (or maybe the code representation of an all-knowing
-game master). The rest of your game will ask this black box questions and get answers back. Exactly
-how it arrives at those results should not need to be known outside the box. Doing it this way
-makes it easier to change and update things in one place later.
-
Store only the minimum stuff you need with each game object. That is, if your Characters need
-values for Health, a list of skills etc, store those things on the Character - don’t store how to
-roll or change them.
-
Next is to determine just how you want to store things on your Objects and Characters. You can
-choose to either store things as individual Attributes, like character.db.STR=34 and
-character.db.Hunting_skill=20. But you could also use some custom storage method, like a
-dictionary character.db.skills={"Hunting":34,"Fishing":20,...}. A much more fancy solution is
-to look at the Ainneve Trait
-handler. Finally you could even go
-with a custom django model. Which is the better depends on your game and the
-complexity of your system.
-
Make a clear API into your
-rules. That is, make methods/functions that you feed with, say, your Character and which skill you
-want to check. That is, you want something similar to this:
You might need to make these functions more or less complex depending on your game. For example the
-properties of the room might matter to the outcome of a roll (if the room is dark, burning etc).
-Establishing just what you need to send into your game mechanic module is a great way to also get a
-feel for what you need to add to your engine.
Inspired by tabletop role playing games, most game systems mimic some sort of die mechanic. To this
-end Evennia offers a full dice
-roller in its contrib
-folder. For custom implementations, Python offers many ways to randomize a result using its in-built
-random module. No matter how it’s implemented, we will in this text refer to the action of
-determining an outcome as a “roll”.
-
In a freeform system, the result of the roll is just compared with values and people (or the game
-master) just agree on what it means. In a coded system the result now needs to be processed somehow.
-There are many things that may happen as a result of rule enforcement:
-
-
Health may be added or deducted. This can effect the character in various ways.
-
Experience may need to be added, and if a level-based system is used, the player might need to be
-informed they have increased a level.
-
Room-wide effects need to be reported to the room, possibly affecting everyone in the room.
-
-
There are also a slew of other things that fall under “Coded systems”, including things like
-weather, NPC artificial intelligence and game economy. Basically everything about the world that a
-Game master would control in a tabletop role playing game can be mimicked to some level by coded
-systems.
Here is a simple example of a rule module. This is what we assume about our simple example game:
-
-
Characters have only four numerical values:
-
-
Their level, which starts at 1.
-
A skill combat, which determines how good they are at hitting things. Starts between 5 and
-
-
-
-
-
-
Their Strength, STR, which determine how much damage they do. Starts between 1 and 10.
-
Their Health points, HP, which starts at 100.
-
-
-
-
-
When a Character reaches HP=0, they are presumed “defeated”. Their HP is reset and they get a
-failure message (as a stand-in for death code).
-
Abilities are stored as simple Attributes on the Character.
-
“Rolls” are done by rolling a 100-sided die. If the result plus the combatvalue is greater than
-the other character, it’s a success and damage is rolled. Damage is rolled as a six-sided die + the
-value of STR (for this example we ignore weapons and assume STR is all that matters).
-
Every successful attack roll gives 1-3 experience points (XP). Every time the number of XP
-reaches (level+1)**2, the Character levels up. When leveling up, the Character’s combat
-value goes up by 2 points and STR by one (this is a stand-in for a real progression system).
-
Characters with the name dummy will gain no XP. Allowing us to make a dummy to train with.
The Character typeclass is simple. It goes in mygame/typeclasses/characters.py. There is already
-an empty Character class there that Evennia will look to and use.
-
fromrandomimportrandint
-fromevenniaimportDefaultCharacter
-
-classCharacter(DefaultCharacter):
- """
- Custom rule-restricted character. We randomize
- the initial skill and ability values bettween 1-10.
- """
- defat_object_creation(self):
- "Called only when first created"
- self.db.level=1
- self.db.HP=100
- self.db.XP=0
- self.db.STR=randint(1,10)
- self.db.combat=randint(5,10)
-
-
-
@reload the server to load up the new code. Doing examineself will however not show the new
-Attributes on yourself. This is because the at_object_creation hook is only called on new
-Characters. Your Character was already created and will thus not have them. To force a reload, use
-the following command:
-
@typeclass/force/resetself
-
-
-
The examineself command will now show the new Attributes.
"""
-mygame/world/rules.py
-"""
-fromrandomimportrandint
-
-
-defroll_hit():
- "Roll 1d100"
- returnrandint(1,100)
-
-
-defroll_dmg():
- "Roll 1d6"
- returnrandint(1,6)
-
-
-defcheck_defeat(character):
- "Checks if a character is 'defeated'."
- ifcharacter.db.HP<=0:
- character.msg("You fall down, defeated!")
- character.db.HP=100# reset
-
-
-defadd_XP(character,amount):
- "Add XP to character, tracking level increases."
- if"training_dummy"incharacter.tags.all():# don't allow the training dummy to level
- character.location.msg_contents("Training Dummies can not gain XP.")
- return
- else:
- character.db.XP+=amount
- ifcharacter.db.XP>=(character.db.level+1)**2:
- character.db.level+=1
- character.db.STR+=1
- character.db.combat+=2
- character.msg("You are now level %i!"%character.db.level)
-
-
-defskill_combat(*args):
- """
- This determines outcome of combat. The one who
- rolls under their combat skill AND higher than
- their opponent's roll hits.
- """
- char1,char2=args
- roll1,roll2=roll_hit(),roll_hit()
- failtext="You are hit by %s for %i damage!"
- wintext="You hit %s for %i damage!"
- xp_gain=randint(1,3)
-
- # display messages showing attack numbers
- attack_message=f"{char1.name} rolls {roll1} + combat {char1.db.combat} " \
- f"= {char1.db.combat+roll1} | {char2.name} rolls {roll2} + combat " \
- f"{char2.db.combat} = {char2.db.combat+roll2}"
- char1.location.msg_contents(attack_message)
- attack_summary=f"{char1.name}{char1.db.combat+roll1} " \
- f"vs {char2.name}{char2.db.combat+roll2}"
- char1.location.msg_contents(attack_summary)
-
- ifchar1.db.combat+roll1>char2.db.combat+roll2:
- # char 1 hits
- dmg=roll_dmg()+char1.db.STR
- char1.msg(wintext%(char2,dmg))
- add_XP(char1,xp_gain)
- char2.msg(failtext%(char1,dmg))
- char2.db.HP-=dmg
- check_defeat(char2)
- elifchar2.db.combat+roll2>char1.db.combat+roll1:
- # char 2 hits
- dmg=roll_dmg()+char2.db.STR
- char1.msg(failtext%(char2,dmg))
- char1.db.HP-=dmg
- check_defeat(char1)
- char2.msg(wintext%(char1,dmg))
- add_XP(char2,xp_gain)
- else:
- # a draw
- drawtext="Neither of you can find an opening."
- char1.msg(drawtext)
- char2.msg(drawtext)
-
-
-SKILLS={"combat":skill_combat}
-
-
-defroll_challenge(character1,character2,skillname):
- """
- Determine the outcome of a skill challenge between
- two characters based on the skillname given.
- """
- ifskillnameinSKILLS:
- SKILLS[skillname](character1,character2)
- else:
- raiseRunTimeError("Skillname %s not found."%skillname)
-
-
-
These few functions implement the entirety of our simple rule system. We have a function to check
-the “defeat” condition and reset the HP back to 100 again. We define a generic “skill” function.
-Multiple skills could all be added with the same signature; our SKILLS dictionary makes it easy to
-look up the skills regardless of what their actual functions are called. Finally, the access
-function roll_challenge just picks the skill and gets the result.
-
In this example, the skill function actually does a lot - it not only rolls results, it also informs
-everyone of their results via character.msg() calls.
fromevenniaimportCommand
-fromworldimportrules
-
-classCmdAttack(Command):
- """
- attack an opponent
-
- Usage:
- attack <target>
-
- This will attack a target in the same room, dealing
- damage with your bare hands.
- """
- deffunc(self):
- "Implementing combat"
-
- caller=self.caller
- ifnotself.args:
- caller.msg("You need to pick a target to attack.")
- return
-
- target=caller.search(self.args)
- iftarget:
- rules.roll_challenge(caller,target,"combat")
-
-
-
Note how simple the command becomes and how generic you can make it. It becomes simple to offer any
-number of Combat commands by just extending this functionality - you can easily roll challenges and
-pick different skills to check. And if you ever decided to, say, change how to determine hit chance,
-you don’t have to change every command, but need only change the single roll_hit function inside
-your rules module.
An inputfunc is an Evennia function that handles a particular input (an inputcommand) from
-the client. The inputfunc is the last destination for the inputcommand along the ingoing message
-path. The inputcommand always has the form (commandname,(args),{kwargs}) and Evennia will use this to try to find and call an inputfunc on the form
This is simple. Add a function on the above form to mygame/server/conf/inputfuncs.py. Your
-function must be in the global, outermost scope of that module and not start with an underscore
-(_) to be recognized as an inputfunc. Reload the server. That’s it. To overload a default
-inputfunc (see below), just add a function with the same name.
-
The modules Evennia looks into for inputfuncs are defined in the list settings.INPUT_FUNC_MODULES.
-This list will be imported from left to right and later imported functions will replace earlier
-ones.
This is the most common of inputcommands, and the only one supported by every traditional mud. The
-argument is usually what the user sent from their command line. Since all text input from the user
-like this is considered a Command, this inputfunc will do things like nick-replacement
-and then pass on the input to the central Commandhandler.
This is a direct command for setting protocol options. These are settable with the @option
-command, but this offers a client-side way to set them. Not all connection protocols makes use of
-all flags, but here are the possible keywords:
-
-
get (bool): If this is true, ignore all other kwargs and immediately return the current settings
-as an outputcommand ("client_options",(),{key=value,...})-
-
client (str): A client identifier, like “mushclient”.
-
version (str): A client version
-
ansi (bool): Supports ansi colors
-
xterm256 (bool): Supports xterm256 colors or not
-
mxp (bool): Supports MXP or not
-
utf-8 (bool): Supports UTF-8 or not
-
screenreader (bool): Screen-reader mode on/off
-
mccp (bool): MCCP compression on/off
-
screenheight (int): Screen height in lines
-
screenwidth (int): Screen width in characters
-
inputdebug (bool): Debug input functions
-
nomarkup (bool): Strip all text tags
-
raw (bool): Leave text tags unparsed
-
-
-
Note that there are two GMCP aliases to this inputfunc - hello and supports_set, which means
-it will be accessed via the GMCP Hello and Supports.Set instructions assumed by some clients.
Returns an outputcommand on the form ("get_inputfuncs",(),{funcname:docstring,...}) - a list of
-all the available inputfunctions along with their docstrings.
Retrieves a value from the Character or Account currently controlled by this Session. Takes one
-argument, This will only accept particular white-listed names, you’ll need to overload the function
-to expand. By default the following values can be retrieved:
-
-
“name” or “key”: The key of the Account or puppeted Character.
-
“location”: Name of the current location, or “None”.
-
“servername”: Name of the Evennia server connected to.
Output: Depends on the repeated function. Will return ("text",(repeatlist),{} with a list of
-accepted names if given an unfamiliar callback name.
-
-
This will tell evennia to repeatedly call a named function at a given interval. Behind the scenes
-this will set up a Ticker. Only previously acceptable functions are possible to
-repeat-call in this way, you’ll need to overload this inputfunc to add the ones you want to offer.
-By default only two example functions are allowed, “test1” and “test2”, which will just echo a text
-back at the given interval. Stop the repeat by sending "stop":True (note that you must include
-both the callback name and interval for Evennia to know what to stop).
Output (on change): ("monitor",(),{"name":name,"value":value})
-
-
This sets up on-object monitoring of Attributes or database fields. Whenever the field or Attribute
-changes in any way, the outputcommand will be sent. This is using the
-MonitorHandler behind the scenes. Pass the “stop” key to stop monitoring. Note
-that you must supply the name also when stopping to let the system know which monitor should be
-cancelled.
-
Only fields/attributes in a whitelist are allowed to be used, you have to overload this function to
-add more. By default the following fields/attributes can be monitored:
This page describes how to install and run the Evennia server on an Android phone. This will involve
-installing a slew of third-party programs from the Google Play store, so make sure you are okay with
-this before starting.
The first thing to do is install a terminal emulator that allows a “full” version of linux to be
-run. Note that Android is essentially running on top of linux so if you have a rooted phone, you may
-be able to skip this step. You don’t require a rooted phone to install Evennia though.
-
Assuming we do not have root, we will install
-Termux.
-Termux provides a base installation of Linux essentials, including apt and Python, and makes them
-available under a writeable directory. It also gives us a terminal where we can enter commands. By
-default, Android doesn’t give you permissions to the root folder, so Termux pretends that its own
-installation directory is the root directory.
-
Termux will set up a base system for us on first launch, but we will need to install some
-prerequisites for Evennia. Commands you should run in Termux will look like this:
-
$ cat file.txt
-
-
-
The $ symbol is your prompt - do not include it when running commands.
To install some of the libraries Evennia requires, namely Pillow and Twisted, we have to first
-install some packages they depend on. In Termux, run the following
Termux ships with Python 3, perfect. Python 3 has venv (virtualenv) and pip (Python’s module
-installer) built-in.
-
So, let’s set up our virtualenv. This keeps the Python packages we install separate from the system
-versions.
-
$ cd
-$ python3 -m venv evenv
-
-
-
This will create a new folder, called evenv, containing the new python executable.
-Next, let’s activate our new virtualenv. Every time you want to work on Evennia, you need to run the
-following command:
-
$ source evenv/bin/activate
-
-
-
Your prompt will change to look like this:
-
(evenv) $
-
-
-
Update the updaters and installers in the venv: pip, setuptools and wheel.
This step will possibly take quite a while - we are downloading Evennia and are then installing it,
-building all of the requirements for Evennia to run. If you run into trouble on this step, please
-see Troubleshooting.
-
You can go to the dir where Evennia is installed with cd$VIRTUAL_ENV/src/evennia. gitgrep(something) can be handy, as can gitdiff
At this point, Evennia is installed on your phone! You can now continue with the original Getting
-Started instruction, we repeat them here for clarity.
Your game should now be running! Open a web browser at http://localhost:4001 or point a telnet
-client to localhost:4000 and log in with the user you created.
When you wish to run Evennia, get into your Termux console and make sure you have activated your
-virtualenv as well as are in your game’s directory. You can then run evennia start as normal.
-
$ cd ~ && source evenv/bin/activate
-(evenv) $ cd mygame
-(evenv) $ evennia start
-
Android’s os module doesn’t support certain functions - in particular getloadavg. Thusly, running
-the command @server in-game will throw an exception. So far, there is no fix for this problem.
-
As you might expect, performance is not amazing.
-
Android is fairly aggressive about memory handling, and you may find that your server process is
-killed if your phone is heavily taxed. Termux seems to keep a notification up to discourage this.
Internationalization (often abbreviated i18n since there are 18 characters between the first “i”
-and the last “n” in that word) allows Evennia’s core server to return texts in other languages than
-English - without anyone having to edit the source code. Take a look at the locale directory of
-the Evennia installation, there you will find which languages are currently supported.
Change language by adding the following to your mygame/server/conf/settings.py file:
-
USE_I18N=True
- LANGUAGE_CODE='en'
-
-
-
Here 'en' should be changed to the abbreviation for one of the supported languages found in
-locale/. Restart the server to activate i18n. The two-character international language codes are
-found here.
-
-
Windows Note: If you get errors concerning gettext or xgettext on Windows, see the Django
-documentation. A
-self-installing and up-to-date version of gettext for Windows (32/64-bit) is available on
-Github.
Important Note: Evennia offers translations of hard-coded strings in the server, things like
-“Connection closed” or “Server restarted”, strings that end users will see and which game devs are
-not supposed to change on their own. Text you see in the log file or on the command line (like error
-messages) are generally not translated (this is a part of Python).
-
-
-
In addition, text in default Commands and in default Typeclasses will not be translated by
-switching i18n language. To translate Commands and Typeclass hooks you must overload them in your
-game directory and translate their returns to the language you want. This is because from Evennia’s
-perspective, adding i18n code to commands tend to add complexity to code that is meant to be
-changed anyway. One of the goals of Evennia is to keep the user-changeable code as clean and easy-
-to-read as possible.
-
-
If you cannot find your language in evennia/locale/ it’s because noone has translated it yet.
-Alternatively you might have the language but find the translation bad … You are welcome to help
-improve the situation!
-
To start a new translation you need to first have cloned the Evennia repositry with GIT and
-activated a python virtualenv as described on the Getting Started page. You now
-need to cd to the evennia/ directory. This is not your created game folder but the main
-Evennia library folder. If you see a folder locale/ then you are in the right place. From here you
-run:
-
evennia makemessages <language-code>
-
-
-
where <language-code> is the two-letter locale code
-for the language you want, like ‘sv’ for Swedish or ‘es’ for Spanish. After a moment it will tell
-you the language has been processed. For instance:
-
evennia makemessages sv
-
-
-
If you started a new language a new folder for that language will have emerged in the locale/
-folder. Otherwise the system will just have updated the existing translation with eventual new
-strings found in the server. Running this command will not overwrite any existing strings so you can
-run it as much as you want.
-
-
Note: in Django, the makemessages command prefixes the locale name by the -l option (...makemessages-lsv for instance). This syntax is not allowed in Evennia, due to the fact that -l
-is the option to tail log files. Hence, makemessages doesn’t use the -l flag.
-
-
Next head to locale/<language-code>/LC_MESSAGES and edit the **.po file you find there. You can
-edit this with a normal text editor but it is easiest if you use a special po-file editor from the
-web (search the web for “po editor” for many free alternatives).
-
The concept of translating is simple, it’s just a matter of taking the english strings you find in
-the **.po file and add your language’s translation best you can. The **.po format (and many
-supporting editors) allow you to mark translations as “fuzzy”. This tells the system (and future
-translators) that you are unsure about the translation, or that you couldn’t find a translation that
-exactly matched the intention of the original text. Other translators will see this and might be
-able to improve it later.
-Finally, you need to compile your translation into a more efficient form. Do so from the evennia
-folder
-again:
-
evennia compilemessages
-
-
-
This will go through all languages and create/update compiled files (**.mo) for them. This needs
-to be done whenever a **.po file is updated.
-
When you are done, send the **.po and *.mo file to the Evennia developer list (or push it into
-your own repository clone) so we can integrate your translation into Evennia!
Evennia provides a great foundation to build your very own MU* whether you have programming
-experience or none at all. Whilst Evennia has a number of in-game building commands and tutorials
-available to get you started, when approaching game systems of any complexity it is advisable to
-have the basics of Python under your belt before jumping into the code. There are many Python
-tutorials freely available online however this page focuses on Learn Python the Hard Way (LPTHW) by
-Zed Shaw. This tutorial takes you through the basics of Python and progresses you to creating your
-very own online text based game. Whilst completing the course feel free to install Evennia and try
-out some of our beginner tutorials. On completion you can return to this page, which will act as an
-overview to the concepts separating your online text based game and the inner-workings of Evennia.
--The latter portion of the tutorial focuses working your engine into a webpage and is not strictly
-required for development in Evennia.
You may have returned here when you were invited to read some code. If you haven’t already, you
-should now have the knowledge necessary to install Evennia. Head over to the Getting Started page
-for install instructions. You can also try some of our tutorials to get you started on working with
-Evennia.
If you have successfully completed the Learn Python the Hard Way tutorial you should now have a
-simple browser based Interactive Fiction engine which looks similar to this.
-This engine is built using a single interactive object type, the Room class. The Room class holds a
-description of itself that is presented to the user and a list of hardcoded commands which if
-selected correctly will present you with the next rooms’ description and commands. Whilst your game
-only has one interactive object, MU* have many more: Swords and shields, potions and scrolls or even
-laser guns and robots. Even the player has an in-game representation in the form of your character.
-Each of these examples are represented by their own object with their own description that can be
-presented to the user.
-
A basic object in Evennia has a number of default functions but perhaps most important is the idea
-of location. In your text engine you receive a description of a room but you are not really in the
-room because you have no in-game representation. However, in Evennia when you enter a Dungeon you
-ARE in the dungeon. That is to say your character.location = Dungeon whilst the Dungeon.contents now
-has a spunky young adventurer added to it. In turn, your character.contents may have amongst it a
-number of swords and potions to help you on your adventure and their location would be you.
-
In reality each of these “objects” are just an entry in your Evennia projects database which keeps
-track of all these attributes, such as location and contents. Making changes to those attributes and
-the rules in which they are changed is the most fundamental perspective of how your game works. We
-define those rules in the objects Typeclass. The Typeclass is a Python class with a special
-connection to the games database which changes values for us through various class methods. Let’s
-look at your characters Typeclass rules for changing location.
First we check if it’s okay to leave our current location, then we tell everyone there that we’re
-leaving. We move locations and tell everyone at our new location that we’ve arrived before checking
-we’re okay to be there. By default stages 1 and 5 are empty ready for us to add some rules. We’ll
-leave an explanation as to how to make those changes for the tutorial section, but imagine if you
-were an astronaut. A smart astronaut might stop at step 1 to remember to put his helmet on whilst a
-slower astronaut might realise he’s forgotten in step 5 before shortly after ceasing to be an
-astronaut.
-
With all these objects and all this moving around it raises another problem. In your text engine the
-commands available to the player were hard-coded to the room. That means if we have commands we
-always want available to the player we’ll need to have those commands hard-coded on every single
-room. What about an armoury? When all the swords are gone the command to take a sword would still
-remain causing confusion. Evennia solves this problem by giving each object the ability to hold
-commands. Rooms can have commands attached to them specific to that location, like climbing a tree;
-Players can have commands which are always available to them, like ‘look’, ‘get’ and ‘say’; and
-objects can have commands attached to them which unlock when taking possession of it, like attack
-commands when obtaining a weapon.
Evennia is licensed under the very friendly BSD
-(3-clause) license. You can find the license as
-LICENSE.txt in the Evennia
-repository’s root.
-
Q: When creating a game using Evennia, what does the license permit me to do with it?
-
A: It’s your own game world to do with as you please! Keep it to yourself or re-distribute it
-under another license of your choice - or sell it and become filthy rich for all we care.
-
Q: I have modified the Evennia library itself, what does the license say about that?
-
A: Our license allows you to do whatever you want with your modified Evennia, including
-re-distributing or selling it, as long as you include our license and copyright info found in
-LICENSE.txt along with your distribution.
-
… Of course, if you fix bugs or add some new snazzy feature we softly nudge you to make those
-changes available so they can be added to the core Evennia package for everyone’s benefit. The
-license doesn’t require you to do it, but that doesn’t mean we won’t still greatly appreciate it
-if you do!
-
Q: Can I re-distribute the Evennia server package along with my custom game implementation?
-
A: Sure. As long as the text in LICENSE.txt is included.
-
Q: What about Contributions?
-
The contributions in evennia/evennia/contrib are considered to be released under the same license
-as Evennia itself, unless the individual contributor has specifically defined otherwise.
nextRPI - A github project for making a toolbox for people
-to make RPI-style Evennia games.
-
Muddery - A mud framework under development, based on an
-older fork of Evennia. It has some specific design goals for building and extending the game based
-on input files.
-
vim-evennia - A mode for editing batch-build files (.ev)
-files in the vim text editor (Emacs users can use evennia-
-mode.el).
MuSoapbox - Very active Mu* game community mainly focused on MUSH-
-type gaming.
-
Imaginary Realities - An e-magazine on game and MUD
-design that has several articles about Evennia. There is also an archive of older
-issues from 1998-2001 that are still very
-relevant.
-
Optional Realities - Mud development discussion forums that has
-regular articles on MUD development focused on roleplay-intensive games. After a HD crash it’s not
-as content-rich as it once was.
Planet Mud-Dev - A blog aggregator following blogs of
-current MUD development (including Evennia) around the ‘net. Worth to put among your RSS
-subscriptions.
-
Mud Dev mailing list archive (mirror) -
-Influential mailing list active 1996-2004. Advanced game design discussions.
-
Mud-dev wiki - A (very) slowly growing resource on MUD creation.
Lost Library of MOO - Archive of scientific articles on mudding (in
-particular moo).
-
Nick Gammon’s hints thread -
-Contains a very useful list of things to think about when starting your new MUD.
-
Lost Garden - A game development blog with long and interesting
-articles (not MUD-specific)
-
What Games Are - A blog about general game design (not MUD-specific)
-
The Alexandrian - A blog about tabletop roleplaying and board games,
-but with lots of general discussion about rule systems and game balance that could be applicable
-also for MUDs.
-
Raph Koster’s laws of game design - thought-provoking guidelines and things to think about
-when designing a virtual multiplayer world (Raph is known for Ultima Online among other things).
Richard Bartle Designing Virtual Worlds (amazon page) - Essential reading for the design of any persistent game
-world, written by the co-creator of the original game MUD. Published in 2003 but it’s still as
-relevant now as when it came out. Covers everything you need to know and then some.
-
Zed A. Shaw Learn Python the Hard way (homepage) - Despite
-the imposing name this book is for the absolute Python/programming beginner. One learns the language
-by gradually creating a small text game! It has been used by multiple users before moving on to
-Evennia. Update: This used to be free to read online, this is no longer the case.
-
David M. Beazley Python Essential Reference (4th ed) (amazon
-page) - Our
-recommended book on Python; it not only efficiently summarizes the language but is also an excellent
-reference to the standard library for more experienced Python coders.
-
Luciano Ramalho, Fluent Python (o’reilly
-page) - This is an excellent book for experienced
-Python coders willing to take their code to the next level. A great read with a lot of useful info
-also for veteran Pythonistas.
-
Richard Cantillon An Essay on Economic Theory (free
-pdf) - A very good English
-translation of Essai sur la Nature du Commerce en Général, one of the foundations of modern
-economic theory. Written in 1730 but the translation is annotated and the essay is actually very
-easy to follow also for a modern reader. Required reading if you think of implementing a sane game
-economic system.
For most games it is a good idea to restrict what people can do. In Evennia such restrictions are
-applied and checked by something called locks. All Evennia entities (Commands,
-Objects, Scripts, Accounts, Help System,
-messages and channels) are accessed through locks.
-
A lock can be thought of as an “access rule” restricting a particular use of an Evennia entity.
-Whenever another entity wants that kind of access the lock will analyze that entity in different
-ways to determine if access should be granted or not. Evennia implements a “lockdown” philosophy -
-all entities are inaccessible unless you explicitly define a lock that allows some or full access.
-
Let’s take an example: An object has a lock on itself that restricts how people may “delete” that
-object. Apart from knowing that it restricts deletion, the lock also knows that only players with
-the specific ID of, say, 34 are allowed to delete it. So whenever a player tries to run delete
-on the object, the delete command makes sure to check if this player is really allowed to do so.
-It calls the lock, which in turn checks if the player’s id is 34. Only then will it allow delete
-to go on with its job.
The in-game command for setting locks on objects is lock:
-
> lock obj = <lockstring>
-
-
-
The <lockstring> is a string of a certain form that defines the behaviour of the lock. We will go
-into more detail on how <lockstring> should look in the next section.
-
Code-wise, Evennia handles locks through what is usually called locks on all relevant entities.
-This is a handler that allows you to add, delete and check locks.
-
myobj.locks.add(<lockstring>)
-
-
-
One can call locks.check() to perform a lock check, but to hide the underlying implementation all
-objects also have a convenience function called access. This should preferably be used. In the
-example below, accessing_obj is the object requesting the ‘delete’ access whereas obj is the
-object that might get deleted. This is how it would look (and does look) from inside the delete
-command:
-
ifnotobj.access(accessing_obj,'delete'):
- accessing_obj.msg("Sorry, you may not delete that.")
- return
-
Defining a lock (i.e. an access restriction) in Evennia is done by adding simple strings of lock
-definitions to the object’s locks property using obj.locks.add().
-
Here are some examples of lock strings (not including the quotes):
-
delete:id(34)# only allow obj #34 to delete
- edit:all()# let everyone edit
- # only those who are not "very_weak" or are Admins may pick this up
- get:notattr(very_weak)orperm(Admin)
-
where [] marks optional parts. AND, OR and NOT are not case sensitive and excess spaces are
-ignored. lockfunc1,lockfunc2 etc are special lock functions available to the lock system.
-
So, a lockstring consists of the type of restriction (the access_type), a colon (:) and then an
-expression involving function calls that determine what is needed to pass the lock. Each function
-returns either True or False. AND, OR and NOT work as they do normally in Python. If the
-total result is True, the lock is passed.
-
You can create several lock types one after the other by separating them with a semicolon (;) in
-the lockstring. The string below yields the same result as the previous example:
-
delete:id(34);edit:all();get: not attr(very_weak) or perm(Admin)
-
An access_type, the first part of a lockstring, defines what kind of capability a lock controls,
-such as “delete” or “edit”. You may in principle name your access_type anything as long as it is
-unique for the particular object. The name of the access types is not case-sensitive.
-
If you want to make sure the lock is used however, you should pick access_type names that you (or
-the default command set) actually checks for, as in the example of delete above that uses the
-‘delete’ access_type.
-
Below are the access_types checked by the default commandset.
control - who is the “owner” of the object. Can set locks, delete it etc. Defaults to the
-creator of the object.
-
call - who may call Object-commands stored on this Object except for the Object itself. By
-default, Objects share their Commands with anyone in the same location (e.g. so you can ‘press’ a
-Button object in the room). For Characters and Mobs (who likely only use those Commands for
-themselves and don’t want to share them) this should usually be turned off completely, using
-something like call:false().
-
examine - who may examine this object’s properties.
-
delete - who may delete the object.
-
edit - who may edit properties and attributes of the object.
-
view - if the look command will display/list this object
-
get- who may pick up the object and carry it around.
-
puppet - who may “become” this object and control it as their “character”.
-
attrcreate - who may create new attributes on the object (default True)
examine - who may view this help entry (usually everyone)
-
edit - who may edit this help entry.
-
-
-
-
So to take an example, whenever an exit is to be traversed, a lock of the type traverse will be
-checked. Defining a suitable lock type for an exit object would thus involve a lockstring traverse:<lockfunctions>.
As stated above, the access_type part of the lock is simply the ‘name’ or ‘type’ of the lock. The
-text is an arbitrary string that must be unique for an object. If adding a lock with the same
-access_type as one that already exists on the object, the new one override the old one.
-
For example, if you wanted to create a bulletin board system and wanted to restrict who can either
-read a board or post to a board. You could then define locks such as:
This will create a ‘read’ access type for Characters having the Player permission or above and a
-‘post’ access type for those with Admin permissions or above (see below how the perm() lock
-function works). When it comes time to test these permissions, simply check like this (in this
-example, the obj may be a board on the bulletin board system and accessing_obj is the player
-trying to read the board):
-
ifnotobj.access(accessing_obj,'read'):
- accessing_obj.msg("Sorry, you may not read that.")
- return
-
A lock function is a normal Python function put in a place Evennia looks for such functions. The
-modules Evennia looks at is the list settings.LOCK_FUNC_MODULES. All functions in any of those
-modules will automatically be considered a valid lock function. The default ones are found in
-evennia/locks/lockfuncs.py and you can start adding your own in mygame/server/conf/lockfuncs.py.
-You can append the setting to add more module paths. To replace a default lock function, just add
-your own with the same name.
-
A lock function must always accept at least two arguments - the accessing object (this is the
-object wanting to get access) and the accessed object (this is the object with the lock). Those
-two are fed automatically as the first two arguments to the function when the lock is checked. Any
-arguments explicitly given in the lock definition will appear as extra arguments.
-
# A simple example lock function. Called with e.g. `id(34)`. This is
- # defined in, say mygame/server/conf/lockfuncs.py
-
- defid(accessing_obj,accessed_obj,*args,**kwargs):
- ifargs:
- wanted_id=args[0]
- returnaccessing_obj.id==wanted_id
- returnFalse
-
-
-
The above could for example be used in a lock function like this:
-
# we have `obj` and `owner_object` from before
- obj.locks.add("edit: id(%i)"%owner_object.id)
-
-
-
We could check if the “edit” lock is passed with something like this:
-
# as part of a Command's func() method, for example
- ifnotobj.access(caller,"edit"):
- caller.msg("You don't have access to edit this!")
- return
-
-
-
In this example, everyone except the caller with the right id will get the error.
-
-
(Using the * and ** syntax causes Python to magically put all extra arguments into a list
-args and all keyword arguments into a dictionary kwargs respectively. If you are unfamiliar with
-how *args and **kwargs work, see the Python manuals).
-
-
Some useful default lockfuncs (see src/locks/lockfuncs.py for more):
-
-
true()/all() - give access to everyone
-
false()/none()/superuser() - give access to none. Superusers bypass the check entirely and are
-thus the only ones who will pass this check.
-
perm(perm) - this tries to match a given permission property, on an Account firsthand, on a
-Character second. See below.
-
perm_above(perm) - like perm but requires a “higher” permission level than the one given.
-
id(num)/dbref(num) - checks so the access_object has a certain dbref/id.
-
attr(attrname) - checks if a certain Attribute exists on accessing_object.
-
attr(attrname,value) - checks so an attribute exists on accessing_object and has the given
-value.
-
attr_gt(attrname,value) - checks so accessing_object has a value larger (>) than the given
-value.
-
attr_ge,attr_lt,attr_le,attr_ne - corresponding for >=, <, <= and !=.
-
holds(objid) - checks so the accessing objects contains an object of given name or dbref.
-
inside() - checks so the accessing object is inside the accessed object (the inverse of
-holds()).
-
pperm(perm), pid(num)/pdbref(num) - same as perm, id/dbref but always looks for
-permissions and dbrefs of Accounts, not on Characters.
-
serversetting(settingname,value) - Only returns True if Evennia has a given setting or a
-setting set to a given value.
Sometimes you don’t really need to look up a certain lock, you just want to check a lockstring. A
-common use is inside Commands, in order to check if a user has a certain permission. The lockhandler
-has a method check_lockstring(accessing_obj,lockstring,bypass_superuser=False) that allows this.
-
# inside command definition
- ifnotself.caller.locks.check_lockstring(self.caller,"dummy:perm(Admin)"):
- self.caller.msg("You must be an Admin or higher to do this!")
- return
-
-
-
Note here that the access_type can be left to a dummy value since this method does not actually do
-a Lock lookup.
Evennia sets up a few basic locks on all new objects and accounts (if we didn’t, noone would have
-any access to anything from the start). This is all defined in the root Typeclasses
-of the respective entity, in the hook method basetype_setup() (which you usually don’t want to
-edit unless you want to change how basic stuff like rooms and exits store their internal variables).
-This is called once, before at_object_creation, so just put them in the latter method on your
-child object to change the default. Also creation commands like create changes the locks of
-objects you create - for example it sets the control lock_type so as to allow you, its creator, to
-control and delete the object.
This section covers the underlying code use of permissions. If you just want to learn how to
-practically assign permissions in-game, refer to the Building Permissions
-page, which details how you use the perm command.
-
-
A permission is simply a list of text strings stored in the handler permissions on Objects
-and Accounts. Permissions can be used as a convenient way to structure access levels and
-hierarchies. It is set by the perm command. Permissions are especially handled by the perm() and
-pperm() lock functions listed above.
-
Let’s say we have a red_key object. We also have red chests that we want to unlock with this key.
-
perm red_key = unlocks_red_chests
-
-
-
This gives the red_key object the permission “unlocks_red_chests”. Next we lock our red chests:
-
lock red chest = unlock:perm(unlocks_red_chests)
-
-
-
What this lock will expect is to the fed the actual key object. The perm() lock function will
-check the permissions set on the key and only return true if the permission is the one given.
-
Finally we need to actually check this lock somehow. Let’s say the chest has an command open<key>
-sitting on itself. Somewhere in its code the command needs to figure out which key you are using and
-test if this key has the correct permission:
-
# self.obj is the chest
- # and used_key is the key we used as argument to
- # the command. The self.caller is the one trying
- # to unlock the chest
- ifnotself.obj.access(used_key,"unlock"):
- self.caller.msg("The key does not fit!")
- return
-
-
-
All new accounts are given a default set of permissions defined by
-settings.PERMISSION_ACCOUNT_DEFAULT.
-
Selected permission strings can be organized in a permission hierarchy by editing the tuple
-settings.PERMISSION_HIERARCHY. Evennia’s default permission hierarchy is as follows:
-
Developer # like superuser but affected by locks
- Admin # can administrate accounts
- Builder # can edit the world
- Helper # can edit help files
- Player # can chat and send tells (default level)
-
-
-
(Also the plural form works, so you could use Developers etc too).
-
-
There is also a Guest level below Player that is only active if settings.GUEST_ENABLED is
-set. This is never part of settings.PERMISSION_HIERARCHY.
-
-
The main use of this is that if you use the lock function perm() mentioned above, a lock check for
-a particular permission in the hierarchy will also grant access to those with higher hierarchy
-access. So if you have the permission “Admin” you will also pass a lock defined as perm(Builder)
-or any of those levels below “Admin”.
-
When doing an access check from an Object or Character, the perm() lock function will
-always first use the permissions of any Account connected to that Object before checking for
-permissions on the Object. In the case of hierarchical permissions (Admins, Builders etc), the
-Account permission will always be used (this stops an Account from escalating their permission by
-puppeting a high-level Character). If the permission looked for is not in the hierarchy, an exact
-match is required, first on the Account and if not found there (or if no Account is connected), then
-on the Object itself.
-
Here is how you use perm to give an account more permissions:
-
perm/account Tommy = Builders
- perm/account/del Tommy = Builders # remove it again
-
-
-
Note the use of the /account switch. It means you assign the permission to the
-Accounts Tommy instead of any Character that also happens to be named
-“Tommy”.
-
Putting permissions on the Account guarantees that they are kept, regardless of which Character
-they are currently puppeting. This is especially important to remember when assigning permissions
-from the hierarchy tree - as mentioned above, an Account’s permissions will overrule that of its
-character. So to be sure to avoid confusion you should generally put hierarchy permissions on the
-Account, not on their Characters (but see also quelling).
-
Below is an example of an object without any connected account
-
obj1.permissions=["Builders","cool_guy"]
- obj2.locks.add("enter:perm_above(Accounts) and perm(cool_guy)")
-
- obj2.access(obj1,"enter")# this returns True!
-
-
-
And one example of a puppet with a connected account:
-
account.permissions.add("Accounts")
- puppet.permissions.add("Builders","cool_guy")
- obj2.locks.add("enter:perm_above(Accounts) and perm(cool_guy)")
-
- obj2.access(puppet,"enter")# this returns False!
-
There is normally only one superuser account and that is the one first created when starting
-Evennia (User #1). This is sometimes known as the “Owner” or “God” user. A superuser has more than
-full access - it completely bypasses all locks so no checks are even run. This allows for the
-superuser to always have access to everything in an emergency. But it also hides any eventual errors
-you might have made in your lock definitions. So when trying out game systems you should either use
-quelling (see below) or make a second Developer-level character so your locks get tested correctly.
The quell command can be used to enforce the perm() lockfunc to ignore permissions on the
-Account and instead use the permissions on the Character only. This can be used e.g. by staff to
-test out things with a lower permission level. Return to the normal operation with unquell. Note
-that quelling will use the smallest of any hierarchical permission on the Account or Character, so
-one cannot escalate one’s Account permission by quelling to a high-permission Character. Also the
-superuser can quell their powers this way, making them affectable by locks.
examine: attr(eyesight, excellent) or perm(Builders)
-
-
-
You are only allowed to do examine on this object if you have ‘excellent’ eyesight (that is, has
-an Attribute eyesight with the value excellent defined on yourself) or if you have the
-“Builders” permission string assigned to you.
-
open: holds('the green key') or perm(Builder)
-
-
-
This could be called by the open command on a “door” object. The check is passed if you are a
-Builder or has the right key in your inventory.
-
cmd: perm(Builders)
-
-
-
Evennia’s command handler looks for a lock of type cmd to determine if a user is allowed to even
-call upon a particular command or not. When you define a command, this is the kind of lock you must
-set. See the default command set for lots of examples. If a character/account don’t pass the cmd
-lock type the command will not even appear in their help list.
-
cmd: not perm(no_tell)
-
-
-
“Permissions” can also be used to block users or implement highly specific bans. The above example
-would be be added as a lock string to the tell command. This will allow everyone not having the
-“permission” no_tell to use the tell command. You could easily give an account the “permission”
-no_tell to disable their use of this particular command henceforth.
-
dbref=caller.id
- lockstring="control:id(%s);examine:perm(Builders);delete:id(%s) or perm(Admin);get:all()"%
-(dbref,dbref)
- new_obj.locks.add(lockstring)
-
-
-
This is how the create command sets up new objects. In sequence, this permission string sets the
-owner of this object be the creator (the one running create). Builders may examine the object
-whereas only Admins and the creator may delete it. Everyone can pick it up.
Assume we have two objects - one is ourselves (not superuser) and the other is an Object
-called box.
-
> create/drop box
- > desc box = "This is a very big and heavy box."
-
-
-
We want to limit which objects can pick up this heavy box. Let’s say that to do that we require the
-would-be lifter to to have an attribute strength on themselves, with a value greater than 50. We
-assign it to ourselves to begin with.
-
> set self/strength = 45
-
-
-
Ok, so for testing we made ourselves strong, but not strong enough. Now we need to look at what
-happens when someone tries to pick up the the box - they use the get command (in the default set).
-This is defined in evennia/commands/default/general.py. In its code we find this snippet:
So the get command looks for a lock with the type get (not so surprising). It also looks for an
-Attribute on the checked object called get_err_msg in order to return a customized
-error message. Sounds good! Let’s start by setting that on the box:
-
> set box/get_err_msg = You are not strong enough to lift this box.
-
-
-
Next we need to craft a Lock of type get on our box. We want it to only be passed if the accessing
-object has the attribute strength of the right value. For this we would need to create a lock
-function that checks if attributes have a value greater than a given value. Luckily there is already
-such a one included in evennia (see evennia/locks/lockfuncs.py), called attr_gt.
-
So the lock string will look like this: get:attr_gt(strength,50). We put this on the box now:
-
lock box = get:attr_gt(strength, 50)
-
-
-
Try to get the object and you should get the message that we are not strong enough. Increase your
-strength above 50 however and you’ll pick it up no problem. Done! A very heavy box!
-
If you wanted to set this up in python code, it would look something like this:
-
- fromevenniaimportcreate_object
-
- # create, then set the lock
- box=create_object(None,key="box")
- box.locks.add("get:attr_gt(strength, 50)")
-
- # or we can assign locks in one go right away
- box=create_object(None,key="box",locks="get:attr_gt(strength, 50)")
-
- # set the attributes
- box.db.desc="This is a very big and heavy box."
- box.db.get_err_msg="You are not strong enough to lift this box."
-
- # one heavy box, ready to withstand all but the strongest...
-
Django also implements a comprehensive permission/security system of its own. The reason we don’t
-use that is because it is app-centric (app in the Django sense). Its permission strings are of the
-form appname.permstring and it automatically adds three of them for each database model in the app
-
-
for the app evennia/object this would be for example ‘object.create’, ‘object.admin’ and
-‘object.edit’. This makes a lot of sense for a web application, not so much for a MUD, especially
-when we try to hide away as much of the underlying architecture as possible.
-
-
The django permissions are not completely gone however. We use it for validating passwords during
-login. It is also used exclusively for managing Evennia’s web-based admin site, which is a graphical
-front-end for the database of Evennia. You edit and assign such permissions directly from the web
-interface. It’s stand-alone from the permissions described above.
This is a small tutorial for customizing your character objects, using the example of letting users
-turn on and off ANSI color parsing as an example. @optionsNOCOLOR=True will now do what this
-tutorial shows, but the tutorial subject can be applied to other toggles you may want, as well.
-
In the Building guide’s Colors page you can learn how to add color to your
-game by using special markup. Colors enhance the gaming experience, but not all users want color.
-Examples would be users working from clients that don’t support color, or people with various seeing
-disabilities that rely on screen readers to play your game. Also, whereas Evennia normally
-automatically detects if a client supports color, it may get it wrong. Being able to turn it on
-manually if you know it should work could be a nice feature.
-
So here’s how to allow those users to remove color. It basically means you implementing a simple
-configuration system for your characters. This is the basic sequence:
-
-
Define your own default character typeclass, inheriting from Evennia’s default.
-
Set an attribute on the character to control markup on/off.
-
Set your custom character class to be the default for new accounts.
-
Overload the msg() method on the typeclass and change how it uses markup.
-
Create a custom command to allow users to change their setting.
Create a new module in mygame/typeclasses named, for example, mycharacter.py. Alternatively you
-can simply add a new class to ‘mygamegame/typeclasses/characters.py’.
-
In your new module(or characters.py), create a new Typeclass inheriting from
-evennia.DefaultCharacter. We will also import evennia.utils.ansi, which we will use later.
-
fromevenniaimportCharacter
- fromevennia.utilsimportansi
-
- classColorableCharacter(Character):
- at_object_creation(self):
- # set a color config value
- self.db.config_color=True
-
-
-
Above we set a simple config value as an Attribute.
-
Let’s make sure that new characters are created of this type. Edit your
-mygame/server/conf/settings.py file and add/change BASE_CHARACTER_TYPECLASS to point to your new
-character class. Observe that this will only affect new characters, not those already created. You
-have to convert already created characters to the new typeclass by using the @typeclass command
-(try on a secondary character first though, to test that everything works - you don’t want to render
-your root user unusable!).
-
@typeclass/reset/force Bob = mycharacter.ColorableCharacter
-
-
-
@typeclass changes Bob’s typeclass and runs all its creation hooks all over again. The /reset
-switch clears all attributes and properties back to the default for the new typeclass - this is
-useful in this case to avoid ending up with an object having a “mixture” of properties from the old
-typeclass and the new one. /force might be needed if you edit the typeclass and want to update the
-object despite the actual typeclass name not having changed.
Next we need to overload the msg() method. What we want is to check the configuration value before
-calling the main function. The original msg method call is seen in evennia/objects/objects.py
-and is called like this:
As long as we define a method on our custom object with the same name and keep the same number of
-arguments/keywords we will overload the original. Here’s how it could look:
-
classColorableCharacter(Character):
- # [...]
- msg(self,text=None,from_obj=None,session=None,options=None,
- **kwargs):
- "our custom msg()"
- ifself.db.config_colorisnotNone:# this would mean it was not set
- ifnotself.db.config_color:
- # remove the ANSI from the text
- text=ansi.strip_ansi(text)
- super().msg(text=text,from_obj=from_obj,
- session=session,**kwargs)
-
-
-
Above we create a custom version of the msg() method. If the configuration Attribute is set, it
-strips the ANSI from the text it is about to send, and then calls the parent msg() as usual. You
-need to @reload before your changes become visible.
-
There we go! Just flip the attribute config_color to False and your users will not see any color.
-As superuser (assuming you use the Typeclass ColorableCharacter) you can test this with the @py
-command:
For completeness, let’s add a custom command so users can turn off their color display themselves if
-they want.
-
In mygame/commands, create a new file, call it for example configcmds.py (it’s likely that
-you’ll want to add other commands for configuration down the line). You can also copy/rename the
-command template.
-
fromevenniaimportCommand
-
- classCmdConfigColor(Command):
- """
- Configures your color
-
- Usage:
- @togglecolor on|off
-
- This turns ANSI-colors on/off.
- Default is on.
- """
-
- key="@togglecolor"
- aliases=["@setcolor"]
-
- deffunc(self):
- "implements the command"
- # first we must remove whitespace from the argument
- self.args=self.args.strip()
- ifnotself.argsornotself.argsin("on","off"):
- self.caller.msg("Usage: @setcolor on|off")
- return
- ifself.args=="on":
- self.caller.db.config_color=True
- # send a message with a tiny bit of formatting, just for fun
- self.caller.msg("Color was turned |won|W.")
- else:
- self.caller.db.config_color=False
- self.caller.msg("Color was turned off.")
-
-
-
Lastly, we make this command available to the user by adding it to the default CharacterCmdSet in
-mygame/commands/default_cmdsets.py and reloading the server. Make sure you also import the
-command:
-
frommygame.commandsimportconfigcmds
-classCharacterCmdSet(default_cmds.CharacterCmdSet):
- # [...]
- defat_cmdset_creation(self):
- """
- Populates the cmdset
- """
- super().at_cmdset_creation()
- #
- # any commands you add below will overload the default ones.
- #
-
- # here is the only line that we edit
- self.add(configcmds.CmdConfigColor())
-
Apart from ANSI colors, Evennia also supports Xterm256 colors (See [Colors](./TextTags.md#colored-
-text)). The msg() method supports the xterm256 keyword for manually activating/deactiving
-xterm256. It should be easy to expand the above example to allow players to customize xterm256
-regardless of if Evennia thinks their client supports it or not.
-
To get a better understanding of how msg() works with keywords, you can try this as superuser:
-
@py self.msg("|123Dark blue with xterm256, bright blue with ANSI", xterm256=True)
-@py self.msg("|gThis should be uncolored", nomarkup=True)
-
An easy addition to add dynamic variety to your world objects is to give them some mass. Why mass
-and not weight? Weight varies in setting; for example things on the Moon weigh 1/6 as much. On
-Earth’s surface and in most environments, no relative weight factor is needed.
-
In most settings, mass can be used as weight to spring a pressure plate trap or a floor giving way,
-determine a character’s burden weight for travel speed… The total mass of an object can
-contribute to the force of a weapon swing, or a speeding meteor to give it a potential striking
-force.
Now that we have reasons for keeping track of object mass, let’s look at the default object class
-inside your mygame/typeclasses/objects.py and see how easy it is to total up mass from an object and
-its contents.
-
# inside your mygame/typeclasses/objects.py
-
-classObject(DefaultObject):
-# [...]
- defget_mass(self):
- mass=self.attributes.get('mass',1)# Default objects have 1 unit mass.
- returnmass+sum(obj.get_mass()forobjinself.contents)
-
-
-
Adding the get_mass definition to the objects you want to sum up the masses for is done with
-Python’s “sum” function which operates on all the contents, in this case by summing them to
-return a total mass value.
-
If you only wanted specific object types to have mass or have the new object type in a different
-module, see [[Adding-Object-Typeclass-Tutorial]] with its Heavy class object. You could set the
-default for Heavy types to something much larger than 1 gram or whatever unit you want to use. Any
-non-default mass would be stored on the mass [[Attributes]] of the objects.
You can add a get_mass definition to characters and rooms, also.
-
If you were in a one metric-ton elevator with four other friends also wearing armor and carrying
-gold bricks, you might wonder if this elevator’s going to move, and how fast.
-
Assuming the unit is grams and the elevator itself weights 1,000 kilograms, it would already be
-@setelevator/mass=1000000, we’re @setme/mass=85000 and our armor is @setarmor/mass=50000.
-We’re each carrying 20 gold bars each @setgoldbar/mass=12400 then step into the elevator and see
-the following message in the elevator’s appearance: Elevatorweightandcontentsshouldnotexceed3metrictons. Are we safe? Maybe not if you consider dynamic loading. But at rest:
-
# Elevator object knows when it checks itself:
-ifself.get_mass()<3000000:
- pass# Elevator functions as normal.
-else:
- pass# Danger! Alarm sounds, cable snaps, elevator stops...
-
The main functionality of Evennia is to communicate with clients connected to it; a player enters
-commands or their client queries for a gui update (ingoing data). The server responds or sends data
-on its own as the game changes (outgoing data). It’s important to understand how this flow of
-information works in Evennia.
When first connecting, the client can send data to the server about its
-capabilities. This is things like “I support xterm256 but not unicode” and is
-mainly used when a Telnet client connects. This is called a “handshake” and
-will generally set some flags on the Portal Session that
-are later synced to the Server Session. Since this is not something the player
-controls, we’ll not explore this further here.
-
The client can send an inputcommand to the server. Traditionally this only
-happens when the player enters text on the command line. But with a custom
-client GUI, a command could also come from the pressing of a button. Finally
-the client may send commands based on a timer or some trigger.
-
-
Exactly how the inputcommand looks when it travels from the client to Evennia
-depends on the Protocol used:
-
-
Telnet: A string. If GMCP or MSDP OOB protocols are used, this string will
-be formatted in a special way, but it’s still a raw string. If Telnet SSL is
-active, the string will be encrypted.
Each client is connected to the game via a Portal Session, one per connection. This Session is
-different depending on the type of connection (telnet, webclient etc) and thus know how to handle
-that particular data type. So regardless of how the data arrives, the Session will identify the type
-of the instruction and any arguments it should have. For example, the telnet protocol will figure
-that anything arriving normally over the wire should be passed on as a “text” type.
The PortalSessionhandler manages all connected Sessions in the Portal. Its data_in method
-(called by each Portal Session) will parse the command names and arguments from the protocols and
-convert them to a standardized form we call the inputcommand:
-
(commandname,(args),{kwargs})
-
-
-
All inputcommands must have a name, but they may or may not have arguments and keyword arguments -
-in fact no default inputcommands use kwargs at all. The most common inputcommand is “text”, which
-has the argument the player input on the command line:
-
("text",("look",),{})
-
-
-
This inputcommand-structure is pickled together with the unique session-id of the Session to which
-it belongs. This is then sent over the AMP connection.
On the Server side, the AMP unpickles the data and associates the session id with the server-side
-Session. Data and Session are passed to the server-side SessionHandler.data_in. This
-in turn calls ServerSession.data_in()
The method ServerSession.data_in is meant to offer a single place to override if they want to
-examine all data passing into the server from the client. It is meant to call the
-ssessionhandler.call_inputfuncs with the (potentially processed) data (so this is technically a
-sort of detour back to the sessionhandler).
-
In call_inputfuncs, the inputcommand’s name is compared against the names of all the inputfuncs
-registered with the server. The inputfuncs are named the same as the inputcommand they are supposed
-to handle, so the (default) inputfunc for handling our “look” command is called “text”. These are
-just normal functions and one can plugin new ones by simply putting them in a module where Evennia
-looks for such functions.
-
If a matching inputfunc is found, it will be called with the Session and the inputcommand’s
-arguments:
-
text(session,*("look",),**{})
-
-
-
If no matching inputfunc is found, an inputfunc named “default” will be tried and if that is also
-not found, an error will be raised.
The Inputfunc must be on the form func(session,*args,**kwargs). An exception is
-the default inputfunc which has form default(session,cmdname,*args,**kwargs), where cmdname
-is the un-matched inputcommand string.
-
This is where the message’s path diverges, since just what happens next depends on the type of
-inputfunc was triggered. In the example of sending “look”, the inputfunc is named “text”. It will
-pass the argument to the cmdhandler which will eventually lead to the look command being
-executed.
For our purposes, what is important to know is that with the exception of from_obj, session and
-options, all keywords given to the msg method is the name of an outputcommand and its
-arguments. So text is actually such a command, taking a string as its argument. The reason text
-sits as the first keyword argument is that it’s so commonly used (caller.msg("Text") for example).
-Here are some examples
-
msg("Hello!")# using the 'text' outputfunc
- msg(prompt="HP:%i, SP: %i, MP: %i"%(HP,SP,MP))
- msg(mycommand=((1,2,3,4),{"foo":"bar"})
-
-
-
-
Note the form of the mycommand outputfunction. This explicitly defines the arguments and keyword
-arguments for the function. In the case of the text and prompt calls we just specify a string -
-this works too: The system will convert this into a single argument for us later in the message
-path.
-
-
Note: The msg method sits on your Object- and Account typeclasses. It means you can easily
-override msg and make custom- or per-object modifications to the flow of data as it passes
-through.
In the ServerSessionhandler, the keywords from the msg method are collated into one or more
-outputcommands on a standardized form (identical to inputcommands):
-
(commandname,(args),{kwargs})
-
-
-
This will intelligently convert different input to the same form. So msg("Hello") will end up as
-an outputcommand ("text",("Hello",),{}).
-
This is also the point where Inlinefuncs are parsed, depending on the
-session to receive the data. Said data is pickled together with the Session id then sent over the
-AMP bridge.
After the AMP connection has unpickled the data and paired the session id to the matching
-PortalSession, the handler next determines if this Session has a suitable method for handling the
-outputcommand.
-
The situation is analogous to how inputfuncs work, except that protocols are fixed things that don’t
-need a plugin infrastructure like the inputfuncs are handled. So instead of an “outputfunc”, the
-handler looks for methods on the PortalSession with names of the form send_<commandname>.
-
For example, the common sending of text expects a PortalSession method send_text. This will be
-called as send_text(*("Hello",),**{}). If the “prompt” outputfunction was used, send_prompt is
-called. In all other cases the send_default(cmdname,*args,**kwargs) will be called - this is the
-case for all client-custom outputcommands, like when wanting to tell the client to update a graphic
-or play a sound.
At this point it is up to the session to convert the command into a form understood by this
-particular protocol. For telnet, send_text will just send the argument as a string (since that is
-what telnet clients expect when “text” is coming). If send_default was called (basically
-everything that is not traditional text or a prompt), it will pack the data as an GMCP or MSDP
-command packet if the telnet client supports either (otherwise it won’t send at all). If sending to
-the webclient, the data will get packed into a JSON structure at all times.
Once arrived at the client, the outputcommand is handled in the way supported by the client (or it
-may be quietly ignored if not). “text” commands will be displayed in the main window while others
-may trigger changes in the GUI or play a sound etc.
For a full outgoing message, you need to have the outgoing function defined in the javascript. See
-https://evennia.readthedocs.io/en/latest/Web-Client-Webclient.html for getting set up with custom
-webclient code. Once you have a custom plugin defined and loaded, create a new function in the
-plugin, onCustomFunc() for example:
You’ll also need to add the function to what the main plugin function returns:
-
return{
- init:init,
- onCustomFunc,
- }
-
-
-
This defines the function and looks for “var” as a variable that is passed to it. Once you have this
-in place in your custom plugin, you also need to update the static/webclient/js/webclient_gui.js
-file to recognize the new function when it’s called. First you should add a new function inside the
-plugin_handler function to recognize the new function:
This looks through all the plugins for a function that corresponds to the custom function being
-called. Next, add the custom function to the return statement of the plugin handler:
Lastly, you will also need to need to add an entry to the Evennia emitter to tie the python function
-call to this new javascript function (this is in the $(document).ready function):
Now you can make a call from your python code to the new custom function to pass information from
-the server to the client:
-
character.msg(customFunc=({"var":"blarg"}))
-
-
-
When this code in your python is run, you should be able to see the “blarg” string printed in the
-web client console. You should now be able to update the function call and definition to pass any
-information needed between server and client.
The MonitorHandler is a system for watching changes in properties or Attributes on objects. A
-monitor can be thought of as a sort of trigger that responds to change.
-
The main use for the MonitorHandler is to report changes to the client; for example the client
-Session may ask Evennia to monitor the value of the Characer’s health attribute and report
-whenever it changes. This way the client could for example update its health bar graphic as needed.
obj (Typeclassed entity) - the object to monitor. Since this must be
-typeclassed, it means you can’t monitor changes on Sessions with the monitorhandler, for
-example.
-
fieldname (str) - the name of a field or Attribute on obj. If you want to
-monitor a database field you must specify its full name, including the starting db_ (like
-db_key, db_location etc). Any names not starting with db_ are instead assumed to be the names
-of Attributes. This difference matters, since the MonitorHandler will automatically know to watch
-the db_value field of the Attribute.
-
callback(callable) - This will be called as callback(fieldname=fieldname,obj=obj,**kwargs)
-when the field updates.
-
idstring (str) - this is used to separate multiple monitors on the same object and fieldname.
-This is required in order to properly identify and remove the monitor later. It’s also used for
-saving it.
-
persistent (bool) - if True, the monitor will survive a server reboot.
-
-
Example:
-
fromevenniaimportMONITOR_HANDLERasmonitorhandler
-
-def_monitor_callback(fieldname="",obj=None,**kwargs):
- # reporting callback that works both
- # for db-fields and Attributes
- iffieldname.startswith("db_"):
- new_value=getattr(obj,fieldname)
- else:# an attribute
- new_value=obj.attributes.get(fieldname)
-
- obj.msg("%s.%s changed to '%s'."% \
- (obj.key,fieldname,new_value))
-
-# (we could add _some_other_monitor_callback here too)
-
-# monitor Attribute (assume we have obj from before)
-monitorhandler.add(obj,"desc",_monitor_callback)
-
-# monitor same db-field with two different callbacks (must separate by id_string)
-monitorhandler.add(obj,"db_key",_monitor_callback,id_string="foo")
-monitorhandler.add(obj,"db_key",_some_other_monitor_callback,id_string="bar")
-
-
-
-
A monitor is uniquely identified by the combination of the object instance it is monitoring, the
-name of the field/attribute to monitor on that object and its idstring (obj + fieldname +
-idstring). The idstring will be the empty string unless given explicitly.
-
So to “un-monitor” the above you need to supply enough information for the system to uniquely find
-the monitor to remove:
This tutorial will describe how to make an NPC-run shop. We will make use of the EvMenu
-system to present shoppers with a menu where they can buy things from the store’s stock.
-
Our shop extends over two rooms - a “front” room open to the shop’s customers and a locked “store
-room” holding the wares the shop should be able to sell. We aim for the following features:
-
-
The front room should have an Attribute storeroom that points to the store room.
-
Inside the front room, the customer should have a command buy or browse. This will open a
-menu listing all items available to buy from the store room.
-
A customer should be able to look at individual items before buying.
-
We use “gold” as an example currency. To determine cost, the system will look for an Attribute
-gold_value on the items in the store room. If not found, a fixed base value of 1 will be assumed.
-The wealth of the customer should be set as an Attribute gold on the Character. If not set, they
-have no gold and can’t buy anything.
-
When the customer makes a purchase, the system will check the gold_value of the goods and
-compare it to the gold Attribute of the customer. If enough gold is available, this will be
-deducted and the goods transferred from the store room to the inventory of the customer.
-
We will lock the store room so that only people with the right key can get in there.
We want to show a menu to the customer where they can list, examine and buy items in the store. This
-menu should change depending on what is currently for sale. Evennia’s EvMenu utility will manage
-the menu for us. It’s a good idea to read up on EvMenu if you are not familiar with it.
The shopping menu’s design is straightforward. First we want the main screen. You get this when you
-enter a shop and use the browse or buy command:
-
*** Welcome to ye Old Sword shop! ***
- Things for sale (choose 1-3 to inspect, quit to exit):
-_________________________________________________________
-1. A rusty sword (5 gold)
-2. A sword with a leather handle (10 gold)
-3. Excalibur (100 gold)
-
-
-
There are only three items to buy in this example but the menu should expand to however many items
-are needed. When you make a selection you will get a new screen showing the options for that
-particular item:
EvMenu defines the nodes (each menu screen with options) as normal Python functions. Each node
-must be able to change on the fly depending on what items are currently for sale. EvMenu will
-automatically make the quit command available to us so we won’t add that manually. For compactness
-we will put everything needed for our shop in one module, mygame/typeclasses/npcshop.py.
-
# mygame/typeclasses/npcshop.py
-
-fromevennia.utilsimportevmenu
-
-defmenunode_shopfront(caller):
- "This is the top-menu screen."
-
- shopname=caller.location.key
- wares=caller.location.db.storeroom.contents
-
- # Wares includes all items inside the storeroom, including the
- # door! Let's remove that from our for sale list.
- wares=[wareforwareinwaresifware.key.lower()!="door"]
-
- text="*** Welcome to %s! ***\n"%shopname
- ifwares:
- text+=" Things for sale (choose 1-%i to inspect);" \
- " quit to exit:"%len(wares)
- else:
- text+=" There is nothing for sale; quit to exit."
-
- options=[]
- forwareinwares:
- # add an option for every ware in store
- options.append({"desc":"%s (%s gold)"%
- (ware.key,ware.db.gold_valueor1),
- "goto":"menunode_inspect_and_buy"})
- returntext,options
-
-
-
In this code we assume the caller to be inside the shop when accessing the menu. This means we can
-access the shop room via caller.location and get its key to display as the shop’s name. We also
-assume the shop has an Attribute storeroom we can use to get to our stock. We loop over our goods
-to build up the menu’s options.
-
Note that all options point to the same menu node called menunode_inspect_and_buy! We can’t know
-which goods will be available to sale so we rely on this node to modify itself depending on the
-circumstances. Let’s create it now.
-
# further down in mygame/typeclasses/npcshop.py
-
-defmenunode_inspect_and_buy(caller,raw_string):
- "Sets up the buy menu screen."
-
- wares=caller.location.db.storeroom.contents
- # Don't forget, we will need to remove that pesky door again!
- wares=[wareforwareinwaresifware.key.lower()!="door"]
- iware=int(raw_string)-1
- ware=wares[iware]
- value=ware.db.gold_valueor1
- wealth=caller.db.goldor0
- text="You inspect %s:\n\n%s"%(ware.key,ware.db.desc)
-
- defbuy_ware_result(caller):
- "This will be executed first when choosing to buy."
- ifwealth>=value:
- rtext="You pay %i gold and purchase %s!"% \
- (value,ware.key)
- caller.db.gold-=value
- ware.move_to(caller,quiet=True)
- else:
- rtext="You cannot afford %i gold for %s!"% \
- (value,ware.key)
- caller.msg(rtext)
-
- options=({"desc":"Buy %s for %s gold"% \
- (ware.key,ware.db.gold_valueor1),
- "goto":"menunode_shopfront",
- "exec":buy_ware_result},
- {"desc":"Look for something else",
- "goto":"menunode_shopfront"})
-
- returntext,options
-
-
-
In this menu node we make use of the raw_string argument to the node. This is the text the menu
-user entered on the previous node to get here. Since we only allow numbered options in our menu,
-raw_input must be an number for the player to get to this point. So we convert it to an integer
-index (menu lists start from 1, whereas Python indices always starts at 0, so we need to subtract
-1). We then use the index to get the corresponding item from storage.
-
We just show the customer the desc of the item. In a more elaborate setup you might want to show
-things like weapon damage and special stats here as well.
-
When the user choose the “buy” option, EvMenu will execute the exec instruction before we go
-back to the top node (the goto instruction). For this we make a little inline function
-buy_ware_result. EvMenu will call the function given to exec like any menu node but it does not
-need to return anything. In buy_ware_result we determine if the customer can afford the cost and
-give proper return messages. This is also where we actually move the bought item into the inventory
-of the customer.
We could in principle launch the shopping menu the moment a customer steps into our shop room, but
-this would probably be considered pretty annoying. It’s better to create a Command for
-customers to explicitly wanting to shop around.
-
# mygame/typeclasses/npcshop.py
-
-fromevenniaimportCommand
-
-classCmdBuy(Command):
- """
- Start to do some shopping
-
- Usage:
- buy
- shop
- browse
-
- This will allow you to browse the wares of the
- current shop and buy items you want.
- """
- key="buy"
- aliases=("shop","browse")
-
- deffunc(self):
- "Starts the shop EvMenu instance"
- evmenu.EvMenu(self.caller,
- "typeclasses.npcshop",
- startnode="menunode_shopfront")
-
-
-
This will launch the menu. The EvMenu instance is initialized with the path to this very module -
-since the only global functions available in this module are our menu nodes, this will work fine
-(you could also have put those in a separate module). We now just need to put this command in a
-CmdSet so we can add it correctly to the game:
There are really only two things that separate our shop from any other Room:
-
-
The shop has the storeroom Attribute set on it, pointing to a second (completely normal) room.
-
It has the ShopCmdSet stored on itself. This makes the buy command available to users entering
-the shop.
-
-
For testing we could easily add these features manually to a room using @py or other admin
-commands. Just to show how it can be done we’ll instead make a custom Typeclass for
-the shop room and make a small command that builders can use to build both the shop and the
-storeroom at once.
-
# bottom of mygame/typeclasses/npcshop.py
-
-fromevenniaimportDefaultRoom,DefaultExit,DefaultObject
-fromevennia.utils.createimportcreate_object
-
-# class for our front shop room
-classNPCShop(DefaultRoom):
- defat_object_creation(self):
- # we could also use add(ShopCmdSet, permanent=True)
- self.cmdset.add_default(ShopCmdSet)
- self.db.storeroom=None
-
-# command to build a complete shop (the Command base class
-# should already have been imported earlier in this file)
-classCmdBuildShop(Command):
- """
- Build a new shop
-
- Usage:
- @buildshop shopname
-
- This will create a new NPCshop room
- as well as a linked store room (named
- simply <storename>-storage) for the
- wares on sale. The store room will be
- accessed through a locked door in
- the shop.
- """
- key="@buildshop"
- locks="cmd:perm(Builders)"
- help_category="Builders"
-
- deffunc(self):
- "Create the shop rooms"
- ifnotself.args:
- self.msg("Usage: @buildshop <storename>")
- return
- # create the shop and storeroom
- shopname=self.args.strip()
- shop=create_object(NPCShop,
- key=shopname,
- location=None)
- storeroom=create_object(DefaultRoom,
- key="%s-storage"%shopname,
- location=None)
- shop.db.storeroom=storeroom
- # create a door between the two
- shop_exit=create_object(DefaultExit,
- key="back door",
- aliases=["storage","store room"],
- location=shop,
- destination=storeroom)
- storeroom_exit=create_object(DefaultExit,
- key="door",
- location=storeroom,
- destination=shop)
- # make a key for accessing the store room
- storeroom_key_name="%s-storekey"%shopname
- storeroom_key=create_object(DefaultObject,
- key=storeroom_key_name,
- location=shop)
- # only allow chars with this key to enter the store room
- shop_exit.locks.add("traverse:holds(%s)"%storeroom_key_name)
-
- # inform the builder about progress
- self.caller.msg("The shop %s was created!"%shop)
-
-
-
Our typeclass is simple and so is our buildshop command. The command (which is for Builders only)
-just takes the name of the shop and builds the front room and a store room to go with it (always
-named "<shopname>-storage". It connects the rooms with a two-way exit. You need to add
-CmdBuildShop [to the default cmdset](./Adding-Command-Tutorial.md#step-2-adding-the-command-to-a-
-default-cmdset) before you can use it. Once having created the shop you can now @teleport to it or
-@open a new exit to it. You could also easily expand the above command to automatically create
-exits to and from the new shop from your current location.
-
To avoid customers walking in and stealing everything, we create a Lock on the storage
-door. It’s a simple lock that requires the one entering to carry an object named
-<shopname>-storekey. We even create such a key object and drop it in the shop for the new shop
-keeper to pick up.
-
-
If players are given the right to name their own objects, this simple lock is not very secure and
-you need to come up with a more robust lock-key solution.
-
-
-
We don’t add any descriptions to all these objects so looking “at” them will not be too thrilling.
-You could add better default descriptions as part of the @buildshop command or leave descriptions
-this up to the Builder.
We now have a functioning shop and an easy way for Builders to create it. All you need now is to
-@open a new exit from the rest of the game into the shop and put some sell-able items in the store
-room. Our shop does have some shortcomings:
-
-
For Characters to be able to buy stuff they need to also have the gold Attribute set on
-themselves.
-
We manually remove the “door” exit from our items for sale. But what if there are other unsellable
-items in the store room? What if the shop owner walks in there for example - anyone in the store
-could then buy them for 1 gold.
-
What if someone else were to buy the item we’re looking at just before we decide to buy it? It
-would then be gone and the counter be wrong - the shop would pass us the next item in the list.
-
-
Fixing these issues are left as an exercise.
-
If you want to keep the shop fully NPC-run you could add a Script to restock the shop’s
-store room regularly. This shop example could also easily be owned by a human Player (run for them
-by a hired NPC) - the shop owner would get the key to the store room and be responsible for keeping
-it well stocked.
Evennia offers many convenient ways to store object data, such as via Attributes or Scripts. This is
-sufficient for most use cases. But if you aim to build a large stand-alone system, trying to squeeze
-your storage requirements into those may be more complex than you bargain for. Examples may be to
-store guild data for guild members to be able to change, tracking the flow of money across a game-
-wide economic system or implement other custom game systems that requires the storage of custom data
-in a quickly accessible way. Whereas Tags or Scripts can handle many situations,
-sometimes things may be easier to handle by adding your own database model.
SQL-type databases (which is what Evennia supports) are basically highly optimized systems for
-retrieving text stored in tables. A table may look like this
Each line is considerably longer in your database. Each column is referred to as a “field” and every
-row is a separate object. You can check this out for yourself. If you use the default sqlite3
-database, go to your game folder and run
-
evennia dbshell
-
-
-
You will drop into the database shell. While there, try:
-
sqlite> .help # view help
-
- sqlite> .tables # view all tables
-
- # show the table field names for objects_objectdb
- sqlite> .schema objects_objectdb
-
- # show the first row from the objects_objectdb table
- sqlite> select * from objects_objectdb limit 1;
-
- sqlite> .exit
-
-
-
Evennia uses Django, which abstracts away the database SQL
-manipulation and allows you to search and manipulate your database entirely in Python. Each database
-table is in Django represented by a class commonly called a model since it describes the look of
-the table. In Evennia, Objects, Scripts, Channels etc are examples of Django models that we then
-extend and build on.
Here is how you add your own database table/models:
-
-
In Django lingo, we will create a new “application” - a subsystem under the main Evennia program.
-For this example we’ll call it “myapp”. Run the following (you need to have a working Evennia
-running before you do this, so make sure you have run the steps in [Getting Started](Getting-
-Started) first):
-
cd mygame/world
- evennia startapp myapp
-
-
-
-
A new folder myapp is created. “myapp” will also be the name (the “app label”) from now on. We
-chose to put it in the world/ subfolder here, but you could put it in the root of your mygame if
-that makes more sense.
-
The myapp folder contains a few empty default files. What we are
-interested in for now is models.py. In models.py you define your model(s). Each model will be a
-table in the database. See the next section and don’t continue until you have added the models you
-want.
-
You now need to tell Evennia that the models of your app should be a part of your database
-scheme. Add this line to your mygame/server/conf/settings.pyfile (make sure to use the path where
-you put myapp and don’t forget the comma at the end of the tuple):
-
INSTALLED_APPS=INSTALLED_APPS+("world.myapp",)
-
-
-
-
From mygame/, run
-
evennia makemigrations myapp
- evennia migrate
-
-
-
-
-
This will add your new database table to the database. If you have put your game under version
-control (if not, you should), don’t forget to gitaddmyapp/* to add all items
-to version control.
A Django model is the Python representation of a database table. It can be handled like any other
-Python class. It defines fields on itself, objects of a special type. These become the “columns”
-of the database table. Finally, you create new instances of the model to add new rows to the
-database.
-
We won’t describe all aspects of Django models here, for that we refer to the vast Django
-documentation on the subject. Here is a
-(very) brief example:
-
fromdjango.dbimportmodels
-
-classMyDataStore(models.Model):
- "A simple model for storing some data"
- db_key=models.CharField(max_length=80,db_index=True)
- db_category=models.CharField(max_length=80,null=True,blank=True)
- db_text=models.TextField(null=True,blank=True)
- # we need this one if we want to be
- # able to store this in an Evennia Attribute!
- db_date_created=models.DateTimeField('date created',editable=False,
- auto_now_add=True,db_index=True)
-
-
-
We create four fields: two character fields of limited length and one text field which has no
-maximum length. Finally we create a field containing the current time of us creating this object.
-
-
The db_date_created field, with exactly this name, is required if you want to be able to store
-instances of your custom model in an Evennia Attribute. It will automatically be set
-upon creation and can after that not be changed. Having this field will allow you to do e.g.
-obj.db.myinstance=mydatastore. If you know you’ll never store your model instances in Attributes
-the db_date_created field is optional.
-
-
You don’t have to start field names with db_, this is an Evennia convention. It’s nevertheless
-recommended that you do use db_, partly for clarity and consistency with Evennia (if you ever want
-to share your code) and partly for the case of you later deciding to use Evennia’s
-SharedMemoryModel parent down the line.
-
The field keyword db_index creates a database index for this field, which allows quicker
-lookups, so it’s recommended to put it on fields you know you’ll often use in queries. The
-null=True and blank=True keywords means that these fields may be left empty or set to the empty
-string without the database complaining. There are many other field types and keywords to define
-them, see django docs for more info.
-
Similar to using django-admin you
-are able to do evenniainspectdb to get an automated listing of model information for an existing
-database. As is the case with any model generating tool you should only use this as a starting
-point for your models.
To create a new row in your table, you instantiate the model and then call its save() method:
-
fromevennia.myappimportMyDataStore
-
- new_datastore=MyDataStore(db_key="LargeSword",
- db_category="weapons",
- db_text="This is a huge weapon!")
- # this is required to actually create the row in the database!
- new_datastore.save()
-
-
-
-
Note that the db_date_created field of the model is not specified. Its flag at_now_add=True
-makes sure to set it to the current date when the object is created (it can also not be changed
-further after creation).
-
When you update an existing object with some new field value, remember that you have to save the
-object afterwards, otherwise the database will not update:
Evennia’s normal models don’t need to explicitly save, since they are based on SharedMemoryModel
-rather than the raw django model. This is covered in the next section.
Evennia doesn’t base most of its models on the raw django.db.models but on the Evennia base model
-evennia.utils.idmapper.models.SharedMemoryModel. There are two main reasons for this:
-
-
Ease of updating fields without having to explicitly call save()
-
On-object memory persistence and database caching
-
-
The first (and least important) point means that as long as you named your fields db_*, Evennia
-will automatically create field wrappers for them. This happens in the model’s
-Metaclass so there is no speed
-penalty for this. The name of the wrapper will be the same name as the field, minus the db_
-prefix. So the db_key field will have a wrapper property named key. You can then do:
-
my_datastore.key="Larger Sword"
-
-
-
and don’t have to explicitly call save() afterwards. The saving also happens in a more efficient
-way under the hood, updating only the field rather than the entire model using django optimizations.
-Note that if you were to manually add the property or method key to your model, this will be used
-instead of the automatic wrapper and allows you to fully customize access as needed.
-
To explain the second and more important point, consider the following example using the default
-Django model parent:
-
shield=MyDataStore.objects.get(db_key="SmallShield")
- shield.cracked=True# where cracked is not a database field
-
The outcome of that last print statement is undefined! It could maybe randomly work but most
-likely you will get an AttributeError for not finding the cracked property. The reason is that
-cracked doesn’t represent an actual field in the database. It was just added at run-time and thus
-Django don’t care about it. When you retrieve your shield-match later there is no guarantee you
-will get back the same Python instance of the model where you defined cracked, even if you
-search for the same database object.
-
Evennia relies heavily on on-model handlers and other dynamically created properties. So rather than
-using the vanilla Django models, Evennia uses SharedMemoryModel, which levies something called
-idmapper. The idmapper caches model instances so that we will always get the same instance back
-after the first lookup of a given object. Using idmapper, the above example would work fine and you
-could retrieve your cracked property at any time - until you rebooted when all non-persistent data
-goes.
-
Using the idmapper is both more intuitive and more efficient per object; it leads to a lot less
-reading from disk. The drawback is that this system tends to be more memory hungry overall. So if
-you know that you’ll never need to add new properties to running instances or know that you will
-create new objects all the time yet rarely access them again (like for a log system), you are
-probably better off making “plain” Django models rather than using SharedMemoryModel and its
-idmapper.
-
To use the idmapper and the field-wrapper functionality you just have to have your model classes
-inherit from evennia.utils.idmapper.models.SharedMemoryModel instead of from the default
-django.db.models.Model:
-
fromevennia.utils.idmapper.modelsimportSharedMemoryModel
-
-classMyDataStore(SharedMemoryModel):
- # the rest is the same as before, but db_* is important; these will
- # later be settable as .key, .category, .text ...
- db_key=models.CharField(max_length=80,db_index=True)
- db_category=models.CharField(max_length=80,null=True,blank=True)
- db_text=models.TextField(null=True,blank=True)
- db_date_created=models.DateTimeField('date created',editable=False,
- auto_now_add=True,db_index=True)
-
To search your new custom database table you need to use its database manager to build a query.
-Note that even if you use SharedMemoryModel as described in the previous section, you have to use
-the actual field names in the query, not the wrapper name (so db_key and not just key).
-
fromworld.myappimportMyDataStore
-
- # get all datastore objects exactly matching a given key
- matches=MyDataStore.objects.filter(db_key="Larger Sword")
- # get all datastore objects with a key containing "sword"
- # and having the category "weapons" (both ignoring upper/lower case)
- matches2=MyDataStore.objects.filter(db_key__icontains="sword",
- db_category__iequals="weapons")
- # show the matching data (e.g. inside a command)
- formatchinmatches2:
- self.caller.msg(match.db_text)
-
Nicks, short for Nicknames is a system allowing an object (usually a Account) to
-assign custom replacement names for other game entities.
-
Nicks are not to be confused with Aliases. Setting an Alias on a game entity actually changes an
-inherent attribute on that entity, and everyone in the game will be able to use that alias to
-address the entity thereafter. A Nick on the other hand, is used to map a different way you
-alone can refer to that entity. Nicks are also commonly used to replace your input text which means
-you can create your own aliases to default commands.
-
Default Evennia use Nicks in three flavours that determine when Evennia actually tries to do the
-substitution.
-
-
inputline - replacement is attempted whenever you write anything on the command line. This is the
-default.
-
objects - replacement is only attempted when referring to an object
-
accounts - replacement is only attempted when referring an account
-
-
Here’s how to use it in the default command set (using the nick command):
-
nick ls = look
-
-
-
This is a good one for unix/linux users who are accustomed to using the ls command in their daily
-life. It is equivalent to nick/inputlinels=look.
-
nick/object mycar2 = The red sports car
-
-
-
With this example, substitutions will only be done specifically for commands expecting an object
-reference, such as
-
look mycar2
-
-
-
becomes equivalent to “lookTheredsportscar”.
-
nick/accounts tom = Thomas Johnsson
-
-
-
This is useful for commands searching for accounts explicitly:
-
@find *tom
-
-
-
One can use nicks to speed up input. Below we add ourselves a quicker way to build red buttons. In
-the future just writing rb will be enough to execute that whole long string.
-
nick rb = @create button:examples.red_button.RedButton
-
-
-
Nicks could also be used as the start for building a “recog” system suitable for an RP mud.
-
nick/account Arnold = The mysterious hooded man
-
-
-
The nick replacer also supports unix-style templating:
-
nick build $1 $2 = @create/drop $1;$2
-
-
-
This will catch space separated arguments and store them in the the tags $1 and $2, to be
-inserted in the replacement string. This example allows you to do buildboxcrate and have Evennia
-see @create/dropbox;crate. You may use any $ numbers between 1 and 99, but the markers must
-match between the nick pattern and the replacement.
-
-
If you want to catch “the rest” of a command argument, make sure to put a $ tag with no spaces
-to the right of it - it will then receive everything up until the end of the line.
Nicks are stored as the Nick database model and are referred from the normal Evennia
-object through the nicks property - this is known as the NickHandler. The NickHandler
-offers effective error checking, searches and conversion.
-
# A command/channel nick:
- obj.nicks.add("greetjack","tell Jack = Hello pal!")
-
- # An object nick:
- obj.nicks.add("rose","The red flower",nick_type="object")
-
- # An account nick:
- obj.nicks.add("tom","Tommy Hill",nick_type="account")
-
- # My own custom nick type (handled by my own game code somehow):
- obj.nicks.add("hood","The hooded man",nick_type="my_identsystem")
-
- # get back the translated nick:
- full_name=obj.nicks.get("rose",nick_type="object")
-
- # delete a previous set nick
- object.nicks.remove("rose",nick_type="object")
-
-
-
In a command definition you can reach the nick handler through self.caller.nicks. See the nick
-command in evennia/commands/default/general.py for more examples.
-
As a last note, The Evennia channel alias systems are using nicks with the
-nick_type="channel" in order to allow users to create their own custom aliases to channels.
Internally, nicks are Attributes saved with the db_attrype set to “nick” (normal
-Attributes has this set to None).
-
The nick stores the replacement data in the Attribute.db_value field as a tuple with four fields
-(regex_nick,template_string,raw_nick,raw_template). Here regex_nick is the converted regex
-representation of the raw_nick and the template-string is a version of the raw_template
-prepared for efficient replacement of any $- type markers. The raw_nick and raw_template are
-basically the unchanged strings you enter to the nick command (with unparsed $ etc).
-
If you need to access the tuple for some reason, here’s how:
OOB, or Out-Of-Band, means sending data between Evennia and the user’s client without the user
-prompting it or necessarily being aware that it’s being passed. Common uses would be to update
-client health-bars, handle client button-presses or to display certain tagged text in a different
-window pane.
Inside Evennia, all server-client communication happens in the same way (so plain text is also an
-‘OOB message’ as far as Evennia is concerned). The message follows the Message Path.
-You should read up on that if you are unfamiliar with it. As the message travels along the path it
-has a standardized internal form: a tuple with a string, a tuple and a dict:
-
("cmdname", (args), {kwargs})
-
-
-
This is often referred to as an inputcommand or outputcommand, depending on the direction it’s
-traveling. The end point for an inputcommand, (the ‘Evennia-end’ of the message path) is a matching
-Inputfunc. This function is called as cmdname(session,*args,**kwargs) where
-session is the Session-source of the command. Inputfuncs can easily be added by the developer to
-support/map client commands to actions inside Evennia (see the inputfunc page for more
-details).
-
When a message is outgoing (at the ‘Client-end’ of the message path) the outputcommand is handled by
-a matching Outputfunc. This is responsible for converting the internal Evennia representation to a
-form suitable to send over the wire to the Client. Outputfuncs are hard-coded. Which is chosen and
-how it processes the outgoing data depends on the nature of the client it’s connected to. The only
-time one would want to add new outputfuncs is as part of developing support for a new Evennia
-Protocol.
A special case is the text input/outputfunc. It’s so common that it’s the default of the msg
-method. So these are equivalent:
-
caller.msg("Hello")
- caller.msg(text="Hello")
-
-
-
You don’t have to specify the full output/input definition. So for example, if your particular
-command only needs kwargs, you can skip the (args) part. Like in the text case you can skip
-writing the tuple if there is only one arg … and so on - the input is pretty flexible. If there
-are no args at all you need to give the empty tuple msg(cmdname=(,) (giving None would mean a
-single argument None).
-
Which commands you can send depends on the client. If the client does not support an explicit OOB
-protocol (like many old/legacy MUD clients) Evennia can only send text to them and will quietly
-drop any other types of outputfuncs.
-
-
Remember that a given message may go to multiple clients with different capabilities. So unless
-you turn off telnet completely and only rely on the webclient, you should never rely on non-text
-OOB messages always reaching all targets.
-
-
Inputfuncs lists the default inputfuncs available to handle incoming OOB messages. To
-accept more you need to add more inputfuncs (see that page for more info).
By default telnet (and telnet+SSL) supports only the plain text outputcommand. Evennia however
-detects if the Client supports one of two MUD-specific OOB extensions to the standard telnet
-protocol - GMCP or MSDP. Evennia supports both simultaneously and will switch to the protocol the
-client uses. If the client supports both, GMCP will be used.
-
-
Note that for Telnet, text has a special status as the “in-band” operation. So the text
-outputcommand sends the text argument directly over the wire, without going through the OOB
-translations described below.
GMCP, the Generic Mud Communication Protocol sends data on the
-form cmdname+JSONdata. Here the cmdname is expected to be on the form “Package.Subpackage”.
-There could also be additional Sub-sub packages etc. The names of these ‘packages’ and ‘subpackages’
-are not that well standardized beyond what individual MUDs or companies have chosen to go with over
-the years. You can decide on your own package names, but here are what others are using:
Evennia will translate underscores to . and capitalize to fit the specification. So the
-outputcommand foo_bar will become a GMCP command-name Foo.Bar. A GMCP command “Foo.Bar” will be
-come foo_bar. To send a GMCP command that turns into an Evennia inputcommand without an
-underscore, use the Core package. So Core.Cmdname becomes just cmdname in Evennia and vice
-versa.
-
On the wire, a GMCP instruction for ("cmdname",("arg",),{}) will look like this:
-
IAC SB GMCP "cmdname" "arg" IAC SE
-
-
-
where all the capitalized words are telnet character constants specified in
-evennia/server/portal/telnet_oob.py. These are parsed/added by the protocol and we don’t include
-these in the listings below.
Since Evennia already supplies default inputfuncs that don’t match the names expected by the most
-common GMCP implementations we have a few hard-coded mappings for those:
MSDP, the Mud Server Data Protocol, is a competing standard
-to GMCP. The MSDP protocol page specifies a range of “recommended” available MSDP command names.
-Evennia does not support those - since MSDP doesn’t specify a special format for its command names
-(like GMCP does) the client can and should just call the internal Evennia inputfunc by its actual
-name.
-
MSDP uses Telnet character constants to package various structured data over the wire. MSDP supports
-strings, arrays (lists) and tables (dicts). These are used to define the cmdname, args and kwargs
-needed. When sending MSDP for ("cmdname",("arg",),{}) the resulting MSDP instruction will look
-like this:
-
IAC SB MSDP VAR cmdname VAL arg IAC SE
-
-
-
The various available MSDP constants like VAR (variable), VAL (value), ARRAYOPEN/ARRAYCLOSE
-and TABLEOPEN/TABLECLOSE are specified in evennia/server/portal/telnet_oob.
[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
-
Observe that VAR...VAL always identifies cmdnames, so if there are multiple arrays/dicts tagged
-with the same cmdname they will be appended to the args, kwargs of that inputfunc. Vice-versa, a
-different VAR...VAL (outside a table) will come out as a second, different command input.
Our web client uses pure JSON structures for all its communication, including text. This maps
-directly to the Evennia internal output/inputcommand, including eventual empty args/kwargs. So the
-same example ("cmdname",("arg",),{}) will be sent/received as a valid JSON structure
-
["cmdname, ["arg"], {}]
-
-
-
Since JSON is native to Javascript, this becomes very easy for the webclient to handle.
All in-game objects in Evennia, be it characters, chairs, monsters, rooms or hand grenades are
-represented by an Evennia Object. Objects form the core of Evennia and is probably what you’ll
-spend most time working with. Objects are Typeclassed entities.
An Evennia Object is, per definition, a Python class that includes evennia.DefaultObject among its
-parents. In mygame/typeclasses/objects.py there is already a class Object that inherits from
-DefaultObject and that you can inherit from. You can put your new typeclass directly in that
-module or you could organize your code in some other way. Here we assume we make a new module
-mygame/typeclasses/flowers.py:
-
# mygame/typeclasses/flowers.py
-
- fromtypeclasses.objectsimportObject
-
- classRose(Object):
- """
- This creates a simple rose object
- """
- defat_object_creation(self):
- "this is called only once, when object is first created"
- # add a persistent attribute 'desc'
- # to object (silly example).
- self.db.desc="This is a pretty rose with thorns."
-
-
-
You could save this in the mygame/typeclasses/objects.py (then you’d not need to import Object)
-or you can put it in a new module. Let’s say we do the latter, making a module
-typeclasses/flowers.py. Now you just need to point to the class Rose with the @create command
-to make a new rose:
-
@create/drop MyRose:flowers.Rose
-
-
-
What the @create command actually does is to use evennia.create_object. You can do the same
-thing yourself in code:
(The @create command will auto-append the most likely path to your typeclass, if you enter the
-call manually you have to give the full path to the class. The create.create_object function is
-powerful and should be used for all coded object creating (so this is what you use when defining
-your own building commands). Check out the ev.create_* functions for how to build other entities
-like Scripts).
-
This particular Rose class doesn’t really do much, all it does it make sure the attribute
-desc(which is what the look command looks for) is pre-set, which is pretty pointless since you
-will usually want to change this at build time (using the @desc command or using the
-Spawner). The Object typeclass offers many more hooks that is available
-to use though - see next section.
Beyond the properties assigned to all typeclassed objects (see that page for a list
-of those), the Object also has the following custom properties:
-
-
aliases - a handler that allows you to add and remove aliases from this object. Use
-aliases.add() to add a new alias and aliases.remove() to remove one.
-
location - a reference to the object currently containing this object.
-
home is a backup location. The main motivation is to have a safe place to move the object to if
-its location is destroyed. All objects should usually have a home location for safety.
-
destination - this holds a reference to another object this object links to in some way. Its
-main use is for Exits, it’s otherwise usually unset.
-
nicks - as opposed to aliases, a Nick holds a convenient nickname replacement for a
-real name, word or sequence, only valid for this object. This mainly makes sense if the Object is
-used as a game character - it can then store briefer shorts, example so as to quickly reference game
-commands or other characters. Use nicks.add(alias, realname) to add a new one.
-
account - this holds a reference to a connected Account controlling this object (if
-any). Note that this is set also if the controlling account is not currently online - to test if
-an account is online, use the has_account property instead.
-
sessions - if account field is set and the account is online, this is a list of all active
-sessions (server connections) to contact them through (it may be more than one if multiple
-connections are allowed in settings).
-
has_account - a shorthand for checking if an online account is currently connected to this
-object.
-
contents - this returns a list referencing all objects ‘inside’ this object (i,e. which has this
-object set as their location).
-
exits - this returns all objects inside this object that are Exits, that is, has the
-destination property set.
-
-
The last two properties are special:
-
-
cmdset - this is a handler that stores all command sets defined on the
-object (if any).
-
scripts - this is a handler that manages Scripts attached to the object (if any).
-
-
The Object also has a host of useful utility functions. See the function headers in
-src/objects/objects.py for their arguments and more details.
-
-
msg() - this function is used to send messages from the server to an account connected to this
-object.
-
msg_contents() - calls msg on all objects inside this object.
-
search() - this is a convenient shorthand to search for a specific object, at a given location
-or globally. It’s mainly useful when defining commands (in which case the object executing the
-command is named caller and one can do caller.search() to find objects in the room to operate
-on).
-
execute_cmd() - Lets the object execute the given string as if it was given on the command line.
-
move_to - perform a full move of this object to a new location. This is the main move method
-and will call all relevant hooks, do all checks etc.
-
clear_exits() - will delete all Exits to and from this object.
-
clear_contents() - this will not delete anything, but rather move all contents (except Exits) to
-their designated Home locations.
-
delete() - deletes this object, first calling clear_exits() and
-clear_contents().
-
-
The Object Typeclass defines many more hook methods beyond at_object_creation. Evennia calls
-these hooks at various points. When implementing your custom objects, you will inherit from the
-base parent and overload these hooks with your own custom code. See evennia.objects.objects for an
-updated list of all the available hooks or the API for DefaultObject
-here.
There are three special subclasses of Object in default Evennia - Characters, Rooms and
-Exits. The reason they are separated is because these particular object types are fundamental,
-something you will always need and in some cases requires some extra attention in order to be
-recognized by the game engine (there is nothing stopping you from redefining them though). In
-practice they are all pretty similar to the base Object.
Characters are objects controlled by Accounts. When a new Account
-logs in to Evennia for the first time, a new Character object is created and
-the Account object is assigned to the account attribute. A Character object
-must have a Default Commandset set on itself at
-creation, or the account will not be able to issue any commands! If you just
-inherit your own class from evennia.DefaultCharacter and make sure to use
-super() to call the parent methods you should be fine. In
-mygame/typeclasses/characters.py is an empty Character class ready for you
-to modify.
Rooms are the root containers of all other objects. The only thing really separating a room from
-any other object is that they have no location of their own and that default commands like @dig
-creates objects of this class - so if you want to expand your rooms with more functionality, just
-inherit from ev.DefaultRoom. In mygame/typeclasses/rooms.py is an empty Room class ready for
-you to modify.
Exits are objects connecting other objects (usually Rooms) together. An object named North or
-in might be an exit, as well as door, portal or jump out the window. An exit has two things
-that separate them from other objects. Firstly, their destination property is set and points to a
-valid object. This fact makes it easy and fast to locate exits in the database. Secondly, exits
-define a special Transit Command on themselves when they are created. This command is
-named the same as the exit object and will, when called, handle the practicalities of moving the
-character to the Exits’s destination - this allows you to just enter the name of the exit on its
-own to move around, just as you would expect.
-
The exit functionality is all defined on the Exit typeclass, so you could in principle completely
-change how exits work in your game (it’s not recommended though, unless you really know what you are
-doing). Exits are locked using an access_type called traverse and also make use of a few
-hook methods for giving feedback if the traversal fails. See evennia.DefaultExit for more info.
-In mygame/typeclasses/exits.py there is an empty Exit class for you to modify.
-
The process of traversing an exit is as follows:
-
-
The traversing obj sends a command that matches the Exit-command name on the Exit object. The
-cmdhandler detects this and triggers the command defined on the Exit. Traversal always
-involves the “source” (the current location) and the destination (this is stored on the Exit
-object).
-
The Exit command checks the traverse lock on the Exit object
-
The Exit command triggers at_traverse(obj,destination) on the Exit object.
-
In at_traverse, object.move_to(destination) is triggered. This triggers the following hooks,
-in order:
-
-
obj.at_before_move(destination) - if this returns False, move is aborted.
-
origin.at_object_leave(obj,destination)
-
obj.announce_move_from(destination)
-
Move is performed by changing obj.location from source location to destination.
-
obj.announce_move_to(source)
-
destination.at_object_receive(obj,source)
-
obj.at_after_move(source)
-
-
-
On the Exit object, at_after_traverse(obj,source) is triggered.
-
-
If the move fails for whatever reason, the Exit will look for an Attribute err_traverse on itself
-and display this as an error message. If this is not found, the Exit will instead call
-at_failed_traverse(obj) on itself.
Evennia development can be made without any Internet connection beyond fetching updates. At some
-point however, you are likely to want to make your game visible online, either as part opening it to
-the public or to allow other developers or beta testers access to it.
Accessing your Evennia server from the outside is not hard on its own. Any issues are usually due to
-the various security measures your computer, network or hosting service has. These will generally
-(and correctly) block outside access to servers on your machine unless you tell them otherwise.
-
We will start by showing how to host your server on your own local computer. Even if you plan to
-host your “real” game on a remote host later, setting it up locally is useful practice. We cover
-remote hosting later in this document.
-
Out of the box, Evennia uses three ports for outward communication. If your computer has a firewall,
-these should be open for in/out communication (and only these, other ports used by Evennia are
-internal to your computer only).
-
-
4000, telnet, for traditional mud clients
-
4001, HTTP for the website)
-
4002, websocket, for the web client
-
-
Evennia will by default accept incoming connections on all interfaces (0.0.0.0) so in principle
-anyone knowing the ports to use and has the IP address to your machine should be able to connect to
-your game.
-
-
Make sure Evennia is installed and that you have activated the virtualenv. Start the server with
-evenniastart--log. The --log (or -l) will make sure that the logs are echoed to the
-terminal.
-
-
-
Note: If you need to close the log-view, use Ctrl-C. Use just evennia--log on its own to
-start tailing the logs again.
-
-
-
Make sure you can connect with your web browser to http://localhost:4001 or, alternatively,
-http://127.0.0.1:4001 which is the same thing. You should get your Evennia web site and be able to
-play the game in the web client. Also check so that you can connect with a mud client to host
-localhost, port 4000 or host 127.0.0.1, port 4000.
-
Google for “my ip” or use any online service to figure out
-what your “outward-facing” IP address is. For our purposes, let’s say your outward-facing IP is
-203.0.113.0.
-
Next try your outward-facing IP by opening http://203.0.113.0:4001 in a browser. If this works,
-that’s it! Also try telnet, with the server set to 203.0.113.0 and port 4000. However, most
-likely it will not work. If so, read on.
-
If your computer has a firewall, it may be blocking the ports we need (it may also block telnet
-overall). If so, you need to open the outward-facing ports to in/out communication. See the
-manual/instructions for your firewall software on how to do this. To test you could also temporarily
-turn off your firewall entirely to see if that was indeed the problem.
-
Another common problem for not being able to connect is that you are using a hardware router
-(like a wifi router). The router sits ‘between’ your computer and the Internet. So the IP you find
-with Google is the router’s IP, not that of your computer. To resolve this you need to configure
-your router to forward data it gets on its ports to the IP and ports of your computer sitting in
-your private network. How to do this depends on the make of your router; you usually configure it
-using a normal web browser. In the router interface, look for “Port forwarding” or maybe “Virtual
-server”. If that doesn’t work, try to temporarily wire your computer directly to the Internet outlet
-(assuming your computer has the ports for it). You’ll need to check for your IP again. If that
-works, you know the problem is the router.
-
-
-
Note: If you need to reconfigure a router, the router’s Internet-facing ports do not have to
-have to have the same numbers as your computer’s (and Evennia’s) ports! For example, you might want
-to connect Evennia’s outgoing port 4001 to an outgoing router port 80 - this is the port HTTP
-requests use and web browsers automatically look for - if you do that you could go to
-http://203.0.113.0 without having to add the port at the end. This would collide with any other
-web services you are running through this router though.
You can connect Evennia to the Internet without any changes to your settings. The default settings
-are easy to use but are not necessarily the safest. You can customize your online presence in your
-settings file. To have Evennia recognize changed port settings you have
-to do a full evenniareboot to also restart the Portal and not just the Server component.
-
Below is an example of a simple set of settings, mostly using the defaults. Evennia will require
-access to five computer ports, of which three (only) should be open to the outside world. Below we
-continue to assume that our server address is 203.0.113.0.
-
# in mygame/server/conf/settings.py
-
-SERVERNAME="MyGame"
-
-# open to the internet: 4000, 4001, 4002
-# closed to the internet (internal use): 4005, 4006
-TELNET_PORTS=[4000]
-WEBSOCKET_CLIENT_PORT=4002
-WEBSERVER_PORTS=[(4001,4005)]
-AMP_PORT=4006
-
-# Optional - security measures limiting interface access
-# (don't set these before you know things work without them)
-TELNET_INTERFACES=['203.0.113.0']
-WEBSOCKET_CLIENT_INTERFACE='203.0.113.0'
-ALLOWED_HOSTS=[".mymudgame.com"]
-
-# uncomment if you want to lock the server down for maintenance.
-# LOCKDOWN_MODE = True
-
-
-
-
Read on for a description of the individual settings.
# Required. Change to whichever outgoing Telnet port(s)
-# you are allowed to use on your host.
-TELNET_PORTS=[4000]
-# Optional for security. Restrict which telnet
-# interfaces we should accept. Should be set to your
-# outward-facing IP address(es). Default is ´0.0.0.0´
-# which accepts all interfaces.
-TELNET_INTERFACES=['0.0.0.0']
-
-
-
The TELNET_* settings are the most important ones for getting a traditional base game going. Which
-IP addresses you have available depends on your server hosting solution (see the next sections).
-Some hosts will restrict which ports you are allowed you use so make sure to check.
# Required. This is a list of tuples
-# (outgoing_port, internal_port). Only the outgoing
-# port should be open to the world!
-# set outgoing port to 80 if you want to run Evennia
-# as the only web server on your machine (if available).
-WEBSERVER_PORTS=[(4001,4005)]
-# Optional for security. Change this to the IP your
-# server can be reached at (normally the same
-# as TELNET_INTERFACES)
-WEBSERVER_INTERFACES=['0.0.0.0']
-# Optional for security. Protects against
-# man-in-the-middle attacks. Change it to your server's
-# IP address or URL when you run a production server.
-ALLOWED_HOSTS=['*']
-
-
-
The web server is always configured with two ports at a time. The outgoing port (4001 by
-default) is the port external connections can use. If you don’t want users to have to specify the
-port when they connect, you should set this to 80 - this however only works if you are not running
-any other web server on the machine.
-The internal port (4005 by default) is used internally by Evennia to communicate between the
-Server and the Portal. It should not be available to the outside world. You usually only need to
-change the outgoing port unless the default internal port is clashing with some other program.
# Required. Change this to the main IP address of your server.
-WEBSOCKET_CLIENT_INTERFACE='0.0.0.0'
-# Optional and needed only if using a proxy or similar. Change
-# to the IP or address where the client can reach
-# your server. The ws:// part is then required. If not given, the client
-# will use its host location.
-WEBSOCKET_CLIENT_URL=""
-# Required. Change to a free port for the websocket client to reach
-# the server on. This will be automatically appended
-# to WEBSOCKET_CLIENT_URL by the web client.
-WEBSOCKET_CLIENT_PORT=4002
-
-
-
The websocket-based web client needs to be able to call back to the server, and these settings must
-be changed for it to find where to look. If it cannot find the server you will get an warning in
-your browser’s Console (in the dev tools of the browser), and the client will revert to the AJAX-
-based of the client instead, which tends to be slower.
# Optional public facing. Only allows SSL connections (off by default).
-SSL_PORTS=[4003]
-SSL_INTERFACES=['0.0.0.0']
-# Optional public facing. Only if you allow SSH connections (off by default).
-SSH_PORTS=[4004]
-SSH_INTERFACES=['0.0.0.0']
-# Required private. You should only change this if there is a clash
-# with other services on your host. Should NOT be open to the
-# outside world.
-AMP_PORT=4006
-
-
-
The AMP_PORT is required to work, since this is the internal port linking Evennia’s
-Server and Portal components together. The other ports are encrypted ports that may be
-useful for custom protocols but are otherwise not used.
When you test things out and check configurations you may not want players to drop in on you.
-Similarly, if you are doing maintenance on a live game you may want to take it offline for a while
-to fix eventual problems without risking people connecting. To do this, stop the server with
-evenniastop and add LOCKDOWN_MODE=True to your settings file. When you start the server
-again, your game will only be accessible from localhost.
Once your game is online you should make sure to register it with the Evennia Game
-Index. Registering with the index will help people find your server,
-drum up interest for your game and also shows people that Evennia is being used. You can do this
-even if you are just starting development - if you don’t give any telnet/web address it will appear
-as Not yet public and just be a teaser. If so, pick pre-alpha as the development status.
-
To register, stand in your game dir, run
-
evennia connections
-
-
-
and follow the instructions. See the Game index page for more details.
SSL can be very useful for web clients. It will protect the credentials and gameplay of your users
-over a web client if they are in a public place, and your websocket can also be switched to WSS for
-the same benefit. SSL certificates used to cost money on a yearly basis, but there is now a program
-that issues them for free with assisted setup to make the entire process less painful.
-
Options that may be useful in combination with an SSL proxy:
-
# See above for the section on Lockdown Mode.
-# Useful for a proxy on the public interface connecting to Evennia on localhost.
-LOCKDOWN_MODE=True
-
-# Have clients communicate via wss after connecting with https to port 4001.
-# Without this, you may get DOMException errors when the browser tries
-# to create an insecure websocket from a secure webpage.
-WEBSOCKET_CLIENT_URL="wss://fqdn:4002"
-
Let’s Encrypt is a certificate authority offering free certificates to
-secure a website with HTTPS. To get started issuing a certificate for your web server using Let’s
-Encrypt, see these links:
The CertBot Client is a program for automatically obtaining a
-certificate, use it and maintain it with your website.
-
-
Also, on Freenode visit the #letsencrypt channel for assistance from the community. For an
-additional resource, Let’s Encrypt has a very active community
-forum.
The only process missing from all of the above documentation is how to pass verification. This is
-how Let’s Encrypt verifies that you have control over your domain (not necessarily ownership, it’s
-Domain Validation (DV)). This can be done either with configuring a certain path on your web server
-or through a TXT record in your DNS. Which one you will want to do is a personal preference, but can
-also be based on your hosting choice. In a controlled/cPanel environment, you will most likely have
-to use DNS verification.
What we showed above is by far the simplest and probably cheapest option: Run Evennia on your own
-home computer. Moreover, since Evennia is its own web server, you don’t need to install anything
-extra to have a website.
-
Advantages
-
-
Free (except for internet costs and the electrical bill).
-
Full control over the server and hardware (it sits right there!).
-
Easy to set up.
-
Suitable for quick setups - e.g. to briefly show off results to your collaborators.
-
-
Disadvantages
-
-
You need a good internet connection, ideally without any upload/download limits/costs.
-
If you want to run a full game this way, your computer needs to always be on. It could be noisy,
-and as mentioned, the electrical bill must be considered.
-
No support or safety - if your house burns down, so will your game. Also, you are yourself
-responsible for doing regular backups.
-
Potentially not as easy if you don’t know how to open ports in your firewall or router.
-
Home IP numbers are often dynamically allocated, so for permanent online time you need to set up a
-DNS to always re-point to the right place (see below).
-
You are personally responsible for any use/misuse of your internet connection– though unlikely
-(but not impossible) if running your server somehow causes issues for other customers on the
-network, goes against your ISP’s terms of service (many ISPs insist on upselling you to a business-
-tier connection) or you are the subject of legal action by a copyright holder, you may find your
-main internet connection terminated as a consequence.
The first section of this page describes how to do this
-and allow users to connect to the IP address of your machine/router.
-
A complication with using a specific IP address like this is that your home IP might not remain the
-same. Many ISPs (Internet Service Providers) allocates a dynamic IP to you which could change at
-any time. When that happens, that IP you told people to go to will be worthless. Also, that long
-string of numbers is not very pretty, is it? It’s hard to remember and not easy to use in marketing
-your game. What you need is to alias it to a more sensible domain name - an alias that follows you
-around also when the IP changes.
-
-
To set up a domain name alias, we recommend starting with a free domain name from
-FreeDNS. Once you register there (it’s free) you have access to tens
-of thousands domain names that people have “donated” to allow you to use for your own sub domain.
-For example, strangled.net is one of those available domains. So tying our IP address to
-strangled.net using the subdomain evennia would mean that one could henceforth direct people to
-http://evennia.strangled.net:4001 for their gaming needs - far easier to remember!
-
So how do we make this new, nice domain name follow us also if our IP changes? For this we need
-to set up a little program on our computer. It will check whenever our ISP decides to change our IP
-and tell FreeDNS that. There are many alternatives to be found from FreeDNS:s homepage, one that
-works on multiple platforms is inadyn. Get it from their page or,
-in Linux, through something like apt-getinstallinadyn.
-
Next, you login to your account on FreeDNS and go to the
-Dynamic page. You should have a list of your subdomains. Click
-the DirectURL link and you’ll get a page with a text message. Ignore that and look at the URL of
-the page. It should be ending in a lot of random letters. Everything after the question mark is your
-unique “hash”. Copy this string.
-
You now start inadyn with the following command (Linux):
where <my.domain> would be evennia.strangled.net and <hash> the string of numbers we copied
-from FreeDNS. The & means we run in the background (might not be valid in other operating
-systems). inadyn will henceforth check for changes every 60 seconds. You should put the inadyn
-command string in a startup script somewhere so it kicks into gear whenever your computer starts.
Your normal “web hotel” will probably not be enough to run Evennia. A web hotel is normally aimed at
-a very specific usage - delivering web pages, at the most with some dynamic content. The “Python
-scripts” they refer to on their home pages are usually only intended to be CGI-like scripts launched
-by their webserver. Even if they allow you shell access (so you can install the Evennia dependencies
-in the first place), resource usage will likely be very restricted. Running a full-fledged game
-server like Evennia will probably be shunned upon or be outright impossible. If you are unsure,
-contact your web hotel and ask about their policy on you running third-party servers that will want
-to open custom ports.
-
The options you probably need to look for are shell account services, VPS:es or Cloud
-services. A “Shell account” service means that you get a shell account on a server and can log in
-like any normal user. By contrast, a VPS (Virtual Private Server) service usually means that you
-get root access, but in a virtual machine. There are also Cloud-type services which allows for
-starting up multiple virtual machines and pay for what resources you use.
-
Advantages
-
-
Shell accounts/VPS/clouds offer more flexibility than your average web hotel - it’s the ability to
-log onto a shared computer away from home.
-
Usually runs a Linux flavor, making it easy to install Evennia.
-
Support. You don’t need to maintain the server hardware. If your house burns down, at least your
-game stays online. Many services guarantee a certain level of up-time and also do regular backups
-for you. Make sure to check, some offer lower rates in exchange for you yourself being fully
-responsible for your data/backups.
-
Usually offers a fixed domain name, so no need to mess with IP addresses.
-
May have the ability to easily deploy docker versions of evennia
-and/or your game.
-
-
Disadvantages
-
-
Might be pretty expensive (more so than a web hotel). Note that Evennia will normally need at
-least 100MB RAM and likely much more for a large production game.
-
Linux flavors might feel unfamiliar to users not used to ssh/PuTTy and the Linux command line.
-
You are probably sharing the server with many others, so you are not completely in charge. CPU
-usage might be limited. Also, if the server people decides to take the server down for maintenance,
-you have no choice but to sit it out (but you’ll hopefully be warned ahead of time).
Firstly, if you are familiar with server infrastructure, consider using [Docker](Running-Evennia-in-
-Docker) to deploy your game to the remote server; it will likely ease installation and deployment.
-Docker images may be a little confusing if you are completely new to them though.
-
If not using docker, and assuming you know how to connect to your account over ssh/PuTTy, you should
-be able to follow the Setup Quickstart instructions normally. You only need Python
-and GIT pre-installed; these should both be available on any servers (if not you should be able to
-easily ask for them to be installed). On a VPS or Cloud service you can install them yourself as
-needed.
-
If virtualenv is not available and you can’t get it, you can download it (it’s just a single file)
-from the virtualenv pypi. Using virtualenv you can
-install everything without actually needing to have further root access. Ports might be an issue,
-so make sure you know which ports are available to use and reconfigure Evennia accordingly.
To find commercial solutions, browse the web for “shell access”, “VPS” or “Cloud services” in your
-region. You may find useful offers for “low cost” VPS hosting on Low End Box. The associated
-Low End Talk forum can be useful for health checking the many small businesses that offer
-“value” hosting, and occasionally for technical suggestions.
-
There are all sorts of services available. Below are some international suggestions offered by
-Evennia users:
Private hobby provider so don’t assume backups or expect immediate support. To ask for an account,connect with a MUD client to rostdev.mushpark.com, port 4201 and ask for “Jarin”.
You can get a $50 credit if you use the referral link https://m.do.co/c/8f64fec2670c - if you do, once you’ve had it long enough to have paid $25 we will get that as a referral bonus to help Evennia development.
Dedicated MUD host with very limited memory offerings. As for 2017, runs a 13 years old Python version (2.4) so you’d need to either convince them to update or compile yourself. Note that Evennia needs at least the “Deluxe” package (50MB RAM) and probably a lot higher for a production game. This host is not recommended for Evennia.
If you are interested in running Evennia in the online dev environment Cloud9, you
-can spin it up through their normal online setup using the Evennia Linux install instructions. The
-one extra thing you will have to do is update mygame/server/conf/settings.py and add
-WEBSERVER_PORTS=[(8080,4001)]. This will then let you access the web server and do everything
-else as normal.
-
Note that, as of December 2017, Cloud9 was re-released by Amazon as a service within their AWS cloud
-service offering. New customers entitled to the 1 year AWS “free tier” may find it provides
-sufficient resources to operate a Cloud9 development environment without charge.
-https://aws.amazon.com/cloud9/
Parsing command arguments, theory and best practices¶
-
This tutorial will elaborate on the many ways one can parse command arguments. The first step after
-adding a command usually is to parse its arguments. There are lots of
-ways to do it, but some are indeed better than others and this tutorial will try to present them.
-
If you’re a Python beginner, this tutorial might help you a lot. If you’re already familiar with
-Python syntax, this tutorial might still contain useful information. There are still a lot of
-things I find in the standard library that come as a surprise, though they were there all along.
-This might be true for others.
I’m going to talk about command arguments and parsing a lot in this tutorial. So let’s be sure we
-talk about the same thing before going any further:
-
-
A command is an Evennia object that handles specific user input.
-
-
For instance, the default look is a command. After having created your Evennia game, and
-connected to it, you should be able to type look to see what’s around. In this context, look is
-a command.
-
-
Command arguments are additional text passed after the command.
-
-
Following the same example, you can type lookself to look at yourself. In this context, self
-is the text specified after look. "self" is the argument to the look command.
-
Part of our task as a game developer is to connect user inputs (mostly commands) with actions in the
-game. And most of the time, entering commands is not enough, we have to rely on arguments for
-specifying actions with more accuracy.
-
Take the say command. If you couldn’t specify what to say as a command argument (sayhello!),
-you would have trouble communicating with others in the game. One would need to create a different
-command for every kind of word or sentence, which is, of course, not practical.
-
Last thing: what is parsing?
-
-
In our case, parsing is the process by which we convert command arguments into something we can
-work with.
-
-
We don’t usually use the command argument as is (which is just text, of type str in Python). We
-need to extract useful information. We might want to ask the user for a number, or the name of
-another character present in the same room. We’re going to see how to do all that now.
In object terms, when you write a command in Evennia (when you write the Python class), the
-arguments are stored in the args attribute. Which is to say, inside your func method, you can
-access the command arguments in self.args.
The lines starting with > indicate what you enter into your client. The other lines are what
-you receive from the game server.
-
-
Notice two things here:
-
-
The left space between our command key (“test”, here) and our command argument is not removed.
-That’s why there are two spaces in our output at line 2. Try entering something like “testok”.
-
Even if you don’t enter command arguments, the command will still be called with an empty string
-in self.args.
-
-
Perhaps a slight modification to our code would be appropriate to see what’s happening. We will
-force Python to display the command arguments as a debug string using a little shortcut.
-
classCmdTest(Command):
-
- """
- Test command.
-
- Syntax:
- test [argument]
-
- Enter any argument after test.
-
- """
-
- key="test"
-
- deffunc(self):
- self.msg(f"You have entered: {self.args!r}.")
-
-
-
The only line we have changed is the last one, and we have added !r between our braces to tell
-Python to print the debug version of the argument (the repr-ed version). Let’s see the result:
-
>testWhatever
-Youhaveentered:' Whatever'.
->test
-Youhaveentered:''.
->testAndsomethingwith'?
-Youhaveentered:" And something with '?".
-
-
-
This displays the string in a way you could see in the Python interpreter. It might be easier to
-read… to debug, anyway.
-
I insist so much on that point because it’s crucial: the command argument is just a string (of type
-str) and we will use this to parse it. What you will see is mostly not Evennia-specific, it’s
-Python-specific and could be used in any other project where you have the same need.
As you’ve seen, our command arguments are stored with the space. And the space between the command
-and the arguments is often of no importance.
-
-
Why is it ever there?
-
-
Evennia will try its best to find a matching command. If the user enters your command key with
-arguments (but omits the space), Evennia will still be able to find and call the command. You might
-have seen what happened if the user entered testok. In this case, testok could very well be a
-command (Evennia checks for that) but seeing none, and because there’s a test command, Evennia
-calls it with the arguments "ok".
-
But most of the time, we don’t really care about this left space, so you will often see code to
-remove it. There are different ways to do it in Python, but a command use case is the strip
-method on str and its cousins, lstrip and rstrip.
-
-
strip: removes one or more characters (either spaces or other characters) from both ends of the
-string.
-
lstrip: same thing but only removes from the left end (left strip) of the string.
-
rstrip: same thing but only removes from the right end (right strip) of the string.
-
-
Some Python examples might help:
-
>>> ' this is '.strip()# remove spaces by default
-'this is'
->>> " What if I'm right? ".lstrip()# strip spaces from the left
-"What if I'm right? "
->>> 'Looks good to me...'.strip('.')# removes '.'
-'Looks good to me'
->>> '"Now, what is it?"'.strip('"?')# removes '"' and '?' from both ends
-'Now, what is it'
-
-
-
Usually, since we don’t need the space separator, but still want our command to work if there’s no
-separator, we call lstrip on the command arguments:
-
classCmdTest(Command):
-
- """
- Test command.
-
- Syntax:
- test [argument]
-
- Enter any argument after test.
-
- """
-
- key="test"
-
- defparse(self):
- """Parse arguments, just strip them."""
- self.args=self.args.lstrip()
-
- deffunc(self):
- self.msg(f"You have entered: {self.args!r}.")
-
-
-
-
We are now beginning to override the command’s parse method, which is typically useful just for
-argument parsing. This method is executed before func and so self.args in func() will contain
-our self.args.lstrip().
-
-
Let’s try it:
-
>testWhatever
-Youhaveentered:'Whatever'.
->test
-Youhaveentered:''.
->testAndsomethingwith'?
-Youhaveentered:"And something with '?".
->testAndsomethingwithlotsofspaces
-Youhaveentered:'And something with lots of spaces'.
-
-
-
Spaces at the end of the string are kept, but all spaces at the beginning are removed:
-
-
strip, lstrip and rstrip without arguments will strip spaces, line breaks and other common
-separators. You can specify one or more characters as a parameter. If you specify more than one
-character, all of them will be stripped from your original string.
As pointed out, self.args is a string (of type str). What if we want the user to enter a
-number?
-
Let’s take a very simple example: creating a command, roll, that allows to roll a six-sided die.
-The player has to guess the number, specifying the number as argument. To win, the player has to
-match the number with the die. Let’s see an example:
-
> roll 3
-You roll a die. It lands on the number 4.
-You played 3, you have lost.
-> dice 1
-You roll a die. It lands on the number 2.
-You played 1, you have lost.
-> dice 1
-You roll a die. It lands on the number 1.
-You played 1, you have won!
-
-
-
If that’s your first command, it’s a good opportunity to try to write it. A command with a simple
-and finite role always is a good starting choice. Here’s how we could (first) write it… but it
-won’t work as is, I warn you:
-
fromrandomimportrandint
-
-fromevenniaimportCommand
-
-classCmdRoll(Command):
-
- """
- Play random, enter a number and try your luck.
-
- Usage:
- roll <number>
-
- Enter a valid number as argument. A random die will be rolled and you
- will win if you have specified the correct number.
-
- Example:
- roll 3
-
- """
-
- key="roll"
-
- defparse(self):
- """Convert the argument to a number."""
- self.args=self.args.lstrip()
-
- deffunc(self):
- # Roll a random die
- figure=randint(1,6)# return a pseudo-random number between 1 and 6, including both
- self.msg(f"You roll a die. It lands on the number {figure}.")
-
- ifself.args==figure:# THAT WILL BREAK!
- self.msg(f"You played {self.args}, you have won!")
- else:
- self.msg(f"You played {self.args}, you have lost.")
-
-
-
If you try this code, Python will complain that you try to compare a number with a string: figure
-is a number and self.args is a string and can’t be compared as-is in Python. Python doesn’t do
-“implicit converting” as some languages do. By the way, this might be annoying sometimes, and other
-times you will be glad it tries to encourage you to be explicit rather than implicit about what to
-do. This is an ongoing debate between programmers. Let’s move on!
-
So we need to convert the command argument from a str into an int. There are a few ways to do
-it. But the proper way is to try to convert and deal with the ValueError Python exception.
-
Converting a str into an int in Python is extremely simple: just use the int function, give it
-the string and it returns an integer, if it could. If it can’t, it will raise ValueError. So
-we’ll need to catch that. However, we also have to indicate to Evennia that, should the number be
-invalid, no further parsing should be done. Here’s a new attempt at our command with this
-converting:
-
fromrandomimportrandint
-
-fromevenniaimportCommand,InterruptCommand
-
-classCmdRoll(Command):
-
- """
- Play random, enter a number and try your luck.
-
- Usage:
- roll <number>
-
- Enter a valid number as argument. A random die will be rolled and you
- will win if you have specified the correct number.
-
- Example:
- roll 3
-
- """
-
- key="roll"
-
- defparse(self):
- """Convert the argument to number if possible."""
- args=self.args.lstrip()
-
- # Convert to int if possible
- # If not, raise InterruptCommand. Evennia will catch this
- # exception and not call the 'func' method.
- try:
- self.entered=int(args)
- exceptValueError:
- self.msg(f"{args} is not a valid number.")
- raiseInterruptCommand
-
- deffunc(self):
- # Roll a random die
- figure=randint(1,6)# return a pseudo-random number between 1 and 6, including both
- self.msg(f"You roll a die. It lands on the number {figure}.")
-
- ifself.entered==figure:
- self.msg(f"You played {self.entered}, you have won!")
- else:
- self.msg(f"You played {self.entered}, you have lost.")
-
-
-
Before enjoying the result, let’s examine the parse method a little more: what it does is try to
-convert the entered argument from a str to an int. This might fail (if a user enters rollsomething). In such a case, Python raises a ValueError exception. We catch it in our
-try/except block, send a message to the user and raise the InterruptCommand exception in
-response to tell Evennia to not run func(), since we have no valid number to give it.
-
In the func method, instead of using self.args, we use self.entered which we have defined in
-our parse method. You can expect that, if func() is run, then self.entered contains a valid
-number.
-
If you try this command, it will work as expected this time: the number is converted as it should
-and compared to the die roll. You might spend some minutes playing this game. Time out!
-
Something else we could want to address: in our small example, we only want the user to enter a
-positive number between 1 and 6. And the user can enter roll0 or roll-8 or roll208 for
-that matter, the game still works. It might be worth addressing. Again, you could write a
-condition to do that, but since we’re catching an exception, we might end up with something cleaner
-by grouping:
-
fromrandomimportrandint
-
-fromevenniaimportCommand,InterruptCommand
-
-classCmdRoll(Command):
-
- """
- Play random, enter a number and try your luck.
-
- Usage:
- roll <number>
-
- Enter a valid number as argument. A random die will be rolled and you
- will win if you have specified the correct number.
-
- Example:
- roll 3
-
- """
-
- key="roll"
-
- defparse(self):
- """Convert the argument to number if possible."""
- args=self.args.lstrip()
-
- # Convert to int if possible
- try:
- self.entered=int(args)
- ifnot1<=self.entered<=6:
- # self.entered is not between 1 and 6 (including both)
- raiseValueError
- exceptValueError:
- self.msg(f"{args} is not a valid number.")
- raiseInterruptCommand
-
- deffunc(self):
- # Roll a random die
- figure=randint(1,6)# return a pseudo-random number between 1 and 6, including both
- self.msg(f"You roll a die. It lands on the number {figure}.")
-
- ifself.entered==figure:
- self.msg(f"You played {self.entered}, you have won!")
- else:
- self.msg(f"You played {self.entered}, you have lost.")
-
-
-
Using grouped exceptions like that makes our code easier to read, but if you feel more comfortable
-checking, afterward, that the number the user entered is in the right range, you can do so in a
-latter condition.
-
-
Notice that we have updated our parse method only in this last attempt, not our func() method
-which remains the same. This is one goal of separating argument parsing from command processing,
-these two actions are best kept isolated.
Often a command expects several arguments. So far, in our example with the “roll” command, we only
-expect one argument: a number and just a number. What if we want the user to specify several
-numbers? First the number of dice to roll, then the guess?
-
-
You won’t win often if you roll 5 dice but that’s for the example.
-
-
So we would like to interpret a command like this:
-
> roll 3 12
-
-
-
(To be understood: roll 3 dice, my guess is the total number will be 12.)
-
What we need is to cut our command argument, which is a str, break it at the space (we use the
-space as a delimiter). Python provides the str.split method which we’ll use. Again, here are
-some examples from the Python interpreter:
-
>>> args = "3 12"
->>> args.split(" ")
-['3', '12']
->>> args = "a command with several arguments"
->>> args.split(" ")
-['a', 'command', 'with', 'several', 'arguments']
->>>
-
-
-
As you can see, str.split will “convert” our strings into a list of strings. The specified
-argument ("" in our case) is used as delimiter. So Python browses our original string. When it
-sees a delimiter, it takes whatever is before this delimiter and append it to a list.
-
The point here is that str.split will be used to split our argument. But, as you can see from the
-above output, we can never be sure of the length of the list at this point:
Again we could use a condition to check the number of split arguments, but Python offers a better
-approach, making use of its exception mechanism. We’ll give a second argument to str.split, the
-maximum number of splits to do. Let’s see an example, this feature might be confusing at first
-glance:
-
>>> args = "that is something great"
->>> args.split(" ", 1) # one split, that is a list with two elements (before, after)
-
-
-
[‘that’, ‘is something great’]
-
-
-
-
-
Read this example as many times as needed to understand it. The second argument we give to
-str.split is not the length of the list that should be returned, but the number of times we have
-to split. Therefore, we specify 1 here, but we get a list of two elements (before the separator,
-after the separator).
-
-
What will happen if Python can’t split the number of times we ask?
-
-
It won’t:
-
>>> args = "whatever"
->>> args.split(" ", 1) # there isn't even a space here...
-['whatever']
->>>
-
-
-
This is one moment I would have hoped for an exception and didn’t get one. But there’s another way
-which will raise an exception if there is an error: variable unpacking.
-
We won’t talk about this feature in details here. It would be complicated. But the code is really
-straightforward to use. Let’s take our example of the roll command but let’s add a first argument:
-the number of dice to roll.
-
fromrandomimportrandint
-
-fromevenniaimportCommand,InterruptCommand
-
-classCmdRoll(Command):
-
- """
- Play random, enter a number and try your luck.
-
- Specify two numbers separated by a space. The first number is the
- number of dice to roll (1, 2, 3) and the second is the expected sum
- of the roll.
-
- Usage:
- roll <dice> <number>
-
- For instance, to roll two 6-figure dice, enter 2 as first argument.
- If you think the sum of these two dice roll will be 10, you could enter:
-
- roll 2 10
-
- """
-
- key="roll"
-
- defparse(self):
- """Split the arguments and convert them."""
- args=self.args.lstrip()
-
- # Split: we expect two arguments separated by a space
- try:
- number,guess=args.split(" ",1)
- exceptValueError:
- self.msg("Invalid usage. Enter two numbers separated by a space.")
- raiseInterruptCommand
-
- # Convert the entered number (first argument)
- try:
- self.number=int(number)
- ifself.number<=0:
- raiseValueError
- exceptValueError:
- self.msg(f"{number} is not a valid number of dice.")
- raiseInterruptCommand
-
- # Convert the entered guess (second argument)
- try:
- self.guess=int(guess)
- ifnot1<=self.guess<=self.number*6:
- raiseValueError
- exceptValueError:
- self.msg(f"{self.guess} is not a valid guess.")
- raiseInterruptCommand
-
- deffunc(self):
- # Roll a random die X times (X being self.number)
- figure=0
- for_inrange(self.number):
- figure+=randint(1,6)
-
- self.msg(f"You roll {self.number} dice and obtain the sum {figure}.")
-
- ifself.guess==figure:
- self.msg(f"You played {self.guess}, you have won!")
- else:
- self.msg(f"You played {self.guess}, you have lost.")
-
-
-
The beginning of the parse() method is what interests us most:
-
try:
- number,guess=args.split(" ",1)
-exceptValueError:
- self.msg("Invalid usage. Enter two numbers separated by a space.")
- raiseInterruptCommand
-
-
-
We split the argument using str.split but we capture the result in two variables. Python is smart
-enough to know that we want what’s left of the space in the first variable, what’s right of the
-space in the second variable. If there is not even a space in the string, Python will raise a
-ValueError exception.
-
This code is much easier to read than browsing through the returned strings of str.split. We can
-convert both variables the way we did previously. Actually there are not so many changes in this
-version and the previous one, most of it is due to name changes for clarity.
-
-
Splitting a string with a maximum of splits is a common occurrence while parsing command
-arguments. You can also see the str.rspli8t method that does the same thing but from the right of
-the string. Therefore, it will attempt to find delimiters at the end of the string and work toward
-the beginning of it.
-
-
We have used a space as a delimiter. This is absolutely not necessary. You might remember that
-most default Evennia commands can take an = sign as a delimiter. Now you know how to parse them
-as well:
-
>>> cmd_key = "tel"
->>> cmd_args = "book = chest"
->>> left, right = cmd_args.split("=") # mighht raise ValueError!
->>> left
-'book '
->>> right
-' chest'
->>>
-
Sometimes, you’ll come across commands that have optional arguments. These arguments are not
-necessary but they can be set if more information is needed. I will not provide the entire command
-code here but just enough code to show the mechanism in Python:
-
Again, we’ll use str.split, knowing that we might not have any delimiter at all. For instance,
-the player could enter the “tel” command like this:
-
> tel book
-> tell book = chest
-
-
-
The equal sign is optional along with whatever is specified after it. A possible solution in our
-parse method would be:
This code would place everything the user entered in obj if she didn’t specify any equal sign.
-Otherwise, what’s before the equal sign will go in obj, what’s after the equal sign will go in
-destination. This makes for quick testing after that, more robust code with less conditions that
-might too easily break your code if you’re not careful.
-
-
Again, here we specified a maximum numbers of splits. If the users enters:
-
-
> tel book = chest = chair
-
-
-
Then destination will contain: "chest=chair". This is often desired, but it’s up to you to
-set parsing however you like.
After this quick tour of some str methods, we’ll take a look at some Evennia-specific features
-that you won’t find in standard Python.
-
One very common task is to convert a str into an Evennia object. Take the previous example:
-having "book" in a variable is great, but we would prefer to know what the user is talking
-about… what is this "book"?
-
To get an object from a string, we perform an Evennia search. Evennia provides a search method on
-all typeclassed objects (you will most likely use the one on characters or accounts). This method
-supports a very wide array of arguments and has its own tutorial.
-Some examples of useful cases follow:
When an account or a character enters a command, the account or character is found in the caller
-attribute. Therefore, self.caller will contain an account or a character (or a session if that’s
-a session command, though that’s not as frequent). The search method will be available on this
-caller.
-
Let’s take the same example of our little “tel” command. The user can specify an object as
-argument:
-
defparse(self):
- name=self.args.lstrip()
-
-
-
We then need to “convert” this string into an Evennia object. The Evennia object will be searched
-in the caller’s location and its contents by default (that is to say, if the command has been
-entered by a character, it will search the object in the character’s room and the character’s
-inventory).
We specify only one argument to the search method here: the string to search. If Evennia finds a
-match, it will return it and we keep it in the obj attribute. If it can’t find anything, it will
-return None so we need to check for that:
-
defparse(self):
- name=self.args.lstrip()
-
- self.obj=self.caller.search(name)
- ifself.objisNone:
- # A proper error message has already been sent to the caller
- raiseInterruptCommand
-
-
-
That’s it. After this condition, you know that whatever is in self.obj is a valid Evennia object
-(another character, an object, an exit…).
By default, Evennia will handle the case when more than one match is found in the search. The user
-will be asked to narrow down and re-enter the command. You can, however, ask to be returned the
-list of matches and handle this list yourself:
-
defparse(self):
- name=self.args.lstrip()
-
- objs=self.caller.search(name,quiet=True)
- ifnotobjs:
- # This is an empty list, so no match
- self.msg(f"No {name!r} was found.")
- raiseInterruptCommand
-
- self.obj=objs[0]# Take the first match even if there are several
-
-
-
All we have changed to obtain a list is a keyword argument in the search method: quiet. If set
-to True, then errors are ignored and a list is always returned, so we need to handle it as such.
-Notice in this example, self.obj will contain a valid object too, but if several matches are
-found, self.obj will contain the first one, even if more matches are available.
By default, Evennia will perform a local search, that is, a search limited by the location in which
-the caller is. If you want to perform a global search (search in the entire database), just set the
-global_search keyword argument to True:
Parsing command arguments is vital for most game designers. If you design “intelligent” commands,
-users should be able to guess how to use them without reading the help, or with a very quick peek at
-said help. Good commands are intuitive to users. Better commands do what they’re told to do. For
-game designers working on MUDs, commands are the main entry point for users into your game. This is
-no trivial. If commands execute correctly (if their argument is parsed, if they don’t behave in
-unexpected ways and report back the right errors), you will have happier players that might stay
-longer on your game. I hope this tutorial gave you some pointers on ways to improve your command
-parsing. There are, of course, other ways you will discover, or ways you are already using in your
-code.
Evennia consists of two processes, known as Portal and Server. They can be controlled from
-inside the game or from the command line as described here.
-
If you are new to the concept, the main purpose of separating the two is to have accounts connect to
-the Portal but keep the MUD running on the Server. This way one can restart/reload the game (the
-Server part) without Accounts getting disconnected.
-
-
The Server and Portal are glued together via an AMP (Asynchronous Messaging Protocol) connection.
-This allows the two programs to communicate seamlessly.
Sometimes it can be useful to try to determine just how efficient a particular piece of code is, or
-to figure out if one could speed up things more than they are. There are many ways to test the
-performance of Python and the running server.
-
Before digging into this section, remember Donald Knuth’s words of
-wisdom:
-
-
[…]about 97% of the time: Premature optimization is the root of all evil.
-
-
That is, don’t start to try to optimize your code until you have actually identified a need to do
-so. This means your code must actually be working before you start to consider optimization.
-Optimization will also often make your code more complex and harder to read. Consider readability
-and maintainability and you may find that a small gain in speed is just not worth it.
Python’s timeit module is very good for testing small things. For example, in order to test if it
-is faster to use a for loop or a list comprehension you could use the following code:
-
importtimeit
- # Time to do 1000000 for loops
- timeit.timeit("for i in range(100):\n a.append(i)",setup="a = []")
- <<<10.70982813835144
- # Time to do 1000000 list comprehensions
- timeit.timeit("a = [i for i in range(100)]")
- <<<5.358283996582031
-
-
-
The setup keyword is used to set up things that should not be included in the time measurement,
-like a=[] in the first call.
-
By default the timeit function will re-run the given test 1000000 times and returns the total
-time to do so (so not the average per test). A hint is to not use this default for testing
-something that includes database writes - for that you may want to use a lower number of repeats
-(say 100 or 1000) using the number=100 keyword.
Python comes with its own profiler, named cProfile (this is for cPython, no tests have been done
-with pypy at this point). Due to the way Evennia’s processes are handled, there is no point in
-using the normal way to start the profiler (python-mcProfileevennia.py). Instead you start the
-profiler through the launcher:
-
evennia --profiler start
-
-
-
This will start Evennia with the Server component running (in daemon mode) under cProfile. You could
-instead try --profile with the portal argument to profile the Portal (you would then need to
-start the Server separately).
-
Please note that while the profiler is running, your process will use a lot more memory than usual.
-Memory usage is even likely to climb over time. So don’t leave it running perpetually but monitor it
-carefully (for example using the top command on Linux or the Task Manager’s memory display on
-Windows).
-
Once you have run the server for a while, you need to stop it so the profiler can give its report.
-Do not kill the program from your task manager or by sending it a kill signal - this will most
-likely also mess with the profiler. Instead either use evennia.pystop or (which may be even
-better), use @shutdown from inside the game.
-
Once the server has fully shut down (this may be a lot slower than usual) you will find that
-profiler has created a new file mygame/server/logs/server.prof.
The server.prof file is a binary file. There are many ways to analyze and display its contents,
-all of which has only been tested in Linux (If you are a Windows/Mac user, let us know what works).
-
We recommend the
-Runsnake visualizer to see the processor usage
-of different processes in a graphical form. For more detailed listing of usage time, you can use
-KCachegrind. To make KCachegrind work with
-Python profiles you also need the wrapper script
-pyprof2calltree. You can get pyprof2calltree via
-pip whereas KCacheGrind is something you need to get via your package manager or their homepage.
-
How to analyze and interpret profiling data is not a trivial issue and depends on what you are
-profiling for. Evennia being an asynchronous server can also confuse profiling. Ask on the mailing
-list if you need help and be ready to be able to supply your server.prof file for comparison,
-along with the exact conditions under which it was obtained.
It is difficult to test “actual” game performance without having players in your game. For this
-reason Evennia comes with the Dummyrunner system. The Dummyrunner is a stress-testing system: a
-separate program that logs into your game with simulated players (aka “bots” or “dummies”). Once
-connected these dummies will semi-randomly perform various tasks from a list of possible actions.
-Use Ctrl-C to stop the Dummyrunner.
-
-
Warning: You should not run the Dummyrunner on a production database. It will spawn many objects
-and also needs to run with general permissions.
-
-
To launch the Dummyrunner, first start your server normally (with or without profiling, as above).
-Then start a new terminal/console window and active your virtualenv there too. In the new terminal,
-try to connect 10 dummy players:
-
evennia --dummyrunner 10
-
-
-
The first time you do this you will most likely get a warning from Dummyrunner. It will tell you to
-copy an import string to the end of your settings file. Quit the Dummyrunner (Ctrl-C) and follow
-the instructions. Restart Evennia and try evennia--dummyrunner10 again. Make sure to remove that
-extra settings line when running a public server.
-
The actions perform by the dummies is controlled by a settings file. The default Dummyrunner
-settings file is evennia/server/server/profiling/dummyrunner_settings.py but you shouldn’t modify
-this directly. Rather create/copy the default file to mygame/server/conf/ and modify it there. To
-make sure to use your file over the default, add the following line to your settings file:
Hint: Don’t start with too many dummies. The Dummyrunner defaults to taxing the server much more
-intensely than an equal number of human players. A good dummy number to start with is 10-100.
-
-
Once you have the dummyrunner running, stop it with Ctrl-C.
-
Generally, the dummyrunner system makes for a decent test of general performance; but it is of
-course hard to actually mimic human user behavior. For this, actual real-game testing is required.
This is the first part of our beginner’s guide to the basics of using Python with Evennia. It’s
-aimed at you with limited or no programming/Python experience. But also if you are an experienced
-programmer new to Evennia or Python you might still pick up a thing or two. It is by necessity brief
-and low on detail. There are countless Python guides and tutorials, books and videos out there for
-learning more in-depth - use them!
This quickstart assumes you have gotten Evennia started. You should make sure
-that you are able to see the output from the server in the console from which you started it. Log
-into the game either with a mud client on localhost:4000 or by pointing a web browser to
-localhost:4001/webclient. Log in as your superuser (the user you created during install).
-
Below, lines starting with a single > means command input.
The py (or ! which is an alias) command allows you as a superuser to run raw Python from in-
-game. From the game’s input line, enter the following:
-
> py print("Hello World!")
-
-
-
You will see
-
> print("Hello world!")
-Hello World
-
-
-
To understand what is going on: some extra info: The print(...)function is the basic, in-built
-way to output text in Python. The quotes "..." means you are inputing a string (i.e. text). You
-could also have used single-quotes '...', Python accepts both.
-
The first return line (with >>>) is just py echoing what you input (we won’t include that in the
-examples henceforth).
-
-
Note: You may sometimes see people/docs refer to @py or other commands starting with @.
-Evennia ignores @ by default, so @py is the exact same thing as py.
-
-
The print command is a standard Python structure. We can use that here in the py command, and
-it’s great for debugging and quick testing. But if you need to send a text to an actual player,
-print won’t do, because it doesn’t know who to send to. Try this:
-
> py me.msg("Hello world!")
-Hello world!
-
-
-
This looks the same as the print result, but we are now actually messaging a specific object,
-me. The me is something uniquely available in the py command (we could also use self, it’s
-an alias). It represents “us”, the ones calling the py command. The me is an example of an
-Object instance. Objects are fundamental in Python and Evennia. The me object not only
-represents the character we play in the game, it also contains a lot of useful resources for doing
-things with that Object. One such resource is msg. msg works like print except it sends the
-text to the object it is attached to. So if we, for example, had an object you, doing
-you.msg(...) would send a message to the object you.
-
You access an Object’s resources by using the full-stop character .. So self.msg accesses the
-msg resource and then we call it like we did print, with our “Hello World!” greeting in
-parentheses.
-
-
Important: something like print(...) we refer to as a function, while msg(...) which sits on
-an object is called a method.
-
-
For now, print and me.msg behaves the same, just remember that you’re going to mostly be using
-the latter in the future. Try printing other things. Also try to include |r at the start of your
-string to make the output red in-game. Use color to learn more color tags.
Keep your game running, then open a text editor of your choice. If your game folder is called
-mygame, create a new text file test.py in the subfolder mygame/world. This is how the file
-structure should look:
-
mygame/
- world/
- test.py
-
-
-
For now, only add one line to test.py:
-
print("Hello World!")
-
-
-
Don’t forget to save the file. A file with the ending .py is referred to as a Python module. To
-use this in-game we have to import it. Try this:
-
>@pyimportworld.test
-HelloWorld
-
-
-
If you make some error (we’ll cover how to handle errors below) you may need to run the @reload
-command for your changes to take effect.
-
So importing world.test actually means importing world/test.py. Think of the period . as
-replacing / (or \ for Windows) in your path. The .py ending of test.py is also never
-included in this “Python-path”, but only files with that ending can be imported this way. Where is
-mygame in that Python-path? The answer is that Evennia has already told Python that your mygame
-folder is a good place to look for imports. So we don’t include mygame in the path - Evennia
-handles this for us.
-
When you import the module, the top “level” of it will execute. In this case, it will immediately
-print “Hello World”.
-
-
If you look in the folder you’ll also often find new files ending with .pyc. These are compiled
-Python binaries that Python auto-creates when running code. Just ignore them, you should never edit
-those anyway.
-
-
Now try to run this a second time:
-
>pyimportworld.test
-
-
-
You will not see any output this second time or any subsequent times! This is not a bug. Rather
-it is because Python is being clever - it stores all imported modules and to be efficient it will
-avoid importing them more than once. So your print will only run the first time, when the module
-is first imported. To see it again you need to @reload first, so Python forgets about the module
-and has to import it again.
-
We’ll get back to importing code in the second part of this tutorial. For now, let’s press on.
Next, erase the single print statement you had in test.py and replace it with this instead:
-
me.msg("Hello World!")
-
-
-
As you recall we used this from py earlier - it echoed “Hello World!” in-game.
-Save your file and reload your server - this makes sure Evennia sees the new version of your code.
-Try to import it from py in the same way as earlier:
This is called a traceback. Python’s errors are very friendly and will most of the time tell you
-exactly what and where things are wrong. It’s important that you learn to parse tracebacks so you
-can fix your code. Let’s look at this one. A traceback is to be read from the bottom up. The last
-line is the error Python balked at, while the two lines above it details exactly where that error
-was encountered.
-
-
An error of type NameError is the problem …
-
… more specifically it is due to the variable me not being defined.
-
This happened on the line me.msg("Helloworld!") …
-
… which is on line 1 of the file ./world/test.py.
-
-
In our case the traceback is short. There may be many more lines above it, tracking just how
-different modules called each other until it got to the faulty line. That can sometimes be useful
-information, but reading from the bottom is always a good start.
-
The NameError we see here is due to a module being its own isolated thing. It knows nothing about
-the environment into which it is imported. It knew what print is because that is a special
-reserved Python keyword. But me is not such a
-reserved word. As far as the module is concerned me is just there out of nowhere. Hence the
-NameError.
Let’s see if we can resolve that NameError from the previous section. We know that me is defined
-at the time we use the @py command because if we do pyme.msg("HelloWorld!") directly in-game
-it works fine. What if we could send that me to the test.py module so it knows what it is? One
-way to do this is with a function.
-
Change your mygame/world/test.py file to look like this:
-
defhello_world(who):
- who.msg("Hello World!")
-
-
-
Now that we are moving onto multi-line Python code, there are some important things to remember:
-
-
Capitalization matters in Python. It must be def and not DEF, who is not the same as Who
-etc.
-
Indentation matters in Python. The second line must be indented or it’s not valid code. You should
-also use a consistent indentation length. We strongly recommend that you set up your editor to
-always indent 4 spaces (not a single tab-character) when you press the TAB key - it will make
-your life a lot easier.
-
def is short for “define” and defines a function (or a method, if sitting on an object).
-This is a reserved Python keyword; try not to use
-these words anywhere else.
who is what we call the argument to our function. Arguments are variables we pass to the
-function. We could have named it anything and we could also have multiple arguments separated by
-commas. What who is depends on what we pass to this function when we call it later (hint: we’ll
-pass me to it).
-
The colon (:) at the end of the first line indicates that the header of the function is
-complete.
-
The indentation marks the beginning of the actual operating code of the function (the function’s
-body). If we wanted more lines to belong to this function those lines would all have to have to
-start at this indentation level.
-
In the function body we take the who argument and treat it as we would have treated me earlier
-
we expect it to have a .msg method we can use to send “Hello World” to.
-
-
First, reload your game to make it aware of the updated Python module. Now we have defined our
-first function, let’s use it.
-
> reload
-> py import world.test
-
-
-
Nothing happened! That is because the function in our module won’t do anything just by importing it.
-It will only act when we call it. We will need to enter the module we just imported and do so.
There is our “Hello World”! The ; is the way to put multiple Python-statements on one line.
-
-
Some MUD clients use ; for their own purposes to separate client-inputs. If so you’ll get a
-NameError stating that world is not defined. Check so you understand why this is! Change the use
-of ; in your client or use the Evennia web client if this is a problem.
-
-
In the second statement we access the module path we imported (world.test) and reach for the
-hello_world function within. We call the function with me, which becomes the who variable we
-use inside the hello_function.
-
-
As an exercise, try to pass something else into hello_world. Try for example to pass who as
-the number 5 or the simple string "foo". You’ll get errors that they don’t have the attribute
-msg. As we’ve seen, medoes make msg available which is why it works (you’ll learn more
-about Objects like me in the next part of this tutorial). If you are familiar with other
-programming languages you may be tempted to start validatingwho to make sure it works as
-expected. This is usually not recommended in Python which suggests it’s better to
-handle the error if it happens rather than to make
-a lot of code to prevent it from happening. See also duck
-typing.
As you start to explore Evennia, it’s important that you know where to look when things go wrong.
-While using the friendly py command you’ll see errors directly in-game. But if something goes
-wrong in your code while the game runs, you must know where to find the log.
-
Open a terminal (or go back to the terminal you started Evennia in), make sure your virtualenv is
-active and that you are standing in your game directory (the one created with evennia--init
-during installation). Enter
-
evennia--log
-
-
-
(or evennia-l)
-
This will show the log. New entries will show up in real time. Whenever you want to leave the log,
-enter Ctrl-C or Cmd-C depending on your system. As a game dev it is important to look at the
-log output when working in Evennia - many errors will only appear with full details here. You may
-sometimes have to scroll up in the history if you miss it.
-
This tutorial is continued in Part 2, where we’ll start learning
-about objects and to explore the Evennia library.
In the first part of this Python-for-Evennia basic tutorial we learned
-how to run some simple Python code from inside the game. We also made our first new module
-containing a function that we called. Now we’re going to start exploring the very important
-subject of objects.
In the first part of the tutorial we did things like
-
> py me.msg("Hello World!")
-
-
-
To learn about functions and imports we also passed that me on to a function hello_world in
-another module.
-
Let’s learn some more about this me thing we are passing around all over the place. In the
-following we assume that we named our superuser Character “Christine”.
-
> py me
-Christine
-> py me.key
-Christine
-
-
-
These returns look the same at first glance, but not if we examine them more closely:
Note: In some MU clients, such as Mudlet and MUSHclient simply returning type(me), you may not
-see the proper return from the above commands. This is likely due to the HTML-like tags <...>,
-being swallowed by the client.
-
-
The type function is, like print, another in-built function in Python. It
-tells us that we (me) are of the classtypeclasses.characters.Character.
-Meanwhile me.key is a property on us, a string. It holds the name of this
-object.
-
-
When you do pyme, the me is defined in such a way that it will use its .key property to
-represent itself. That is why the result is the same as when doing pyme.key. Also, remember that
-as noted in the first part of the tutorial, the me is not a reserved Python word; it was just
-defined by the Evennia developers as a convenient short-hand when creating the py command. So
-don’t expect me to be available elsewhere.
-
-
A class is like a “factory” or blueprint. From a class you then create individual instances. So
-if class isDog, an instance of Dog might be fido. Our in-game persona is of a class
-Character. The superuser christine is an instance of the Character class (an instance is
-also often referred to as an object). This is an important concept in object oriented
-programming. You are wise to familiarize yourself with it a little.
-
-
In other terms:
-
-
class: A description of a thing, all the methods (code) and data (information)
-
object: A thing, defined as an instance of a class.
-
-
So in “Fido is a Dog”, “Fido” is an object–a unique thing–and “Dog” is a class. Coders would
-also say, “Fido is an instance of Dog”. There can be other dogs too, such as Butch and Fifi. They,
-too, would be instances of Dog.
-
As another example: “Christine is a Character”, or “Christine is an instance of
-typeclasses.characters.Character”. To start, all characters will be instances of
-typeclass.characters.Character.
-
You’ll be writing your own class soon! The important thing to know here is how classes and objects
-relate.
-
-
The string 'typeclasses.characters.Character' we got from the type() function is not arbitrary.
-You’ll recognize this from when we importedworld.test in part one. This is a path exactly
-describing where to find the python code describing this class. Python treats source code files on
-your hard drive (known as modules) as well as folders (known as packages) as objects that you
-access with the . operator. It starts looking at a place that Evennia has set up for you - namely
-the root of your own game directory.
-
Open and look at your game folder (named mygame if you exactly followed the Getting Started
-instructions) in a file editor or in a new terminal/console. Locate the file
-mygame/typeclasses/characters.py
-
mygame/
- typeclasses
- characters.py
-
-
-
This represents the first part of the python path - typeclasses.characters (the .py file ending
-is never included in the python path). The last bit, .Character is the actual class name inside
-the characters.py module. Open that file in a text editor and you will see something like this:
There is Character, the last part of the path. Note how empty this file is. At first glance one
-would think a Character had no functionality at all. But from what we have used already we know it
-has at least the key property and the method msg! Where is the code? The answer is that this
-‘emptiness’ is an illusion caused by something called inheritance. Read on.
-
Firstly, in the same way as the little hello.py we did in the first part of the tutorial, this is
-an example of full, multi-line Python code. Those triple-quoted strings are used for strings that
-have line breaks in them. When they appear on their own like this, at the top of a python module,
-class or similar they are called doc strings. Doc strings are read by Python and is used for
-producing online help about the function/method/class/module. By contrast, a line starting with #
-is a comment. It is ignored completely by Python and is only useful to help guide a human to
-understand the code.
-
The line
-
classCharacter(DefaultCharacter):
-
-
-
means that the class Character is a child of the class DefaultCharacter. This is called
-inheritance and is another fundamental concept. The answer to the question “where is the code?” is
-that the code is inherited from its parent, DefaultCharacter. And that in turn may inherit code
-from its parent(s) and so on. Since our child, Character is empty, its functionality is exactly
-identical to that of its parent. The moment we add new things to Character, these will take
-precedence. And if we add something that already existed in the parent, our child-version will
-override the version in the parent. This is very practical: It means that we can let the parent do
-the heavy lifting and only tweak the things we want to change. It also means that we could easily
-have many different Character classes, all inheriting from DefaultCharacter but changing different
-things. And those can in turn also have children …
-
Let’s go on an expedition up the inheritance tree.
Let’s figure out how to tweak Character. Right now we don’t know much about DefaultCharacter
-though. Without knowing that we won’t know what to override. At the top of the file you find
-
fromevenniaimportDefaultCharacter
-
-
-
This is an import statement again, but on a different form to what we’ve seen before. from...import... is very commonly used and allows you to precisely dip into a module to extract just the
-component you need to use. In this case we head into the evennia package to get
-DefaultCharacter.
-
Where is evennia? To find it you need to go to the evennia folder (repository) you originally
-cloned from us. If you open it, this is how it looks:
There are lots of things in there. There are some docs but most of those have to do with the
-distribution of Evennia and does not concern us right now. The evennia subfolder is what we are
-looking for. This is what you are accessing when you do fromevenniaimport.... It’s set up by
-Evennia as a good place to find modules when the server starts. The exact layout of the Evennia
-library is covered by our directory overview. You can
-also explore it online on github.
-
The structure of the library directly reflects how you import from it.
-
-
To, for example, import the text justify function from
-evennia/utils/utils.py you would do fromevennia.utils.utilsimportjustify. In your code you
-could then just call justify(...) to access its functionality.
-
You could also do fromevennia.utilsimportutils. In code you would then have to write
-utils.justify(...). This is practical if want a lot of stuff from that utils.py module and don’t
-want to import each component separately.
-
You could also do importevennia. You would then have to enter the full
-evennia.utils.utils.justify(...) every time you use it. Using from to only import the things you
-need is usually easier and more readable.
-
See this overview about the different ways to
-import in Python.
-
-
Now, remember that our characters.py module did fromevenniaimportDefaultCharacter. But if we
-look at the contents of the evennia folder, there is no DefaultCharacter anywhere! This is
-because Evennia gives a large number of optional “shortcuts”, known as the “flat” API. The intention is to make it easier to remember where to find stuff. The flat API is defined in
-that weirdly named __init__.py file. This file just basically imports useful things from all over
-Evennia so you can more easily find them in one place.
-
We could just look at the documenation to find out where we can look
-at our DefaultCharacter parent. But for practice, let’s figure it out. Here is where
-DefaultCharacteris imported from inside __init__.py:
-
from.objects.objectsimportDefaultCharacter
-
-
-
The period at the start means that it imports beginning from the same location this module sits(i.e.
-the evennia folder). The full python-path accessible from the outside is thus
-evennia.objects.objects.DefaultCharacter. So to import this into our game it’d be perfectly valid
-to do
is the same thing, just a little easier to remember.
-
-
To access the shortcuts of the flat API you must use fromevenniaimport.... Using something like importevennia.DefaultCharacter will not work.
-See more about the Flat API here.
In the previous section we traced the parent of our Character class to be
-DefaultCharacter in
-evennia/objects/objects.py.
-Open that file and locate the DefaultCharacter class. It’s quite a bit down
-in this module so you might want to search using your editor’s (or browser’s)
-search function. Once you find it, you’ll find that the class starts like this:
-
-classDefaultCharacter(DefaultObject):
- """
- This implements an Object puppeted by a Session - that is, a character
- avatar controlled by an account.
- """
-
- defbasetype_setup(self):
- """
- Setup character-specific security.
- You should normally not need to overload this, but if you do,
- make sure to reproduce at least the two last commands in this
- method (unless you want to fundamentally change how a
- Character object works).
- """
- super().basetype_setup()
- self.locks.add(";".join(["get:false()",# noone can pick up the character
- "call:false()"]))# no commands can be called on character from
-outside
- # add the default cmdset
- self.cmdset.add_default(settings.CMDSET_CHARACTER,permanent=True)
-
- defat_after_move(self,source_location,**kwargs):
- """
- We make sure to look around after a move.
- """
- ifself.location.access(self,"view"):
- self.msg(self.at_look(self.location))
-
- defat_pre_puppet(self,account,session=None,**kwargs):
- """
- Return the character from storage in None location in `at_post_unpuppet`.
- """
-
- # ...
-
-
-
-
… And so on (you can see the full class online here). Here we
-have functional code! These methods may not be directly visible in Character back in our game dir,
-but they are still available since Character is a child of DefaultCharacter above. Here is a
-brief summary of the methods we find in DefaultCharacter (follow in the code to see if you can see
-roughly where things happen)::
-
-
basetype_setup is called by Evennia only once, when a Character is first created. In the
-DefaultCharacter class it sets some particular Locks so that people can’t pick up and
-puppet Characters just like that. It also adds the Character Cmdset so that
-Characters always can accept command-input (this should usually not be modified - the normal hook to
-override is at_object_creation, which is called after basetype_setup (it’s in the parent)).
-
at_after_move makes it so that every time the Character moves, the look command is
-automatically fired (this would not make sense for just any regular Object).
-
at_pre_puppet is called when an Account begins to puppet this Character. When not puppeted, the
-Character is hidden away to a None location. This brings it back to the location it was in before.
-Without this, “headless” Characters would remain in the game world just standing around.
-
at_post_puppet is called when puppeting is complete. It echoes a message to the room that his
-Character has now connected.
-
at_post_unpuppet is called once stopping puppeting of the Character. This hides away the
-Character to a None location again.
-
There are also some utility properties which makes it easier to get some time stamps from the
-Character.
-
-
Reading the class we notice another thing:
-
classDefaultCharacter(DefaultObject):
- # ...
-
-
-
This means that DefaultCharacter is in itself a child of something called DefaultObject! Let’s
-see what this parent class provides. It’s in the same module as DefaultCharacter, you just need to
-scroll up near the top:
This is a really big class where the bulk of code defining an in-game object resides. It consists of
-a large number of methods, all of which thus also becomes available on the DefaultCharacter class
-below and by extension in your Character class over in your game dir. In this class you can for
-example find the msg method we have been using before.
-
-
You should probably not expect to understand all details yet, but as an exercise, find and read
-the doc string of msg.
-
-
-
As seen, DefaultObject actually has multiple parents. In one of those the basic key property
-is defined, but we won’t travel further up the inheritance tree in this tutorial. If you are
-interested to see them, you can find TypeclassBase in evennia/typeclasses/models.py and ObjectDB in evennia/obj
-ects/models.py. We
-will also not go into the details of Multiple Inheritance or
-Metaclasses here. The general rule
-is that if you realize that you need these features, you already know enough to use them.
-
-
Remember the at_pre_puppet method we looked at in DefaultCharacter? If you look at the
-at_pre_puppet hook as defined in DefaultObject you’ll find it to be completely empty (just a
-pass). So if you puppet a regular object it won’t be hiding/retrieving the object when you
-unpuppet it. The DefaultCharacter class overrides its parent’s functionality with a version of
-its own. And since it’s DefaultCharacter that our Character class inherits back in our game dir,
-it’s that version of at_pre_puppet we’ll get. Anything not explicitly overridden will be passed
-down as-is.
-
While it’s useful to read the code, we should never actually modify anything inside the evennia
-folder. Only time you would want that is if you are planning to release a bug fix or new feature for
-Evennia itself. Instead you override the default functionality inside your game dir.
-
So to conclude our little foray into classes, objects and inheritance, locate the simple little
-at_before_say method in the DefaultObject class:
If you read the doc string you’ll find that this can be used to modify the output of say before it
-goes out. You can think of it like this: Evennia knows the name of this method, and when someone
-speaks, Evennia will make sure to redirect the outgoing message through this method. It makes it
-ripe for us to replace with a version of our own.
-
-
In the Evennia documentation you may sometimes see the term hook used for a method explicitly
-meant to be overridden like this.
-
-
As you can see, the first argument to at_before_say is self. In Python, the first argument of a
-method is always a back-reference to the object instance on which the method is defined. By
-convention this argument is always called self but it could in principle be named anything. The
-self is very useful. If you wanted to, say, send a message to the same object from inside
-at_before_say, you would do self.msg(...).
-
What can trip up newcomers is that you don’t include self when you call the method. Try:
Note that we don’t send self but only the message argument. Python will automatically add self
-for us. In this case, self will become equal to the Character instance me.
-
By default the at_before_say method doesn’t do anything. It just takes the message input and
-returns it just the way it was (the return is another reserved Python word).
-
-
We won’t go into **kwargs here, but it (and its sibling *args) is also important to
-understand, extra reading is here for **kwargs.
-
-
Now, open your game folder and edit mygame/typeclasses/characters.py. Locate your Character
-class and modify it as such:
So we add our own version of at_before_say, duplicating the def line from the parent but putting
-new code in it. All we do in this tutorial is to add an ellipsis (...) to the message as it passes
-through the method.
-
Note that f in front of the string, it means we turned the string into a ‘formatted string’. We
-can now easily inject stuff directly into the string by wrapping them in curly brackets {}. In
-this example, we put the incoming message into the string, followed by an ellipsis. This is only
-one way to format a string. Python has very powerful string formatting and
-you are wise to learn it well, considering your game will be mainly text-based.
-
-
You could also copy & paste the relevant method from DefaultObject here to get the full doc
-string. For more complex methods, or if you only want to change some small part of the default
-behavior, copy & pasting will eliminate the need to constantly look up the original method and keep
-you sane.
-
-
In-game, now try
-
> @reload
-> say Hello
-You say, "Hello ..."
-
-
-
An ellipsis ... is added to what you said! This is a silly example but you have just made your
-first code change to core functionality - without touching any of Evennia’s original code! We just
-plugged in our own version of the at_before_say method and it replaced the default one. Evennia
-happily redirected the message through our version and we got a different output.
-
-
For sane overriding of parent methods you should also be aware of Python’s
-super, which allows you to call the
-methods defined on a parent in your child class.
Now on to some generally useful tools as you continue learning Python and Evennia. We have so far
-explored using py and have inserted Python code directly in-game. We have also modified Evennia’s
-behavior by overriding default functionality with our own. There is a third way to conveniently
-explore Evennia and Python - the Evennia shell.
-
Outside of your game, cd to your mygame folder and make sure any needed virtualenv is running.
-Next:
-
> pip install ipython # only needed once
-
-
-
The IPython program is just a nicer interface to the
-Python interpreter - you only need to install it once, after which Evennia will use it
-automatically.
-
> evennia shell
-
-
-
If you did this call from your game dir you will now be in a Python prompt managed by the IPython
-program.
-
IPython ...
-...
-In [1]: IPython has some very nice ways to explore what Evennia has to offer.
-
-> import evennia
-> evennia.<TAB>
-
-
-
That is, write evennia. and press the Tab key. You will be presented with a list of all available
-resources in the Evennia Flat API. We looked at the __init__.py file in the evennia folder
-earlier, so some of what you see should be familiar. From the IPython prompt, do:
-
> from evennia import DefaultCharacter
-> DefaultCharacter.at_before_say?
-
-
-
Don’t forget that you can use <TAB> to auto-complete code as you write. Appending a single ? to
-the end will show you the doc-string for at_before_say we looked at earlier. Use ?? to get the
-whole source code.
-
Let’s look at our over-ridden version instead. Since we started the evenniashell from our game
-dir we can easily get to our code too:
-
> from typeclasses.characters import Character
-> Character.at_before_say??
-
-
-
This will show us the changed code we just did. Having a window with IPython running is very
-convenient for quickly exploring code without having to go digging through the file structure!
This should give you a running start using Python with Evennia. If you are completely new to
-programming or Python you might want to look at a more formal Python tutorial. You can find links
-and resources on our link page.
Once you have familiarized yourself, or if you prefer to pick Python up as you go, continue to one
-of the beginning-level Evennia tutorials to gradually build up your understanding.
This is a list of various quirks or common stumbling blocks that people often ask about or report
-when using (or trying to use) Evennia. They are not bugs.
-
-
Forgetting to use @reload to see changes to your typeclasses¶
-
Firstly: Reloading the server is a safe and usually quick operation which will not disconnect any
-accounts.
-
New users tend to forget this step. When editing source code (such as when tweaking typeclasses and
-commands or adding new commands to command sets) you need to either use the in-game @reload
-command or, from the command line do pythonevennia.pyreload before you see your changes.
If you use the default login system and are trying to use the Web admin to create a new Player
-account, you need to consider which MULTIACCOUNT_MODE you are in. If you are in
-MULTIACCOUNT_MODE0 or 1, the login system expects each Account to also have a Character
-object named the same as the Account - there is no character creation screen by default. If using
-the normal mud login screen, a Character with the same name is automatically created and connected
-to your Account. From the web interface you must do this manually.
-
So, when creating the Account, make sure to also create the Character from the same form as you
-create the Account from. This should set everything up for you. Otherwise you need to manually set
-the “account” property on the Character and the “character” property on the Account to point to each
-other. You must also set the lockstring of the Character to allow the Account to “puppet” this
-particular character.
-
-
-
Mutable attributes and their connection to the database¶
-
When storing a mutable object (usually a list or a dictionary) in an Attribute
-
object.db.mylist=[1,2,3]
-
-
-
you should know that the connection to the database is retained also if you later extract that
-Attribute into another variable (what is stored and retrieved is actually a PackedList or a
-PackedDict that works just like their namesakes except they save themselves to the database when
-changed). So if you do
-
alist=object.db.mylist
- alist.append(4)
-
-
-
this updates the database behind the scenes, so both alist and object.db.mylist are now
-[1,2,3,4]
-
If you don’t want this, Evennia provides a way to stably disconnect the mutable from the database by
-use of evennia.utils.dbserialize.deserialize:
The property blist is now [1,2,3,4] whereas object.db.mylist remains unchanged. If you want to
-update the database you’d need to explicitly re-assign the updated data to the mylist Attribute.
When merging command sets it’s important to remember that command objects are identified
-both by key or alias. So if you have a command with a key look and an alias ls, introducing
-another command with a key ls will be assumed by the system to be identical to the first one.
-This usually means merging cmdsets will overload one of them depending on priority. Whereas this is
-logical once you know how command objects are handled, it may be confusing if you are just looking
-at the command strings thinking they are parsed as-is.
A common confusing error for new developers is finding that one or more objects in-game are suddenly
-of the type DefaultObject rather than the typeclass you wanted it to be. This happens when you
-introduce a critical Syntax error to the module holding your custom class. Since such a module is
-not valid Python, Evennia can’t load it at all to get to the typeclasses within. To keep on running,
-Evennia will solve this by printing the full traceback to the terminal/console and temporarily fall
-back to the safe DefaultObject until you fix the problem and reload. Most errors of this kind will
-be caught by any good text editors. Keep an eye on the terminal/console during a reload to catch
-such errors - you may have to scroll up if your window is small.
Python implements a system of magic
-methods, usually
-prefixed and suffixed by double-underscores (__example__) that allow object instances to have
-certain operations performed on them without needing to do things like turn them into strings or
-numbers first– for example, is obj1 greater than or equal to obj2?
-
Neither object is a number, but given obj1.size=="small" and obj2.size=="large", how might
-one compare these two arbitrary English adjective strings to figure out which is greater than the
-other? By defining the __ge__ (greater than or equal to) magic method on the object class in which
-you figure out which word has greater significance, perhaps through use of a mapping table
-({'small':0,'large':10}) or other lookup and comparing the numeric values of each.
-
Evennia extensively makes use of magic methods on typeclasses to do things like initialize objects,
-check object existence or iterate over objects in an inventory or container. If you override or
-interfere with the return values from the methods Evennia expects to be both present and working, it
-can result in very inconsistent and hard-to-diagnose errors.
-
The moral of the story– it can be dangerous to tinker with magic methods on typeclassed objects.
-Try to avoid doing so.
There is currently (Autumn 2017) a bug in the zope.interface installer on some Linux Ubuntu
-distributions (notably Ubuntu 16.04 LTS). Zope is a dependency of Twisted. The error manifests in
-the server not starting with an error that zope.interface is not found even though piplist
-shows it’s installed. The reason is a missing empty __init__.py file at the root of the zope
-package. If the virtualenv is named “evenv” as suggested in the Getting Started
-instructions, use the following command to fix it:
RSS is a format for easily tracking updates on websites. The
-principle is simple - whenever a site is updated, a small text file is updated. An RSS reader can
-then regularly go online, check this file for updates and let the user know what’s new.
-
Evennia allows for connecting any number of RSS feeds to any number of in-game channels. Updates to
-the feed will be conveniently echoed to the channel. There are many potential uses for this: For
-example the MUD might use a separate website to host its forums. Through RSS, the players can then
-be notified when new posts are made. Another example is to let everyone know you updated your dev
-blog. Admins might also want to track the latest Evennia updates through our own RSS feed
-here.
You can connect RSS to any Evennia channel, but for testing, let’s set up a new channel “rss”.
-
@ccreate rss = RSS feeds are echoed to this channel!
-
-
-
Let’s connect Evennia’s code-update feed to this channel. The RSS url for evennia updates is
-https://github.com/evennia/evennia/commits/master.atom, so let’s add that:
That’s it, really. New Evennia updates will now show up as a one-line title and link in the channel.
-Give the @rss2chan command on its own to show all connections. To remove a feed from a channel,
-you specify the connection again (use the command to see it in the list) but add the /delete
-switch:
You can connect any number of RSS feeds to a channel this way. You could also connect them to the
-same channels as IRC to have the feed echo to external chat channels as well.
First, install the docker program so you can run the Evennia container. You can get it freely from
-docker.com. Linux users can likely also get it through their normal
-package manager.
-
To fetch the latest evennia docker image, run:
-
docker pull evennia/evennia
-
-
-
This is a good command to know, it is also how you update to the latest version when we make updates
-in the future. This tracks the master branch of Evennia.
-
-
Note: If you want to experiment with the (unstable) develop branch, use dockerpullevennia/evennia:develop.
-
-
Next cd to a place where your game dir is, or where you want to create it. Then run:
Having run this (see next section for a description of what’s what), you will be at a prompt inside
-the docker container:
-
evennia|docker /usr/src/game $
-
-
-
This is a normal shell prompt. We are in the /usr/src/game location inside the docker container.
-If you had anything in the folder you started from, you should see it here (with ls) since we
-mounted the current directory to /usr/src/game (with -v above). You have the evennia command
-available and can now proceed to create a new game as per the Getting Started
-instructions (you can skip the virtualenv and install ‘globally’ in the container though).
-
You can run Evennia from inside this container if you want to, it’s like you are root in a little
-isolated Linux environment. To exit the container and all processes in there, press Ctrl-D. If you
-created a new game folder, you will find that it has appeared on-disk.
-
-
The game folder or any new files that you created from inside the container will appear as owned
-by root. If you want to edit the files outside of the container you should change the ownership.
-On Linux/Mac you do this with sudochownmyname:myname-Rmygame, where you replace myname with
-your username and mygame with whatever your game folder is named.
dockerrun...evennia/evennia tells us that we want to run a new container based on the
-evennia/evennia docker image. Everything in between are options for this. The evennia/evennia is
-the name of our official docker image on the dockerhub
-repository. If you didn’t do dockerpullevennia/evennia first, the image will be downloaded when running this, otherwise your already
-downloaded version will be used. It contains everything needed to run Evennia.
-
-it has to do with creating an interactive session inside the container we start.
-
--rm will make sure to delete the container when it shuts down. This is nice to keep things tidy
-on your drive.
-
-p4000:4000-p4001:4001-p4002:4002 means that we map ports 4000, 4001 and 4002 from
-inside the docker container to same-numbered ports on our host machine. These are ports for telnet,
-webserver and websockets. This is what allows your Evennia server to be accessed from outside the
-container (such as by your MUD client)!
-
-v$PWD:/usr/src/game mounts the current directory (outside the container) to the path
-/usr/src/gameinside the container. This means that when you edit that path in the container you
-will actually be modifying the “real” place on your hard drive. If you didn’t do this, any changes
-would only exist inside the container and be gone if we create a new one. Note that in linux a
-shortcut for the current directory is $PWD. If you don’t have this for your OS, you can replace it
-with the full path to the current on-disk directory (like C:/Development/evennia/game or wherever
-you want your evennia files to appear).
-
--user$UID:$GID ensures the container’s modifications to $PWD are done with you user and
-group IDs instead of root’s IDs (root is the user running evennia inside the container). This avoids
-having stale .pid files in your filesystem between container reboots which you have to force
-delete with sudormserver/*.pid before each boot.
If you run the docker command given in the previous section from your game dir you can then
-easily start Evennia and have a running server without any further fuss.
-
But apart from ease of install, the primary benefit to running an Evennia-based game in a container
-is to simplify its deployment into a public production environment. Most cloud-based hosting
-providers these days support the ability to run container-based applications. This makes deploying
-or updating your game as simple as building a new container image locally, pushing it to your Docker
-Hub account, and then pulling from Docker Hub into your AWS/Azure/other docker-enabled hosting
-account. The container eliminates the need to install Python, set up a virtualenv, or run pip to
-install dependencies.
For remote or automated deployment you may want to start Evennia immediately as soon as the docker
-container comes up. If you already have a game folder with a database set up you can also start the
-docker container and pass commands directly to it. The command you pass will be the main process to
-run in the container. From your game dir, run for example this command:
This will start Evennia as the foreground process, echoing the log to the terminal. Closing the
-terminal will kill the server. Note that you must use a foreground command like evenniastart--log or evenniaipstart to start the server - otherwise the foreground process will finish
-immediately and the container go down.
You may want to create your own image in order to bake in your gamedir directly into the docker
-container for easy upload and deployment.
-These steps assume that you have created or otherwise obtained a game directory already. First, cd
-to your game dir and create a new empty text file named Dockerfile. Save the following two lines
-into it:
These are instructions for building a new docker image. This one is based on the official
-evennia/evennia image. We add the second line to
-make make sure evennia starts immediately along with the container (so we don’t need to enter it and
-run commands).
-
To build the image:
-
docker build -t mydhaccount/mygame .
-
-
-
(don’t forget the period . at the end, it tells docker to use use the Dockerfile from the
-current location). Here mydhaccount is the name of your dockerhub account. If you don’t have a
-dockerhub account you can build the image locally only (name the container whatever you like in that
-case, like just mygame).
-
Docker images are stored centrally on your computer. You can see which ones you have available
-locally with dockerimages. Once built, you have a couple of options to run your game.
-
If you have a docker-hub account, you can push your (new or updated) image there:
-
docker push myhdaccount/mygame
-
-
-
-
-
Run container from your game image for development¶
-
To run the container based on your game image locally for development, mount the local game
-directory as before:
Evennia will start and you’ll get output in the terminal, perfect for development. You should be
-able to connect to the game with your clients normally.
Each time you rebuild the docker image as per the above instructions, the latest copy of your game
-directory is actually copied inside the image (at /usr/src/game/). If you don’t mount your on-disk
-folder there, the internal one will be used. So for deploying evennia on a server, omit the -v
-option and just give the following command:
Your game will be downloaded from your docker-hub account and a new container will be built using
-the image and started on the server! If your server environment forces you to use different ports,
-you can just map the normal ports differently in the command above.
-
Above we added the -d option, which starts the container in daemon mode - you won’t see any
-return in the console. You can see it running with dockerps:
-
$ docker ps
-
-CONTAINER ID IMAGE COMMAND CREATED ...
-f6d4ca9b2b22 mygame "/bin/sh -c 'evenn..." About a minute ago ...
-
-
-
Note the container ID, this is how you manage the container as it runs.
-
dockerlogsf6d4ca9b2b22
-
-
-
Looks at the STDOUT output of the container (i.e. the normal server log)
-
dockerlogs-ff6d4ca9b2b22
-
-
-
Tail the log (so it updates to your screen ‘live’).
-
dockerpausef6d4ca9b2b22
-
-
-
Suspend the state of the container.
-
dockerunpausef6d4ca9b2b22
-
-
-
Un-suspend it again after a pause. It will pick up exactly where it were.
-
dockerstopf6d4ca9b2b22
-
-
-
Stop the container. To get it up again you need to use dockerrun, specifying ports etc. A new
-container will get a new container id to reference.
The evennia/evennia docker image holds the evennia library and all of its dependencies. It also
-has an ONBUILD directive which is triggered during builds of images derived from it. This
-ONBUILD directive handles setting up a volume and copying your game directory code into the proper
-location within the container.
-
In most cases, the Dockerfile for an Evennia-based game will only need the FROMevennia/evennia:latest directive, and optionally a MAINTAINER directive if you plan to publish
-your image on Docker Hub and would like to provide contact info.
A new evennia/evennia image is built automatically whenever there is a new commit to the master
-branch of Evennia. It is possible to create your own custom evennia base docker image based on any
-arbitrary commit.
-
-
Use git tools to checkout the commit that you want to base your image upon. (In the example
-below, we’re checking out commit a8oc3d5b.)
-
-
gitcheckout-bmy-stable-brancha8oc3d5b
-
-
-
-
Change your working directory to the evennia directory containing Dockerfile. Note that
-Dockerfile has changed over time, so if you are going far back in the commit history you might
-want to bring a copy of the latest Dockerfile with you and use that instead of whatever version
-was used at the time.
-
Use the dockerbuild command to build the image based off of the currently checked out commit.
-The example below assumes your docker account is mydhaccount.
-
-
dockerbuild-tmydhaccount/evennia.
-
-
-
-
Now you have a base evennia docker image built off of a specific commit. To use this image to
-build your game, you would modify FROM directive in the Dockerfile for your game directory
-to be:
-
-
FROMmydhacct/evennia:latest
-
-
-
Note: From this point, you can also use the dockertag command to set a specific tag on your image
-and/or upload it into Docker Hub under your account.
-
-
At this point, build your game using the same dockerbuild command as usual. Change your
-working directory to be your game directory and run
The Docker ecosystem includes a tool called docker-compose, which can orchestrate complex multi-
-container applications, or in our case, store the default port and terminal parameters that we want
-specified every time we run our container. A sample docker-compose.yml file to run a containerized
-Evennia game in development might look like this:
Note that with this setup you lose the --user$UID option. The problem is that the variable
-UID is not available inside the configuration file docker-compose.yml. A workaround is to
-hardcode your user and group id. In a terminal run echo$UID:$GID and if for example you get
-1000:1000 you can add to docker-compose.yml a line user:1000:1000 just below the image:...
-line.
-(right-click and choose your browser’s equivalent of “view image” to see it full size)
-
This screenshot shows a vanilla install of the just started Evennia MUD server.
-In the bottom window we can see the log messages from the running server.
-
In the top left window we see the default website of our new game displayed in a web browser (it has
-shrunk to accomodate the smaller screen). Evennia contains its own webserver to serve this page. The
-default site shows some brief info about the database. From here you can also reach Django’s admin
-interface for editing the database online.
-
To the upper right is the included web-browser client showing a connection to the server on port
-4001. This allows users to access the game without downloading a mud client separately.
-
Bottom right we see a login into the stock game using a third-party MUD client
-(Mudlet). This connects to the server via the telnet protocol on port 4000.
Scripts are the out-of-character siblings to the in-character
-Objects. Scripts are so flexible that the “Script” is a bit limiting
-
-
we had to pick something to name them after all. Other possible names
-(depending on what you’d use them for) would be OOBObjects,
-StorageContainers or TimerObjects.
-
-
Scripts can be used for many different things in Evennia:
-
-
They can attach to Objects to influence them in various ways - or exist
-independently of any one in-game entity (so-called Global Scripts).
-
They can work as timers and tickers - anything that may change with Time. But
-they can also have no time dependence at all. Note though that if all you want
-is just to have an object method called repeatedly, you should consider using
-the TickerHandler which is more limited but is specialized on
-just this task.
-
They can describe State changes. A Script is an excellent platform for
-hosting a persistent, but unique system handler. For example, a Script could be
-used as the base to track the state of a turn-based combat system. Since
-Scripts can also operate on a timer they can also update themselves regularly
-to perform various actions.
-
They can act as data stores for storing game data persistently in the database
-(thanks to its ability to have Attributes).
-
They can be used as OOC stores for sharing data between groups of objects, for
-example for tracking the turns in a turn-based combat system or barter exchange.
-
-
Scripts are Typeclassed entities and are manipulated in a similar
-way to how it works for other such Evennia entities:
-
# create a new script
-new_script=evennia.create_script(key="myscript",typeclass=...)
-
-# search (this is always a list, also if there is only one match)
-list_of_myscript=evennia.search_script("myscript")
-
-
A Script is defined as a class and is created in the same way as other
-typeclassed entities. The class has several properties
-to control the timer-component of the scripts. These are all optional -
-leaving them out will just create a Script with no timer components (useful to act as
-a database store or to hold a persistent game system, for example).
-
This you can do for example in the module
-evennia/typeclasses/scripts.py. Below is an example Script
-Typeclass.
-
fromevenniaimportDefaultScript
-
-classMyScript(DefaultScript):
-
- defat_script_creation(self):
- self.key="myscript"
- self.interval=60# 1 min repeat
-
- defat_repeat(self):
- # do stuff every minute
-
-
-
In mygame/typeclasses/scripts.py is the Script class which inherits from DefaultScript
-already. This is provided as your own base class to do with what you like: You can tweak Script if
-you want to change the default behavior and it is usually convenient to inherit from this instead.
-Here’s an example:
-
# for example in mygame/typeclasses/scripts.py
- # Script class is defined at the top of this module
-
- importrandom
-
- classWeather(Script):
- """
- A timer script that displays weather info. Meant to
- be attached to a room.
-
- """
- defat_script_creation(self):
- self.key="weather_script"
- self.desc="Gives random weather messages."
- self.interval=60*5# every 5 minutes
- self.persistent=True# will survive reload
-
- defat_repeat(self):
- "called every self.interval seconds."
- rand=random.random()
- ifrand<0.5:
- weather="A faint breeze is felt."
- elifrand<0.7:
- weather="Clouds sweep across the sky."
- else:
- weather="There is a light drizzle of rain."
- # send this message to everyone inside the object this
- # script is attached to (likely a room)
- self.obj.msg_contents(weather)
-
-
-
If we put this script on a room, it will randomly report some weather
-to everyone in the room every 5 minutes.
-
To activate it, just add it to the script handler (scripts) on an
-Room. That object becomes self.obj in the example above. Here we
-put it on a room called myroom:
-
myroom.scripts.add(scripts.Weather)
-
-
-
-
Note that typeclasses in your game dir is added to the setting TYPECLASS_PATHS.
-Therefore we don’t need to give the full path (typeclasses.scripts.Weather
-but only scripts.Weather above.
-
-
If you wanted to stop and delete that script on the Room. You could do that
-with the script handler by passing the delete method with the script key (self.key) as:
-
myroom.scripts.delete('weather_script')
-
-
-
-
Note that If no key is given, this will delete all scripts on the object!
-
-
You can also create scripts using the evennia.create_script function:
Note that if you were to give a keyword argument to create_script, that would
-override the default value in your Typeclass. So for example, here is an instance
-of the weather script that runs every 10 minutes instead (and also not survive
-a server reload):
A Script has all the properties of a typeclassed object, such as db and ndb(see
-Typeclasses). Setting key is useful in order to manage scripts (delete them by name
-etc). These are usually set up in the Script’s typeclass, but can also be assigned on the fly as
-keyword arguments to evennia.create_script.
-
-
desc - an optional description of the script’s function. Seen in script listings.
-
interval - how often the script should run. If interval==0 (default), this script has no
-timing component, will not repeat and will exist forever. This is useful for Scripts used for
-storage or acting as bases for various non-time dependent game systems.
-
start_delay - (bool), if we should wait interval seconds before firing for the first time or
-not.
-
repeats - How many times we should repeat, assuming interval>0. If repeats is set to <=0,
-the script will repeat indefinitely. Note that each firing of the script (including the first one)
-counts towards this value. So a Script with start_delay=False and repeats=1 will start,
-immediately fire and shut down right away.
-
persistent- if this script should survive a server reset or server shutdown. (You don’t need
-to set this for it to survive a normal reload - the script will be paused and seamlessly restart
-after the reload is complete).
-
-
There is one special property:
-
-
obj - the Object this script is attached to (if any). You should not need to set
-this manually. If you add the script to the Object with myobj.scripts.add(myscriptpath) or give
-myobj as an argument to the utils.create.create_script function, the obj property will be set
-to myobj for you.
-
-
It’s also imperative to know the hook functions. Normally, overriding
-these are all the customization you’ll need to do in Scripts. You can
-find longer descriptions of these in src/scripts/scripts.py.
-
-
at_script_creation() - this is usually where the script class sets things like interval and
-repeats; things that control how the script runs. It is only called once - when the script is
-first created.
-
is_valid() - determines if the script should still be running or not. This is called when
-running obj.scripts.validate(), which you can run manually, but which is also called by Evennia
-during certain situations such as reloads. This is also useful for using scripts as state managers.
-If the method returns False, the script is stopped and cleanly removed.
-
at_start() - this is called when the script starts or is unpaused. For persistent scripts this
-is at least once ever server startup. Note that this will always be called right away, also if
-start_delay is True.
-
at_repeat() - this is called every interval seconds, or not at all. It is called right away at
-startup, unless start_delay is True, in which case the system will wait interval seconds
-before calling.
-
at_stop() - this is called when the script stops for whatever reason. It’s a good place to do
-custom cleanup.
-
at_server_reload() - this is called whenever the server is warm-rebooted (e.g. with the
-@reload command). It’s a good place to save non-persistent data you might want to survive a
-reload.
-
at_server_shutdown() - this is called when a system reset or systems shutdown is invoked.
-
-
Running methods (usually called automatically by the engine, but possible to also invoke manually)
-
-
start() - this will start the script. This is called automatically whenever you add a new script
-to a handler. at_start() will be called.
-
stop() - this will stop the script and delete it. Removing a script from a handler will stop it
-automatically. at_stop() will be called.
-
pause() - this pauses a running script, rendering it inactive, but not deleting it. All
-properties are saved and timers can be resumed. This is called automatically when the server reloads
-and will not lead to the at_stop() hook being called. This is a suspension of the script, not a
-change of state.
-
unpause() - resumes a previously paused script. The at_start() hook will be called to allow
-it to reclaim its internal state. Timers etc are restored to what they were before pause. The server
-automatically unpauses all paused scripts after a server reload.
-
force_repeat() - this will forcibly step the script, regardless of when it would otherwise have
-fired. The timer will reset and the at_repeat() hook is called as normal. This also counts towards
-the total number of repeats, if limited.
-
time_until_next_repeat() - for timed scripts, this returns the time in seconds until it next
-fires. Returns None if interval==0.
-
remaining_repeats() - if the Script should run a limited amount of times, this tells us how many
-are currently left.
-
reset_callcount(value=0) - this allows you to reset the number of times the Script has fired. It
-only makes sense if repeats>0.
-
restart(interval=None,repeats=None,start_delay=None) - this method allows you to restart the
-Script in-place with different run settings. If you do, the at_stop hook will be called and the
-Script brought to a halt, then the at_start hook will be called as the Script starts up with your
-(possibly changed) settings. Any keyword left at None means to not change the original setting.
A script does not have to be connected to an in-game object. If not it is
-called a Global script. You can create global scripts by simply not supplying an object to store
-it on:
-
# adding a global script
- fromevenniaimportcreate_script
- create_script("typeclasses.globals.MyGlobalEconomy",
- key="economy",persistent=True,obj=None)
-
-
-
Henceforth you can then get it back by searching for its key or other identifier with
-evennia.search_script. In-game, the scripts command will show all scripts.
-
Evennia supplies a convenient “container” called GLOBAL_SCRIPTS that can offer an easy
-way to access global scripts. If you know the name (key) of the script you can get it like so:
-
fromevenniaimportGLOBAL_SCRIPTS
-
-my_script=GLOBAL_SCRIPTS.my_script
-# needed if there are spaces in name or name determined on the fly
-another_script=GLOBAL_SCRIPTS.get("another script")
-# get all global scripts (this returns a Queryset)
-all_scripts=GLOBAL_SCRIPTS.all()
-# you can operate directly on the script
-GLOBAL_SCRIPTS.weather.db.current_weather="Cloudy"
-
-
-
-
-
Note that global scripts appear as properties on GLOBAL_SCRIPTS based on their key.
-If you were to create two global scripts with the same key (even with different typeclasses),
-the GLOBAL_SCRIPTS container will only return one of them (which one depends on order in
-the database). Best is to organize your scripts so that this does not happen. Otherwise, use
-evennia.search_script to get exactly the script you want.
-
-
There are two ways to make a script appear as a property on GLOBAL_SCRIPTS. The first is
-to manually create a new global script with create_script as mentioned above. Often you want this
-to happen automatically when the server starts though. For this you add a python global dictionary
-named GLOBAL_SCRIPTS to your settings.py file. The settings.py fie is located in
-mygame/conf/settings.py:
Here the key (myscript and storagescript above) is required, all other fields are optional. If
-typeclass is not given, a script of type settings.BASE_SCRIPT_TYPECLASS is assumed. The keys
-related to timing and intervals are only needed if the script is timed.
-
-
Note: Provide the full path to the scripts module in GLOBAL_SCRIPTS. In the example with
-another_script, you can also create separate script modules that exist beyond scrypt.py for
-further organizational requirements if needed.
-
-
Evennia will use the information in settings.GLOBAL_SCRIPTS to automatically create and start
-these
-scripts when the server starts (unless they already exist, based on their key). You need to reload
-the server before the setting is read and new scripts become available. You can then find the key
-you gave as properties on evennia.GLOBAL_SCRIPTS
-(such as evennia.GLOBAL_SCRIPTS.storagescript).
-
-
Note: Make sure that your Script typeclass does not have any critical errors. If so, you’ll see
-errors in your log and your Script will temporarily fall back to being a DefaultScript type.
-
-
Moreover, a script defined this way is guaranteed to exist when you try to access it:
-
fromevenniaimportGLOBAL_SCRIPTS
-# first stop the script
-GLOBAL_SCRIPTS.storagescript.stop()
-# running the `scripts` command now will show no storagescript
-# but below now it's recreated again!
-storage=GLOBAL_SCRIPTS.storagescript
-
-
-
That is, if the script is deleted, next time you get it from GLOBAL_SCRIPTS, it will use the
-information
-in settings to recreate it for you.
-
-
Note that if your goal with the Script is to store persistent data, you should set it as
-persistent=True, either in settings.GLOBAL_SCRIPTS or in the Scripts typeclass. Otherwise any
-data you wanted to store on it will be gone (since a new script of the same name is restarted
-instead).
Errors inside an timed, executing script can sometimes be rather terse or point to
-parts of the execution mechanism that is hard to interpret. One way to make it
-easier to debug scripts is to import Evennia’s native logger and wrap your
-functions in a try/catch block. Evennia’s logger can show you where the
-traceback occurred in your script.
In-game you can try out scripts using the @script command. In the
-evennia/contrib/tutorial_examples/bodyfunctions.py is a little example script
-that makes you do little ‘sounds’ at random intervals. Try the following to apply an
-example time-based script to your character.
-
> @script self = bodyfunctions.BodyFunctions
-
-
-
-
Note: Since evennia/contrib/tutorial_examples is in the default setting
-TYPECLASS_PATHS, we only need to specify the final part of the path,
-that is, bodyfunctions.BodyFunctions.
-
-
If you want to inflict your flatulence script on another person, place or
-thing, try something like the following:
Hackers these days aren’t discriminating, and their backgrounds range from bored teenagers to
-international intelligence agencies. Their scripts and bots endlessly crawl the web, looking for
-vulnerable systems they can break into. Who owns the system is irrelevant– it doesn’t matter if it
-belongs to you or the Pentagon, the goal is to take advantage of poorly-secured systems and see what
-resources can be controlled or stolen from them.
-
If you’re considering deploying to a cloud-based host, you have a vested interest in securing your
-applications– you likely have a credit card on file that your host can freely bill. Hackers pegging
-your CPU to mine cryptocurrency or saturating your network connection to participate in a botnet or
-send spam can run up your hosting bill, get your service suspended or get your address/site
-blacklisted by ISPs. It can be a difficult legal or political battle to undo this damage after the
-fact.
-
As a developer about to expose a web application to the threat landscape of the modern internet,
-here are a few tips to consider to increase the security of your Evennia install.
In case of emergency, check your logs! By default they are located in the server/logs/ folder.
-Here are some of the more important ones and why you should care:
-
-
http_requests.log will show you what HTTP requests have been made against Evennia’s built-in
-webserver (TwistedWeb). This is a good way to see if people are innocuously browsing your site or
-trying to break it through code injection.
-
portal.log will show you various networking-related information. This is a good place to check
-for odd or unusual types or amounts of connections to your game, or other networking-related
-issues– like when users are reporting an inability to connect.
-
server.log is the MUX administrator’s best friend. Here is where you’ll find information
-pertaining to who’s trying to break into your system by guessing at passwords, who created what
-objects, and more. If your game fails to start or crashes and you can’t tell why, this is the first
-place you should look for answers. Security-related events are prefixed with an [SS] so when
-there’s a problem you might want to pay special attention to those.
There are a few Evennia/Django options that are set when you first create your game to make it more
-obvious to you where problems arise. These options should be disabled before you push your game into
-production– leaving them on can expose variables or code someone with malicious intent can easily
-abuse to compromise your environment.
-
In server/conf/settings.py:
-
# Disable Django's debug mode
-DEBUG = False
-# Disable the in-game equivalent
-IN_GAME_ERRORS = False
-# If you've registered a domain name, force Django to check host headers. Otherwise leave this
-
-
-
as-is.
-# Note the leading period– it is not a typo!
-ALLOWED_HOSTS = [‘.example.com’]
If you decide to allow users to upload their own images to be served from your site, special care
-must be taken. Django will read the file headers to confirm it’s an image (as opposed to a document
-or zip archive), but code can be injected into an image
-fileafter the headers
-that can be interpreted as HTML and/or give an attacker a web shell through which they can access
-other filesystem resources.
Serve all user-uploaded assets from a separate domain or CDN (not a subdomain of the one you
-already have!). For example, you may be browsing reddit.com but note that all the user-submitted
-images are being served from the redd.it domain. There are both security and performance benefits
-to this (webservers tend to load local resources one-by-one, whereas they will request external
-resources in bulk).
-
If you don’t want to pay for a second domain, don’t understand what any of this means or can’t be
-bothered with additional infrastructure, then simply reprocess user images upon receipt using an
-image library. Convert them to a different format, for example. Destroy the originals!
The web interface allows visitors to see an informational page as well as log into a browser-based
-telnet client with which to access Evennia. It also provides authentication endpoints against which
-an attacker can attempt to validate stolen lists of credentials to see which ones might be shared by
-your users. Django’s security is robust, but if you don’t want/need these features and fully intend
-to force your users to use traditional clients to access your game, you might consider disabling
-either/both to minimize your attack surface.
-
In server/conf/settings.py:
-
# Disable the Javascript webclient
-WEBCLIENT_ENABLED = False
-# Disable the website altogether
-WEBSERVER_ENABLED = False
-
Automated attacks will often target port 22 seeing as how it’s the standard port for SSH traffic.
-Also,
-many public wifi hotspots block ssh traffic over port 22 so you might not be able to access your
-server from these locations if you like to work remotely or don’t have a home internet connection.
-
If you don’t intend on running a website or securing it with TLS, you can mitigate both problems by
-changing the port used for ssh to 443, which most/all hotspot providers assume is HTTPS traffic and
-allows through.
-
(Ubuntu) In /etc/ssh/sshd_config, change the following variable:
-
# What ports, IPs and protocols we listen for
-Port 443
-
Though not officially supported, there are some benefits to deploying a webserver
-to handle/proxy traffic to your Evennia instance.
-
For example, Evennia’s game engine and webservice are tightly integrated. If you bring your game
-down for maintenance (or if it simply crashes) your website will go down with it. In these cases a
-standalone webserver can still be used to display a maintenance page or otherwise communicate to
-your users the reason for the downtime, instead of disappearing off the face of the earth and
-returning opaque SERVERNOTFOUND error messages.
-
Proper webservers are also written in more efficient programming languages than Python, and while
-Twisted can handle its own, putting a webserver in front of it is like hiring a bouncer to deal with
-nuisances and crowds before they even get in the door.
-
Many of the popular webservers also let you plug in additional modules (like
-mod_security for Apache) that can be used to detect
-(and block!) malicious users or requests before they even touch your game or site. There are also
-automated solutions for installing and configuring TLS (via Certbot/Let’s
-Encrypt) to secure your website against hotspot and
-ISP snooping.
Evennia runs out of the box without any changes to its settings. But there are several important
-ways to customize the server and expand it with your own plugins.
The “Settings” file referenced throughout the documentation is the file
-mygame/server/conf/settings.py. This is automatically created on the first run of evennia--init
-(see the Getting Started page).
-
Your new settings.py is relatively bare out of the box. Evennia’s core settings file is actually
-[evennia/settings_default.py](https://github.com/evennia/evennia/blob/master/evennia/settings_default
-.py) and is considerably more extensive (it is also heavily documented so you should refer to this
-file directly for the available settings).
-
Since mygame/server/conf/settings.py is a normal Python module, it simply imports
-evennia/settings_default.py into itself at the top.
-
This means that if any setting you want to change were to depend on some other default setting,
-you might need to copy & paste both in order to change them and get the effect you want (for most
-commonly changed settings, this is not something you need to worry about).
-
You should never edit evennia/settings_default.py. Rather you should copy&paste the select
-variables you want to change into your settings.py and edit them there. This will overload the
-previously imported defaults.
-
-
Warning: It may be tempting to copy everything from settings_default.py into your own settings
-file. There is a reason we don’t do this out of the box though: it makes it directly clear what
-changes you did. Also, if you limit your copying to the things you really need you will directly be
-able to take advantage of upstream changes and additions to Evennia for anything you didn’t
-customize.
Each setting appears as a property on the imported settings object. You can also explore all
-possible options with evennia.settings_full (this also includes advanced Django defaults that are
-not touched in default Evennia).
-
-
It should be pointed out that when importing settings into your code like this, it will be read
-only. You cannot edit your settings from your code! The only way to change an Evennia setting is
-to edit mygame/server/conf/settings.py directly. You also generally need to restart the server
-(possibly also the Portal) before a changed setting becomes available.
at_initial_setup.py - this allows you to add a custom startup method to be called (only) the
-very first time Evennia starts (at the same time as user #1 and Limbo is created). It can be made to
-start your own global scripts or set up other system/world-related things your game needs to have
-running from the start.
-
at_server_startstop.py - this module contains two functions that Evennia will call every time
-the Server starts and stops respectively - this includes stopping due to reloading and resetting as
-well as shutting down completely. It’s a useful place to put custom startup code for handlers and
-other things that must run in your game but which has no database persistence.
-
connection_screens.py - all global string variables in this module are interpreted by Evennia as
-a greeting screen to show when an Account first connects. If more than one string variable is
-present in the module a random one will be picked.
-
inlinefuncs.py - this is where you can define custom Inline functions.
-
inputfuncs.py - this is where you define custom Input functions to handle data
-from the client.
-
lockfuncs.py - this is one of many possible modules to hold your own “safe” lock functions to
-make available to Evennia’s Locks.
-
mssp.py - this holds meta information about your game. It is used by MUD search engines (which
-you often have to register with) in order to display what kind of game you are running along with
-statistics such as number of online accounts and online status.
-
oobfuncs.py - in here you can define custom OOB functions.
-
portal_services_plugin.py - this allows for adding your own custom services/protocols to the
-Portal. It must define one particular function that will be called by Evennia at startup. There can
-be any number of service plugin modules, all will be imported and used if defined. More info can be
-found here.
-
server_services_plugin.py - this is equivalent to the previous one, but used for adding new
-services to the Server instead. More info can be found
-here.
-
-
Some other Evennia systems can be customized by plugin modules but has no explicit template in
-conf/:
-
-
cmdparser.py - a custom module can be used to totally replace Evennia’s default command parser.
-All this does is to split the incoming string into “command name” and “the rest”. It also handles
-things like error messages for no-matches and multiple-matches among other things that makes this
-more complex than it sounds. The default parser is very generic, so you are most often best served
-by modifying things further down the line (on the command parse level) than here.
-
at_search.py - this allows for replacing the way Evennia handles search results. It allows to
-change how errors are echoed and how multi-matches are resolved and reported (like how the default
-understands that “2-ball” should match the second “ball” object if there are two of them in the
-room).
There is a special database model called ServerConf that stores server internal data and settings
-such as current account count (for interfacing with the webserver), startup status and many other
-things. It’s rarely of use outside the server core itself but may be good to
-know about if you are an Evennia developer.
An Evennia Session represents one single established connection to the server. Depending on the
-Evennia session, it is possible for a person to connect multiple times, for example using different
-clients in multiple windows. Each such connection is represented by a session object.
-
A session object has its own cmdset, usually the “unloggedin” cmdset. This is what
-is used to show the login screen and to handle commands to create a new account (or
-Account in evennia lingo) read initial help and to log into the game with an existing
-account. A session object can either be “logged in” or not. Logged in means that the user has
-authenticated. When this happens the session is associated with an Account object (which is what
-holds account-centric stuff). The account can then in turn puppet any number of objects/characters.
-
-
Warning: A Session is not persistent - it is not a Typeclass and has no
-connection to the database. The Session will go away when a user disconnects and you will lose any
-custom data on it if the server reloads. The .db handler on Sessions is there to present a uniform
-API (so you can assume .db exists even if you don’t know if you receive an Object or a Session),
-but this is just an alias to .ndb. So don’t store any data on Sessions that you can’t afford to
-lose in a reload. You have been warned.
The number of sessions possible to connect to a given account at the same time and how it works is
-given by the MULTISESSION_MODE setting:
-
-
MULTISESSION_MODE=0: One session per account. When connecting with a new session the old one is
-disconnected. This is the default mode and emulates many classic mud code bases. In default Evennia,
-this mode also changes how the createaccount Command works - it will automatically create a
-Character with the same name as the Account. When logging in, the login command is also modified
-to have the player automatically puppet that Character. This makes the distinction between Account
-and Character minimal from the player’s perspective.
-
MULTISESSION_MODE=1: Many sessions per account, input/output from/to each session is treated the
-same. For the player this means they can connect to the game from multiple clients and see the same
-output in all of them. The result of a command given in one client (that is, through one Session)
-will be returned to all connected Sessions/clients with no distinction. This mode will have the
-Session(s) auto-create and puppet a Character in the same way as mode 0.
-
MULTISESSION_MODE=2: Many sessions per account, one character per session. In this mode,
-puppeting an Object/Character will link the puppet back only to the particular Session doing the
-puppeting. That is, input from that Session will make use of the CmdSet of that Object/Character and
-outgoing messages (such as the result of a look) will be passed back only to that puppeting
-Session. If another Session tries to puppet the same Character, the old Session will automatically
-un-puppet it. From the player’s perspective, this will mean that they can open separate game clients
-and play a different Character in each using one game account.
-This mode will not auto-create a Character and not auto-puppet on login like in modes 0 and 1.
-Instead it changes how the account-cmdsets’s OOCLook command works so as to show a simple
-‘character select’ menu.
-
MULTISESSION_MODE=3: Many sessions per account and character. This is the full multi-puppeting
-mode, where multiple sessions may not only connect to the player account but multiple sessions may
-also puppet a single character at the same time. From the user’s perspective it means one can open
-multiple client windows, some for controlling different Characters and some that share a Character’s
-input/output like in mode 1. This mode otherwise works the same as mode 2.
-
-
-
Note that even if multiple Sessions puppet one Character, there is only ever one instance of that
-Character.
When you use msg() to return data to a user, the object on which you call the msg() matters. The
-MULTISESSION_MODE also matters, especially if greater than 1.
-
For example, if you use account.msg("hello") there is no way for evennia to know which session it
-should send the greeting to. In this case it will send it to all sessions. If you want a specific
-session you need to supply its session to the msg call (account.msg("hello",session=mysession)).
-
On the other hand, if you call the msg() message on a puppeted object, like
-character.msg("hello"), the character already knows the session that controls it - it will
-cleverly auto-add this for you (you can specify a different session if you specifically want to send
-stuff to another session).
-
Finally, there is a wrapper for msg() on all command classes: command.msg(). This will
-transparently detect which session was triggering the command (if any) and redirects to that session
-(this is most often what you want). If you are having trouble redirecting to a given session,
-command.msg() is often the safest bet.
-
You can get the session in two main ways:
-
-
Accounts and Objects (including Characters) have a sessions property.
-This is a handler that tracks all Sessions attached to or puppeting them. Use e.g.
-accounts.sessions.get() to get a list of Sessions attached to that entity.
-
A Command instance has a session property that always points back to the Session that triggered
-it (it’s always a single one). It will be None if no session is involved, like when a mob or
-script triggers the Command.
When would one want to customize the Session object? Consider for example a character creation
-system: You might decide to keep this on the out-of-character level. This would mean that you create
-the character at the end of some sort of menu choice. The actual char-create cmdset would then
-normally be put on the account. This works fine as long as you are MULTISESSION_MODE below 2.
-For higher modes, replacing the Account cmdset will affect all your connected sessions, also those
-not involved in character creation. In this case you want to instead put the char-create cmdset on
-the Session level - then all other sessions will keep working normally despite you creating a new
-character in one of them.
-
By default, the session object gets the commands.default_cmdsets.UnloggedinCmdSet when the user
-first connects. Once the session is authenticated it has no default sets. To add a “logged-in”
-cmdset to the Session, give the path to the cmdset class with settings.CMDSET_SESSION. This set
-will then henceforth always be present as soon as the account logs in.
-
To customize further you can completely override the Session with your own subclass. To replace the
-default Session class, change settings.SERVER_SESSION_CLASS to point to your custom class. This is
-a dangerous practice and errors can easily make your game unplayable. Make sure to take heed of the
-original and make your
-changes carefully.
Note: This is considered an advanced topic. You don’t need to know this on a first read-through.
-
Evennia is split into two parts, the Portal and the Server. Each side tracks
-its own Sessions, syncing them to each other.
-
The “Session” we normally refer to is actually the ServerSession. Its counter-part on the Portal
-side is the PortalSession. Whereas the server sessions deal with game states, the portal session
-deals with details of the connection-protocol itself. The two are also acting as backups of critical
-data such as when the server reboots.
-
New Account connections are listened for and handled by the Portal using the [protocols](Portal-And-
-Server) it understands (such as telnet, ssh, webclient etc). When a new connection is established, a
-PortalSession is created on the Portal side. This session object looks different depending on
-which protocol is used to connect, but all still have a minimum set of attributes that are generic
-to all
-sessions.
-
These common properties are piped from the Portal, through the AMP connection, to the Server, which
-is now informed a new connection has been established. On the Server side, a ServerSession object
-is created to represent this. There is only one type of ServerSession; It looks the same
-regardless of how the Account connects.
-
From now on, there is a one-to-one match between the ServerSession on one side of the AMP
-connection and the PortalSession on the other. Data arriving to the Portal Session is sent on to
-its mirror Server session and vice versa.
-
During certain situations, the portal- and server-side sessions are
-“synced” with each other:
-
-
The Player closes their client, killing the Portal Session. The Portal syncs with the Server to
-make sure the corresponding Server Session is also deleted.
-
The Player quits from inside the game, killing the Server Session. The Server then syncs with the
-Portal to make sure to close the Portal connection cleanly.
-
The Server is rebooted/reset/shutdown - The Server Sessions are copied over (“saved”) to the
-Portal side. When the Server comes back up, this data is returned by the Portal so the two are again
-in sync. This way an Account’s login status and other connection-critical things can survive a
-server reboot (assuming the Portal is not stopped at the same time, obviously).
Both the Portal and Server each have a sessionhandler to manage the connections. These handlers
-are global entities contain all methods for relaying data across the AMP bridge. All types of
-Sessions hold a reference to their respective Sessionhandler (the property is called
-sessionhandler) so they can relay data. See protocols for more info
-on building new protocols.
-
To get all Sessions in the game (i.e. all currently connected clients), you access the server-side
-Session handler, which you get by
PyCharm is a Python developer’s IDE from Jetbrains available
-for Windows, Mac and Linux. It is a commercial product but offer free trials, a scaled-down
-community edition and also generous licenses for OSS projects like Evennia.
-
-
This page was originally tested on Windows (so use Windows-style path examples), but should work
-the same for all platforms.
-
-
First, install Evennia on your local machine with [[Getting Started]]. If you’re new to PyCharm,
-loading your project is as easy as selecting the Open option when PyCharm starts, and browsing to
-your game folder (the one created with evennia--init). We refer to it as mygame here.
-
If you want to be able to examine evennia’s core code or the scripts inside your virtualenv, you’ll
-need to add them to your project too:
-
-
Go to File>Open...
-
Select the folder (i.e. the evennia root)
-
Select “Open in current window” and “Add to currently opened projects”
Launch Evennia in your preferred way (usually from a console/terminal)
-
Open your project in PyCharm
-
In the PyCharm menu, select Run>AttachtoLocalProcess...
-
From the list, pick the twistd process with the server.py parameter (Example: twistd.exe--nodaemon--logfile=\<mygame\>\server\logs\server.log--python=\<evenniarepo\>\evennia\server\server.py)
-
-
Of course you can attach to the portal process as well. If you want to debug the Evennia launcher
-or runner for some reason (or just learn how they work!), see Run Configuration below.
-
-
NOTE: Whenever you reload Evennia, the old Server process will die and a new one start. So when
-you restart you have to detach from the old and then reattach to the new process that was created.
-
-
-
To make the process less tedious you can apply a filter in settings to show only the server.py
-process in the list. To do that navigate to: Settings/Preferences|Build,Execution,Deployment|PythonDebugger and then in Attachtoprocess field put in: twistd.exe"--nodaemon. This is an
-example for windows, I don’t have a working mac/linux box.
-
This configuration allows you to launch Evennia from inside PyCharm. Besides convenience, it also
-allows suspending and debugging the evennia_launcher or evennia_runner at points earlier than you
-could by running them externally and attaching. In fact by the time the server and/or portal are
-running the launcher will have exited already.
-
-
Go to Run>EditConfigutations...
-
Click the plus-symbol to add a new configuration and choose Python
-
Add the script: \<yourrepo\>\evenv\Scripts\evennia_launcher.py (substitute your virtualenv if
-it’s not named evenv)
-
Set script parameters to: start-l (-l enables console logging)
-
Ensure the chosen interpreter is from your virtualenv
-
Set Working directory to your mygame folder (not evenv nor evennia)
-
You can refer to the PyCharm documentation for general info, but you’ll want to set at least a
-config name (like “MyMUD start” or similar).
-
-
Now set up a “stop” configuration by following the same steps as above, but set your Script
-parameters to: stop (and name the configuration appropriately).
-
A dropdown box holding your new configurations should appear next to your PyCharm run button.
-Select MyMUD start and press the debug icon to begin debugging. Depending on how far you let the
-program run, you may need to run your “MyMUD stop” config to actually stop the server, before you’ll
-be able start it again.
-
-
-
Alternative run configuration - utilizing logfiles as source of data¶
-
This configuration takes a bit different approach as instead of focusing on getting the data back
-through logfiles. Reason for that is this way you can easily separate data streams, for example you
-rarely want to follow both server and portal at the same time, and this will allow it. This will
-also make sure to stop the evennia before starting it, essentially working as reload command (it
-will also include instructions how to disable that part of functionality). We will start by defining
-a configuration that will stop evennia. This assumes that upfire is your pycharm project name, and
-also the game name, hence the upfire/upfire path.
-
-
Go to Run>EditConfigutations...\
-
Click the plus-symbol to add a new configuration and choose the python interpreter to use (should
-be project default)
-
Name the configuration as “stop evennia” and fill rest of the fields accordingly to the image:
-
-
Press Apply
-
-
Now we will define the start/reload command that will make sure that evennia is not running already,
-and then start the server in one go.
-
-
Go to Run>EditConfigutations...\
-
Click the plus-symbol to add a new configuration and choose the python interpreter to use (should
-be project default)
-
Name the configuration as “start evennia” and fill rest of the fields accordingly to the image:
-
-
Navigate to the Logs tab and add the log files you would like to follow. The picture shows
-adding portal.log which will show itself in portal tab when running:
-
-
Skip the following steps if you don’t want the launcher to stop evennia before starting.
-
Head back to Configuration tab and press the + sign at the bottom, under Beforelaunch....
-and select Runanotherconfiguration from the submenu that will pop up.
-
Click stopevennia and make sure that it’s added to the list like on the image above.
-
Click Apply and close the run configuration window.
-
-
You are now ready to go, and if you will fire up startevennia configuration you should see
-following in the bottom panel:
-
-and you can click through the tabs to check appropriate logs, or even the console output as it is
-still running in interactive mode.
This is feature available from evennia 0.9 and onward.
-
There are multiple ways for you to plug in your own functionality into Evennia.
-The most common way to do so is through hooks - methods on typeclasses that
-gets called at particular events. Hooks are great when you want a game entity
-to behave a certain way when something happens to it. Signals complements
-hooks for cases when you want to easily attach new functionality without
-overriding things on the typeclass.
-
When certain events happen in Evennia, a Signal is fired. The idea is that
-you can “attach” any number of event-handlers to these signals. You can attach
-any number of handlers and they’ll all fire whenever any entity triggers the
-signal.
This particular signal fires after (post) an Account has connected to the game.
-When that happens, myhandler will fire with the sender being the Account that just connected.
-
If you want to respond only to the effects of a specific entity you can do so
-like this:
All signals (including some django-specific defaults) are available in the module
-evennia.server.signals
-(with a shortcut evennia.signals). Signals are named by the sender type. So SIGNAL_ACCOUNT_*
-returns
-Account instances as senders, SIGNAL_OBJECT_* returns Objects etc. Extra keywords (kwargs)
-should
-be extracted from the **kwargs dict in the signal handler.
-
-
SIGNAL_ACCOUNT_POST_CREATE - this is triggered at the very end of Account.create(). Note that
-calling evennia.create.create_account (which is called internally by Account.create) will
-not
-trigger this signal. This is because using Account.create() is expected to be the most commonly
-used way for users to themselves create accounts during login. It passes and extra kwarg ip with
-the client IP of the connecting account.
-
SIGNAL_ACCOUNT_POST_LOGIN - this will always fire when the account has authenticated. Sends
-extra kwarg session with the new Session object involved.
-
SIGNAL_ACCCOUNT_POST_FIRST_LOGIN - this fires just before SIGNAL_ACCOUNT_POST_LOGIN but only
-if
-this is the first connection done (that is, if there are no previous sessions connected). Also
-passes the session along as a kwarg.
-
SIGNAL_ACCOUNT_POST_LOGIN_FAIL - sent when someone tried to log into an account by failed.
-Passes
-the session as an extra kwarg.
-
SIGNAL_ACCOUNT_POST_LOGOUT - always fires when an account logs off, no matter if other sessions
-remain or not. Passes the disconnecting session along as a kwarg.
-
SIGNAL_ACCOUNT_POST_LAST_LOGOUT - fires before SIGNAL_ACCOUNT_POST_LOGOUT, but only if this is
-the last Session to disconnect for that account. Passes the session as a kwarg.
-
SIGNAL_OBJECT_POST_PUPPET - fires when an account puppets this object. Extra kwargs session
-and account represent the puppeting entities.
-SIGNAL_OBJECT_POST_UNPUPPET - fires when the sending object is unpuppeted. Extra kwargs are
-session and account.
-
SIGNAL_ACCOUNT_POST_RENAME - triggered by the setting of Account.username. Passes extra
-kwargs old_name, new_name.
-
SIGNAL_TYPED_OBJECT_POST_RENAME - triggered when any Typeclassed entity’s key is changed.
-Extra
-kwargs passed are old_key and new_key.
-
SIGNAL_SCRIPT_POST_CREATE - fires when a script is first created, after any hooks.
-
SIGNAL_CHANNEL_POST_CREATE - fires when a Channel is first created, after any hooks.
-
SIGNAL_HELPENTRY_POST_CREATE - fires when a help entry is first created.
-
-
The evennia.signals module also gives you conveneient access to the default Django signals (these
-use a
-different naming convention).
-
-
pre_save - fired when any database entitiy’s .save method fires, before any saving has
-happened.
-
post_save - fires after saving a database entity.
-
pre_delete - fires just before a database entity is deleted.
-
post_delete - fires after a database entity was deleted.
-
pre_init - fires before a typeclass’ __init__ method (which in turn
-happens before the at_init hook fires).
-
post_init - triggers at the end of __init__ (still before the at_init hook).
-
-
These are highly specialized Django signals that are unlikely to be useful to most users. But
-they are included here for completeness.
-
-
m2m_changed - fires after a Many-to-Many field (like db_attributes) changes.
-
pre_migrate - fires before database migration starts with evenniamigrate.
-
post_migrate - fires after database migration finished.
-
request_started - sent when HTTP request begins.
-
request_finished - sent when HTTP request ends.
-
settings_changed - sent when changing settings due to @override_settings
-decorator (only relevant for unit testing)
-
template_rendered - sent when test system renders http template (only useful for unit tests).
-
connection_creation - sent when making initial connection to database.
Softcode is a very simple programming language that was created for in-game development on TinyMUD
-derivatives such as MUX, PennMUSH, TinyMUSH, and RhostMUSH. The idea is that by providing a stripped
-down, minimalistic language for in-game use, you can allow quick and easy building and game
-development to happen without having to learn C/C++. There is an added benefit of not having to have
-to hand out shell access to all developers, and permissions can be used to alleviate many security
-problems.
-
Writing and installing softcode is done through a MUD client. Thus it is not a formatted language.
-Each softcode function is a single line of varying size. Some functions can be a half of a page long
-or more which is obviously not very readable nor (easily) maintainable over time.
Pasting this into a MUX/MUSH and typing ‘hello’ will theoretically yield ‘Hello World!’, assuming
-certain flags are not set on your account object.
-
Setting attributes is done via @set. Softcode also allows the use of the ampersand (&) symbol.
-This shorter version looks like this:
-
&HELLO_WORLD.C me=$hello:@pemit %#=Hello World!
-
-
-
Perhaps I want to break the Hello World into an attribute which is retrieved when emitting:
-
&HELLO_VALUE.D me=Hello World
- &HELLO_WORLD.C me=$hello:@pemit %#=[v(HELLO_VALUE.D)]
-
-
-
The v() function returns the HELLO_VALUE.D attribute on the object that the command resides
-(me, which is yourself in this case). This should yield the same output as the first example.
-
If you are still curious about how Softcode works, take a look at some external resources:
Softcode is excellent at what it was intended for: simple things. It is a great tool for making an
-interactive object, a room with ambiance, simple global commands, simple economies and coded
-systems. However, once you start to try to write something like a complex combat system or a higher
-end economy, you’re likely to find yourself buried under a mountain of functions that span multiple
-objects across your entire code.
-
Not to mention, softcode is not an inherently fast language. It is not compiled, it is parsed with
-each calling of a function. While MUX and MUSH parsers have jumped light years ahead of where they
-once were they can still stutter under the weight of more complex systems if not designed properly.
Now that starting text-based games is easy and an option for even the most technically inarticulate,
-new projects are a dime a dozen. People are starting new MUDs every day with varying levels of
-commitment and ability. Because of this shift from fewer, larger, well-staffed games to a bunch of
-small, one or two developer games, some of the benefit of softcode fades.
-
Softcode is great in that it allows a mid to large sized staff all work on the same game without
-stepping on one another’s toes. As mentioned before, shell access is not necessary to develop a MUX
-or a MUSH. However, now that we are seeing a lot more small, one or two-man shops, the issue of
-shell access and stepping on each other’s toes is a lot less.
Evennia shuns in-game softcode for on-disk Python modules. Python is a popular, mature and
-professional programming language. You code it using the conveniences of modern text editors.
-Evennia developers have access to the entire library of Python modules out there in the wild - not
-to mention the vast online help resources available. Python code is not bound to one-line functions
-on objects but complex systems may be organized neatly into real source code modules, sub-modules,
-or even broken out into entire Python packages as desired.
-
So what is not included in Evennia is a MUX/MOO-like online player-coding system. Advanced coding
-in Evennia is primarily intended to be done outside the game, in full-fledged Python modules.
-Advanced building is best handled by extending Evennia’s command system with your own sophisticated
-building commands. We feel that with a small development team you are better off using a
-professional source-control system (svn, git, bazaar, mercurial etc) anyway.
Adding advanced and flexible building commands to your game is easy and will probably be enough to
-satisfy most creative builders. However, if you really, really want to offer online coding, there
-is of course nothing stopping you from adding that to Evennia, no matter our recommendations. You
-could even re-implement MUX’ softcode in Python should you be very ambitious. The
-in-game-python is an optional
-pseudo-softcode plugin aimed at developers wanting to script their game from inside it.
The spawner is a system for defining and creating individual objects from a base template called a
-prototype. It is only designed for use with in-game Objects, not any other type of
-entity.
-
The normal way to create a custom object in Evennia is to make a Typeclass. If you
-haven’t read up on Typeclasses yet, think of them as normal Python classes that save to the database
-behind the scenes. Say you wanted to create a “Goblin” enemy. A common way to do this would be to
-first create a Mobile typeclass that holds everything common to mobiles in the game, like generic
-AI, combat code and various movement methods. A Goblin subclass is then made to inherit from
-Mobile. The Goblin class adds stuff unique to goblins, like group-based AI (because goblins are
-smarter in a group), the ability to panic, dig for gold etc.
-
But now it’s time to actually start to create some goblins and put them in the world. What if we
-wanted those goblins to not all look the same? Maybe we want grey-skinned and green-skinned goblins
-or some goblins that can cast spells or which wield different weapons? We could make subclasses of
-Goblin, like GreySkinnedGoblin and GoblinWieldingClub. But that seems a bit excessive (and a
-lot of Python code for every little thing). Using classes can also become impractical when wanting
-to combine them - what if we want a grey-skinned goblin shaman wielding a spear - setting up a web
-of classes inheriting each other with multiple inheritance can be tricky.
-
This is what the prototype is for. It is a Python dictionary that describes these per-instance
-changes to an object. The prototype also has the advantage of allowing an in-game builder to
-customize an object without access to the Python backend. Evennia also allows for saving and
-searching prototypes so other builders can find and use (and tweak) them later. Having a library of
-interesting prototypes is a good reasource for builders. The OLC system allows for creating, saving,
-loading and manipulating prototypes using a menu system.
-
The spawner takes a prototype and uses it to create (spawn) new, custom objects.
Enter the olc command or @spawn/olc to enter the prototype wizard. This is a menu system for
-creating, loading, saving and manipulating prototypes. It’s intended to be used by in-game builders
-and will give a better understanding of prototypes in general. Use help on each node of the menu
-for more information. Below are further details about how prototypes work and how they are used.
The prototype dictionary can either be created for you by the OLC (see above), be written manually
-in a Python module (and then referenced by the @spawn command/OLC), or created on-the-fly and
-manually loaded into the spawner function or @spawn command.
-
The dictionary defines all possible database-properties of an Object. It has a fixed set of allowed
-keys. When preparing to store the prototype in the database (or when using the OLC), some
-of these keys are mandatory. When just passing a one-time prototype-dict to the spawner the system
-is
-more lenient and will use defaults for keys not explicitly provided.
-
In dictionary form, a prototype can look something like this:
Note that the prototype dict as given on the command line must be a valid Python structure -
-so you need to put quotes around strings etc. For security reasons, a dict inserted from-in game
-cannot have any
-other advanced Python functionality, such as executable code, lambda etc. If builders are supposed
-to be able to use such features, you need to offer them through [$protfuncs](Spawner-and-
-Prototypes#protfuncs), embedded runnable functions that you have full control to check and vet
-before running.
All keys starting with prototype_ are for book keeping.
-
-
prototype_key - the ‘name’ of the prototype. While this can sometimes be skipped (such as when
-defining a prototype in a module or feeding a prototype-dict manually to the spawner function),
-it’s good
-practice to try to include this. It is used for book-keeping and storing of the prototype so you
-can find it later.
-
prototype_parent - If given, this should be the prototype_key of another prototype stored in
-the system or available in a module. This makes this prototype inherit the keys from the
-parent and only override what is needed. Give a tuple (parent1,parent2,...) for multiple
-left-right inheritance. If this is not given, a typeclass should usually be defined (below).
-
prototype_desc - this is optional and used when listing the prototype in in-game listings.
-
protototype_tags - this is optional and allows for tagging the prototype in order to find it
-easier later.
-
prototype_locks - two lock types are supported: edit and spawn. The first lock restricts
-the copying and editing of the prototype when loaded through the OLC. The second determines who
-may use the prototype to create new objects.
-
-
The remaining keys determine actual aspects of the objects to spawn from this prototype:
-
-
key - the main object identifier. Defaults to “Spawned Object X”, where X is a random
-integer.
-
typeclass - A full python-path (from your gamedir) to the typeclass you want to use. If not
-set, the prototype_parent should be
-defined, with typeclass defined somewhere in the parent chain. When creating a one-time
-prototype
-dict just for spawning, one could omit this - settings.BASE_OBJECT_TYPECLASS will be used
-instead.
-
location - this should be a #dbref.
-
home - a valid #dbref. Defaults to location or settings.DEFAULT_HOME if location does not
-exist.
-
destination - a valid #dbref. Only used by exits.
-
permissions - list of permission strings, like ["Accounts","may_use_red_door"]
-
locks - a lock-string like "edit:all();control:perm(Builder)"
-
aliases - list of strings for use as aliases
-
tags - list Tags. These are given as tuples (tag,category,data).
-
attrs - list of Attributes. These are given as tuples (attrname,value,category,lockstring)
-
Any other keywords are interpreted as non-category Attributes and their values.
-This is
-convenient for simple Attributes - use attrs for full control of Attributes.
-
-
Deprecated as of Evennia 0.8:
-
-
ndb_<name> - sets the value of a non-persistent attribute ("ndb_" is stripped from the name).
-This is simply not useful in a prototype and is deprecated.
-
exec - This accepts a code snippet or a list of code snippets to run. This should not be used -
-use callables or $protfuncs instead (see below).
Finally, the value can be a prototype function (Protfunc). These look like simple function calls
-that you embed in strings and that has a $ in front, like
-
{"key":"$choice(Urfgar, Rick the smelly, Blargh the foul)",
- "attrs":{"desc":"This is a large $red(and very red) demon. "
- "He has $randint(2,5) skulls in a chain around his neck."}
-
-
-
At execution time, the place of the protfunc will be replaced with the result of that protfunc being
-called (this is always a string). A protfunc works in much the same way as an
-InlineFunc - they are actually
-parsed using the same parser - except protfuncs are run every time the prototype is used to spawn a
-new object (whereas an inlinefunc is called when a text is returned to the user).
-
Here is how a protfunc is defined (same as an inlinefunc).
-
# this is a silly example, you can just color the text red with |r directly!
-defred(*args,**kwargs):
- """
- Usage: $red(<text>)
- Returns the same text you entered, but red.
- """
- ifnotargsorlen(args)>1:
- raiseValueError("Must have one argument, the text to color red!")
- return"|r{}|n".format(args[0])
-
-
-
-
Note that we must make sure to validate input and raise ValueError if that fails. Also, it is
-not possible to use keywords in the call to the protfunc (so something like $echo(text,align=left) is invalid). The kwargs requred is for internal evennia use and not used at all for
-protfuncs (only by inlinefuncs).
-
-
To make this protfunc available to builders in-game, add it to a new module and add the path to that
-module to settings.PROT_FUNC_MODULES:
-
# in mygame/server/conf/settings.py
-
-PROT_FUNC_MODULES+=["world.myprotfuncs"]
-
-
-
-
All global callables in your added module will be considered a new protfunc. To avoid this (e.g.
-to have helper functions that are not protfuncs on their own), name your function something starting
-with _.
-
The default protfuncs available out of the box are defined in evennia/prototypes/profuncs.py. To
-override the ones available, just add the same-named function in your own protfunc module.
-
| Protfunc | Description |
-
| $random() | Returns random value in range [0, 1) |
-| $randint(start,end) | Returns random value in range [start, end] |
-| $left_justify(<text>) | Left-justify text |
-| $right_justify(<text>) | Right-justify text to screen width |
-| $center_justify(<text>) | Center-justify text to screen width |
-| $full_justify(<text>) | Spread text across screen width by adding spaces |
-| $protkey(<name>) | Returns value of another key in this prototype (self-reference) |
-| $add(<value1>,<value2>) | Returns value1 + value2. Can also be lists, dicts etc |
-| $sub(<value1>,<value2>) | Returns value1 - value2 |
-| $mult(<value1>,<value2>) | Returns value1 * value2 |
-| $div(<value1>,<value2>) | Returns value2 / value1 |
-| $toint(<value>) | Returns value converted to integer (or value if not possible) |
-| $eval(<code>) | Returns result of literal-
-eval of code string. Only simple
-python expressions. |
-| $obj(<query>) | Returns object #dbref searched globally by key, tag or #dbref. Error if more
-than one found.” |
-| $objlist(<query>) | Like $obj, except always returns a list of zero, one or more results. |
-| $dbref(dbref) | Returns argument if it is formed as a #dbref (e.g. #1234), otherwise error.
-
For developers with access to Python, using protfuncs in prototypes is generally not useful. Passing
-real Python functions is a lot more powerful and flexible. Their main use is to allow in-game
-builders to
-do limited coding/scripting for their prototypes without giving them direct access to raw Python.
Stored as Scripts in the database. These are sometimes referred to as database-
-prototypes This is the only way for in-game builders to modify and add prototypes. They have the
-advantage of being easily modifiable and sharable between builders but you need to work with them
-using in-game tools.
These prototypes are defined as dictionaries assigned to global variables in one of the modules
-defined in settings.PROTOTYPE_MODULES. They can only be modified from outside the game so they are
-are necessarily “read-only” from in-game and cannot be modified (but copies of them could be made
-into database-prototypes). These were the only prototypes available before Evennia 0.8. Module based
-prototypes can be useful in order for developers to provide read-only “starting” or “base”
-prototypes to build from or if they just prefer to work offline in an external code editor.
-
By default mygame/world/prototypes.py is set up for you to add your own prototypes. All global
-dicts in this module will be considered by Evennia to be a prototype. You could also tell Evennia
-to look for prototypes in more modules if you want:
-
# in mygame/server/conf/settings.py
-
-PROTOTYPE_MODULES+=["world.myownprototypes","combat.prototypes"]
-
-
-
-
-
Note the += operator in the above example. This will extend the already defined world.prototypes
-definition in the settings_default.py file in Evennia. If you would like to completely override the
-location of your PROTOTYPE_MODULES then set this to just = without the addition operator.
-
-
Here is an example of a prototype defined in a module:
-
# in a module Evennia looks at for prototypes,
- # (like mygame/world/prototypes.py)
-
- ORC_SHAMAN={"key":"Orc shaman",
- "typeclass":"typeclasses.monsters.Orc",
- "weapon":"wooden staff",
- "health":20}
-
-
-
-
Note that in the example above, "ORC_SHAMAN" will become the prototype_key of this prototype.
-It’s the only case when prototype_key can be skipped in a prototype. However, if prototype_key
-was given explicitly, that would take precedence. This is a legacy behavior and it’s recommended
-that you always add prototype_key to be consistent.
The spawner can be used from inside the game through the Builder-only @spawn command. Assuming the
-“goblin” typeclass is available to the system (either as a database-prototype or read from module),
-you can spawn a new goblin with
-
@spawn goblin
-
-
-
You can also specify the prototype directly as a valid Python dictionary:
Note: The @spawn command is more lenient about the prototype dictionary than shown here. So you
-can for example skip the prototype_key if you are just testing a throw-away prototype. A random
-hash will be used to please the validation. You could also skip prototype_parent/typeclass - then
-the typeclass given by settings.BASE_OBJECT_TYPECLASS will be used.
All arguments are prototype dictionaries or the unique prototype_keys of prototypes
-known to the system (either database- or module-based). The function will return a matching list of
-created objects. Example:
Hint: Same as when using @spawn, when spawning from a one-time prototype dict like this, you can
-skip otherwise required keys, like prototype_key or typeclass/prototype_parent. Defaults will
-be used.
-
-
Note that no location will be set automatically when using evennia.prototypes.spawner.spawn(),
-you
-have to specify location explicitly in the prototype dict.
-
If the prototypes you supply are using prototype_parent keywords, the spawner will read prototypes
-from modules
-in settings.PROTOTYPE_MODULES as well as those saved to the database to determine the body of
-available parents. The spawn command takes many optional keywords, you can find its definition in
-the api docs.
You control Evennia from your game folder (we refer to it as mygame/ here), using the evennia
-program. If the evennia program is not available on the command line you must first install
-Evennia as described in the Getting Started page.
-
-
Hint: If you ever try the evennia command and get an error complaining that the command is not
-available, make sure your virtualenv is active.
-
-
Below are described the various management options. Run
Evennia consists of two components, the Evennia Server and Portal. Briefly,
-the Server is what is running the mud. It handles all game-specific things but doesn’t care
-exactly how players connect, only that they have. The Portal is a gateway to which players
-connect. It knows everything about telnet, ssh, webclient protocols etc but very little about the
-game. Both are required for a functioning mud.
-
evennia start
-
-
-
The above command will start the Portal, which in turn will boot up the Server. The command will
-print a summary of the process and unless there is an error you will see no further output. Both
-components will instead log to log files in mygame/server/logs/. For convenience you can follow
-those logs directly in your terminal by attaching -l to commands:
-
evennia -l
-
-
-
Will start following the logs of an already running server. When starting Evennia you can also do
Normally, Evennia runs as a ‘daemon’, in the background. If you want you can start either of the
-processes (but not both) as foreground processes in interactive mode. This means they will log
-directly to the terminal (rather than to log files that we then echo to the terminal) and you can
-kill the process (not just the log-file view) with Ctrl-C.
-
evennia istart
-
-
-
will start/restart the Server in interactive mode. This is required if you want to run a
-debugger. Next time you reload the server, it will return to normal mode.
-
evennia ipstart
-
-
-
will start the Portal in interactive mode. This is usually only necessary if you want to run
-Evennia under the control of some other type of process.
The act of reloading means the Portal will tell the Server to shut down and then boot it back up
-again. Everyone will get a message and the game will be briefly paused for all accounts as the
-server
-reboots. Since they are connected to the Portal, their connections are not lost.
-
Reloading is as close to a “warm reboot” you can get. It reinitializes all code of Evennia, but
-doesn’t kill “persistent” Scripts. It also calls at_server_reload() hooks on all
-objects so you
-can save eventual temporary properties you want.
-
From in-game the @reload command is used. You can also reload the server from outside the game:
-
evennia reload
-
-
-
Sometimes reloading from “the outside” is necessary in case you have added some sort of bug that
-blocks in-game input.
Resetting is the equivalent of a “cold reboot” - the Server will shut down and then restarted
-again, but will behave as if it was fully shut down. As opposed to a “real” shutdown, no accounts
-will be disconnected during a
-reset. A reset will however purge all non-persistent scripts and will call at_server_shutdown()
-hooks. It can be a good way to clean unsafe scripts during development, for example.
-
From in-game the @reset command is used. From the terminal:
A full shutdown closes Evennia completely, both Server and Portal. All accounts will be booted and
-systems saved and turned off cleanly.
-
From inside the game you initiate a shutdown with the @shutdown command. From command line you do
-
evennia stop
-
-
-
You will see messages of both Server and Portal closing down. All accounts will see the shutdown
-message and then be disconnected. The same effect happens if you press Ctrl+C while the server
-runs in interactive mode.
In the extreme case that neither of the server processes locks up and does not respond to commands,
-you can send them kill-signals to force them to shut down. To kill only the Server:
-
evennia skill
-
-
-
To kill both Server and Portal:
-
evennia kill
-
-
-
Note that this functionality is not supported on Windows.
If you should need to manually manage Evennia’s processors (or view them in a task manager program
-such as Linux’ top or the more advanced htop), you will find the following processes to be
-related to Evennia:
-
-
1 x twistd...evennia/server/portal/portal.py - this is the Portal process.
-
3 x twistd...server.py - One of these processes manages Evennia’s Server component, the main
-game. The other processes (with the same name but different process id) handle’s Evennia’s
-internal web server threads. You can look at mygame/server/server.pid to determine which is the
-main process.
During development, you will usually modify code and then reload the server to see your changes.
-This is done by Evennia re-importing your custom modules from disk. Usually bugs in a module will
-just have you see a traceback in the game, in the log or on the command line. For some really
-serious syntax errors though, your module might not even be recognized as valid Python. Evennia may
-then fail to restart correctly.
-
From inside the game you see a text about the Server restarting followed by an ever growing list of
-“…”. Usually this only lasts a very short time (up to a few seconds). If it seems to go on, it
-means the Portal is still running (you are still connected to the game) but the Server-component of
-Evennia failed to restart (that is, it remains in a shut-down state). Look at your log files or
-terminal to see what the problem is - you will usually see a clear traceback showing what went
-wrong.
-
Fix your bug then run
-
evennia start
-
-
-
Assuming the bug was fixed, this will start the Server manually (while not restarting the Portal).
-In-game you should now get the message that the Server has successfully restarted.
This tutorial describes the creation of an in-game map display based on a pre-drawn map. It also
-details how to use the Batch code processor for advanced building. There is
-also the Dynamic in-game map tutorial that works in the opposite direction,
-by generating a map from an existing grid of rooms.
-
Evennia does not require its rooms to be positioned in a “logical” way. Your exits could be named
-anything. You could make an exit “west” that leads to a room described to be in the far north. You
-could have rooms inside one another, exits leading back to the same room or describing spatial
-geometries impossible in the real world.
-
That said, most games do organize their rooms in a logical fashion, if nothing else to retain the
-sanity of their players. And when they do, the game becomes possible to map. This tutorial will give
-an example of a simple but flexible in-game map system to further help player’s to navigate. We will
-
To simplify development and error-checking we’ll break down the work into bite-size chunks, each
-building on what came before. For this we’ll make extensive use of the Batch code processor, so you may want to familiarize yourself with that.
-
-
Planning the map - Here we’ll come up with a small example map to use for the rest of the
-tutorial.
-
Making a map object - This will showcase how to make a static in-game “map” object a
-Character could pick up and look at.
-
Building the map areas - Here we’ll actually create the small example area according to the
-map we designed before.
-
Map code - This will link the map to the location so our output looks something like this:
-
crossroads(#3)
-↑╚∞╝↑
-≈↑│↑∩ The merger of two roads. To the north looms a mighty castle.
-O─O─O To the south, the glow of a campfire can be seen. To the east lie
-≈↑│↑∩ the vast mountains and to the west is heard the waves of the sea.
-↑▲O▲↑
-
-Exits: north(#8), east(#9), south(#10), west(#11)
-
-
-
-
-
We will henceforth assume your game folder is name named mygame and that you haven’t modified the
-default commands. We will also not be using Colors for our map since they
-don’t show in the documentation wiki.
Let’s begin with the fun part! Maps in MUDs come in many different shapes and sizes. Some appear as just boxes connected by lines. Others have complex graphics that are
-external to the game itself.
-
Our map will be in-game text but that doesn’t mean we’re restricted to the normal alphabet! If
-you’ve ever selected the Wingdings font in Microsoft Word
-you will know there are a multitude of other characters around to use. When creating your game with
-Evennia you have access to the UTF-8 character encoding which
-put at your disposal thousands of letters, number and geometric shapes.
-
For this exercise, we’ve copy-and-pasted from the pallet of special characters used over at Dwarf
-Fortress to create what is hopefully a
-pleasing and easy to understood landscape:
-
≈≈↑↑↑↑↑∩∩
-≈≈↑╔═╗↑∩∩ Places the account can visit are indicated by "O".
-≈≈↑║O║↑∩∩ Up the top is a castle visitable by the account.
-≈≈↑╚∞╝↑∩∩ To the right is a cottage and to the left the beach.
-≈≈≈↑│↑∩∩∩ And down the bottom is a camp site with tents.
-≈≈O─O─O⌂∩ In the center is the starting location, a crossroads
-≈≈≈↑│↑∩∩∩ which connect the four other areas.
-≈≈↑▲O▲↑∩∩
-≈≈↑↑▲↑↑∩∩
-≈≈↑↑↑↑↑∩∩
-
-
-
There are many considerations when making a game map depending on the play style and requirements
-you intend to implement. Here we will display a 5x5 character map of the area surrounding the
-account. This means making sure to account for 2 characters around every visitable location. Good
-planning at this stage can solve many problems before they happen.
In this section we will try to create an actual “map” object that an account can pick up and look
-at.
-
Evennia offers a range of default commands for creating objects and rooms
-in-game. While readily accessible, these commands are made to do very
-specific, restricted things and will thus not offer as much flexibility to experiment (for an
-advanced exception see in-line functions). Additionally, entering long
-descriptions and properties over and over in the game client can become tedious; especially when
-testing and you may want to delete and recreate things over and over.
-
To overcome this, Evennia offers batch processors that work as input-files
-created out-of-game. In this tutorial we’ll be using the more powerful of the two available batch
-processors, the Batch Code Processor , called with the @batchcode command.
-This is a very powerful tool. It allows you to craft Python files to act as blueprints of your
-entire game world. These files have access to use Evennia’s Python API directly. Batchcode allows
-for easy editing and creation in whatever text editor you prefer, avoiding having to manually build
-the world line-by-line inside the game.
-
-
Important warning: @batchcode’s power is only rivaled by the @py command. Batchcode is so
-powerful it should be reserved only for the superuser. Think carefully
-before you let others (such as Developer- level staff) run @batchcode on their own - make sure
-you are okay with them running arbitrary Python code on your server.
-
-
While a simple example, the map object it serves as good way to try out @batchcode. Go to
-mygame/world and create a new file there named batchcode_map.py:
-
# mygame/world/batchcode_map.py
-
-fromevenniaimportcreate_object
-fromevenniaimportDefaultObject
-
-# We use the create_object function to call into existence a
-# DefaultObject named "Map" wherever you are standing.
-
-map=create_object(DefaultObject,key="Map",location=caller.location)
-
-# We then access its description directly to make it our map.
-
-map.db.desc="""
-≈≈↑↑↑↑↑∩∩
-≈≈↑╔═╗↑∩∩
-≈≈↑║O║↑∩∩
-≈≈↑╚∞╝↑∩∩
-≈≈≈↑│↑∩∩∩
-≈≈O─O─O⌂∩
-≈≈≈↑│↑∩∩∩
-≈≈↑▲O▲↑∩∩
-≈≈↑↑▲↑↑∩∩
-≈≈↑↑↑↑↑∩∩
-"""
-
-# This message lets us know our map was created successfully.
-caller.msg("A map appears out of thin air and falls to the ground.")
-
-
-
Log into your game project as the superuser and run the command
-
@batchcodebatchcode_map
-
-
-
This will load your batchcode_map.py file and execute the code (Evennia will look in your world/
-folder automatically so you don’t need to specify it).
-
A new map object should have appeared on the ground. You can view the map by using lookmap. Let’s
-take it with the getmap command. We’ll need it in case we get lost!
We’ve just used batchcode to create an object useful for our adventures. But the locations on that
-map does not actually exist yet - we’re all mapped up with nowhere to go! Let’s use batchcode to
-build a game area based on our map. We have five areas outlined: a castle, a cottage, a campsite, a
-coastal beach and the crossroads which connects them. Create a new batchcode file for this in
-mygame/world, named batchcode_world.py.
-
# mygame/world/batchcode_world.py
-
-fromevenniaimportcreate_object,search_object
-fromtypeclassesimportrooms,exits
-
-# We begin by creating our rooms so we can detail them later.
-
-centre=create_object(rooms.Room,key="crossroads")
-north=create_object(rooms.Room,key="castle")
-east=create_object(rooms.Room,key="cottage")
-south=create_object(rooms.Room,key="camp")
-west=create_object(rooms.Room,key="coast")
-
-# This is where we set up the cross roads.
-# The rooms description is what we see with the 'look' command.
-
-centre.db.desc="""
-The merger of two roads. A single lamp post dimly illuminates the lonely crossroads.
-To the north looms a mighty castle. To the south the glow of a campfire can be seen.
-To the east lie a wall of mountains and to the west the dull roar of the open sea.
-"""
-
-# Here we are creating exits from the centre "crossroads" location to
-# destinations to the north, east, south, and west. We will be able
-# to use the exit by typing it's key e.g. "north" or an alias e.g. "n".
-
-centre_north=create_object(exits.Exit,key="north",
- aliases=["n"],location=centre,destination=north)
-centre_east=create_object(exits.Exit,key="east",
- aliases=["e"],location=centre,destination=east)
-centre_south=create_object(exits.Exit,key="south",
- aliases=["s"],location=centre,destination=south)
-centre_west=create_object(exits.Exit,key="west",
- aliases=["w"],location=centre,destination=west)
-
-# Now we repeat this for the other rooms we'll be implementing.
-# This is where we set up the northern castle.
-
-north.db.desc="An impressive castle surrounds you. " \
- "There might be a princess in one of these towers."
-north_south=create_object(exits.Exit,key="south",
- aliases=["s"],location=north,destination=centre)
-
-# This is where we set up the eastern cottage.
-
-east.db.desc="A cosy cottage nestled among mountains " \
- "stretching east as far as the eye can see."
-east_west=create_object(exits.Exit,key="west",
- aliases=["w"],location=east,destination=centre)
-
-# This is where we set up the southern camp.
-
-south.db.desc="Surrounding a clearing are a number of " \
- "tribal tents and at their centre a roaring fire."
-south_north=create_object(exits.Exit,key="north",
- aliases=["n"],location=south,destination=centre)
-
-# This is where we set up the western coast.
-
-west.db.desc="The dark forest halts to a sandy beach. " \
- "The sound of crashing waves calms the soul."
-west_east=create_object(exits.Exit,key="east",
- aliases=["e"],location=west,destination=centre)
-
-# Lastly, lets make an entrance to our world from the default Limbo room.
-
-limbo=search_object('Limbo')[0]
-limbo_exit=create_object(exits.Exit,key="enter world",
- aliases=["enter"],location=limbo,destination=centre)
-
-
-
Apply this new batch code with @batchcodebatchcode_world. If there are no errors in the code we
-now have a nice mini-world to explore. Remember that if you get lost you can look at the map we
-created!
Now we have a landscape and matching map, but what we really want is a mini-map that displays
-whenever we move to a room or use the look command.
-
We could manually enter a part of the map into the description of every room like we did our map
-object description. But some MUDs have tens of thousands of rooms! Besides, if we ever changed our
-map we would have to potentially alter a lot of those room descriptions manually to match the
-change. So instead we will make one central module to hold our map. Rooms will reference this
-central location on creation and the map changes will thus come into effect when next running our
-batchcode.
-
To make our mini-map we need to be able to cut our full map into parts. To do this we need to put it
-in a format which allows us to do that easily. Luckily, python allows us to treat strings as lists
-of characters allowing us to pick out the characters we need.
-
mygame/world/map_module.py
-
# We place our map into a sting here.
-world_map="""\
-≈≈↑↑↑↑↑∩∩
-≈≈↑╔═╗↑∩∩
-≈≈↑║O║↑∩∩
-≈≈↑╚∞╝↑∩∩
-≈≈≈↑│↑∩∩∩
-≈≈O─O─O⌂∩
-≈≈≈↑│↑∩∩∩
-≈≈↑▲O▲↑∩∩
-≈≈↑↑▲↑↑∩∩
-≈≈↑↑↑↑↑∩∩
-"""
-
-# This turns our map string into a list of rows. Because python
-# allows us to treat strings as a list of characters, we can access
-# those characters with world_map[5][5] where world_map[row][column].
-world_map=world_map.split('\n')
-
-defreturn_map():
- """
- This function returns the whole map
- """
- map=""
-
- #For each row in our map, add it to map
- forvalueyinworld_map:
- map+=valuey
- map+="\n"
-
- returnmap
-
-defreturn_minimap(x,y,radius=2):
- """
- This function returns only part of the map.
- Returning all chars in a 2 char radius from (x,y)
- """
- map=""
-
- #For each row we need, add the characters we need.
- forvalueyinworld_map[y-radius:y+radius+1]:forvaluexinvaluey[x-radius:x+radius+1]:
- map+=valuex
- map+="\n"
-
- returnmap
-
-
-
With our map_module set up, let’s replace our hardcoded map in mygame/world/batchcode_map.py with
-a reference to our map module. Make sure to import our map_module!
-
# mygame/world/batchcode_map.py
-
-fromevenniaimportcreate_object
-fromevenniaimportDefaultObject
-fromworldimportmap_module
-
-map=create_object(DefaultObject,key="Map",location=caller.location)
-
-map.db.desc=map_module.return_map()
-
-caller.msg("A map appears out of thin air and falls to the ground.")
-
-
-
Log into Evennia as the superuser and run this batchcode. If everything worked our new map should
-look exactly the same as the old map - you can use @delete to delete the old one (use a number to
-pick which to delete).
-
Now, lets turn our attention towards our game’s rooms. Let’s use the return_minimap method we
-created above in order to include a minimap in our room descriptions. This is a little more
-complicated.
-
By itself we would have to settle for either the map being above the description with
-room.db.desc=map_string+description_string, or the map going below by reversing their order.
-Both options are rather unsatisfactory - we would like to have the map next to the text! For this
-solution we’ll explore the utilities that ship with Evennia. Tucked away in evennia\evennia\utils
-is a little module called EvTable . This is an advanced ASCII table
-creator for you to utilize in your game. We’ll use it by creating a basic table with 1 row and two
-columns (one for our map and one for our text) whilst also hiding the borders. Open the batchfile
-again
-
# mygame\world\batchcode_world.py
-
-# Add to imports
-fromevennia.utilsimportevtable
-fromworldimportmap_module
-
-# [...]
-
-# Replace the descriptions with the below code.
-
-# The cross roads.
-# We pass what we want in our table and EvTable does the rest.
-# Passing two arguments will create two columns but we could add more.
-# We also specify no border.
-centre.db.desc=evtable.EvTable(map_module.return_minimap(4,5),
- "The merger of two roads. A single lamp post dimly " \
- "illuminates the lonely crossroads. To the north " \
- "looms a mighty castle. To the south the glow of " \
- "a campfire can be seen. To the east lie a wall of " \
- "mountains and to the west the dull roar of the open sea.",
- border=None)
-# EvTable allows formatting individual columns and cells. We use that here
-# to set a maximum width for our description, but letting the map fill
-# whatever space it needs.
-centre.db.desc.reformat_column(1,width=70)
-
-# [...]
-
-# The northern castle.
-north.db.desc=evtable.EvTable(map_module.return_minimap(4,2),
- "An impressive castle surrounds you. There might be " \
- "a princess in one of these towers.",
- border=None)
-north.db.desc.reformat_column(1,width=70)
-
-# [...]
-
-# The eastern cottage.
-east.db.desc=evtable.EvTable(map_module.return_minimap(6,5),
- "A cosy cottage nestled among mountains stretching " \
- "east as far as the eye can see.",
- border=None)
-east.db.desc.reformat_column(1,width=70)
-
-# [...]
-
-# The southern camp.
-south.db.desc=evtable.EvTable(map_module.return_minimap(4,7),
- "Surrounding a clearing are a number of tribal tents " \
- "and at their centre a roaring fire.",
- border=None)
-south.db.desc.reformat_column(1,width=70)
-
-# [...]
-
-# The western coast.
-west.db.desc=evtable.EvTable(map_module.return_minimap(2,5),
- "The dark forest halts to a sandy beach. The sound of " \
- "crashing waves calms the soul.",
- border=None)
-west.db.desc.reformat_column(1,width=70)
-
-
-
Before we run our new batchcode, if you are anything like me you would have something like 100 maps
-lying around and 3-4 different versions of our rooms extending from limbo. Let’s wipe it all and
-start with a clean slate. In Command Prompt you can run evenniaflush to clear the database and
-start anew. It won’t reset dbref values however, so if you are at #100 it will start from there.
-Alternatively you can navigate to mygame/server and delete the evennia.db3 file. Now in Command
-Prompt use evenniamigrate to have a completely freshly made database.
-
Log in to evennia and run @batchcodebatchcode_world and you’ll have a little world to explore.
You should now have a mapped little world and a basic understanding of batchcode, EvTable and how
-easily new game defining features can be added to Evennia.
A common task of a game designer is to organize and find groups of objects and do operations on
-them. A classic example is to have a weather script affect all “outside” rooms. Another would be for
-a player casting a magic spell that affects every location “in the dungeon”, but not those
-“outside”. Another would be to quickly find everyone joined with a particular guild or everyone
-currently dead.
-
Tags are short text labels that you attach to objects so as to easily be able to retrieve and
-group them. An Evennia entity can be tagged with any number of Tags. On the database side, Tag
-entities are shared between all objects with that tag. This makes them very efficient but also
-fundamentally different from Attributes, each of which always belongs to one single
-object.
-
In Evennia, Tags are technically also used to implement Aliases (alternative names for objects)
-and Permissions (simple strings for Locks to check for).
Tags are unique per object model. This means that for each object model (Objects, Scripts,
-Msgs, etc.) there is only ever one Tag object with a given key and category.
-
-
Not specifying a category (default) gives the tag a category of None, which is also considered a
-unique key + category combination.
-
-
When Tags are assigned to game entities, these entities are actually sharing the same Tag. This
-means that Tags are not suitable for storing information about a single object - use an
-Attribute for this instead. Tags are a lot more limited than Attributes but this also
-makes them very quick to lookup in the database - this is the whole point.
-
Tags have the following properties, stored in the database:
-
-
key - the name of the Tag. This is the main property to search for when looking up a Tag.
-
category - this category allows for retrieving only specific subsets of tags used for
-different purposes. You could have one category of tags for “zones”, another for “outdoor
-locations”, for example. If not given, the category will be None, which is also considered a
-separate, default, category.
-
data - this is an optional text field with information about the tag. Remember that Tags are
-shared between entities, so this field cannot hold any object-specific information. Usually it would
-be used to hold info about the group of entities the Tag is tagging - possibly used for contextual
-help like a tool tip. It is not used by default.
-
-
There are also two special properties. These should usually not need to be changed or set, it is
-used internally by Evennia to implement various other uses it makes of the Tag object:
-
-
model - this holds a natural-key description of the model object that this tag deals with,
-on the form application.modelclass, for example objects.objectdb. It used by the TagHandler of
-each entity type for correctly storing the data behind the scenes.
-
tagtype - this is a “top-level category” of sorts for the inbuilt children of Tags, namely
-Aliases and Permissions. The Taghandlers using this special field are especially intended to
-free up the category property for any use you desire.
You can tag any typeclassed object, namely Objects, Accounts,
-Scripts and Channels. General tags are added by the Taghandler. The
-tag handler is accessed as a property tags on the relevant entity:
-
mychair.tags.add("furniture")
- mychair.tags.add("furniture",category="luxurious")
- myroom.tags.add("dungeon#01")
- myscript.tags.add("weather",category="climate")
- myaccount.tags.add("guestaccount")
-
- mychair.tags.all()# returns a list of Tags
- mychair.tags.remove("furniture")
- mychair.tags.clear()
-
-
-
Adding a new tag will either create a new Tag or re-use an already existing one. Note that there are
-two “furniture” tags, one with a None category, and one with the “luxurious” category.
-
When using remove, the Tag is not deleted but are just disconnected from the tagged object. This
-makes for very quick operations. The clear method removes (disconnects) all Tags from the object.
-You can also use the default @tag command:
-
@tag mychair = furniture
-
-
-
This tags the chair with a ‘furniture’ Tag (the one with a None category).
Note that searching for just “furniture” will only return the objects tagged with the “furniture”
-tag that
-has a category of None. We must explicitly give the category to get the “luxurious” furniture.
-
-
Using any of the search_tag variants will all return Django
-Querysets, including if you only have
-one match. You can treat querysets as lists and iterate over them, or continue building search
-queries with them.
-
Remember when searching that not setting a category means setting it to None - this does not
-mean that category is undefined, rather None is considered the default, unnamed category.
-
importevennia
-
-myobj1.tags.add("foo")# implies category=None
-myobj2.tags.add("foo",category="bar")
-
-# this returns a queryset with *only* myobj1
-objs=evennia.search_tag("foo")
-
-# these return a queryset with *only* myobj2
-objs=evennia.search_tag("foo",category="bar")
-# or
-objs=evennia.search_tag(category="bar")
-
-
-
-
There is also an in-game command that deals with assigning and using (Object-) tags:
Aliases and Permissions are implemented using normal TagHandlers that simply save Tags with a
-different tagtype. These handlers are named aliases and permissions on all Objects. They are
-used in the same way as Tags above:
Generally, tags are enough on their own for grouping objects. Having no tag category is perfectly
-fine and the normal operation. Simply adding a new Tag for grouping objects is often better than
-making a new category. So think hard before deciding you really need to categorize your Tags.
-
That said, tag categories can be useful if you build some game system that uses tags. You can then
-use tag categories to make sure to separate tags created with this system from any other tags
-created elsewhere. You can then supply custom search methods that only find objects tagged with
-tags of that category. An example of this
-is found in the Zone tutorial.
Evennia is a text-based game server. This makes it important to understand how
-it actually deals with data in the form of text.
-
Text byte encodings describe how a string of text is actually stored in the
-computer - that is, the particular sequence of bytes used to represent the
-letters of your particular alphabet. A common encoding used in English-speaking
-languages is the ASCII encoding. This describes the letters in the English
-alphabet (Aa-Zz) as well as a bunch of special characters. For describing other
-character sets (such as that of other languages with other letters than
-English), sets with names such as Latin-1, ISO-8859-3 and ARMSCII-8
-are used. There are hundreds of different byte encodings in use around the
-world.
-
A string of letters in a byte encoding is represented with the bytes type.
-In contrast to the byte encoding is the unicode representation. In Python
-this is the str type. The unicode is an internationally agreed-upon table
-describing essentially all available letters you could ever want to print.
-Everything from English to Chinese alphabets and all in between. So what
-Evennia (as well as Python and Django) does is to store everything in Unicode
-internally, but then converts the data to one of the encodings whenever
-outputting data to the user.
-
An easy memory aid is that bytes are what are sent over the network wire. At
-all other times, str (unicode) is used. This means that we must convert
-between the two at the points where we send/receive network data.
-
The problem is that when receiving a string of bytes over the network it’s
-impossible for Evennia to guess which encoding was used - it’s just a bunch of
-bytes! Evennia must know the encoding in order to convert back and from the
-correct unicode representation.
As long as you stick to the standard ASCII character set (which means the
-normal English characters, basically) you should not have to worry much
-about this section.
-
If you want to build your game in another language however, or expect your
-users to want to use special characters not in ASCII, you need to consider
-which encodings you want to support.
-
As mentioned, there are many, many byte-encodings used around the world. It
-should be clear at this point that Evennia can’t guess but has to assume or
-somehow be told which encoding you want to use to communicate with the server.
-Basically the encoding used by your client must be the same encoding used by
-the server. This can be customized in two complementary ways.
-
-
Point users to the default @encoding command or the @options command.
-This allows them to themselves set which encoding they (and their client of
-choice) uses. Whereas data will remain stored as unicode strings internally in
-Evennia, all data received from and sent to this particular player will be
-converted to the given format before transmitting.
-
As a back-up, in case the user-set encoding translation is erroneous or
-fails in some other way, Evennia will fall back to trying with the names
-defined in the settings variable ENCODINGS. This is a list of encoding
-names Evennia will try, in order, before giving up and giving an encoding
-error message.
-
-
Note that having to try several different encodings every input/output adds
-unneccesary overhead. Try to guess the most common encodings you players will
-use and make sure these are tried first. The International UTF-8 encoding is
-what Evennia assumes by default (and also what Python/Django use normally). See
-the Wikipedia article here for more help.
This documentation details the various text tags supported by Evennia, namely colours, command
-links and inline functions.
-
There is also an Understanding Color Tags tutorial which expands on the
-use of ANSI color tags and the pitfalls of mixing ANSI and Xterms256 color tags in the same context.
Note that the Documentation does not display colour the way it would look on the screen.
-
Color can be a very useful tool for your game. It can be used to increase readability and make your
-game more appealing visually.
-
Remember however that, with the exception of the webclient, you generally don’t control the client
-used to connect to the game. There is, for example, one special tag meaning “yellow”. But exactly
-which hue of yellow is actually displayed on the user’s screen depends on the settings of their
-particular mud client. They could even swap the colours around or turn them off altogether if so
-desired. Some clients don’t even support color - text games are also played with special reading
-equipment by people who are blind or have otherwise diminished eyesight.
-
So a good rule of thumb is to use colour to enhance your game but don’t rely on it to display
-critical information. If you are coding the game, you can add functionality to let users disable
-colours as they please, as described here.
-
To see which colours your client support, use the default @color command. This will list all
-available colours for ANSI and Xterm256 along with the codes you use for them. You can find a list
-of all the parsed ANSI-colour codes in evennia/utils/ansi.py.
Evennia supports the ANSI standard for text. This is by far the most supported MUD-color standard,
-available in all but the most ancient mud clients. The ANSI colours are red, green,
-yellow, blue, magenta, cyan, white and black. They are abbreviated by their
-first letter except for black which is abbreviated with the letter x. In ANSI there are “bright”
-and “normal” (darker) versions of each color, adding up to a total of 16 colours to use for
-foreground text. There are also 8 “background” colours. These have no bright alternative in ANSI
-(but Evennia uses the Xterm256 extension behind the scenes to offer
-them anyway).
-
To colour your text you put special tags in it. Evennia will parse these and convert them to the
-correct markup for the client used. If the user’s client/console/display supports ANSI colour, they
-will see the text in the specified colour, otherwise the tags will be stripped (uncolored text).
-This works also for non-terminal clients, such as the webclient. For the webclient, Evennia will
-translate the codes to HTML RGB colors.
-
Here is an example of the tags in action:
-
|rThis text is bright red.|n This is normal text.
- |RThis is a dark red text.|n This is normal text.
- |[rThis text has red background.|n This is normal text.
- |b|[yThis is bright blue text on yellow background.|n This is normal text.
-
-
-
-
|n - this tag will turn off all color formatting, including background colors.
-
|#- markup marks the start of foreground color. The case defines if the text is “bright” or
-“normal”. So |g is a bright green and |G is “normal” (darker) green.
-
|[# is used to add a background colour to the text. The case again specifies if it is “bright”
-or “normal”, so |[c starts a bright cyan background and |[C a darker cyan background.
-
|!# is used to add foreground color without any enforced brightness/normal information.
-These are normal-intensity and are thus always given as uppercase, such as
-|!R for red. The difference between e.g. |!R and |R is that
-|!R will “inherit” the brightness setting from previously set color tags, whereas |R will
-always reset to the normal-intensity red. The |# format contains an implicit |h/|H tag in it:
-disabling highlighting when switching to a normal color, and enabling it for bright ones. So |btest|!Rtest2 will result in a bright red test2 since the brightness setting from |b “bleeds over”.
-You could use this to for example quickly switch the intensity of a multitude of color tags. There
-is no background-color equivalent to |! style tags.
-
|h is used to make any following foreground ANSI colors bright (it has no effect on Xterm
-colors). This is only relevant to use with |! type tags and will be valid until the next |n,
-|H or normal (upper-case) |# tag. This tag will never affect background colors, those have to be
-set bright/normal explicitly. Technically, |h|!G is identical to |g.
-
|H negates the effects |h and returns all ANSI foreground colors (|! and | types) to
-‘normal’ intensity. It has no effect on background and Xterm colors.
-
-
-
Note: The ANSI standard does not actually support bright backgrounds like |[r - the standard
-only supports “normal” intensity backgrounds. To get around this Evennia instead implements these
-as Xterm256 colours behind the scenes. If the client does not support
-Xterm256 the ANSI colors will be used instead and there will be no visible difference between using
-upper- and lower-case background tags.
-
-
If you want to display an ANSI marker as output text (without having any effect), you need to escape
-it by preceding its | with another |:
-
sayThe||rANSImarkerchangestextcolortobrightred.
-
-
-
This will output the raw |r without any color change. This can also be necessary if you are doing
-ansi art that uses | with a letter directly following it.
-
Use the command
-
@color ansi
-
-
-
to get a list of all supported ANSI colours and the tags used to produce them.
-
A few additional ANSI codes are supported:
-
-
|/ A line break. You cannot put the normal Python \n line breaks in text entered inside the
-game (Evennia will filter this for security reasons). This is what you use instead: use the |/
-marker to format text with line breaks from the game command line.
-
`` This will translate into a TAB character. This will not always show (or show differently) to
-the client since it depends on their local settings. It’s often better to use multiple spaces.
-
|_ This is a space. You can usually use the normal space character, but if the space is at the
-end of the line, Evennia will likely crop it. This tag will not be cropped but always result in a
-space.
-
|* This will invert the current text/background colours. Can be useful to mark things (but see
-below).
The |* tag (inverse video) is an old ANSI standard and should usually not be used for more than to
-mark short snippets of text. If combined with other tags it comes with a series of potentially
-confusing behaviors:
-
-
The |* tag will only work once in a row:, ie: after using it once it won’t have an effect again
-until you declare another tag. This is an example:
-
Normaltext,|*reversedtext|*,stillreversedtext.
-
-
-
that is, it will not reverse to normal at the second |*. You need to reset it manually:
-
Normaltext,|*reversedtext|n,normalagain.
-
-
-
-
The |* tag does not take “bright” colors into account:
-
|RNormalred,|hnowbrightened.|*BGisnormalred.
-
-
-
-
-
So |* only considers the ‘true’ foreground color, ignoring any highlighting. Think of the bright
-state (|h) as something like like <strong> in HTML: it modifies the appearance of a normal
-foreground color to match its bright counterpart, without changing its normal color.
-
-
Finally, after a |*, if the previous background was set to a dark color (via |[), |!#) will
-actually change the background color instead of the foreground:
-
|*reversed text |!R now BG is red.
-
-
-
-
-
For a detailed explanation of these caveats, see the [Understanding Color Tags](Understanding-Color-
-Tags) tutorial. But most of the time you might be better off to simply avoid |* and mark your text
-manually instead.
The Xterm256 standard is a colour scheme that supports 256 colours for text and/or background.
-While this offers many more possibilities than traditional ANSI colours, be wary that too many text
-colors will be confusing to the eye. Also, not all clients support Xterm256 - these will instead see
-the closest equivalent ANSI color. You can mix Xterm256 tags with ANSI tags as you please.
-
|555 This is pure white text.|n This is normal text.
-|230 This is olive green text.
-|[300 This text has a dark red background.
-|005|[054 This is dark blue text on a bright cyan background.
-|=a This is a greyscale value, equal to black.
-|=m This is a greyscale value, midway between white and black.
-|=z This is a greyscale value, equal to white.
-|[=m This is a background greyscale value.
-
-
-
-
|### - markup consists of three digits, each an integer from 0 to 5. The three digits describe
-the amount of red, green and blue (RGB) components used in the colour. So |500 means
-maximum red and none of the other colours - the result is a bright red. |520 is red with a touch
-of green - the result is orange. As opposed to ANSI colors, Xterm256 syntax does not worry about
-bright/normal intensity, a brighter (lighter) color is just achieved by upping all RGB values with
-the same amount.
-
|[### - this works the same way but produces a coloured background.
-
|=# - markup produces the xterm256 gray scale tones, where # is a letter from a (black) to
-z (white). This offers many more nuances of gray than the normal |### markup (which only has
-four gray tones between solid black and white (|000, |111, |222, |333 and |444)).
-
|[=# - this works in the same way but produces background gray scale tones.
-
-
If you have a client that supports Xterm256, you can use
-
@color xterm256
-
-
-
to get a table of all the 256 colours and the codes that produce them. If the table looks broken up
-into a few blocks of colors, it means Xterm256 is not supported and ANSI are used as a replacement.
-You can use the @options command to see if xterm256 is active for you. This depends on if your
-client told Evennia what it supports - if not, and you know what your client supports, you may have
-to activate some features manually.
Evennia supports clickable links for clients that supports it. This marks certain text so it can be
-clicked by a mouse and trigger a given Evennia command. To support clickable links, Evennia requires
-the webclient or an third-party telnet client with MXP
-support (Note: Evennia only supports clickable links, no other MXP features).
-
-
|lc to start the link, by defining the command to execute.
-
|lt to continue with the text to show to the user (the link text).
-
|le to end the link text and the link definition.
-
-
All elements must appear in exactly this order to make a valid link. For example,
-
"If you go |lcnorth|ltto the north|le you will find a cottage."
-
-
-
This will display as “If you go to the north you will find a cottage.” where clicking the link
-will execute the command north. If the client does not support clickable links, only the link text
-will be shown.
Note: Inlinefuncs are not activated by default. To use them you need to add
-INLINEFUNC_ENABLED=True to your settings file.
-
-
Evennia has its own inline text formatting language, known as inlinefuncs. It allows the builder
-to include special function calls in code. They are executed dynamically by each session that
-receives them.
-
To add an inlinefunc, you embed it in a text string like this:
-
"A normal string with $funcname(arg, arg, ...) embedded inside it."
-
-
-
When this string is sent to a session (with the msg() method), these embedded inlinefuncs will be
-parsed. Their return value (which always is a string) replace their call location in the finalized
-string. The interesting thing with this is that the function called will have access to which
-session is seeing the string, meaning the string can end up looking different depending on who is
-looking. It could of course also vary depending on other factors like game time.
-
Any number of comma-separated arguments can be given (or none). No keywords are supported. You can
-also nest inlinefuncs by letting an argument itself also be another $funcname(arg,arg,...) call
-(down to a depth of nesting given by settings.INLINEFUNC_STACK_MAXSIZE). Function call resolution
-happens as in all programming languages inside-out, with the nested calls replacing the argument
-with their return strings before calling he parent.
-
>say"This is $pad(a center-padded text, 30,c,-) of width 30."
- Yousay,"This is ---- a center-padded text----- of width 30."
- >say"I roll a die and the result is ... a $random(1,6)!"
- Yousay,"I roll a die and the result is ... a 5!"
-
-
-
A special case happens if wanting to use an inlinefunc argument that itself includes a comma - this
-would be parsed as an argument separator. To escape commas you can either escape each comma manually
-with a backslash \,, or you can embed the entire string in python triple-quotes """ or ''' -
-this will escape the entire argument, including commas and any nested inlinefunc calls within.
-
Only certain functions are available to use as inlinefuncs and the game developer may add their own
-functions as needed.
To add new inlinefuncs, edit the file mygame/server/conf/inlinefuncs.py.
-
All globally defined functions in this module are considered inline functions by the system. The
-only exception is functions whose name starts with an underscore _. An inlinefunc must be of the
-following form:
where *args denotes all the arguments this function will accept as an $inlinefunc. The inline
-function is expected to clean arguments and check that they are valid. If needed arguments are not
-given, default values should be used. The function should always return a string (even if it’s
-empty). An inlinefunc should never cause a traceback regardless of the input (but it could log
-errors if desired).
-
Note that whereas the function should accept **kwargs, keyword inputs are not usable in the call
-to the inlinefunction. The kwargs part is instead intended for Evennia to be able to supply extra
-information. Currently Evennia sends a single keyword to every inline function and that is
-session, which holds the serversession this text is targeted at. Through the session
-object, a lot of dynamic possibilities are opened up for your inline functions.
-
The settings.INLINEFUNC_MODULES configuration option is a list that decides which modules should
-be parsed for inline function definitions. This will include mygame/server/conf/inlinefuncs.py but
-more could be added. The list is read from left to right so if you want to overload default
-functions you just have to put your custom module-paths later in the list and name your functions
-the same as default ones.
-
Here is an example, the crop default inlinefunction:
-
fromevennia.utilsimportutils
-
-defcrop(*args,**kwargs):
- """
- Inlinefunc. Crops ingoing text to given widths.
- Args:
- text (str, optional): Text to crop.
- width (str, optional): Will be converted to an integer. Width of
- crop in characters.
- suffix (str, optional): End string to mark the fact that a part
- of the string was cropped. Defaults to `[...]`.
- Kwargs:
- session (Session): Session performing the crop.
- Example:
- `$crop(text, 50, [...])`
-
- """
- text,width,suffix="",78,"[...]"
- nargs=len(args)
- ifnargs>0:
- text=args[0]
- ifnargs>1:
- width=int(args[1])ifargs[1].strip().isdigit()else78
- ifnargs>2:
- suffix=args[2]
- returnutils.crop(text,width=width,suffix=suffix)
-
-
-
Another example, making use of the Session:
-
defcharactername(*args,**kwargs):
- """
- Inserts the character name of whomever sees the string
- (so everyone will see their own name). Uses the account
- name for OOC communications.
-
- Example:
- say "This means YOU, $charactername()!"
-
- """
- session=kwargs["session"]
- ifsession.puppet:
- returnkwargs["session"].puppet.key
- else:
- returnsession.account.key
-
-
-
Evennia itself offers the following default inline functions (mostly as examples):
-
-
crop(text,width,suffix) - See above.
-
pad(text,width,align,fillchar) - this pads the text to width (default 78), alignment (“c”,
-“l” or “r”, defaulting to “c”) and fill-in character (defaults to space). Example: $pad(40,l,-)
-
clr(startclr,text,endclr) - A programmatic way to enter colored text for those who don’t want
-to use the normal |c type color markers for some reason. The color argument is the same as the
-color markers except without the actual pre-marker, so |r would be just r. If endclr is not
-given, it defaults to resetting the color (n). Example: $clr(b,Abluetext)
-
space(number) - Inserts the given number of spaces. If no argument is given, use 4 spaces.
-
random(), random(max), random(min,max) - gives a random value between min and max. With no
-arguments, give 0 or 1 (on/off). If one argument, returns 0…max. If values are floats, random
-value will be a float (so random(1.0) replicates the normal Python random function).
One way to implement a dynamic MUD is by using “tickers”, also known as “heartbeats”. A ticker is a
-timer that fires (“ticks”) at a given interval. The tick triggers updates in various game systems.
Tickers are very common or even unavoidable in other mud code bases. Certain code bases are even
-hard-coded to rely on the concept of the global ‘tick’. Evennia has no such notion - the decision to
-use tickers is very much up to the need of your game and which requirements you have. The “ticker
-recipe” is just one way of cranking the wheels.
-
The most fine-grained way to manage the flow of time is of course to use Scripts. Many
-types of operations (weather being the classic example) are however done on multiple objects in the
-same way at regular intervals, and for this, storing separate Scripts on each object is inefficient.
-The way to do this is to use a ticker with a “subscription model” - let objects sign up to be
-triggered at the same interval, unsubscribing when the updating is no longer desired.
-
Evennia offers an optimized implementation of the subscription model - the TickerHandler. This is
-a singleton global handler reachable from evennia.TICKER_HANDLER. You can assign any callable (a
-function or, more commonly, a method on a database object) to this handler. The TickerHandler will
-then call this callable at an interval you specify, and with the arguments you supply when adding
-it. This continues until the callable un-subscribes from the ticker. The handler survives a reboot
-and is highly optimized in resource usage.
-
Here is an example of importing TICKER_HANDLER and using it:
-
# we assume that obj has a hook "at_tick" defined on itself
- fromevenniaimportTICKER_HANDLERastickerhandler
-
- tickerhandler.add(20,obj.at_tick)
-
-
-
That’s it - from now on, obj.at_tick() will be called every 20 seconds.
Note that you have to also supply interval to identify which subscription to remove. This is
-because the TickerHandler maintains a pool of tickers and a given callable can subscribe to be
-ticked at any number of different intervals.
-
The full definition of the tickerhandler.add method is
Here *args and **kwargs will be passed to callback every interval seconds. If persistent
-is False, this subscription will not survive a server reload.
-
Tickers are identified and stored by making a key of the callable itself, the ticker-interval, the
-persistent flag and the idstring (the latter being an empty string when not given explicitly).
-
Since the arguments are not included in the ticker’s identification, the idstring must be used to
-have a specific callback triggered multiple times on the same interval but with different arguments:
Note that, when we want to send arguments to our callback within a ticker handler, we need to
-specify idstring and persistent before, unless we call our arguments as keywords, which would
-often be more readable:
If you add a ticker with exactly the same combination of callback, interval and idstring, it will
-overload the existing ticker. This identification is also crucial for later removing (stopping) the
-subscription:
The callable can be on any form as long as it accepts the arguments you give to send to it in
-TickerHandler.add.
-
-
Note that everything you supply to the TickerHandler will need to be pickled at some point to be
-saved into the database. Most of the time the handler will correctly store things like database
-objects, but the same restrictions as for Attributes apply to what the TickerHandler
-may store.
-
-
When testing, you can stop all tickers in the entire game with tickerhandler.clear(). You can also
-view the currently subscribed objects with tickerhandler.all().
-
See the Weather Tutorial for an example of using the TickerHandler.
Using the TickerHandler may sound very useful but it is important to consider when not to use it.
-Even if you are used to habitually relying on tickers for everything in other code bases, stop and
-think about what you really need it for. This is the main point:
-
-
You should never use a ticker to catch changes.
-
-
Think about it - you might have to run the ticker every second to react to the change fast enough.
-Most likely nothing will have changed at a given moment. So you are doing pointless calls (since
-skipping the call gives the same result as doing it). Making sure nothing’s changed might even be
-computationally expensive depending on the complexity of your system. Not to mention that you might
-need to run the check on every object in the database. Every second. Just to maintain status quo
-…
-
Rather than checking over and over on the off-chance that something changed, consider a more
-proactive approach. Could you implement your rarely changing system to itself report when its
-status changes? It’s almost always much cheaper/efficient if you can do things “on demand”. Evennia
-itself uses hook methods for this very reason.
-
So, if you consider a ticker that will fire very often but which you expect to have no effect 99% of
-the time, consider handling things things some other way. A self-reporting on-demand solution is
-usually cheaper also for fast-updating properties. Also remember that some things may not need to be
-updated until someone actually is examining or using them - any interim changes happening up to that
-moment are pointless waste of computing time.
-
The main reason for needing a ticker is when you want things to happen to multiple objects at the
-same time without input from something else.
Most MUDs will use some sort of combat system. There are several main variations:
-
-
Freeform - the simplest form of combat to implement, common to MUSH-style roleplaying games.
-This means the system only supplies dice rollers or maybe commands to compare skills and spit out
-the result. Dice rolls are done to resolve combat according to the rules of the game and to direct
-the scene. A game master may be required to resolve rule disputes.
-
Twitch - This is the traditional MUD hack&slash style combat. In a twitch system there is often
-no difference between your normal “move-around-and-explore mode” and the “combat mode”. You enter an
-attack command and the system will calculate if the attack hits and how much damage was caused.
-Normally attack commands have some sort of timeout or notion of recovery/balance to reduce the
-advantage of spamming or client scripting. Whereas the simplest systems just means entering kill<target> over and over, more sophisticated twitch systems include anything from defensive stances
-to tactical positioning.
-
Turn-based - a turn based system means that the system pauses to make sure all combatants can
-choose their actions before continuing. In some systems, such entered actions happen immediately
-(like twitch-based) whereas in others the resolution happens simultaneously at the end of the turn.
-The disadvantage of a turn-based system is that the game must switch to a “combat mode” and one also
-needs to take special care of how to handle new combatants and the passage of time. The advantage is
-that success is not dependent on typing speed or of setting up quick client macros. This potentially
-allows for emoting as part of combat which is an advantage for roleplay-heavy games.
-
-
To implement a freeform combat system all you need is a dice roller and a roleplaying rulebook. See
-contrib/dice.py for an
-example dice roller. To implement at twitch-based system you basically need a few combat
-commands, possibly ones with a cooldown. You also need a game rule
-module that makes use of it. We will focus on the turn-based
-variety here.
This tutorial will implement the slightly more complex turn-based combat system. Our example has the
-following properties:
-
-
Combat is initiated with attack<target>, this initiates the combat mode.
-
Characters may join an ongoing battle using attack<target> against a character already in
-combat.
-
Each turn every combating character will get to enter two commands, their internal order matters
-and they are compared one-to-one in the order given by each combatant. Use of say and pose is
-free.
-
The commands are (in our example) simple; they can either hit<target>, feint<target> or
-parry<target>. They can also defend, a generic passive defense. Finally they may choose to
-disengage/flee.
-
When attacking we use a classic rock-paper-scissors mechanic to determine success: hit defeats feint, which defeats parry which defeats
-hit. defend is a general passive action that has a percentage chance to win against hit
-(only).
-
disengage/flee must be entered two times in a row and will only succeed if there is no hit
-against them in that time. If so they will leave combat mode.
-
Once every player has entered two commands, all commands are resolved in order and the result is
-reported. A new turn then begins.
-
If players are too slow the turn will time out and any unset commands will be set to defend.
-
-
For creating the combat system we will need the following components:
-
-
A combat handler. This is the main mechanic of the system. This is a Script object
-created for each combat. It is not assigned to a specific object but is shared by the combating
-characters and handles all the combat information. Since Scripts are database entities it also means
-that the combat will not be affected by a server reload.
-
A combat command set with the relevant commands needed for combat, such as the
-various attack/defend options and the flee/disengage command to leave the combat mode.
-
A rule resolution system. The basics of making such a module is described in the rule system tutorial. We will only sketch such a module here for our end-turn
-combat resolution.
-
An attackcommand for initiating the combat mode. This is added to the default
-command set. It will create the combat handler and add the character(s) to it. It will also assign
-the combat command set to the characters.
The combat handler is implemented as a stand-alone Script. This Script is created when
-the first Character decides to attack another and is deleted when no one is fighting any more. Each
-handler represents one instance of combat and one combat only. Each instance of combat can hold any
-number of characters but each character can only be part of one combat at a time (a player would
-need to disengage from the first combat before they could join another).
-
The reason we don’t store this Script “on” any specific character is because any character may leave
-the combat at any time. Instead the script holds references to all characters involved in the
-combat. Vice-versa, all characters holds a back-reference to the current combat handler. While we
-don’t use this very much here this might allow the combat commands on the characters to access and
-update the combat handler state directly.
-
Note: Another way to implement a combat handler would be to use a normal Python object and handle
-time-keeping with the TickerHandler. This would require either adding custom hook
-methods on the character or to implement a custom child of the TickerHandler class to track turns.
-Whereas the TickerHandler is easy to use, a Script offers more power in this case.
-
Here is a basic combat handler. Assuming our game folder is named mygame, we store it in
-mygame/typeclasses/combat_handler.py:
-
# mygame/typeclasses/combat_handler.py
-
-importrandom
-fromevenniaimportDefaultScript
-fromworld.rulesimportresolve_combat
-
-classCombatHandler(DefaultScript):
- """
- This implements the combat handler.
- """
-
- # standard Script hooks
-
- defat_script_creation(self):
- "Called when script is first created"
-
- self.key="combat_handler_%i"%random.randint(1,1000)
- self.desc="handles combat"
- self.interval=60*2# two minute timeout
- self.start_delay=True
- self.persistent=True
-
- # store all combatants
- self.db.characters={}
- # store all actions for each turn
- self.db.turn_actions={}
- # number of actions entered per combatant
- self.db.action_count={}
-
- def_init_character(self,character):
- """
- This initializes handler back-reference
- and combat cmdset on a character
- """
- character.ndb.combat_handler=self
- character.cmdset.add("commands.combat.CombatCmdSet")
-
- def_cleanup_character(self,character):
- """
- Remove character from handler and clean
- it of the back-reference and cmdset
- """
- dbref=character.id
- delself.db.characters[dbref]
- delself.db.turn_actions[dbref]
- delself.db.action_count[dbref]
- delcharacter.ndb.combat_handler
- character.cmdset.delete("commands.combat.CombatCmdSet")
-
- defat_start(self):
- """
- This is called on first start but also when the script is restarted
- after a server reboot. We need to re-assign this combat handler to
- all characters as well as re-assign the cmdset.
- """
- forcharacterinself.db.characters.values():
- self._init_character(character)
-
- defat_stop(self):
- "Called just before the script is stopped/destroyed."
- forcharacterinlist(self.db.characters.values()):
- # note: the list() call above disconnects list from database
- self._cleanup_character(character)
-
- defat_repeat(self):
- """
- This is called every self.interval seconds (turn timeout) or
- when force_repeat is called (because everyone has entered their
- commands). We know this by checking the existence of the
- `normal_turn_end` NAttribute, set just before calling
- force_repeat.
-
- """
- ifself.ndb.normal_turn_end:
- # we get here because the turn ended normally
- # (force_repeat was called) - no msg output
- delself.ndb.normal_turn_end
- else:
- # turn timeout
- self.msg_all("Turn timer timed out. Continuing.")
- self.end_turn()
-
- # Combat-handler methods
-
- defadd_character(self,character):
- "Add combatant to handler"
- dbref=character.id
- self.db.characters[dbref]=character
- self.db.action_count[dbref]=0
- self.db.turn_actions[dbref]=[("defend",character,None),
- ("defend",character,None)]
- # set up back-reference
- self._init_character(character)
-
- defremove_character(self,character):
- "Remove combatant from handler"
- ifcharacter.idinself.db.characters:
- self._cleanup_character(character)
- ifnotself.db.characters:
- # if no more characters in battle, kill this handler
- self.stop()
-
- defmsg_all(self,message):
- "Send message to all combatants"
- forcharacterinself.db.characters.values():
- character.msg(message)
-
- defadd_action(self,action,character,target):
- """
- Called by combat commands to register an action with the handler.
-
- action - string identifying the action, like "hit" or "parry"
- character - the character performing the action
- target - the target character or None
-
- actions are stored in a dictionary keyed to each character, each
- of which holds a list of max 2 actions. An action is stored as
- a tuple (character, action, target).
- """
- dbref=character.id
- count=self.db.action_count[dbref]
- if0<=count<=1:# only allow 2 actions
- self.db.turn_actions[dbref][count]=(action,character,target)
- else:
- # report if we already used too many actions
- returnFalse
- self.db.action_count[dbref]+=1
- returnTrue
-
- defcheck_end_turn(self):
- """
- Called by the command to eventually trigger
- the resolution of the turn. We check if everyone
- has added all their actions; if so we call force the
- script to repeat immediately (which will call
- `self.at_repeat()` while resetting all timers).
- """
- ifall(count>1forcountinself.db.action_count.values()):
- self.ndb.normal_turn_end=True
- self.force_repeat()
-
- defend_turn(self):
- """
- This resolves all actions by calling the rules module.
- It then resets everything and starts the next turn. It
- is called by at_repeat().
- """
- resolve_combat(self,self.db.turn_actions)
-
- iflen(self.db.characters)<2:
- # less than 2 characters in battle, kill this handler
- self.msg_all("Combat has ended")
- self.stop()
- else:
- # reset counters before next turn
- forcharacterinself.db.characters.values():
- self.db.characters[character.id]=character
- self.db.action_count[character.id]=0
- self.db.turn_actions[character.id]=[("defend",character,None),
- ("defend",character,None)]
- self.msg_all("Next turn begins ...")
-
-
-
This implements all the useful properties of our combat handler. This Script will survive a reboot
-and will automatically re-assert itself when it comes back online. Even the current state of the
-combat should be unaffected since it is saved in Attributes at every turn. An important part to note
-is the use of the Script’s standard at_repeat hook and the force_repeat method to end each turn.
-This allows for everything to go through the same mechanisms with minimal repetition of code.
-
What is not present in this handler is a way for players to view the actions they set or to change
-their actions once they have been added (but before the last one has added theirs). We leave this as
-an exercise.
Our combat commands - the commands that are to be available to us during the combat - are (in our
-example) very simple. In a full implementation the commands available might be determined by the
-weapon(s) held by the player or by which skills they know.
-
We create them in mygame/commands/combat.py.
-
# mygame/commands/combat.py
-
-fromevenniaimportCommand
-
-classCmdHit(Command):
- """
- hit an enemy
-
- Usage:
- hit <target>
-
- Strikes the given enemy with your current weapon.
- """
- key="hit"
- aliases=["strike","slash"]
- help_category="combat"
-
- deffunc(self):
- "Implements the command"
- ifnotself.args:
- self.caller.msg("Usage: hit <target>")
- return
- target=self.caller.search(self.args)
- ifnottarget:
- return
- ok=self.caller.ndb.combat_handler.add_action("hit",
- self.caller,
- target)
- ifok:
- self.caller.msg("You add 'hit' to the combat queue")
- else:
- self.caller.msg("You can only queue two actions per turn!")
-
- # tell the handler to check if turn is over
- self.caller.ndb.combat_handler.check_end_turn()
-
-
-
The other commands CmdParry, CmdFeint, CmdDefend and CmdDisengage look basically the same.
-We should also add a custom help command to list all the available combat commands and what they
-do.
-
We just need to put them all in a cmdset. We do this at the end of the same module:
A general way to implement a rule module is found in the rule system tutorial. Proper resolution would likely require us to change our Characters to store things
-like strength, weapon skills and so on. So for this example we will settle for a very simplistic
-rock-paper-scissors kind of setup with some randomness thrown in. We will not deal with damage here
-but just announce the results of each turn. In a real system the Character objects would hold stats
-to affect their skills, their chosen weapon affect the choices, they would be able to lose health
-etc.
-
Within each turn, there are “sub-turns”, each consisting of one action per character. The actions
-within each sub-turn happens simultaneously and only once they have all been resolved we move on to
-the next sub-turn (or end the full turn).
-
Note: In our simple example the sub-turns don’t affect each other (except for disengage/flee),
-nor do any effects carry over between turns. The real power of a turn-based system would be to add
-real tactical possibilities here though; For example if your hit got parried you could be out of
-balance and your next action would be at a disadvantage. A successful feint would open up for a
-subsequent attack and so on …
-
Our rock-paper-scissor setup works like this:
-
-
hit beats feint and flee/disengage. It has a random chance to fail against defend.
-
parry beats hit.
-
feint beats parry and is then counted as a hit.
-
defend does nothing but has a chance to beat hit.
-
flee/disengage must succeed two times in a row (i.e. not beaten by a hit once during the
-turn). If so the character leaves combat.
-
-
# mygame/world/rules.py
-
-importrandom
-
-# messages
-
-defresolve_combat(combat_handler,actiondict):
- """
- This is called by the combat handler
- actiondict is a dictionary with a list of two actions
- for each character:
- {char.id:[(action1, char, target), (action2, char, target)], ...}
- """
- flee={}# track number of flee commands per character
- forisubinrange(2):
- # loop over sub-turns
- messages=[]
- forsubturnin(sub[isub]forsubinactiondict.values()):
- # for each character, resolve the sub-turn
- action,char,target=subturn
- iftarget:
- taction,tchar,ttarget=actiondict[target.id][isub]
- ifaction=="hit":
- iftaction=="parry"andttarget==char:
- msg="%s tries to hit %s, but %s parries the attack!"
- messages.append(msg%(char,tchar,tchar))
- eliftaction=="defend"andrandom.random()<0.5:
- msg="%s defends against the attack by %s."
- messages.append(msg%(tchar,char))
- eliftaction=="flee":
- msg="%s stops %s from disengaging, with a hit!"
- flee[tchar]=-2
- messages.append(msg%(char,tchar))
- else:
- msg="%s hits %s, bypassing their %s!"
- messages.append(msg%(char,tchar,taction))
- elifaction=="parry":
- iftaction=="hit":
- msg="%s parries the attack by %s."
- messages.append(msg%(char,tchar))
- eliftaction=="feint":
- msg="%s tries to parry, but %s feints and hits!"
- messages.append(msg%(char,tchar))
- else:
- msg="%s parries to no avail."
- messages.append(msg%char)
- elifaction=="feint":
- iftaction=="parry":
- msg="%s feints past %s's parry, landing a hit!"
- messages.append(msg%(char,tchar))
- eliftaction=="hit":
- msg="%s feints but is defeated by %s hit!"
- messages.append(msg%(char,tchar))
- else:
- msg="%s feints to no avail."
- messages.append(msg%char)
- elifaction=="defend":
- msg="%s defends."
- messages.append(msg%char)
- elifaction=="flee":
- ifcharinflee:
- flee[char]+=1
- else:
- flee[char]=1
- msg="%s tries to disengage (two subsequent turns needed)"
- messages.append(msg%char)
-
- # echo results of each subturn
- combat_handler.msg_all("\n".join(messages))
-
- # at the end of both sub-turns, test if anyone fled
- msg="%s withdraws from combat."
- for(char,fleevalue)inflee.items():
- iffleevalue==2:
- combat_handler.msg_all(msg%char)
- combat_handler.remove_character(char)
-
-
-
To make it simple (and to save space), this example rule module actually resolves each interchange
-twice - first when it gets to each character and then again when handling the target. Also, since we
-use the combat handler’s msg_all method here, the system will get pretty spammy. To clean it up,
-one could imagine tracking all the possible interactions to make sure each pair is only handled and
-reported once.
This is the last component we need, a command to initiate combat. This will tie everything together.
-We store this with the other combat commands.
-
# mygame/commands/combat.py
-
-fromevenniaimportcreate_script
-
-classCmdAttack(Command):
- """
- initiates combat
-
- Usage:
- attack <target>
-
- This will initiate combat with <target>. If <target is
- already in combat, you will join the combat.
- """
- key="attack"
- help_category="General"
-
- deffunc(self):
- "Handle command"
- ifnotself.args:
- self.caller.msg("Usage: attack <target>")
- return
- target=self.caller.search(self.args)
- ifnottarget:
- return
- # set up combat
- iftarget.ndb.combat_handler:
- # target is already in combat - join it
- target.ndb.combat_handler.add_character(self.caller)
- target.ndb.combat_handler.msg_all("%s joins combat!"%self.caller)
- else:
- # create a new combat handler
- chandler=create_script("combat_handler.CombatHandler")
- chandler.add_character(self.caller)
- chandler.add_character(target)
- self.caller.msg("You attack %s! You are in combat."%target)
- target.msg("%s attacks you! You are in combat."%self.caller)
-
-
-
The attack command will not go into the combat cmdset but rather into the default cmdset. See e.g.
-the Adding Command Tutorial if you are unsure about how to do this.
At this point you should have a simple but flexible turn-based combat system. We have taken several
-shortcuts and simplifications in this example. The output to the players is likely too verbose
-during combat and too limited when it comes to informing about things surrounding it. Methods for
-changing your commands or list them, view who is in combat etc is likely needed - this will require
-play testing for each game and style. There is also currently no information displayed for other
-people happening to be in the same room as the combat - some less detailed information should
-probably be echoed to the room to
-show others what’s going on.
This tutorial shows the implementation of an NPC object that responds to characters entering their
-location. In this example the NPC has the option to respond aggressively or not, but any actions
-could be triggered this way.
-
One could imagine using a Script that is constantly checking for newcomers. This would be
-highly inefficient (most of the time its check would fail). Instead we handle this on-demand by
-using a couple of existing object hooks to inform the NPC that a Character has entered.
-
It is assumed that you already know how to create custom room and character typeclasses, please see
-the Basic Game tutorial if you haven’t already done this.
-
What we will need is the following:
-
-
An NPC typeclass that can react when someone enters.
-
A custom Room typeclass that can tell the NPC that someone entered.
-
We will also tweak our default Character typeclass a little.
-
-
To begin with, we need to create an NPC typeclass. Create a new file inside of your typeclasses
-folder and name it npcs.py and then add the following code:
-
fromtypeclasses.charactersimportCharacter
-
-classNPC(Character):
- """
- A NPC typeclass which extends the character class.
- """
- defat_char_entered(self,character):
- """
- A simple is_aggressive check.
- Can be expanded upon later.
- """
- ifself.db.is_aggressive:
- self.execute_cmd(f"say Graaah, die {character}!")
- else:
- self.execute_cmd(f"say Greetings, {character}!")
-
-
-
We will define our custom Character typeclass below. As for the new at_char_entered method we’ve
-just defined, we’ll ensure that it will be called by the room where the NPC is located, when a
-player enters that room. You’ll notice that right now, the NPC merely speaks. You can expand this
-part as you like and trigger all sorts of effects here (like combat code, fleeing, bartering or
-quest-giving) as your game design dictates.
-
Now your typeclasses.rooms module needs to have the following added:
-
# Add this import to the top of your file.
-fromevenniaimportutils
-
- # Add this hook in any empty area within your Room class.
- defat_object_receive(self,obj,source_location):
- ifutils.inherits_from(obj,'typeclasses.npcs.NPC'):# An NPC has entered
- return
- elifutils.inherits_from(obj,'typeclasses.characters.Character'):
- # A PC has entered.
- # Cause the player's character to look around.
- obj.execute_cmd('look')
- foriteminself.contents:
- ifutils.inherits_from(item,'typeclasses.npcs.NPC'):
- # An NPC is in the room
- item.at_char_entered(obj)
-
-
-
inherits_from must be given the full path of the class. If the object inherited a class from your
-world.races module, then you would check inheritance with world.races.Human, for example. There
-is no need to import these prior, as we are passing in the full path. As a matter of a fact,
-inherits_from does not properly work if you import the class and only pass in the name of the
-class.
-
-
Note:
-at_object_receive
-is a default hook of the DefaultObject typeclass (and its children). Here we are overriding this
-hook in our customized room typeclass to suit our needs.
-
-
This room checks the typeclass of objects entering it (using utils.inherits_from and responds to
-Characters, ignoring other NPCs or objects. When triggered the room will look through its
-contents and inform any NPCsinsidebycallingtheirat_char_entered` method.
-
You’ll also see that we have added a ‘look’ into this code. This is because, by default, the
-at_object_receive is carried out before the character’s at_after_move which, we will now
-overload. This means that a character entering would see the NPC perform its actions before the
-‘look’ command. Deactivate the look command in the default Character class within the
-typeclasses.characters module:
-
# Add this hook in any blank area within your Character class.
- defat_after_move(self,source_location):
- """
- Default is to look around after a move
- Note: This has been moved to Room.at_object_receive
- """
- #self.execute_cmd('look')
- pass
-
-
-
Now let’s create an NPC and make it aggressive. Type the following commands into your MUD client:
-
reload
-create/dropOrc:npcs.NPC
-
-
-
-
Note: You could also give the path as typeclasses.npcs.NPC, but Evennia will look into the
-typeclasses folder automatically, so this is a little shorter.
-
-
When you enter the aggressive NPC’s location, it will default to using its peaceful action (say your
-name is Anna):
-
Orcsays,"Greetings, Anna!"
-
-
-
Now we turn on the aggressive mode (we do it manually but it could also be triggered by some sort of
-AI code).
-
setorc/is_aggressive=True
-
-
-
Now it will perform its aggressive action whenever a character enters.
This tutorial shows the implementation of an NPC object that responds to characters speaking in
-their location. In this example the NPC parrots what is said, but any actions could be triggered
-this way.
-
It is assumed that you already know how to create custom room and character typeclasses, please see
-the Basic Game tutorial if you haven’t already done this.
-
What we will need is simply a new NPC typeclass that can react when someone speaks.
-
# mygame/typeclasses/npc.py
-
-fromcharactersimportCharacter
-classNpc(Character):
- """
- A NPC typeclass which extends the character class.
- """
- defat_heard_say(self,message,from_obj):
- """
- A simple listener and response. This makes it easy to change for
- subclasses of NPCs reacting differently to says.
-
- """
- # message will be on the form `<Person> says, "say_text"`
- # we want to get only say_text without the quotes and any spaces
- message=message.split('says, ')[1].strip(' "')
-
- # we'll make use of this in .msg() below
- return"%s said: '%s'"%(from_obj,message)
-
-
-
When someone in the room speaks to this NPC, its msg method will be called. We will modify the
-NPCs .msg method to catch says so the NPC can respond.
-
# mygame/typeclasses/npc.py
-
-fromcharactersimportCharacter
-classNpc(Character):
-
- # [at_heard_say() goes here]
-
- defmsg(self,text=None,from_obj=None,**kwargs):
- "Custom msg() method reacting to say."
-
- iffrom_obj!=self:
- # make sure to not repeat what we ourselves said or we'll create a loop
- try:
- # if text comes from a say, `text` is `('say_text', {'type': 'say'})`
- say_text,is_say=text[0],text[1]['type']=='say'
- exceptException:
- is_say=False
- ifis_say:
- # First get the response (if any)
- response=self.at_heard_say(say_text,from_obj)
- # If there is a response
- ifresponse!=None:
- # speak ourselves, using the return
- self.execute_cmd("say %s"%response)
-
- # this is needed if anyone ever puppets this NPC - without it you would never
- # get any feedback from the server (not even the results of look)
- super().msg(text=text,from_obj=from_obj,**kwargs)
-
-
-
So if the NPC gets a say and that say is not coming from the NPC itself, it will echo it using the
-at_heard_say hook. Some things of note in the above example:
-
-
The text input can be on many different forms depending on where this msg is called from.
-Instead of trying to analyze text in detail with a range of if statements we just assume the
-form we want and catch the error if it does not match. This simplifies the code considerably. It’s
-called ‘leap before you look’ and is a Python paradigm that may feel unfamiliar if you are used to
-other languages. Here we ‘swallow’ the error silently, which is fine when the code checked is
-simple. If not we may want to import evennia.logger.log_trace and add log_trace() in the
-except clause.
-If you would like to learn more about the text list used above refer to the Out-Of-Band
-documentation.
-
We use execute_cmd to fire the say command back. We could also have called
-self.location.msg_contents directly but using the Command makes sure all hooks are called (so
-those seeing the NPC’s say can in turn react if they want).
-
Note the comments about super at the end. This will trigger the ‘default’ msg (in the parent
-class) as well. It’s not really necessary as long as no one puppets the NPC (by @ic<npcname>) but
-it’s wise to keep in there since the puppeting player will be totally blind if msg() is never
-returning anything to them!
-
-
Now that’s done, let’s create an NPC and see what it has to say for itself.
-
@reload
-@create/dropGuildMaster:npc.Npc
-
-
-
(you could also give the path as typeclasses.npc.Npc, but Evennia will look into the typeclasses
-folder automatically so this is a little shorter).
-
> say hi
-You say, "hi"
-Guild Master says, "Anna said: 'hi'"
-
There are many ways to implement this kind of functionality. An alternative example to overriding
-msg would be to modify the at_say hook on the Character instead. It could detect that it’s
-sending to an NPC and call the at_heard_say hook directly.
-
While the tutorial solution has the advantage of being contained only within the NPC class,
-combining this with using the Character class gives more direct control over how the NPC will react.
-Which way to go depends on the design requirements of your particular game.
You will often want to operate on a specific object in the database. For example when a player
-attacks a named target you’ll need to find that target so it can be attacked. Or when a rain storm
-draws in you need to find all outdoor-rooms so you can show it raining in them. This tutorial
-explains Evennia’s tools for searching.
The first thing to consider is the base type of the thing you are searching for. Evennia organizes
-its database into a few main tables: Objects, Accounts, Scripts,
-Channels, Messages and Help Entries.
-Most of the time you’ll likely spend your time searching for Objects and the occasional Accounts.
-
So to find an entity, what can be searched for?
-
-
The key is the name of the entity. While you can get this from obj.key the database field
-is actually named obj.db_key - this is useful to know only when you do direct database
-queries. The one exception is Accounts, where
-the database field for .key is instead named username (this is a Django requirement). When you
-don’t specify search-type, you’ll usually search based on key. Aliases are extra names given to
-Objects using something like @alias or obj.aliases.add('name'). The main search functions (see
-below) will automatically search for aliases whenever you search by-key.
-
Tags are the main way to group and identify objects in Evennia. Tags can most often be
-used (sometimes together with keys) to uniquely identify an object. For example, even though you
-have two locations with the same name, you can separate them by their tagging (this is how Evennia
-implements ‘zones’ seen in other systems). Tags can also have categories, to further organize your
-data for quick lookups.
-
An object’s Attributes can also used to find an object. This can be very useful but
-since Attributes can store almost any data they are far less optimized to search for than Tags or
-keys.
-
The object’s Typeclass indicate the sub-type of entity. A Character, Flower or
-Sword are all types of Objects. A Bot is a kind of Account. The database field is called
-typeclass_path and holds the full Python-path to the class. You can usually specify the
-typeclass as an argument to Evennia’s search functions as well as use the class directly to limit
-queries.
-
The location is only relevant for Objects but is a very common way to weed down the
-number of candidates before starting to search. The reason is that most in-game commands tend to
-operate on things nearby (in the same room) so the choices can be limited from the start.
-
The database id or the ‘#dbref’ is unique (and never re-used) within each database table. So while
-there is one and only one Object with dbref #42 there could also be an Account or Script with the
-dbref #42 at the same time. In almost all search methods you can replace the “key” search
-criterion with "#dbref" to search for that id. This can occasionally be practical and may be what
-you are used to from other code bases. But it is considered bad practice in Evennia to rely on
-hard-coded #dbrefs to do your searches. It makes your code tied to the exact layout of the database.
-It’s also not very maintainable to have to remember abstract numbers. Passing the actual objects
-around and searching by Tags and/or keys will usually get you what you need.
All in-game Objects have a .contents property that returns all objects ‘inside’ them
-(that is, all objects which has its .location property set to that object. This is a simple way to
-get everything in a room and is also faster since this lookup is cached and won’t hit the database.
-
-
roomobj.contents returns a list of all objects inside roomobj.
-
obj.contents same as for a room, except this usually represents the object’s inventory
-
obj.location.contents gets everything in obj’s location (including obj itself).
-
roomobj.exits returns all exits starting from roomobj (Exits are here defined as Objects with
-their destination field set).
-
obj.location.contents_get(exclude=obj) - this helper method returns all objects in obj’s
-location except obj.
Say you have a command, and you want it to do something to a target. You might be
-wondering how you retrieve that target in code, and that’s where Evennia’s search utilities come in.
-In the most common case, you’ll often use the search method of the Object or Account
-typeclasses. In a command, the .caller property will refer back to the object using the command
-(usually a Character, which is a type of Object) while .args will contain Command’s arguments:
-
# e.g. in file mygame/commands/command.py
-
-fromevenniaimportdefault_cmds
-
-classCmdPoke(Command):
- """
- Pokes someone.
-
- Usage: poke <target>
- """
- key="poke"
-
- deffunc(self):
- """Executes poke command"""
- target=self.caller.search(self.args.lstrip())
- ifnottarget:
- # we didn't find anyone, but search has already let the
- # caller know. We'll just return, since we're done
- return
- # we found a target! we'll do stuff to them.
- target.msg(f"{self.caller} pokes you.")
- self.caller.msg(f"You poke {target}.")
-
-
-
By default, the search method of a Character will attempt to find a unique object match for the
-string sent to it (self.args, in this case, which is the arguments passed to the command by the
-player) in the surroundings of the Character - the room or their inventory. If there is no match
-found, the return value (which is assigned to target) will be None, and an appropriate failure
-message will be sent to the Character. If there’s not a unique match, None will again be returned,
-and a different error message will be sent asking them to disambiguate the multi-match. By default,
-the user can then pick out a specific match using with a number and dash preceding the name of the
-object: character.search("2-pinkunicorn") will try to find the second pink unicorn in the room.
-
The search method has many arguments that
-allow you to refine the search, such as by designating the location to search in or only matching
-specific typeclasses.
Sometimes you will want to find something that isn’t tied to the search methods of a character or
-account. In these cases, Evennia provides a utility module with a number of search
-functions. For example, suppose you want a command that will find and
-display all the rooms that are tagged as a ‘hangout’, for people to gather by. Here’s a simple
-Command to do this:
search_account_tag - find Accounts with a given Tag.
-
search_script_tag - find Scripts with a given Tag.
-
search_channel_tag - find Channels with a given Tag.
-
search_object_attribute - find Objects with a given Attribute.
-
search_account_attribute - find Accounts with a given Attribute.
-
search_attribute_object - this returns the actual Attribute, not the object it sits on.
-
-
-
Note: All search functions return a Django queryset which is technically a list-like
-representation of the database-query it’s about to do. Only when you convert it to a real list, loop
-over it or try to slice or access any of its contents will the datbase-lookup happen. This means you
-could yourself customize the query further if you know what you are doing (see the next section).
Evennia’s search methods should be sufficient for the vast majority of situations. But eventually
-you might find yourself trying to figure out how to get searches for unusual circumstances: Maybe
-you want to find all characters who are not in rooms tagged as hangouts and have the lycanthrope
-tag and whose names start with a vowel, but not with ‘Ab’, and only if they have 3 or more
-objects in their inventory … You could in principle use one of the earlier search methods to find
-all candidates and then loop over them with a lot of if statements in raw Python. But you can do
-this much more efficiently by querying the database directly.
-
Enter django’s querysets. A QuerySet
-is the representation of a database query and can be modified as desired. Only once one tries to
-retrieve the data of that query is it evaluated and does an actual database request. This is
-useful because it means you can modify a query as much as you want (even pass it around) and only
-hit the database once you are happy with it.
-Evennia’s search functions are themselves an even higher level wrapper around Django’s queries, and
-many search methods return querysets. That means that you could get the result from a search
-function and modify the resulting query to your own ends to further tweak what you search for.
-
Evaluated querysets can either contain objects such as Character objects, or lists of values derived
-from the objects. Queries usually use the ‘manager’ object of a class, which by convention is the
-.objects attribute of a class. For example, a query of Accounts that contain the letter ‘a’ could
-be:
The filter method of a manager takes arguments that allow you to define the query, and you can
-continue to refine the query by calling additional methods until you evaluate the queryset, causing
-the query to be executed and return a result. For example, if you have the result above, you could,
-without causing the queryset to be evaluated yet, get rid of matches that contain the letter ‘e by
-doing this:
You could also have chained .exclude directly to the end of the previous line.
-
-
Once you try to access the result, the queryset will be evaluated automatically under the hood:
-
accounts=list(queryset)# this fills list with matches
-
-foraccountinqueryset:
- # do something with account
-
-accounts=queryset[:4]# get first four matches
-account=queryset[0]# get first match
-# etc
-
-
Although Characters, Exits, Rooms, and other children of DefaultObject all shares the same
-underlying database table, Evennia provides a shortcut to do more specific queries only for those
-typeclasses. For example, to find only Characters whose names start with ‘A’, you might do:
The higher up in the inheritance hierarchy you go the more objects will be included in these
-searches. There is one special case, if you really want to include everything from a given
-database table. You do that by searching on the database model itself. These are named ObjectDB,
-AccountDB, ScriptDB etc.
-
fromevenniaimportAccountDB
-
-# all Accounts in the database, regardless of typeclass
-all=AccountDB.objects.all()
-
-
-
-
Here are the most commonly used methods to use with the objects managers:
-
-
filter - query for a listing of objects based on search criteria. Gives empty queryset if none
-were found.
-
get - query for a single match - raises exception if none were found, or more than one was
-found.
-
all - get all instances of the particular type.
-
filter_family - like filter, but search all sub classes as well.
-
get_family - like get, but search all sub classes as well.
-
all_family - like all, but return entities of all subclasses as well.
If you pass more than one keyword argument to a query method, the query becomes an AND
-relationship. For example, if we want to find characters whose names start with “A” and are also
-werewolves (have the lycanthrope tag), we might do:
Note the syntax of the keywords in building the queryset. For example, db_location is the name of
-the database field sitting on (in this case) the Character (Object). Double underscore __ works
-like dot-notation in normal Python (it’s used since dots are not allowed in keyword names). So the
-instruction db_location__db_tags__db_key="hangout" should be read as such:
-
-
“On the Character object … (this comes from us building this queryset using the
-Character.objects manager)
-
… get the value of the db_location field … (this references a Room object, normally)
-
… on that location, get the value of the db_tags field … (this is a many-to-many field that
-can be treated like an object for this purpose. It references all tags on the location)
-
… through the db_tag manager, find all Tags having a field db_key set to the value
-“hangout”.”
-
-
This may seem a little complex at first, but this syntax will work the same for all queries. Just
-remember that all database-fields in Evennia are prefaced with db_. So even though Evennia is
-nice enough to alias the db_key field so you can normally just do char.key to get a character’s
-name, the database field is actually called db_key and the real name must be used for the purpose
-of building a query.
-
-
Don’t confuse database fields with Attributes you set via obj.db.attr='foo' or
-obj.attributes.add(). Attributes are custom database entities linked to an object. They are not
-separate fields on that object like db_key or db_location are. You can get attached Attributes
-manually through the db_attributes many-to-many field in the same way as db_tags above.
What if you want to have a query with with OR conditions or negated requirements (NOT)? Enter
-Django’s Complex Query object,
-Q. Q()
-objects take a normal django keyword query as its arguments. The special thing is that these Q
-objects can then be chained together with set operations: | for OR, & for AND, and preceded with
-~ for NOT to build a combined, complex query.
-
In our original Lycanthrope example we wanted our werewolves to have names that could start with any
-vowel except for the specific beginning “ab”.
In the above example, we construct our query our of several Q objects that each represent one part
-of the query. We iterate over the list of vowels, and add an OR condition to the query using |=
-(this is the same idea as using += which may be more familiar). Each OR condition checks that
-the name starts with one of the valid vowels. Afterwards, we add (using &=) an AND condition
-that is negated with the ~ symbol. In other words we require that any match should not start
-with the string “ab”. Note that we don’t actually hit the database until we convert the query to a
-list at the end (we didn’t need to do that either, but could just have kept the query until we
-needed to do something with the matches).
What if we wanted to filter on some condition that isn’t represented easily by a field on the
-object? Maybe we want to find rooms only containing five or more objects?
-
We could retrieve all interesting candidates and run them through a for-loop to get and count
-their .content properties. We’d then just return a list of only those objects with enough
-contents. It would look something like this (note: don’t actually do this!):
-
# probably not a good idea to do it this way
-
-fromtypeclasses.roomsimportRoom
-
-queryset=Room.objects.all()# get all Rooms
-rooms=[roomforroominquerysetiflen(room.contents)>=5]
-
-
-
-
Once the number of rooms in your game increases, this could become quite expensive. Additionally, in
-some particular contexts, like when using the web features of Evennia, you must have the result as a
-queryset in order to use it in operations, such as in Django’s admin interface when creating list
-filters.
-
Enter F objects and
-annotations. So-called F expressions allow you to do a query that looks at a value of each object
-in the database, while annotations allow you to calculate and attach a value to a query. So, let’s
-do the same example as before directly in the database:
Here we first create an annotation num_objects of type Count, which is a Django class. Note that
-use of location_set in that Count. The *_set is a back-reference automatically created by
-Django. In this case it allows you to find all objects that has the current object as location.
-Once we have those, they are counted.
-Next we filter on this annotation, using the name num_objects as something we can filter for. We
-use num_objects__gte=5 which means that num_objects should be greater than 5. This is a little
-harder to get one’s head around but much more efficient than lopping over all objects in Python.
-
What if we wanted to compare two parameters against one another in a query? For example, what if
-instead of having 5 or more objects, we only wanted objects that had a bigger inventory than they
-had tags? Here an F-object comes in handy:
Suppose you used tags to mark someone belonging an organization. Now you want to make a list and
-need to get the membership count of every organization all at once. That’s where annotations and the
-.values_list queryset method come in. Values/Values Lists are an alternate way of returning a
-queryset - instead of objects, you get a list of dicts or tuples that hold selected properties from
-the the matches. It also allows you a way to ‘group up’ queries for returning information. For
-example, to get a display about each tag per Character and the names of the tag:
This tutorial will create a simple script that will send a tweet to your already configured twitter
-account. Please see: How to connect Evennia to Twitter if you
-haven’t already done so.
-
The script could be expanded to cover a variety of statistics you might wish to tweet about
-regularly, from player deaths to how much currency is in the economy etc.
-
# evennia/typeclasses/tweet_stats.py
-
-importtwitter
-fromrandomimportrandint
-fromdjango.confimportsettings
-fromevenniaimportObjectDB
-fromevenniaimportspawner
-fromevenniaimportlogger
-fromevenniaimportDefaultScript
-
-classTweetStats(DefaultScript):
- """
- This implements the tweeting of stats to a registered twitter account
- """
-
- # standard Script hooks
-
- defat_script_creation(self):
- "Called when script is first created"
-
- self.key="tweet_stats"
- self.desc="Tweets interesting stats about the game"
- self.interval=86400# 1 day timeout
- self.start_delay=False
-
- defat_repeat(self):
- """
- This is called every self.interval seconds to tweet interesting stats about the game.
- """
-
- api=twitter.Api(consumer_key='consumer_key',
- consumer_secret='consumer_secret',
- access_token_key='access_token_key',
- access_token_secret='access_token_secret')
-
- number_tweet_outputs=2
-
- tweet_output=randint(1,number_tweet_outputs)
-
- iftweet_output==1:
- ##Game Chars, Rooms, Objects taken from @stats command
- nobjs=ObjectDB.objects.count()
- base_char_typeclass=settings.BASE_CHARACTER_TYPECLASS
- nchars=ObjectDB.objects.filter(db_typeclass_path=base_char_typeclass).count()
- nrooms=ObjectDB.objects.filter(db_location__isnull=True).exclude(db_typeclass_path=bas
-e_char_typeclass).count()
- nexits=ObjectDB.objects.filter(db_location__isnull=False,
-db_destination__isnull=False).count()
- nother=nobjs-nchars-nrooms-nexits
- tweet="Chars: %s, Rooms: %s, Objects: %s"%(nchars,nrooms,nother)
- else:
- iftweet_output==2:##Number of prototypes and 3 random keys - taken from @spawn
-command
- prototypes=spawner.spawn(return_prototypes=True)
-
- keys=prototypes.keys()
- nprots=len(prototypes)
- tweet="Prototype Count: %s Random Keys: "%nprots
-
- tweet+=" %s"%keys[randint(0,len(keys)-1)]
- forxinrange(0,2):##tweet 3
- tweet+=", %s"%keys[randint(0,len(keys)-1)]
- # post the tweet
- try:
- response=api.PostUpdate(tweet)
- except:
- logger.log_trace("Tweet Error: When attempting to tweet %s"%tweet)
-
-
-
In the at_script_creation method, we configure the script to fire immediately (useful for testing)
-and setup the delay (1 day) as well as script information seen when you use @scripts
-
In the at_repeat method (which is called immediately and then at interval seconds later) we setup
-the Twitter API (just like in the initial configuration of twitter). numberTweetOutputs is used to
-show how many different types of outputs we have (in this case 2). We then build the tweet based on
-randomly choosing between these outputs.
-
-
Shows the number of Player Characters, Rooms and Other/Objects
-
Shows the number of prototypes currently in the game and then selects 3 random keys to show
-
-
Scripts Information will show you how to add it as a Global script, however, for testing
-it may be useful to start/stop it quickly from within the game. Assuming that you create the file
-as mygame/typeclasses/tweet_stats.py it can be started by using the following command
This tutorial explains how you can create vehicles that can move around in your world. The tutorial
-will explain how to create a train, but this can be equally applied to create other kind of vehicles
-(cars, planes, boats, spaceships, submarines, …).
Objects in Evennia have an interesting property: you can put any object inside another object. This
-is most obvious in rooms: a room in Evennia is just like any other game object (except rooms tend to
-not themselves be inside anything else).
-
Our train will be similar: it will be an object that other objects can get inside. We then simply
-move the Train, which brings along everyone inside it.
The first step we need to do is create our train object, including a new typeclass. To do this,
-create a new file, for instance in mygame/typeclasses/train.py with the following content:
Using the @telcommand like shown above is obviously not what we want. @tel is an admin command
-and normal players will thus never be able to enter the train! It is also not really a good idea to
-use Exits to get in and out of the train - Exits are (at least by default) objects
-too. They point to a specific destination. If we put an Exit in this room leading inside the train
-it would stay here when the train moved away (still leading into the train like a magic portal!). In
-the same way, if we put an Exit object inside the train, it would always point back to this room,
-regardless of where the Train has moved. Now, one could define custom Exit types that move with
-the train or change their destination in the right way - but this seems to be a pretty cumbersome
-solution.
-
What we will do instead is to create some new commands: one for entering the train and
-another for leaving it again. These will be stored on the train object and will thus be made
-available to whomever is either inside it or in the same room as the train.
-
Let’s create a new command module as mygame/commands/train.py:
-
# mygame/commands/train.py
-
-fromevenniaimportCommand,CmdSet
-
-classCmdEnterTrain(Command):
- """
- entering the train
-
- Usage:
- enter train
-
- This will be available to players in the same location
- as the train and allows them to embark.
- """
-
- key="enter train"
- locks="cmd:all()"
-
- deffunc(self):
- train=self.obj
- self.caller.msg("You board the train.")
- self.caller.move_to(train)
-
-
-classCmdLeaveTrain(Command):
- """
- leaving the train
-
- Usage:
- leave train
-
- This will be available to everyone inside the
- train. It allows them to exit to the train's
- current location.
- """
-
- key="leave train"
- locks="cmd:all()"
-
- deffunc(self):
- train=self.obj
- parent=train.location
- self.caller.move_to(parent)
-
-
-classCmdSetTrain(CmdSet):
-
- defat_cmdset_creation(self):
- self.add(CmdEnterTrain())
- self.add(CmdLeaveTrain())
-
-
-
Note that while this seems like a lot of text, the majority of lines here are taken up by
-documentation.
-
These commands are work in a pretty straightforward way: CmdEnterTrain moves the location of the
-player to inside the train and CmdLeaveTrain does the opposite: it moves the player back to the
-current location of the train (back outside to its current location). We stacked them in a
-cmdsetCmdSetTrain so they can be used.
-
To make the commands work we need to add this cmdset to our train typeclass:
Note the switches used with the @typeclass command: The /force switch is necessary to assign our
-object the same typeclass we already have. The /reset re-triggers the typeclass’
-at_object_creation() hook (which is otherwise only called the very first an instance is created).
-As seen above, when this hook is called on our train, our new cmdset will be loaded.
If you have played around a bit, you’ve probably figured out that you can use leavetrain when
-outside the train and entertrain when inside. This doesn’t make any sense … so let’s go ahead
-and fix that. We need to tell Evennia that you can not enter the train when you’re already inside
-or leave the train when you’re outside. One solution to this is locks: we will lock down
-the commands so that they can only be called if the player is at the correct location.
-
Right now commands defaults to the lock cmd:all(). The cmd lock type in combination with the
-all() lock function means that everyone can run those commands as long as they are in the same
-room as the train or inside the train. We’re going to change this to check the location of the
-player and only allow access if they are inside the train.
-
First of all we need to create a new lock function. Evennia comes with many lock functions built-in
-already, but none that we can use for locking a command in this particular case. Create a new entry
-in mygame/server/conf/lockfuncs.py:
-
-# file mygame/server/conf/lockfuncs.py
-
-defcmdinside(accessing_obj,accessed_obj,*args,**kwargs):
- """
- Usage: cmdinside()
- Used to lock commands and only allows access if the command
- is defined on an object which accessing_obj is inside of.
- """
- returnaccessed_obj.obj==accessing_obj.location
-
-
-
-
If you didn’t know, Evennia is by default set up to use all functions in this module as lock
-functions (there is a setting variable that points to it).
-
Our new lock function, cmdinside, is to be used by Commands. The accessed_obj is the Command
-object (in our case this will be CmdEnterTrain and CmdLeaveTrain) — Every command has an obj
-property: this is the the object on which the command “sits”. Since we added those commands to our
-train object, the .obj property will be set to the train object. Conversely, accessing_obj is
-the object that called the command: in our case it’s the Character trying to enter or leave the
-train.
-
What this function does is to check that the player’s location is the same as the train object. If
-it is, it means the player is inside the train. Otherwise it means the player is somewhere else and
-the check will fail.
-
The next step is to actually use this new lock function to create a lock of type cmd:
Notice how we use the not here so that we can use the same cmdinside to check if we are inside
-and outside, without having to create two separate lock functions. After a @reload our commands
-should be locked down appropriately and you should only be able to use them at the right places.
-
-
Note: If you’re logged in as the super user (user #1) then this lock will not work: the super
-user ignores lock functions. In order to use this functionality you need to @quell first.
Now that we can enter and leave the train correctly, it’s time to make it move. There are different
-things we need to consider for this:
-
-
Who can control your vehicle? The first player to enter it, only players that have a certain
-“drive” skill, automatically?
-
Where should it go? Can the player steer the vehicle to go somewhere else or will it always follow
-the same route?
-
-
For our example train we’re going to go with automatic movement through a predefined route (its
-track). The train will stop for a bit at the start and end of the route to allow players to enter
-and leave it.
-
Go ahead and create some rooms for our train. Make a list of the room ids along the route (using the
-@ex command).
-
@dig/telSouthstation
-@ex# note the id of the station
-@tunnel/teln=Followingarailroad
-@ex# note the id of the track
-@tunnel/teln=Followingarailroad
-...
-@tunnel/teln=NorthStation
-
-
-
Put the train onto the tracks:
-
@telsouthstation
-@teltrain=here
-
-
-
Next we will tell the train how to move and which route to take.
-
# file typeclasses/train.py
-
-fromevenniaimportDefaultObject,search_object
-
-fromcommands.trainimportCmdSetTrain
-
-classTrainObject(DefaultObject):
-
- defat_object_creation(self):
- self.cmdset.add_default(CmdSetTrain)
- self.db.driving=False
- # The direction our train is driving (1 for forward, -1 for backwards)
- self.db.direction=1
- # The rooms our train will pass through (change to fit your game)
- self.db.rooms=["#2","#47","#50","#53","#56","#59"]
-
- defstart_driving(self):
- self.db.driving=True
-
- defstop_driving(self):
- self.db.driving=False
-
- defgoto_next_room(self):
- currentroom=self.location.dbref
- idx=self.db.rooms.index(currentroom)+self.db.direction
-
- ifidx<0oridx>=len(self.db.rooms):
- # We reached the end of our path
- self.stop_driving()
- # Reverse the direction of the train
- self.db.direction*=-1
- else:
- roomref=self.db.rooms[idx]
- room=search_object(roomref)[0]
- self.move_to(room)
- self.msg_contents("The train is moving forward to %s."%(room.name,))
-
-
-
We added a lot of code here. Since we changed the at_object_creation to add in variables we will
-have to reset our train object like earlier (using the @typeclass/force/reset command).
-
We are keeping track of a few different things now: whether the train is moving or standing still,
-which direction the train is heading to and what rooms the train will pass through.
-
We also added some methods: one to start moving the train, another to stop and a third that actually
-moves the train to the next room in the list. Or makes it stop driving if it reaches the last stop.
-
Let’s try it out, using @py to call the new train functionality:
If we wanted full control of the train we could now just add a command to step it along the track
-when desired. We want the train to move on its own though, without us having to force it by manually
-calling the goto_next_room method.
-
To do this we will create two scripts: one script that runs when the train has stopped at
-a station and is responsible for starting the train again after a while. The other script will take
-care of the driving.
-
Let’s make a new file in mygame/typeclasses/trainscript.py
Those scripts work as a state system: when the train is stopped, it waits for 30 seconds and then
-starts again. When the train is driving, it moves to the next room every second. The train is always
-in one of those two states - both scripts take care of adding the other one once they are done.
-
As a last step we need to link the stopped-state script to our train, reload the game and reset our
-train again., and we’re ready to ride it around!
This train is very basic and still has some flaws. Some more things to do:
-
-
Make it look like a train.
-
Make it impossible to exit and enter the train mid-ride. This could be made by having the
-enter/exit commands check so the train is not moving before allowing the caller to proceed.
-
Have train conductor commands that can override the automatic start/stop.
-
Allow for in-between stops between the start- and end station
-
Have a rail road track instead of hard-coding the rooms in the train object. This could for
-example be a custom Exit only traversable by trains. The train will follow the
-track. Some track segments can split to lead to two different rooms and a player can switch the
-direction to which room it goes.
The Tutorial World is a small and functioning MUD-style game world. It is intended to be
-deconstructed and used as a way to learn Evennia. The game consists of a single-player quest and
-has some 20 rooms that you can explore as you seek to discover the whereabouts of a mythical weapon.
-
The source code is fully documented. You can find the whole thing in
-evennia/contrib/tutorial_world/.
-
Some features exemplified by the tutorial world:
-
-
Tutorial command, giving “behind-the-scenes” help for every room and some of the special objects
-
Rooms with custom return_appearance to show details.
-
Hidden exits
-
Objects with multiple custom interactions
-
Large-area rooms
-
Outdoor weather rooms
-
Dark room, needing light source
-
Puzzle object
-
Multi-room puzzle
-
Aggressive mobile with roam, pursue and battle state-engine AI
The tutorial world consists of a few modules in evennia/contrib/tutorial_world/ containing custom
-Typeclasses for rooms and objects and associated Commands.
-
These reusable bits and pieces are then put together into a functioning game area (“world” is maybe
-too big a word for such a small zone) using a batch script called build.ev. To
-install, log into the server as the superuser (user #1) and run:
-
@batchcommand tutorial_world.build
-
-
-
The world will be built (this might take a while, so don’t rerun the command even if it seems the
-system has frozen). After finishing you will end up back in Limbo with a new exit called tutorial.
-
An alternative is
-
@batchcommand/interactive tutorial_world.build
-
-
-
with the /interactive switch you are able to step through the building process at your own pace to
-see what happens in detail.
Non-superusers entering the tutorial will be auto-quelled so they play with their Character’s
-permission. As superuser you will not be auto-quelled, but it’s recommended that you still quell
-manually to play the tutorial “correctly”. The reason for this is that many game systems ignore the
-presence of a superuser and will thus not work as normal.
-
Use unquell if you want to get back your main account-level permissions to examine things under
-the hood. When you exit the tutorial (either by winning or using the abort/giveup command) you
-will automatically be unquelled.
To get into the mood of this miniature quest, imagine you are an adventurer out to find fame and
-fortune. You have heard rumours of an old castle ruin by the coast. In its depth a warrior princess
-was buried together with her powerful magical weapon - a valuable prize, if it’s true. Of course
-this is a chance to adventure that you cannot turn down!
-
You reach the ocean in the midst of a raging thunderstorm. With wind and rain screaming in your
-face you stand where the moor meets the sea along a high, rocky coast …
-
-
Look at everything.
-
Some objects are interactive in more than one way. Use the normal help command to get a feel for
-which commands are available at any given time. (use the command tutorial to get insight behind
-the scenes of the tutorial).
-
In order to fight, you need to first find some type of weapon.
-
slash is a normal attack
-
stab launches an attack that makes more damage but has a lower chance to hit.
-
defend will lower the chance to taking damage on your enemy’s next attack.
-
You can run from a fight that feels too deadly. Expect to be chased though.
Uninstalling the tutorial world basically means deleting all the rooms and objects it consists of.
-First, move out of the tutorial area.
-
@find tut#01
- @find tut#16
-
-
-
This should locate the first and last rooms created by build.ev - Intro and Outro. If you
-installed normally, everything created between these two numbers should be part of the tutorial.
-Note their dbref numbers, for example 5 and 80. Next we just delete all objects in that range:
-
@del 5-80
-
-
-
You will see some errors since some objects are auto-deleted and so cannot be found when the delete
-mechanism gets to them. That’s fine. You should have removed the tutorial completely once the
-command finishes.
When reading and learning from the code, keep in mind that Tutorial World was created with a very
-specific goal: to install easily and to not permanently modify the rest of the server. It therefore
-goes to some length to use only temporary solutions and to clean up after
-itself.
This tutorial lets you code a small but complete and functioning MUSH-like game in Evennia. A
-MUSH is, for our purposes, a class of roleplay-centric games
-focused on free form storytelling. Even if you are not interested in MUSH:es, this is still a good
-first game-type to try since it’s not so code heavy. You will be able to use the same principles for
-building other types of games.
-
The tutorial starts from scratch. If you did the First Steps Coding tutorial
-already you should have some ideas about how to do some of the steps already.
-
The following are the (very simplistic and cut-down) features we will implement (this was taken from
-a feature request from a MUSH user new to Evennia). A Character in this system should:
-
-
Have a “Power” score from 1 to 10 that measures how strong they are (stand-in for the stat
-system).
-
Have a command (e.g. +setpower4) that sets their power (stand-in for character generation
-code).
-
Have a command (e.g. +attack) that lets them roll their power and produce a “Combat Score”
-between 1 and 10*Power, displaying the result and editing their object to record this number
-(stand-in for +actions in the command code).
-
Have a command that displays everyone in the room and what their most recent “Combat Score” roll
-was (stand-in for the combat code).
-
Have a command (e.g. +createNPCJenkins) that creates an NPC with full abilities.
-
Have a command to control NPCs, such as +npc/cmd(name)=(command) (stand-in for the NPC
-controlling code).
-
-
In this tutorial we will assume you are starting from an empty database without any previous
-modifications.
To emulate a MUSH, the default MULTISESSION_MODE=0 is enough (one unique session per
-account/character). This is the default so you don’t need to change anything. You will still be able
-to puppet/unpuppet objects you have permission to, but there is no character selection out of the
-box in this mode.
-
We will assume our game folder is called mygame henceforth. You should be fine with the default
-SQLite3 database.
First thing is to choose how our Character class works. We don’t need to define a special NPC object
-– an NPC is after all just a Character without an Account currently controlling them.
-
Make your changes in the mygame/typeclasses/characters.py file:
-
# mygame/typeclasses/characters.py
-
-fromevenniaimportDefaultCharacter
-
-classCharacter(DefaultCharacter):
- """
- [...]
- """
- defat_object_creation(self):
- "This is called when object is first created, only."
- self.db.power=1
- self.db.combat_score=1
-
-
-
We defined two new Attributespower and combat_score and set them to default
-values. Make sure to @reload the server if you had it already running (you need to reload every
-time you update your python code, don’t worry, no accounts will be disconnected by the reload).
-
Note that only new characters will see your new Attributes (since the at_object_creation hook is
-called when the object is first created, existing Characters won’t have it). To update yourself,
-run
-
@typeclass/force self
-
-
-
This resets your own typeclass (the /force switch is a safety measure to not do this
-accidentally), this means that at_object_creation is re-run.
-
examine self
-
-
-
Under the “Persistent attributes” heading you should now find the new Attributes power and score
-set on yourself by at_object_creation. If you don’t, first make sure you @reloaded into the new
-code, next look at your server log (in the terminal/console) to see if there were any syntax errors
-in your code that may have stopped your new code from loading correctly.
We assume in this example that Accounts first connect into a “character generation area”. Evennia
-also supports full OOC menu-driven character generation, but for this example, a simple start room
-is enough. When in this room (or rooms) we allow character generation commands. In fact, character
-generation commands will only be available in such rooms.
-
Note that this again is made so as to be easy to expand to a full-fledged game. With our simple
-example, we could simply set an is_in_chargen flag on the account and have the +setpower command
-check it. Using this method however will make it easy to add more functionality later.
-
What we need are the following:
-
-
One character generation Command to set the “Power” on the Character.
-
A chargen CmdSet to hold this command. Lets call it ChargenCmdset.
-
A custom ChargenRoom type that makes this set of commands available to players in such rooms.
For this tutorial we will add all our new commands to mygame/commands/command.py but you could
-split your commands into multiple module if you prefered.
-
For this tutorial character generation will only consist of one Command to set the
-Character s “power” stat. It will be called on the following MUSH-like form:
-
+setpower 4
-
-
-
Open command.py file. It contains documented empty templates for the base command and the
-“MuxCommand” type used by default in Evennia. We will use the plain Command type here, the
-MuxCommand class offers some extra features like stripping whitespace that may be useful - if so,
-just import from that instead.
-
Add the following to the end of the command.py file:
-
# end of command.py
-fromevenniaimportCommand# just for clarity; already imported above
-
-classCmdSetPower(Command):
- """
- set the power of a character
-
- Usage:
- +setpower <1-10>
-
- This sets the power of the current character. This can only be
- used during character generation.
- """
-
- key="+setpower"
- help_category="mush"
-
- deffunc(self):
- "This performs the actual command"
- errmsg="You must supply a number between 1 and 10."
- ifnotself.args:
- self.caller.msg(errmsg)
- return
- try:
- power=int(self.args)
- exceptValueError:
- self.caller.msg(errmsg)
- return
- ifnot(1<=power<=10):
- self.caller.msg(errmsg)
- return
- # at this point the argument is tested as valid. Let's set it.
- self.caller.db.power=power
- self.caller.msg("Your Power was set to %i."%power)
-
-
-
This is a pretty straightforward command. We do some error checking, then set the power on ourself.
-We use a help_category of “mush” for all our commands, just so they are easy to find and separate
-in the help list.
-
Save the file. We will now add it to a new CmdSet so it can be accessed (in a full
-chargen system you would of course have more than one command here).
-
Open mygame/commands/default_cmdsets.py and import your command.py module at the top. We also
-import the default CmdSet class for the next step:
Next scroll down and define a new command set (based on the base CmdSet class we just imported at
-the end of this file, to hold only our chargen-specific command(s):
-
# end of default_cmdsets.py
-
-classChargenCmdset(CmdSet):
- """
- This cmdset it used in character generation areas.
- """
- key="Chargen"
- defat_cmdset_creation(self):
- "This is called at initialization"
- self.add(command.CmdSetPower())
-
-
-
In the future you can add any number of commands to this cmdset, to expand your character generation
-system as you desire. Now we need to actually put that cmdset on something so it’s made available to
-users. We could put it directly on the Character, but that would make it available all the time.
-It’s cleaner to put it on a room, so it’s only available when players are in that room.
We will create a simple Room typeclass to act as a template for all our Chargen areas. Edit
-mygame/typeclasses/rooms.py next:
-
fromcommands.default_cmdsetsimportChargenCmdset
-
-# ...
-# down at the end of rooms.py
-
-classChargenRoom(Room):
- """
- This room class is used by character-generation rooms. It makes
- the ChargenCmdset available.
- """
- defat_object_creation(self):
- "this is called only at first creation"
- self.cmdset.add(ChargenCmdset,permanent=True)
-
-
-
Note how new rooms created with this typeclass will always start with ChargenCmdset on themselves.
-Don’t forget the permanent=True keyword or you will lose the cmdset after a server reload. For
-more information about Command Sets and Commands, see the respective
-links.
First, make sure you have @reloaded the server (or use evenniareload from the terminal) to have
-your new python code added to the game. Check your terminal and fix any errors you see - the error
-traceback lists exactly where the error is found - look line numbers in files you have changed.
-
We can’t test things unless we have some chargen areas to test. Log into the game (you should at
-this point be using the new, custom Character class). Let’s dig a chargen area to test.
-
@dig chargen:rooms.ChargenRoom = chargen,finish
-
-
-
If you read the help for @dig you will find that this will create a new room named chargen. The
-part after the : is the python-path to the Typeclass you want to use. Since Evennia will
-automatically try the typeclasses folder of our game directory, we just specify
-rooms.ChargenRoom, meaning it will look inside the module rooms.py for a class named
-ChargenRoom (which is what we created above). The names given after = are the names of exits to
-and from the room from your current location. You could also append aliases to each one name, such
-as chargen;charactergeneration.
-
So in summary, this will create a new room of type ChargenRoom and open an exit chargen to it and
-an exit back here named finish. If you see errors at this stage, you must fix them in your code.
-@reload
-between fixes. Don’t continue until the creation seems to have worked okay.
-
chargen
-
-
-
This should bring you to the chargen room. Being in there you should now have the +setpower
-command available, so test it out. When you leave (via the finish exit), the command will go away
-and trying +setpower should now give you a command-not-found error. Use exme (as a privileged
-user) to check so the PowerAttribute has been set correctly.
-
If things are not working, make sure your typeclasses and commands are free of bugs and that you
-have entered the paths to the various command sets and commands correctly. Check the logs or command
-line for tracebacks and errors.
We will add our combat command to the default command set, meaning it will be available to everyone
-at all times. The combat system consists of a +attack command to get how successful our attack is.
-We also change the default look command to display the current combat score.
Attacking in this simple system means rolling a random “combat score” influenced by the power stat
-set during Character generation:
-
> +attack
-You +attack with a combat score of 12!
-
-
-
Go back to mygame/commands/command.py and add the command to the end like this:
-
importrandom
-
-# ...
-
-classCmdAttack(Command):
- """
- issues an attack
-
- Usage:
- +attack
-
- This will calculate a new combat score based on your Power.
- Your combat score is visible to everyone in the same location.
- """
- key="+attack"
- help_category="mush"
-
- deffunc(self):
- "Calculate the random score between 1-10*Power"
- caller=self.caller
- power=caller.db.power
- ifnotpower:
- # this can happen if caller is not of
- # our custom Character typeclass
- power=1
- combat_score=random.randint(1,10*power)
- caller.db.combat_score=combat_score
-
- # announce
- message="%s +attack%s with a combat score of %s!"
- caller.msg(message%("You","",combat_score))
- caller.location.msg_contents(message%
- (caller.key,"s",combat_score),
- exclude=caller)
-
-
-
What we do here is simply to generate a “combat score” using Python’s inbuilt random.randint()
-function. We then store that and echo the result to everyone involved.
-
To make the +attack command available to you in game, go back to
-mygame/commands/default_cmdsets.py and scroll down to the CharacterCmdSet class. At the correct
-place add this line:
-
self.add(command.CmdAttack())
-
-
-
@reload Evennia and the +attack command should be available to you. Run it and use e.g. @ex to
-make sure the combat_score attribute is saved correctly.
Players should be able to view all current combat scores in the room. We could do this by simply
-adding a second command named something like +combatscores, but we will instead let the default
-look command do the heavy lifting for us and display our scores as part of its normal output, like
-this:
-
> look Tom
-Tom (combat score: 3)
-This is a great warrior.
-
-
-
We don’t actually have to modify the look command itself however. To understand why, take a look
-at how the default look is actually defined. It sits in evennia/commands/default/general.py (or
-browse it online
-here).
-You will find that the actual return text is done by the look command calling a hook method
-named return_appearance on the object looked at. All the look does is to echo whatever this hook
-returns. So what we need to do is to edit our custom Character typeclass and overload its
-return_appearance to return what we want (this is where the advantage of having a custom typeclass
-comes into play for real).
-
Go back to your custom Character typeclass in mygame/typeclasses/characters.py. The default
-implementation of returnappearance is found in evennia.DefaultCharacter (or online
-here). If you
-want to make bigger changes you could copy & paste the whole default thing into our overloading
-method. In our case the change is small though:
-
classCharacter(DefaultCharacter):
- """
- [...]
- """
- defat_object_creation(self):
- "This is called when object is first created, only."
- self.db.power=1
- self.db.combat_score=1
-
- defreturn_appearance(self,looker):
- """
- The return from this method is what
- looker sees when looking at this object.
- """
- text=super().return_appearance(looker)
- cscore=" (combat score: %s)"%self.db.combat_score
- if"\n"intext:
- # text is multi-line, add score after first line
- first_line,rest=text.split("\n",1)
- text=first_line+cscore+"\n"+rest
- else:
- # text is only one line; add score to end
- text+=cscore
- returntext
-
-
-
What we do is to simply let the default return_appearance do its thing (super will call the
-parent’s version of the same method). We then split out the first line of this text, append our
-combat_score and put it back together again.
-
@reload the server and you should be able to look at other Characters and see their current combat
-scores.
-
-
Note: A potentially more useful way to do this would be to overload the entire return_appearance
-of the Rooms of your mush and change how they list their contents; in that way one could see all
-combat scores of all present Characters at the same time as looking at the room. We leave this as an
-exercise.
Here we will re-use the Character class by introducing a command that can create NPC objects. We
-should also be able to set its Power and order it around.
-
There are a few ways to define the NPC class. We could in theory create a custom typeclass for it
-and put a custom NPC-specific cmdset on all NPCs. This cmdset could hold all manipulation commands.
-Since we expect NPC manipulation to be a common occurrence among the user base however, we will
-instead put all relevant NPC commands in the default command set and limit eventual access with
-Permissions and Locks.
We need a command for creating the NPC, this is a very straightforward command:
-
> +createnpc Anna
-You created the NPC 'Anna'.
-
-
-
At the end of command.py, create our new command:
-
fromevenniaimportcreate_object
-
-classCmdCreateNPC(Command):
- """
- create a new npc
-
- Usage:
- +createNPC <name>
-
- Creates a new, named NPC. The NPC will start with a Power of 1.
- """
- key="+createnpc"
- aliases=["+createNPC"]
- locks="call:not perm(nonpcs)"
- help_category="mush"
-
- deffunc(self):
- "creates the object and names it"
- caller=self.caller
- ifnotself.args:
- caller.msg("Usage: +createNPC <name>")
- return
- ifnotcaller.location:
- # may not create npc when OOC
- caller.msg("You must have a location to create an npc.")
- return
- # make name always start with capital letter
- name=self.args.strip().capitalize()
- # create npc in caller's location
- npc=create_object("characters.Character",
- key=name,
- location=caller.location,
- locks="edit:id(%i) and perm(Builders);call:false()"%caller.id)
- # announce
- message="%s created the NPC '%s'."
- caller.msg(message%("You",name))
- caller.location.msg_contents(message%(caller.key,name),
- exclude=caller)
-
-
-
Here we define a +createnpc (+createNPC works too) that is callable by everyone not having the
-nonpcs “permission” (in Evennia, a “permission” can just as well be used to
-block access, it depends on the lock we define). We create the NPC object in the caller’s current
-location, using our custom Character typeclass to do so.
-
We set an extra lock condition on the NPC, which we will use to check who may edit the NPC later –
-we allow the creator to do so, and anyone with the Builders permission (or higher). See
-Locks for more information about the lock system.
-
Note that we just give the object default permissions (by not specifying the permissions keyword
-to the create_object() call). In some games one might want to give the NPC the same permissions
-as the Character creating them, this might be a security risk though.
-
Add this command to your default cmdset the same way you did the +attack command earlier.
-@reload and it will be available to test.
Since we re-used our custom character typeclass, our new NPC already has a Power value - it
-defaults to 1. How do we change this?
-
There are a few ways we can do this. The easiest is to remember that the power attribute is just a
-simple Attribute stored on the NPC object. So as a Builder or Admin we could set this
-right away with the default @set command:
-
@set mynpc/power = 6
-
-
-
The @set command is too generally powerful though, and thus only available to staff. We will add a
-custom command that only changes the things we want players to be allowed to change. We could in
-principle re-work our old +setpower command, but let’s try something more useful. Let’s make a
-+editNPC command.
This is a slightly more complex command. It goes at the end of your command.py file as before.
-
classCmdEditNPC(Command):
- """
- edit an existing NPC
-
- Usage:
- +editnpc <name>[/<attribute> [= value]]
-
- Examples:
- +editnpc mynpc/power = 5
- +editnpc mynpc/power - displays power value
- +editnpc mynpc - shows all editable
- attributes and values
-
- This command edits an existing NPC. You must have
- permission to edit the NPC to use this.
- """
- key="+editnpc"
- aliases=["+editNPC"]
- locks="cmd:not perm(nonpcs)"
- help_category="mush"
-
- defparse(self):
- "We need to do some parsing here"
- args=self.args
- propname,propval=None,None
- if"="inargs:
- args,propval=[part.strip()forpartinargs.rsplit("=",1)]
- if"/"inargs:
- args,propname=[part.strip()forpartinargs.rsplit("/",1)]
- # store, so we can access it below in func()
- self.name=args
- self.propname=propname
- # a propval without a propname is meaningless
- self.propval=propvalifpropnameelseNone
-
- deffunc(self):
- "do the editing"
-
- allowed_propnames=("power","attribute1","attribute2")
-
- caller=self.caller
- ifnotself.argsornotself.name:
- caller.msg("Usage: +editnpc name[/propname][=propval]")
- return
- npc=caller.search(self.name)
- ifnotnpc:
- return
- ifnotnpc.access(caller,"edit"):
- caller.msg("You cannot change this NPC.")
- return
- ifnotself.propname:
- # this means we just list the values
- output=f"Properties of {npc.key}:"
- forpropnameinallowed_propnames:
- output+=f"\n{propname} = {npc.attributes.get(propname,default='N/A')}"
- caller.msg(output)
- elifself.propnamenotinallowed_propnames:
- caller.msg(f"You may only change {', '.join(allowed_propnames)}.")
- elifself.propval:
- # assigning a new propvalue
- # in this example, the properties are all integers...
- intpropval=int(self.propval)
- npc.attributes.add(self.propname,intpropval)
- caller.msg(f"Set {npc.key}'s property {self.propname} to {self.propval}")
- else:
- # propname set, but not propval - show current value
- caller.msg(f"{npc.key} has property {self.propname} = {npc.attributes.get(self.propname,
-default='N/A')}")
-
-
-
This command example shows off the use of more advanced parsing but otherwise it’s mostly error
-checking. It searches for the given npc in the same room, and checks so the caller actually has
-permission to “edit” it before continuing. An account without the proper permission won’t even be
-able to view the properties on the given NPC. It’s up to each game if this is the way it should be.
-
Add this to the default command set like before and you should be able to try it out.
-
Note: If you wanted a player to use this command to change an on-object property like the NPC’s
-name (the key property), you’d need to modify the command since “key” is not an Attribute (it is
-not retrievable via npc.attributes.get but directly via npc.key). We leave this as an optional
-exercise.
Finally, we will make a command to order our NPC around. For now, we will limit this command to only
-be usable by those having the “edit” permission on the NPC. This can be changed if it’s possible for
-anyone to use the NPC.
-
The NPC, since it inherited our Character typeclass has access to most commands a player does. What
-it doesn’t have access to are Session and Player-based cmdsets (which means, among other things that
-they cannot chat on channels, but they could do that if you just added those commands). This makes
-the +npc command simple:
-
+npc Anna = say Hello!
-Anna says, 'Hello!'
-
-
-
Again, add to the end of your command.py module:
-
classCmdNPC(Command):
- """
- controls an NPC
-
- Usage:
- +npc <name> = <command>
-
- This causes the npc to perform a command as itself. It will do so
- with its own permissions and accesses.
- """
- key="+npc"
- locks="call:not perm(nonpcs)"
- help_category="mush"
-
- defparse(self):
- "Simple split of the = sign"
- name,cmdname=None,None
- if"="inself.args:
- name,cmdname=self.args.rsplit("=",1)
- name=name.strip()
- cmdname=cmdname.strip()
- self.name,self.cmdname=name,cmdname
-
- deffunc(self):
- "Run the command"
- caller=self.caller
- ifnotself.cmdname:
- caller.msg("Usage: +npc <name> = <command>")
- return
- npc=caller.search(self.name)
- ifnotnpc:
- return
- ifnotnpc.access(caller,"edit"):
- caller.msg("You may not order this NPC to do anything.")
- return
- # send the command order
- npc.execute_cmd(self.cmdname)
- caller.msg(f"You told {npc.key} to do '{self.cmdname}'.")
-
-
-
Note that if you give an erroneous command, you will not see any error message, since that error
-will be returned to the npc object, not to you. If you want players to see this, you can give the
-caller’s session ID to the execute_cmd call, like this:
Another thing to remember is however that this is a very simplistic way to control NPCs. Evennia
-supports full puppeting very easily. An Account (assuming the “puppet” permission was set correctly)
-could simply do @icmynpc and be able to play the game “as” that NPC. This is in fact just what
-happens when an Account takes control of their normal Character as well.
This ends the tutorial. It looks like a lot of text but the amount of code you have to write is
-actually relatively short. At this point you should have a basic skeleton of a game and a feel for
-what is involved in coding your game.
-
From here on you could build a few more ChargenRooms and link that to a bigger grid. The +setpower
-command can either be built upon or accompanied by many more to get a more elaborate character
-generation.
-
The simple “Power” game mechanic should be easily expandable to something more full-fledged and
-useful, same is true for the combat score principle. The +attack could be made to target a
-specific player (or npc) and automatically compare their relevant attributes to determine a result.
Before continuing to read these tutorials (and especially before you start to code or build your
-game in earnest) it’s strongly recommended that you read the
-Evennia coding introduction as well as the Planning your own game pages first.
-
Please note that it’s not within the scope of our tutorials to teach you basic Python. If you are
-new to the language, expect to have to look up concepts you are unfamiliar with. Usually a quick
-internet search will give you all info you need. Furthermore, our tutorials tend to focus on
-implementation and concepts. As such they give only brief explanations to use Evennia features while
-providing ample links to the relevant detailed documentation.
Introduction: The Tutorial World - this introduces the full (if
-small) solo-adventure game that comes with the Evennia distribution. It is useful both as an example
-of building and of coding.
Python tutorials for beginners -
-external link with tutorials for those not familiar with coding in general or Python in particular.
-
Tutorial: Version Control - use GIT to organize your code both for your own
-game project and for contributing to Evennia.
-
MIT offers free courses in many subjects. Their [Introduction to Computer Science and Programming](https://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-00sc-
-introduction-to-computer-science-and-programming-spring-2011/) uses Python as its language of
-choice. Longer path, but more in-depth. Definitely worth a look.
This section contains tutorials linked with contribs. These contribs can be used in your game, but
-you’ll need to install them explicitly. They add common features that can earn you time in
-implementation.
Typeclasses form the core of Evennia data storage. It allows Evennia to represent any number of
-different game entities as Python classes, without having to modify the database schema for every
-new type.
-
In Evennia the most important game entities, Accounts, Objects,
-Scripts and Channels are all Python classes inheriting, at
-varying distance, from evennia.typeclasses.models.TypedObject. In the documentation we refer to
-these objects as being “typeclassed” or even “being a typeclass”.
-
This is how the inheritance looks for the typeclasses in Evennia:
Level 1 above is the “database model” level. This describes the database tables and fields
-(this is technically a Django model).
-
Level 2 is where we find Evennia’s default implementations of the various game entities, on
-top of the database. These classes define all the hook methods that Evennia calls in various
-situations. DefaultObject is a little special since it’s the parent for DefaultCharacter,
-DefaultRoom and DefaultExit. They are all grouped under level 2 because they all represents
-defaults to build from.
-
Level 3, finally, holds empty template classes created in your game directory. This is the
-level you are meant to modify and tweak as you please, overloading the defaults as befits your game.
-The templates inherit directly from their defaults, so Object inherits from DefaultObject and
-Room inherits from DefaultRoom.
-
-
The typeclass/list command will provide a list of all typeclasses known to
-Evennia. This can be useful for getting a feel for what is available. Note
-however that if you add a new module with a class in it but do not import that
-module from anywhere, the typeclass/list will not find it. To make it known
-to Evennia you must import that module from somewhere.
All Evennia classes inheriting from class in the table above share one important feature and two
-important limitations. This is why we don’t simply call them “classes” but “typeclasses”.
-
-
A typeclass can save itself to the database. This means that some properties (actually not that
-many) on the class actually represents database fields and can only hold very specific data types.
-This is detailed below.
-
Due to its connection to the database, the typeclass’ name must be unique across the entire
-server namespace. That is, there must never be two same-named classes defined anywhere. So the below
-code would give an error (since DefaultObject is now globally found both in this module and in the
-default library):
A typeclass’ __init__ method should normally not be overloaded. This has mostly to do with the
-fact that the __init__ method is not called in a predictable way. Instead Evennia suggest you use
-the at_*_creation hooks (like at_object_creation for Objects) for setting things the very first
-time the typeclass is saved to the database or the at_init hook which is called every time the
-object is cached to memory. If you know what you are doing and want to use __init__, it must
-both accept arbitrary keyword arguments and use super to call its parent::
-
def__init__(self,**kwargs):
- # my content
- super().__init__(**kwargs)
- # my content
-
-
-
-
-
Apart from this, a typeclass works like any normal Python class and you can
-treat it as such.
It’s easy to work with Typeclasses. Either you use an existing typeclass or you create a new Python
-class inheriting from an existing typeclass. Here is an example of creating a new type of Object:
-
fromevenniaimportDefaultObject
-
- classFurniture(DefaultObject):
- # this defines what 'furniture' is, like
- # storing who sits on it or something.
- pass
-
-
-
-
You can now create a new Furniture object in two ways. First (and usually not the most
-convenient) way is to create an instance of the class and then save it manually to the database:
-
chair=Furniture(db_key="Chair")
-chair.save()
-
-
-
-
To use this you must give the database field names as keywords to the call. Which are available
-depends on the entity you are creating, but all start with db_* in Evennia. This is a method you
-may be familiar with if you know Django from before.
-
It is recommended that you instead use the create_* functions to create typeclassed entities:
-
fromevenniaimportcreate_object
-
-chair=create_object(Furniture,key="Chair")
-# or (if your typeclass is in a module furniture.py)
-chair=create_object("furniture.Furniture",key="Chair")
-
-
-
The create_object (create_account, create_script etc) takes the typeclass as its first
-argument; this can both be the actual class or the python path to the typeclass as found under your
-game directory. So if your Furniture typeclass sits in mygame/typeclasses/furniture.py, you
-could point to it as typeclasses.furniture.Furniture. Since Evennia will itself look in
-mygame/typeclasses, you can shorten this even further to just furniture.Furniture. The create-
-functions take a lot of extra keywords allowing you to set things like Attributes and
-Tags all in one go. These keywords don’t use the db_* prefix. This will also automatically
-save the new instance to the database, so you don’t need to call save() explicitly.
An example of a database field is db_key. This stores the “name” of the entity you are modifying
-and can thus only hold a string. This is one way of making sure to update the db_key:
That is, we change the chair object to have the db_key “Table”, then save this to the database.
-However, you almost never do things this way; Evennia defines property wrappers for all the database
-fields. These are named the same as the field, but without the db_ part:
The key wrapper is not only shorter to write, it will make sure to save the field for you, and
-does so more efficiently by levering sql update mechanics under the hood. So whereas it is good to
-be aware that the field is named db_key you should use key as much as you can.
-
Each typeclass entity has some unique fields relevant to that type. But all also share the
-following fields (the wrapper name without db_ is given):
-
-
key (str): The main identifier for the entity, like “Rose”, “myscript” or “Paul”. name is an
-alias.
-
date_created (datetime): Time stamp when this object was created.
-
typeclass_path (str): A python path pointing to the location of this (type)class
-
-
There is one special field that doesn’t use the db_ prefix (it’s defined by Django):
-
-
id (int): the database id (database ref) of the object. This is an ever-increasing, unique
-integer. It can also be accessed as dbid (database ID) or pk (primary key). The dbref property
-returns the string form “#id”.
-
-
The typeclassed entity has several common handlers:
-
-
tags - the TagHandler that handles tagging. Use tags.add() , tags.get() etc.
-
locks - the LockHandler that manages access restrictions. Use locks.add(),
-locks.get() etc.
-
attributes - the AttributeHandler that manages Attributes on the object. Use
-attributes.add()
-etc.
-
db (DataBase) - a shortcut property to the AttributeHandler; allowing obj.db.attrname=value
ndb (NotDataBase) - a shortcut property to the Non-peristent AttributeHandler. Allows
-obj.ndb.attrname=value
-
-
Each of the typeclassed entities then extend this list with their own properties. Go to the
-respective pages for Objects, Scripts, Accounts and
-Channels for more info. It’s also recommended that you explore the available
-entities using Evennia’s flat API to explore which properties and methods they have
-available.
The way to customize typeclasses is usually to overload hook methods on them. Hooks are methods
-that Evennia call in various situations. An example is the at_object_creation hook on Objects,
-which is only called once, the very first time this object is saved to the database. Other examples
-are the at_login hook of Accounts and the at_repeat hook of Scripts.
Most of the time you search for objects in the database by using convenience methods like the
-caller.search() of Commands or the search functions like evennia.search_objects.
-
You can however also query for them directly using Django’s query
-language. This makes use of a database
-manager that sits on all typeclasses, named objects. This manager holds methods that allow
-database searches against that particular type of object (this is the way Django normally works
-too). When using Django queries, you need to use the full field names (like db_key) to search:
-
matches=Furniture.objects.get(db_key="Chair")
-
-
-
-
It is important that this will only find objects inheriting directly from Furniture in your
-database. If there was a subclass of Furniture named Sitables you would not find any chairs
-derived from Sitables with this query (this is not a Django feature but special to Evennia). To
-find objects from subclasses Evennia instead makes the get_family and filter_family query
-methods available:
-
# search for all furnitures and subclasses of furnitures
-# whose names starts with "Chair"
-matches=Furniture.objects.filter_family(db_key__startswith="Chair")
-
-
-
-
To make sure to search, say, all Scriptsregardless of typeclass, you need to query from the
-database model itself. So for Objects, this would be ObjectDB in the diagram above. Here’s an
-example for Scripts:
When querying from the database model parent you don’t need to use filter_family or get_family -
-you will always query all children on the database model.
If you already have created instances of Typeclasses, you can modify the Python code at any time -
-due to how Python inheritance works your changes will automatically be applied to all children once
-you have reloaded the server.
-
However, database-saved data, like db_* fields, Attributes, Tags etc, are
-not themselves embedded into the class and will not be updated automatically. This you need to
-manage yourself, by searching for all relevant objects and updating or adding the data:
-
# add a worth Attribute to all existing Furniture
-forobjinFurniture.objects.all():
- # this will loop over all Furniture instances
- obj.db.worth=100
-
-
-
A common use case is putting all Attributes in the at_*_creation hook of the entity, such as
-at_object_creation for Objects. This is called every time an object is created - and only then.
-This is usually what you want but it does mean already existing objects won’t get updated if you
-change the contents of at_object_creation later. You can fix this in a similar way as above
-(manually setting each Attribute) or with something like this:
-
# Re-run at_object_creation only on those objects not having the new Attribute
-forobjinFurniture.objects.all():
- ifnotobj.db.worth:
- obj.at_object_creation()
-
-
-
The above examples can be run in the command prompt created by evenniashell. You could also run
-it all in-game using @py. That however requires you to put the code (including imports) as one
-single line using ; and list
-comprehensions, like this (ignore the
-line break, that’s only for readability in the wiki):
If you want to swap an already existing typeclass, there are two ways to do so: From in-game and via
-code. From inside the game you can use the default @typeclass command:
-
@typeclassobjname=path.to.new.typeclass
-
-
-
There are two important switches to this command:
-
-
/reset - This will purge all existing Attributes on the object and re-run the creation hook
-(like at_object_creation for Objects). This assures you get an object which is purely of this new
-class.
-
/force - This is required if you are changing the class to be the same class the object
-already has - it’s a safety check to avoid user errors. This is usually used together with /reset
-to re-run the creation hook on an existing class.
-
-
In code you instead use the swap_typeclass method which you can find on all typeclassed entities:
Technically, typeclasses are Django proxy
-models. The only database
-models that are “real” in the typeclass system (that is, are represented by actual tables in the
-database) are AccountDB, ObjectDB, ScriptDB and ChannelDB (there are also
-Attributes and Tags but they are not typeclasses themselves). All the
-subclasses of them are “proxies”, extending them with Python code without actually modifying the
-database layout.
-
Evennia modifies Django’s proxy model in various ways to allow them to work without any boiler plate
-(for example you don’t need to set the Django “proxy” property in the model Meta subclass, Evennia
-handles this for you using metaclasses). Evennia also makes sure you can query subclasses as well as
-patches django to allow multiple inheritance from the same base class.
Evennia uses the idmapper to cache its typeclasses (Django proxy models) in memory. The idmapper
-allows things like on-object handlers and properties to be stored on typeclass instances and to not
-get lost as long as the server is running (they will only be cleared on a Server reload). Django
-does not work like this by default; by default every time you search for an object in the database
-you’ll get a different instance of that object back and anything you stored on it that was not in
-the database would be lost. The bottom line is that Evennia’s Typeclass instances subside in memory
-a lot longer than vanilla Django model instance do.
-
There is one caveat to consider with this, and that relates to [making your own models](New-
-Models): Foreign relationships to typeclasses are cached by Django and that means that if you were
-to change an object in a foreign relationship via some other means than via that relationship, the
-object seeing the relationship may not reliably update but will still see its old cached version.
-Due to typeclasses staying so long in memory, stale caches of such relationships could be more
-visible than common in Django. See the closed issue #1098 and its
-comments for examples and solutions.
This tutorial aims at dispelling confusions regarding the use of color tags within Evennia.
-
Correct understanding of this topic requires having read the TextTags page and learned
-Evennia’s color tags. Here we’ll explain by examples the reasons behind the unexpected (or
-apparently incoherent) behaviors of some color tags, as mentioned en passant in the
-TextTags page.
-
All you’ll need for this tutorial is access to a running instance of Evennia via a color-enabled
-client. The examples provided are just commands that you can type in your client.
All modern MUD clients support colors; nevertheless, the standards to which all clients abide dates
-back to old day of terminals, and when it comes to colors we are dealing with ANSI and Xterm256
-standards.
-
Evennia handles transparently, behind the scenes, all the code required to enforce these
-standards—so, if a user connects with a client which doesn’t support colors, or supports only ANSI
-(16 colors), Evennia will take all due steps to ensure that the output will be adjusted to look
-right at the client side.
-
As for you, the developer, all you need to care about is knowing how to correctly use the color tags
-within your MUD. Most likely, you’ll be adding colors to help pages, descriptions, automatically
-generated text, etc.
-
You are free to mix together ANSI and Xterm256 color tags, but you should be aware of a few
-pitfalls. ANSI and Xterm256 coexist without conflicts in Evennia, but in many ways they don’t «see»
-each other: ANSI-specific color tags will have no effect on Xterm-defined colors, as we shall see
-here.
ANSI has a set of 16 colors, to be more precise: ANSI has 8 basic colors which come in dark and
-bright flavours—with dark being normal. The colors are: red, green, yellow, blue, magenta,
-cyan, white and black. White in its dark version is usually referred to as gray, and black in its
-bright version as darkgray. Here, for sake of simplicity they’ll be referred to as dark and bright:
-bright/dark black, bright/dark white.
-
The default colors of MUD clients is normal (dark) white on normal black (ie: gray on black).
-
It’s important to grasp that in the ANSI standard bright colors apply only to text (foreground), not
-to background. Evennia allows to bypass this limitation via Xterm256, but doing so will impact the
-behavior of ANSI tags, as we shall see.
-
Also, it’s important to remember that the 16 ANSI colors are a convention, and the final user can
-always customize their appearance—he might decide to have green show as red, and dark green as blue,
-etc.
The 16 colors of ANSI should be more than enough to handle simple coloring of text. But when an
-author wants to be sure that a given color will show as he intended it, she might choose to rely on
-Xterm256 colors.
-
Xterm256 doesn’t rely on a palette of named colors, it instead represent colors by their values. So,
-a red color could be |[500 (bright and pure red), or |[300 (darker red), and so on.
NOTE: for ease of reading, the examples contain extra white spaces after the
-color tags (eg: |ggreen|bblue ). This is done only so that it’s easier
-to see the tags separated from their context; it wouldn’t be good practice
-in real-life coding.
-
-
Let’s proceed by examples. In your MUD client type:
-
say Normal |* Negative
-
-
-
Evennia should output the word “Normal” normally (ie: gray on black) and “Negative” in reversed
-colors (ie: black on gray).
-
This is pretty straight forward, the |* ANSI invert tag switches between foreground and
-background—from now on, FG and BG shorthands will be used to refer to foreground and
-background.
-
But take mental note of this: |* has switched dark white and dark black.
-
Now try this:
-
say |w Bright white FG |* Negative
-
-
-
You’ll notice that the word “Negative” is not black on white, it’s darkgray on gray. Why is this?
-Shouldn’t it be black text on a white BG? Two things are happening here.
-
As mentioned, ANSI has 8 base colors, the dark ones. The bright ones are achieved by means of
-highlighting the base/dark/normal colors, and they only apply to FG.
-
What happened here is that when we set the bright white FG with |w, Evennia translated this into
-the ANSI sequence of Highlight On + White FG. In terms of Evennia’s color tags, it’s as if we typed:
-
say |h|!W Bright white FG |* Negative
-
-
-
Furthermore, the Highlight-On property (which only works for BG!) is preserved after the FG/BG
-switch, this being the reason why we see black as darkgray: highlighting makes it bright black
-(ie: darkgray).
-
As for the BG being also grey, that is normal—ie: you are seeing normal white (ie: dark white =
-gray). Remember that since there are no bright BG colors, the ANSI |* tag will transpose any FG
-color in its normal/dark version. So here the FG’s bright white became dark white in the BG! In
-reality, it was always normal/dark white, except that in the FG is seen as bright because of the
-highlight tag behind the scenes.
-
Let’s try the same thing with some color:
-
say |m |[G Bright Magenta on Dark Green |* Negative
-
-
-
Again, the BG stays dark because of ANSI rules, and the FG stays bright because of the implicit |h
-in |m.
-
Now, let’s see what happens if we set a bright BG and then invert—yes, Evennia kindly allows us to
-do it, even if it’s not within ANSI expectations.
-
say |[b Dark White on Bright Blue |* Negative
-
-
-
Before color inversion, the BG does show in bright blue, and after inversion (as expected) it’s
-dark white (gray). The bright blue of the BG survived the inversion and gave us a bright blue FG.
-This behavior is tricky though, and not as simple as it might look.
-
If the inversion were to be pure ANSI, the bright blue would have been accounted just as normal
-blue, and should have converted to normal blue in the FG (after all, there was no highlighting on).
-The fact is that in reality this color is not bright blue at all, it just an Xterm version of it!
-
To demonstrate this, type:
-
say |[b Dark White on Bright Blue |* Negative |H un-bright
-
-
-
The |H Highlight-Off tag should have turned dark blue the last word; but it didn’t because it
-couldn’t: in order to enforce the non-ANSI bright BG Evennia turned to Xterm, and Xterm entities are
-not affected by ANSI tags!
-
So, we are getting at the heart of all confusions and possible odd-behaviors pertaining color tags
-in Evennia: apart from Evennia’s translations from- and to- ANSI/Xterm, the two systems are
-independent and transparent to each other.
-
The bright blue of the previous example was just an Xterm representation of the ANSI standard blue.
-Try to change the default settings of your client, so that blue shows as some other color, you’ll
-then realize the difference when Evennia is sending a true ANSI color (which will show up according
-to your settings) and when instead it’s sending an Xterm representation of that color (which will
-show up always as defined by Evennia).
-
You’ll have to keep in mind that the presence of an Xterm BG or FG color might affect the way your
-tags work on the text. For example:
-
say |[b Bright Blue BG |* Negative |!Y Dark Yellow |h not bright
-
-
-
Here the |h tag no longer affects the FG color. Even though it was changed via the |! tag, the
-ANSI system is out-of-tune because of the intrusion of an Xterm color (bright blue BG, then moved to
-FG with |*).
-
All unexpected ANSI behaviours are the result of mixing Xterm colors (either on purpose or either
-via bright BG colors). The |n tag will restore things in place and ANSI tags will respond properly
-again. So, at the end is just an issue of being mindful when using Xterm colors or bright BGs, and
-avoid wild mixing them with ANSI tags without normalizing (|n) things again.
-
Try this:
-
say |[b Bright Blue BG |* Negative |!R Red FG
-
-
-
And then:
-
say |[B Dark Blue BG |* Negative |!R Red BG??
-
-
-
In this second example the |! changes the BG color instead of the FG! In fact, the odd behavior is
-the one from the former example, non the latter. When you invert FG and BG with |* you actually
-inverting their references. This is why the last example (which has a normal/dark BG!) allows |!
-to change the BG color. In the first example, it’s again the presence of an Xterm color (bright blue
-BG) which changes the default behavior.
-
Try this:
-
sayNormal|*Negative|!RRedBG
-
This is the normal behavior, and as you can see it allows |! to change BG color after the
-inversion of FG and BG.
-
As long as you have an understanding of how ANSI works, it should be easy to handle color tags
-avoiding the pitfalls of Xterm-ANSI promisquity.
-
One last example:
-
sayNormal|*Negative|*stillNegative
-
Shows that |* only works once in a row and will not (and should not!) revert back if used again.
-Nor it will have any effect until the |n tag is called to “reset” ANSI back to normal. This is how
-it is meant to work.
-
ANSI operates according to a simple states-based mechanism, and it’s important to understand the
-positive effect of resetting with the |n tag, and not try to
-push it over the limit, so to speak.
Unit testing means testing components of a program in isolation from each other to make sure every
-part works on its own before using it with others. Extensive testing helps avoid new updates causing
-unexpected side effects as well as alleviates general code rot (a more comprehensive wikipedia
-article on unit testing can be found here).
-
A typical unit test set calls some function or method with a given input, looks at the result and
-makes sure that this result looks as expected. Rather than having lots of stand-alone test programs,
-Evennia makes use of a central test runner. This is a program that gathers all available tests all
-over the Evennia source code (called test suites) and runs them all in one go. Errors and
-tracebacks are reported.
-
By default Evennia only tests itself. But you can also add your own tests to your game code and have
-Evennia run those for you.
To run the full Evennia test suite, go to your game folder and issue the command
-
evennia test evennia
-
-
-
This will run all the evennia tests using the default settings. You could also run only a subset of
-all tests by specifying a subpackage of the library:
-
evennia test evennia.commands.default
-
-
-
A temporary database will be instantiated to manage the tests. If everything works out you will see
-how many tests were run and how long it took. If something went wrong you will get error messages.
-If you contribute to Evennia, this is a useful sanity check to see you haven’t introduced an
-unexpected bug.
If you have implemented your own tests for your game (see below) you can run them from your game dir
-with
-
evennia test .
-
-
-
The period (.) means to run all tests found in the current directory and all subdirectories. You
-could also specify, say, typeclasses or world if you wanted to just run tests in those subdirs.
-
Those tests will all be run using the default settings. To run the tests with your own settings file
-you must use the --settings option:
-
evennia test --settings settings.py .
-
-
-
The --settings option of Evennia takes a file name in the mygame/server/conf folder. It is
-normally used to swap settings files for testing and development. In combination with test, it
-forces Evennia to use this settings file over the default one.
Evennia’s test suite makes use of Django unit test system, which in turn relies on Python’s
-unittest module.
-
-
If you want to help out writing unittests for Evennia, take a look at Evennia’s coveralls.io page. There you see which modules have any form of
-test coverage and which does not.
-
-
To make the test runner find the tests, they must be put in a module named test*.py (so test.py,
-tests.py etc). Such a test module will be found wherever it is in the package. It can be a good
-idea to look at some of Evennia’s tests.py modules to see how they look.
-
Inside a testing file, a unittest.TestCase class is used to test a single aspect or component in
-various ways. Each test case contains one or more test methods - these define the actual tests to
-run. You can name the test methods anything you want as long as the name starts with “test_”.
-Your TestCase class can also have a method setUp(self):. This is run before each test, setting
-up and storing whatever preparations the test methods need. Conversely, a tearDown(self): method
-can optionally do cleanup after each test.
-
To test the results, you use special methods of the TestCase class. Many of those start with
-“assert”, such as assertEqual or assertTrue.
-
Example of a TestCase class:
-
importunittest
-
- # the function we want to test
- frommypathimportmyfunc
-
- classTestObj(unittest.TestCase):
- "This tests a function myfunc."
-
- deftest_return_value(self):
- "test method. Makes sure return value is as expected."
- expected_return="This is me being nice."
- actual_return=myfunc()
- # test
- self.assertEqual(expected_return,actual_return)
- deftest_alternative_call(self):
- "test method. Calls with a keyword argument."
- expected_return="This is me being baaaad."
- actual_return=myfunc(bad=True)
- # test
- self.assertEqual(expected_return,actual_return)
-
Evennia offers a custom TestCase, the evennia.utils.test_resources.EvenniaTest class. This class
-initiates a range of useful properties on themselves for testing Evennia systems. Examples are
-.account and .session representing a mock connected Account and its Session and .char1 and
-.char2 representing Characters complete with a location in the test database. These are all useful
-when testing Evennia system requiring any of the default Evennia typeclasses as inputs. See the full
-definition of the EvenniaTest class in evennia/utils/test_resources.py.
-
# in a test module
-
-fromevennia.utils.test_resourcesimportEvenniaTest
-
-classTestObject(EvenniaTest):
- deftest_object_search(self):
- # char1 and char2 are both created in room1
- self.assertEqual(self.char1.search(self.char2.key),self.char2)
- self.assertEqual(self.char1.search(self.char1.location.key),self.char1.location)
- # ...
-
In-game Commands are a special case. Tests for the default commands are put in
-evennia/commands/default/tests.py. This uses a custom CommandTest class that inherits from
-evennia.utils.test_resources.EvenniaTest described above. CommandTest supplies extra convenience
-functions for executing commands and check that their return values (calls of msg() returns
-expected values. It uses Characters and Sessions generated on the EvenniaTest class to call each
-class).
-
Each command tested should have its own TestCase class. Inherit this class from the CommandTest
-class in the same module to get access to the command-specific utilities mentioned.
-
fromevennia.commands.default.testsimportCommandTest
- fromevennia.commands.defaultimportgeneral
- classTestSet(CommandTest):
- "tests the look command by simple call, using Char2 as a target"
- deftest_mycmd_char(self):
- self.call(general.CmdLook(),"Char2","Char2(#7)")
- "tests the look command by simple call, with target as room"
- deftest_mycmd_room(self):
- self.call(general.CmdLook(),"Room",
- "Room(#1)\nroom_desc\nExits: out(#3)\n"
- "You see: Obj(#4), Obj2(#5), Char2(#7)")
-
A special case is if you were to create a contribution to go to the evennia/contrib folder that
-uses its own database models. The problem with this is that Evennia (and Django) will
-only recognize models in settings.INSTALLED_APPS. If a user wants to use your contrib, they will
-be required to add your models to their settings file. But since contribs are optional you cannot
-add the model to Evennia’s central settings_default.py file - this would always create your
-optional models regardless of if the user wants them. But at the same time a contribution is a part
-of the Evennia distribution and its unit tests should be run with all other Evennia tests using
-evenniatestevennia.
-
The way to do this is to only temporarily add your models to the INSTALLED_APPS directory when the
-test runs. here is an example of how to do it.
-
-
Note that this solution, derived from this stackexchange answer is currently untested! Please report your findings.
Having an extensive tests suite is very important for avoiding code degradation as Evennia is
-developed. Only a small fraction of the Evennia codebase is covered by test suites at this point.
-Writing new tests is not hard, it’s more a matter of finding the time to do so. So adding new tests
-is really an area where everyone can contribute, also with only limited Python skills.
If you have custom models with a large number of migrations, creating the test database can take a
-very long time. If you don’t require migrations to run for your tests, you can disable them with the
-django-test-without-migrations package. To install it, simply:
-
$ pip install django-test-without-migrations
-
-
-
Then add it to your INSTALLED_APPS in your server.conf.settings.py:
Unit testing can be of paramount importance to game developers. When starting with a new game, it is
-recommended to look into unit testing as soon as possible; an already huge game is much harder to
-write tests for. The benefits of testing a game aren’t different from the ones regarding library
-testing. For example it is easy to introduce bugs that affect previously working code. Testing is
-there to ensure your project behaves the way it should and continue to do so.
-
If you have never used unit testing (with Python or another language), you might want to check the
-official Python documentation about unit testing,
-particularly the first section dedicated to a basic example.
Evennia’s test runner can be used to launch tests in your game directory (let’s call it ‘mygame’).
-Evennia’s test runner does a few useful things beyond the normal Python unittest module:
-
-
It creates and sets up an empty database, with some useful objects (accounts, characters and
-rooms, among others).
-
It provides simple ways to test commands, which can be somewhat tricky at times, if not tested
-properly.
-
-
Therefore, you should use the command-line to execute the test runner, while specifying your own
-game directories (not the one containing evennia). Go to your game directory (referred as ‘mygame’
-in this section) and execute the test runner:
-
evennia test --settings settings.py commands
-
-
-
This command will execute Evennia’s test runner using your own settings file. It will set up a dummy
-database of your choice and look into the ‘commands’ package defined in your game directory
-(mygame/commands in this example) to find tests. The test module’s name should begin with ‘test’
-and contain one or more TestCase. A full example can be found below.
In your game directory, go to commands and create a new file tests.py inside (it could be named
-anything starting with test). We will start by making a test that has nothing to do with Commands,
-just to show how unit testing works:
-
# mygame/commands/tests.py
-
- importunittest
-
- classTestString(unittest.TestCase):
-
- """Unittest for strings (just a basic example)."""
-
- deftest_upper(self):
- """Test the upper() str method."""
- self.assertEqual('foo'.upper(),'FOO')
-
-
-
This example, inspired from the Python documentation, is used to test the ‘upper()’ method of the
-‘str’ class. Not very useful, but it should give you a basic idea of how tests are used.
-
Let’s execute that test to see if it works.
-
> evennia test --settings settings.py commands
-
-TESTING: Using specified settings file 'server.conf.settings'.
-
-(Obs: Evennia's full test suite may not pass if the settings are very
-different from the default. Use 'test .' as arguments to run only tests
-on the game dir.)
-
-Creating test database for alias 'default'...
-.
-----------------------------------------------------------------------
-Ran 1 test in 0.001s
-
-OK
-Destroying test database for alias 'default'...
-
-
-
We specified the commands package to the evennia test command since that’s where we put our test
-file. In this case we could just as well just said . to search all of mygame for testing files.
-If we have a lot of tests it may be useful to test only a single set at a time though. We get an
-information text telling us we are using our custom settings file (instead of Evennia’s default
-file) and then the test runs. The test passes! Change the “FOO” string to something else in the test
-to see how it looks when it fails.
This section will test the proper execution of the ‘abilities’ command, as described in the
-First Steps Coding page. Follow this tutorial to create the ‘abilities’ command, we
-will need it to test it.
-
Testing commands in Evennia is a bit more complex than the simple testing example we have seen.
-Luckily, Evennia supplies a special test class to do just that … we just need to inherit from it
-and use it properly. This class is called ‘CommandTest’ and is defined in the
-‘evennia.commands.default.tests’ package. To create a test for our ‘abilities’ command, we just
-need to create a class that inherits from ‘CommandTest’ and add methods.
-
We could create a new test file for this but for now we just append to the tests.py file we
-already have in commands from before.
Line 1-4: we do some importing. ‘CommandTest’ is going to be our base class for our test, so we
-need it. We also import our command (‘CmdAbilities’ in this case). Finally we import the
-‘Character’ typeclass. We need it, since ‘CommandTest’ doesn’t use ‘Character’, but
-‘DefaultCharacter’, which means the character calling the command won’t have the abilities we have
-written in the ‘Character’ typeclass.
-
Line 6-8: that’s the body of our test. Here, a single command is tested in an entire class.
-Default commands are usually grouped by category in a single class. There is no rule, as long as
-you know where you put your tests. Note that we set the ‘character_typeclass’ class attribute to
-Character. As explained above, if you didn’t do that, the system would create a ‘DefaultCharacter’
-object, not a ‘Character’. You can try to remove line 4 and 8 to see what happens when running the
-test.
-
Line 10-11: our unique testing method. Note its name: it should begin by ‘test_’. Apart from
-that, the method is quite simple: it’s an instance method (so it takes the ‘self’ argument) but no
-other arguments are needed. Line 11 uses the ‘call’ method, which is defined in ‘CommandTest’.
-It’s a useful method that compares a command against an expected result. It would be like comparing
-two strings with ‘assertEqual’, but the ‘call’ method does more things, including testing the
-command in a realistic way (calling its hooks in the right order, so you don’t have to worry about
-that).
-
-
Line 11 can be understood as: test the ‘abilities’ command (first parameter), with no argument
-(second parameter), and check that the character using it receives his/her abilities (third
-parameter).
-
Let’s run our new test:
-
> evennia test --settings settings.py commands
-[...]
-Creating test database for alias 'default'...
-..
-----------------------------------------------------------------------
-Ran 2 tests in 0.156s
-
-OK
-Destroying test database for alias 'default'...
-
-
-
Two tests were executed, since we have kept ‘TestString’ from last time. In case of failure, you
-will get much more information to help you fix the bug.
Having read the unit test tutorial on Testing commands we can see
-the code expects static unchanging numbers. While very good for learning it is unlikely a project
-will have nothing but static output to test. Here we are going to learn how to test against dynamic
-output.
-
This tutorial assumes you have a basic understanding of what regular expressions are. If you do not
-I recommend reading the Introduction and SimplePattern sections at
-Python regular expressions tutorial. If you do plan on making a complete Evennia
-project learning regular expressions will save a great deal of time.
Noticed that we removed the test string from self.call. That method always returns the string it
-found from the commands output. If we remove the string to test against, all self.call will do is
-return the screen output from the command object passed to it.
-
We are instead using the next line to test our command’s output.
-assertRegex is a
-method of unittest.TestCase this is inherited to TestDynamicAbilities from CommandTest who
-inherited it from EvenniaTest.
-
What we are doing is testing the result of the CmdAbilities method or command against a regular
-expression pattern. In this case, "STR:\d+,AGI:\d+,MAG:\d+". \d in regular expressions or
-regex are digits (numbers), the + is to state we want 1 or more of that pattern. Together \d+ is
-telling assertRegex
-that in that position of the string we expect 1 or more digits (numbers) to appear in the
-string.
Fortunately, it’s extremely easy to keep your Evennia server up-to-date. If you haven’t already, see
-the Getting Started guide and get everything running.
Very commonly we make changes to the Evennia code to improve things. There are many ways to get told
-when to update: You can subscribe to the RSS feed or manually check up on the feeds from
-http://www.evennia.com. You can also simply fetch the latest regularly.
-
When you’re wanting to apply updates, simply cd to your cloned evennia root directory and type:
-
git pull
-
-
-
assuming you’ve got the command line client. If you’re using a graphical client, you will probably
-want to navigate to the evennia directory and either right click and find your client’s pull
-function, or use one of the menus (if applicable).
-
You can review the latest changes with
-
git log
-
-
-
or the equivalent in the graphical client. You can also see the latest changes online
-here.
-
You will always need to do evenniareload (or reload from -in-game) from your game-dir to have
-the new code affect your game. If you want to be really sure you should run a full evenniareboot
-so that both Server and Portal can restart (this will disconnect everyone though, so if you know the
-Portal has had no updates you don’t have to do that).
On occasion we update the versions of third-party libraries Evennia depend on (or we may add a new
-dependency). This will be announced on the mailing list/forum. If you run into errors when starting
-Evennia, always make sure you have the latest versions of everything. In some cases, like for
-Django, starting the server may also give warning saying that you are using a working, but too-old
-version that should not be used in production.
-
Upgrading evennia will automatically fetch all the latest packages that it now need. First cd to
-your cloned evennia folder. Make sure your virtualenv is active and use
-
pip install --upgrade -e .
-
-
-
Remember the period (.) at the end - that applies the upgrade to the current location (your
-evennia dir).
-
-
The -e means that we are linking the evennia sources rather than copying them into the
-environment. This means we can most of the time just update the sources (with gitpull) and see
-those changes directly applied to our installed evennia package. Without installing/upgrading the
-package with -e, we would have to remember to upgrade the package every time we downloaded any new
-source-code changes.
-
-
Follow the upgrade output to make sure it finishes without errors. To check what packages are
-currently available in your python environment after the upgrade, use
-
pip list
-
-
-
This will show you the version of all installed packages. The evennia package will also show the
-location of its source code.
Whenever we change the database layout of Evennia upstream (such as when we add new features) you
-will need to migrate your existing database. When this happens it will be clearly noted in the
-gitlog (it will say something to the effect of “Run migrations”). Database changes will also be
-announced on the Evennia mailing list.
-
When the database schema changes, you just go to your game folder and run
-
evennia migrate
-
-
-
-
Hint: If the evennia command is not found, you most likely need to activate your
-virtualenv.
Should you ever want to start over completely from scratch, there is no need to re-download Evennia
-or anything like that. You just need to clear your database. Once you are done, you just rebuild it
-from scratch as described in the second step of the Getting Started guide.
-
First stop a running server with
-
evennia stop
-
-
-
If you run the default SQlite3 database (to change this you need to edit your settings.py file),
-the database is actually just a normal file in mygame/server/ called evennia.db3. Simply delete
-that file - that’s it. Now run evenniamigrate to recreate a new, fresh one.
-
If you run some other database system you can instead flush the database:
-
evennia flush
-
-
-
This will empty the database. However, it will not reset the internal counters of the database, so
-you will start with higher dbref values. If this is okay, this is all you need.
-
Django also offers an easy way to start the database’s own management should we want more direct
-control:
-
evennia dbshell
-
-
-
In e.g. MySQL you can then do something like this (assuming your MySQL database is named “Evennia”:
-
mysql> DROP DATABASE Evennia;
-mysql> exit
-
-
-
-
NOTE: Under Windows OS, in order to access SQLite dbshell you need to download the SQLite
-command-line shell program. It’s a single executable file
-(sqlite3.exe) that you should place in the root of either your MUD folder or Evennia’s (it’s the
-same, in both cases Django will find it).
If and when an Evennia update modifies the database schema (that is, the under-the-hood details as
-to how data is stored in the database), you must update your existing database correspondingly to
-match the change. If you don’t, the updated Evennia will complain that it cannot read the database
-properly. Whereas schema changes should become more and more rare as Evennia matures, it may still
-happen from time to time.
-
One way one could handle this is to apply the changes manually to your database using the database’s
-command line. This often means adding/removing new tables or fields as well as possibly convert
-existing data to match what the new Evennia version expects. It should be quite obvious that this
-quickly becomes cumbersome and error-prone. If your database doesn’t contain anything critical yet
-it’s probably easiest to simply reset it and start over rather than to bother converting.
-
Enter migrations. Migrations keeps track of changes in the database schema and applies them
-automatically for you. Basically, whenever the schema changes we distribute small files called
-“migrations” with the source. Those tell the system exactly how to implement the change so you don’t
-have to do so manually. When a migration has been added we will tell you so on Evennia’s mailing
-lists and in commit messages -
-you then just run evenniamigrate to be up-to-date again.
Evennia allows for any command syntax. If you like the way DikuMUDs, LPMuds or MOOs handle things,
-you could emulate that with Evennia. If you are ambitious you could even design a whole new style,
-perfectly fitting your own dreams of the ideal game.
-
We do offer a default however. The default Evennia setup tends to resemble
-MUX2, and its cousins PennMUSH,
-TinyMUSH, and RhostMUSH. While the
-reason for this similarity is partly historical, these codebases offer very mature feature sets for
-administration and building.
-
Evennia is not a MUX system though. It works very differently in many ways. For example, Evennia
-deliberately lacks an online softcode language (a policy explained on our softcode policy
-page). Evennia also does not shy from using its own syntax when deemed appropriate: the
-MUX syntax has grown organically over a long time and is, frankly, rather arcane in places. All in
-all the default command syntax should at most be referred to as “MUX-like” or “MUX-inspired”.
Angled brackets <> surround a description of what to write rather than the exact syntax.
-
*Explicit choices are separated by |. To avoid this being parsed as a color code, use || (this
-will come out as a single |) or put spaces around the character (“|”) if there’s plenty of
-room.
-
The Switches and Examples blocks are optional based on the Command.
-
-
Here is the nick command as an example:
-
"""
- Define a personal alias/nick
-
- Usage:
- nick[/switches] <nickname> = [<string>]
- alias ''
-
- Switches:
- object - alias an object
- account - alias an account
- clearall - clear all your aliases
- list - show all defined aliases (also "nicks" works)
-
- Examples:
- nick hi = say Hello, I'm Sarah!
- nick/object tom = the tall man
-
- A 'nick' is a personal shortcut you create for your own use [...]
-
- """
-
-
-
For commands that require arguments, the policy is for it to return a Usage: string if the
-command is entered without any arguments. So for such commands, the Command body should contain
-something to the effect of
Evennia uses Travis CI to check that it’s building successfully after every
-commit to its Github repository (you can for example see the build:passing badge at the top of
-Evennia’s Readme file). If your game is open source on Github
-you may use Travis for free. See the Travis docs
-for how to get started.
-
After logging in you need to point Travis to your repository on github. One further thing you need
-to set up yourself is a Travis config file named .travis.yml (note the initial period .). This
-should be created in the root of your game directory.
Here we tell Travis how to download and install Evennia into a folder a level up from your game dir.
-It will then install the server (so the evennia command is available) and run the tests only for
-your game dir (based on your settings.py file in server/conf/).
-
Running this will not actually do anything though, because there are no unit tests in your game dir
-yet. We have a page on how we set those up for Evennia, you should be able to refer
-to that for making tests fitting your game.
Version control software allows you to track the changes you make to your code, as well as being
-able to easily backtrack these changes, share your development efforts and more. Even if you are not
-contributing to Evennia itself, and only wish to develop your own MU* using Evennia, having a
-version control system in place is a good idea (and standard coding practice). For an introduction
-to the concept, start with the Wikipedia article
-here. Evennia uses the version control system
-Git and this is what will be covered henceforth. Note that this page also
-deals with commands for Linux operating systems, and the steps below may vary for other systems,
-however where possible links will be provided for alternative instructions.
If you have gotten Evennia installed, you will have Git already and can skip to Step 2 below.
-Otherwise you will need to install Git on your platform. You can find expanded instructions for
-installation here.
To avoid a common issue later, you will need to set a couple of settings; first you will need to
-tell Git your username, followed by your e-mail address, so that when you commit code later you will
-be properly credited.
-
-
Note that your commit information will be visible to everyone if you ever contribute to Evennia or
-use an online service like github to host your code. So if you are not comfortable with using your
-real, full name online, put a nickname here.
-
-
-
Set the default name for git to use when you commit:
-
git config --global user.name "Your Name Here"
-
-
-
-
Set the default email for git to use when you commit:
When working on your code or fix bugs in your local branches you may end up creating new files. If
-you do you must tell Git to track them by using the add command:
-
gitadd<filename>
-
-
-
You can check the current status of version control with gitstatus. This will show if you have
-any modified, added or otherwise changed files. Some files, like database files, logs and temporary
-PID files are usually not tracked in version control. These should either not show up or have a
-question mark in front of them.
You will notice that some files are not covered by your git version control, notably your settings
-file (mygame/server/conf/settings.py) and your sqlite3 database file mygame/server/evennia.db3.
-This is controlled by the hidden file mygame/.gitignore. Evennia creates this file as part of the
-creation of your game directory. Everything matched in this file will be ignored by GIT. If you want
-to, for example, include your settings file for collaborators to access, remove that entry in
-.gitignore.
-
-
Note: You should never put your sqlite3 database file into git by removing its entry in
-.gitignore. GIT is for backing up your code, not your database. That way lies madness and a good
-chance you’ll confuse yourself so that after a few commits and reverts don’t know what is in your
-database or not. If you want to backup your database, do so by simply copying the file on your hard
-drive to a backup-name.
Committing means storing the current snapshot of your code within git. This creates a “save point”
-or “history” of your development process. You can later jump back and forth in your history, for
-example to figure out just when a bug was introduced or see what results the code used to produce
-compared to now.
-
-
It’s usually a good idea to commit your changes often. Committing is fast and local only - you will
-never commit anything online at this point. To commit your changes, use
-
gitcommit--all
-
-
-
This will save all changes you made since last commit. The command will open a text editor where you
-can add a message detailing the changes you’ve made. Make it brief but informative. You can see the
-history of commits with gitlog. If you don’t want to use the editor you can set the message
-directly by using the -m flag:
-
gitcommit--all-m"This fixes a bug in the combat code."
-
If you have non-committed changes that you realize you want to throw away, you can do the following:
-
gitcheckout<filetorevert>
-
-
-
This will revert the file to the state it was in at your last commit, throwing away the changes
-you did to it since. It’s a good way to make wild experiments without having to remember just what
-you changed. If you do gitcheckout. you will throw away all changes since the last commit.
So far your code is only located on your private machine. A good idea is to back it up online. The
-easiest way to do this is to push it to your own remote repository on GitHub.
-
-
Make sure you have your game directory setup under git version control as described above. Make
-sure to commit any changes.
-
Create a new, empty repository on Github. Github explains how
-here (do not “Initialize the repository with a
-README” or else you’ll create unrelated histories).
-
From your local game dir, do gitremoteaddorigin<githubURL> where <githubURL> is the URL
-to your online repo. This tells your game dir that it should be pushing to the remote online dir.
-
gitremote-v to verify the online dir.
-
gitpushoriginmaster now pushes your game dir online so you can see it on github.com.
-
-
You can commit your work locally (gitcommit--all-m"Makeachangethat...") as many times as
-you want. When you want to push those changes to your online repo, you do gitpush. You can also
-gitclone<url_to_online_repo> from your online repo to somewhere else (like your production
-server) and henceforth do gitpull to update that to the latest thing you pushed.
-
Note that GitHub’s repos are, by default publicly visible by all. Creating a publicly visible online
-clone might not be what you want for all parts of your development process - you may prefer a more
-private venue when sharing your revolutionary work with your team. If that’s the case you can change
-your repository to “Private” in the github settings. Then your code will only be visible to those
-you specifically grant access.
Before proceeding with the following step, make sure you have registered and created an account on
-GitHub.com. This is necessary in order to create a fork of Evennia’s master
-repository, and to push your commits to your fork either for yourself or for contributing to
-Evennia.
-
-
A fork is a clone of the master repository that you can make your own commits and changes to. At
-the top of this page, click the “Fork” button, as it appears
-below.
The fork only exists online as of yet. In a terminal, change your directory to the folder you wish
-to develop in. From this directory run the following command:
A remote is a repository stored on another computer, in this case on GitHub’s server. When a
-repository is cloned, it has a default remote called origin. This points to your fork on GitHub,
-not the original repository it was forked from. To easily keep track of the original repository
-(that is, Evennia’s official repository), you need to add another remote. The standard name for this
-remote is “upstream”.
-
Below we change the active directory to the newly cloned “evennia” directory and then assign the
-original Evennia repository to a remote called “upstream”:
If you also want to access Evennia’s develop branch (the bleeding edge development branch) do the
-following:
-
gitfetchupstreamdevelop
-gitcheckoutdevelop
-
-
-
You should now have the upstream branch available locally. You can use this instead of master
-below if you are contributing new features rather than bug fixes.
A branch is a separate instance of your code. Changes you do to code in a branch does not affect
-that in other branches (so if you for example add/commit a file to one branch and then switches to
-another branch, that file will be gone until you switch back to the first branch again). One can
-switch between branches at will and create as many branches as one needs for a given project. The
-content of branches can also be merged together or deleted without affecting other branches. This is
-not only a common way to organize development but also to test features without messing with
-existing code.
-
-
The default branch of git is called the “master” branch. As a rule of thumb, you should never
-make modifications directly to your local copy of the master branch. Rather keep the master clean
-and only update it by pulling our latest changes to it. Any work you do should instead happen in a
-local, other branches.
This command will checkout and automatically create the new branch myfixes on your machine. If you
-stared out in the master branch, myfixes will be a perfect copy of the master branch. You can see
-which branch you are on with gitbranch and change between different branches with gitcheckout<branchname>.
-
Branches are fast and cheap to create and manage. It is common practice to create a new branch for
-every bug you want to work on or feature you want to create, then create a pull request for that
-branch to be merged upstream (see below). Not only will this organize your work, it will also make
-sure that your master branch version of Evennia is always exactly in sync with the upstream
-version’s master branch.
When Evennia’s official repository updates, first make sure to commit all your changes to your
-branch and then checkout the “clean” master branch:
-
gitcommit--all
-gitcheckoutmaster
-
-
-
Pull the latest changes from upstream:
-
gitpullupstreammaster
-
-
-
This should sync your local master branch with upstream Evennia’s master branch. Now we go back to
-our own work-branch (let’s say it’s still called “myfixes”) and merge the updated master into our
-branch.
-
gitcheckoutmyfixes
-gitmergemaster
-
-
-
If everything went well, your myfixes branch will now have the latest version of Evennia merged
-with whatever changes you have done. Use gitlog to see what has changed. You may need to restart
-the server or run manage.pymigrate if the database schema changed (this will be seen in the
-commit log and on the mailing list). See the Git manuals for
-learning more about useful day-to-day commands, and special situations such as dealing with merge
-collisions.
Up to this point your myfixes branch only exists on your local computer. No one else can see it.
-If you want a copy of this branch to also appear in your online fork on GitHub, make sure to have
-checked out your “myfixes” branch and then run the following:
-
gitpush-uoriginmyfixes
-
-
-
This will create a new remote branch named “myfixes” in your online repository (which is refered
-to as “origin” by default); the -u flag makes sure to set this to the default push location.
-Henceforth you can just use gitpush from your myfixes branch to push your changes online. This is
-a great way to keep your source backed-up and accessible. Remember though that by default your
-repository will be public so everyone will be able to browse and download your code (same way as you
-can with Evennia itself). If you want secrecy you can change your repository to “Private” in the
-Github settings. Note though that if you do, you might have trouble contributing to Evennia (since
-we can’t see the code you want to share).
-
Note: If you hadn’t setup a public key on GitHub or aren’t asked for a username/password, you might
-get an error 403:ForbiddenAccess at this stage. In that case, some users have reported that the
-workaround is to create a file .netrc under your home directory and add your credentials there:
Contributing can mean both bug-fixes or adding new features to Evennia. Please note that if your
-change is not already listed and accepted in the Issue
-Tracker, it is recommended that you first hit the
-developer mailing list or IRC chat to see beforehand if your feature is deemed suitable to include
-as a core feature in the engine. When it comes to bug-fixes, other developers may also have good
-input on how to go about resolving the issue.
-
To contribute you need to have forked Evennia first. As described
-above you should do your modification in a separate local branch (not in the master branch). This
-branch is what you then present to us (as a Pull request, PR, see below). We can then merge your
-change into the upstream master and you then do gitpull to update master usual. Now that the
-master is updated with your fixes, you can safely delete your local work branch. Below we describe
-this work flow.
-
First update the Evennia master branch to the latest Evennia version:
-
gitcheckoutmaster
-gitpullupstreammaster
-
-
-
Next, create a new branch to hold your contribution. Let’s call it the “fixing_strange_bug” branch:
-
gitcheckout-bfixing_strange_bug
-
-
-
It is wise to make separate branches for every fix or series of fixes you want to contribute. You
-are now in your new fixing_strange_bug branch. You can list all branches with gitbranch and
-jump between branches with gitcheckout<branchname>. Code and test things in here, committing as
-you go:
-
gitcommit--all-m"Fix strange bug in look command. Resolves #123."
-
-
-
You can make multiple commits if you want, depending on your work flow and progress. Make sure to
-always make clear and descriptive commit messages so it’s easy to see what you intended. To refer
-to, say, issue number 123, write #123, it will turn to a link on GitHub. If you include the text
-“Resolves #123”, that issue will be auto-closed on GitHub if your commit gets merged into main
-Evennia.
-
-
If you refer to in-game commands that start with @(such as @examine), please put them in
-backticks `, for example `@examine`. The reason for this is that GitHub uses @username to refer
-to GitHub users, so if you forget the ticks, any user happening to be named examine will get a
-notification …
-
-
If you implement multiple separate features/bug-fixes, split them into different branches if they
-are very different and should be handled as separate PRs. You can do any number of commits to your
-branch as you work. Once you are at a stage where you want to show the world what you did you might
-want to consider making it clean for merging into Evennia’s master branch by using git
-rebase (this is not always necessary,
-and if it sounds too hard, say so and we’ll handle it on our end).
-
Once you are ready, push your work to your online Evennia fork on github, in a new remote branch:
-
gitpush-uoriginfixing_strange_bug
-
-
-
The -u flag is only needed the first time - this tells GIT to create a remote branch. If you
-already created the remote branch earlier, just stand in your fixing_strange_bug branch and do
-gitpush.
-
Now you should tell the Evennia developers that they should consider merging your brilliant changes
-into Evennia proper. Create a pull request and follow
-the instructions. Make sure to specifically select your fixing_strange_bug branch to be the source
-of the merge. Evennia developers will then be able to examine your request and merge it if it’s
-deemed suitable.
-
Once your changes have been merged into Evennia your local fixing_strange_bug can be deleted
-(since your changes are now available in the “clean” Evennia repository). Do
-
gitbranch-Dfixing_strange_bug
-
-
-
to delete your work branch. Update your master branch (checkoutmaster and then gitpull) and
-you should get your fix back, now as a part of official Evennia!
Some of the GIT commands can feel a little long and clunky if you need to do them often. Luckily you
-can create aliases for those. Here are some useful commands to run:
-
# git st
-# - view brief status info
-gitconfig--globalalias.st'status -s'
-
-
-
Above, you only need to ever enter the gitconfig... command once - you have then added the new
-alias. Afterwards, just do gitst to get status info. All the examples below follow the same
-template.
-
# git cl
-# - clone a repository
-gitconfig--globalalias.clclone
-
-
-
# git cma "commit message"
-# - commit all changes without opening editor for message
-gitconfig--globalalias.cma'commit -a -m'
-
-
-
# git ca
-# - amend text to your latest commit message
-gitconfig--globalalias.ca'commit --amend'
-
-
-
# git fl
-# - file log; shows diffs of files in latest commits
-gitconfig--globalalias.fl'log -u'
-
-
-
# git co [branchname]
-# - checkout
-gitconfig--globalalias.cocheckout
-
# git ls
-# - view log tree
-gitconfig--globalalias.ls'log --pretty=format:"%C(green)%h\ %C(yellow)[%ad]%Cred%d\
-%Creset%s%Cblue\ [%cn]" --decorate --date=relative --graph'
-
-
-
# git diff
-# - show current uncommitted changes
-gitconfig--globalalias.diff'diff --word-diff'
-
-
-
# git grep <query>
-# - search (grep) codebase for a search criterion
-gitconfig--globalalias.grep'grep -Ii'
-
-
-
To get a further feel for GIT there is also a good YouTube talk about
-it - it’s a bit long but it will help you
-understand the underlying ideas behind GIT
-(which in turn makes it a lot more intuitive to use).
This tutorial will have us create a simple weather system for our MUD. The way we want to use this
-is to have all outdoor rooms echo weather-related messages to the room at regular and semi-random
-intervals. Things like “Clouds gather above”, “It starts to rain” and so on.
-
One could imagine every outdoor room in the game having a script running on themselves that fires
-regularly. For this particular example it is however more efficient to do it another way, namely by
-using a “ticker-subscription” model. The principle is simple: Instead of having each Object
-individually track the time, they instead subscribe to be called by a global ticker who handles time
-keeping. Not only does this centralize and organize much of the code in one place, it also has less
-computing overhead.
-
Evennia offers the TickerHandler specifically for using the subscription model. We
-will use it for our weather system.
-
We will assume you know how to make your own Typeclasses. If not see one of the beginning tutorials.
-We will create a new WeatherRoom typeclass that is aware of the day-night cycle.
-
- importrandom
- fromevenniaimportDefaultRoom,TICKER_HANDLER
-
- ECHOES=["The sky is clear.",
- "Clouds gather overhead.",
- "It's starting to drizzle.",
- "A breeze of wind is felt.",
- "The wind is picking up"]# etc
-
- classWeatherRoom(DefaultRoom):
- "This room is ticked at regular intervals"
-
- defat_object_creation(self):
- "called only when the object is first created"
- TICKER_HANDLER.add(60*60,self.at_weather_update)
-
- defat_weather_update(self,*args,**kwargs):
- "ticked at regular intervals"
- echo=random.choice(ECHOES)
- self.msg_contents(echo)
-
-
-
In the at_object_creation method, we simply added ourselves to the TickerHandler and tell it to
-call at_weather_update every hour (60*60 seconds). During testing you might want to play with a
-shorter time duration.
-
For this to work we also create a custom hook at_weather_update(*args,**kwargs), which is the
-call sign required by TickerHandler hooks.
-
Henceforth the room will inform everyone inside it when the weather changes. This particular example
-is of course very simplistic - the weather echoes are just randomly chosen and don’t care what
-weather came before it. Expanding it to be more realistic is a useful exercise.
This tutorial will create a simple web-based interface for generating a new in-game Character.
-Accounts will need to have first logged into the website (with their AccountDB account). Once
-finishing character generation the Character will be created immediately and the Accounts can then
-log into the game and play immediately (the Character will not require staff approval or anything
-like that). This guide does not go over how to create an AccountDB on the website with the right
-permissions to transfer to their web-created characters.
-
It is probably most useful to set MULTISESSION_MODE=2 or 3 (which gives you a character-
-selection screen when you log into the game later). Other modes can be used with some adaptation to
-auto-puppet the new Character.
-
You should have some familiarity with how Django sets up its Model Template View framework. You need
-to understand what is happening in the basic Web Character View tutorial. If you don’t understand the listed tutorial or have a grasp of Django basics, please look
-at the Django tutorial to get a taste of what Django
-does, before throwing Evennia into the mix (Evennia shares its API and attributes with the website
-interface). This guide will outline the format of the models, views, urls, and html templates
-needed.
Here are some screenshots of the simple app we will be making.
-
Index page, with no character application yet done:
-
-
-
-
Having clicked the “create” link you get to create your character (here we will only have name and
-background, you can add whatever is needed to fit your game):
-
-
-
-
Back to the index page. Having entered our character application (we called our character “TestApp”)
-you see it listed:
-
-
-
-
We can also view an already written character application by clicking on it - this brings us to the
-detail page:
Assuming your game is named “mygame”, navigate to your mygame/ directory, and type:
-
evennia startapp chargen
-
-
-
This will initialize a new Django app we choose to call “chargen”. It is directory containing some
-basic starting things Django needs. You will need to move this directory: for the time being, it is
-in your mygame directory. Better to move it in your mygame/web directory, so you have
-mygame/web/chargen in the end.
-
Next, navigate to mygame/server/conf/settings.py and add or edit the following line to make
-Evennia (and Django) aware of our new app:
-
INSTALLED_APPS += ('web.chargen',)
-
-
-
After this, we will get into defining our models (the description of the database storage),
-views (the server-side website content generators), urls (how the web browser finds the pages)
-and templates (how the web page should be structured).
Models are created in mygame/web/chargen/models.py.
-
A Django database model is a Python class that describes the database storage of the
-data you want to manage. Any data you choose to store is stored in the same database as the game and
-you have access to all the game’s objects here.
-
We need to define what a character application actually is. This will differ from game to game so
-for this tutorial we will define a simple character sheet with the following database fields:
-
-
app_id (AutoField): Primary key for this character application sheet.
-
char_name (CharField): The new character’s name.
-
date_applied (DateTimeField): Date that this application was received.
-
background (TextField): Character story background.
-
account_id (IntegerField): Which account ID does this application belong to? This is an
-AccountID from the AccountDB object.
-
submitted (BooleanField): True/False depending on if the application has been submitted yet.
-
-
-
Note: In a full-fledged game, you’d likely want them to be able to select races, skills,
-attributes and so on.
-
-
Our models.py file should look something like this:
You should consider how you are going to link your application to your account. For this tutorial,
-we are using the account_id attribute on our character application model in order to keep track of
-which characters are owned by which accounts. Since the account id is a primary key in Evennia, it
-is a good candidate, as you will never have two of the same IDs in Evennia. You can feel free to use
-anything else, but for the purposes of this guide, we are going to use account ID to join the
-character applications with the proper account.
you should have filled out mygame/web/chargen/models.py with the model class shown above
-(eventually adding fields matching what you need for your game).
Views are server-side constructs that make dynamic data available to a web page. We are going to
-add them to mygame/web/chargen.views.py. Each view in our example represents the backbone of a
-specific web page. We will use three views and three pages here:
-
-
The index (managing index.html). This is what you see when you navigate to
-http://yoursite.com/chargen.
-
The detail display sheet (manages detail.html). A page that passively displays the stats of a
-given Character.
-
Character creation sheet (manages create.html). This is the main form with fields to fill in.
We’ll want characters to be able to see their created characters so let’s
-
# file mygame/web/chargen.views.py
-
-from.modelsimportCharApp
-
-defindex(request):
- current_user=request.user# current user logged in
- p_id=current_user.id# the account id
- # submitted Characters by this account
- sub_apps=CharApp.objects.filter(account_id=p_id,submitted=True)
- context={'sub_apps':sub_apps}
- # make the variables in 'context' available to the web page template
- returnrender(request,'chargen/index.html',context)
-
Our detail page will have pertinent character application information our users can see. Since this
-is a basic demonstration, our detail page will only show two fields:
-
-
Character name
-
Character background
-
-
We will use the account ID again just to double-check that whoever tries to check our character page
-is actually the account who owns the application.
Predictably, our create function will be the most complicated of the views, as it needs to accept
-information from the user, validate the information, and send the information to the server. Once
-the form content is validated will actually create a playable Character.
-
The form itself we will define first. In our simple example we are just looking for the Character’s
-name and background. This form we create in mygame/web/chargen/forms.py:
# file mygame/web/chargen/views.py
-
-fromweb.chargen.modelsimportCharApp
-fromweb.chargen.formsimportAppForm
-fromdjango.httpimportHttpResponseRedirect
-fromdatetimeimportdatetime
-fromevennia.objects.modelsimportObjectDB
-fromdjango.confimportsettings
-fromevennia.utilsimportcreate
-
-defcreating(request):
- user=request.user
- ifrequest.method=='POST':
- form=AppForm(request.POST)
- ifform.is_valid():
- name=form.cleaned_data['name']
- background=form.cleaned_data['background']
- applied_date=datetime.now()
- submitted=True
- if'save'inrequest.POST:
- submitted=False
- app=CharApp(char_name=name,background=background,
- date_applied=applied_date,account_id=user.id,
- submitted=submitted)
- app.save()
- ifsubmitted:
- # Create the actual character object
- typeclass=settings.BASE_CHARACTER_TYPECLASS
- home=ObjectDB.objects.get_id(settings.GUEST_HOME)
- # turn the permissionhandler to a string
- perms=str(user.permissions)
- # create the character
- char=create.create_object(typeclass=typeclass,key=name,
- home=home,permissions=perms)
- user.db._playable_characters.append(char)
- # add the right locks for the character so the account can
- # puppet it
- char.locks.add("puppet:id(%i) or pid(%i) or perm(Developers) "
- "or pperm(Developers)"%(char.id,user.id))
- char.db.background=background# set the character background
- returnHttpResponseRedirect('/chargen')
- else:
- form=AppForm()
- returnrender(request,'chargen/create.html',{'form':form})
-
-
-
-
Note also that we basically create the character using the Evennia API, and we grab the proper
-permissions from the AccountDB object and copy them to the character object. We take the user
-permissions attribute and turn that list of strings into a string object in order for the
-create_object function to properly process the permissions.
-
-
Most importantly, the following attributes must be set on the created character object:
The Character’s home room location (#2 by default)
-
-
Other attributes are strictly speaking optional, such as the background attribute on our
-character. It may be a good idea to decompose this function and create a separate _create_character
-function in order to set up your character object the account owns. But with the Evennia API,
-setting custom attributes is as easy as doing it in the meat of your Evennia game directory.
-
After all of this, our views.py file should look like something like this:
-
# file mygame/web/chargen/views.py
-
-fromdjango.shortcutsimportrender
-fromweb.chargen.modelsimportCharApp
-fromweb.chargen.formsimportAppForm
-fromdjango.httpimportHttpResponseRedirect
-fromdatetimeimportdatetime
-fromevennia.objects.modelsimportObjectDB
-fromdjango.confimportsettings
-fromevennia.utilsimportcreate
-
-defindex(request):
- current_user=request.user# current user logged in
- p_id=current_user.id# the account id
- # submitted apps under this account
- sub_apps=CharApp.objects.filter(account_id=p_id,submitted=True)
- context={'sub_apps':sub_apps}
- returnrender(request,'chargen/index.html',context)
-
-defdetail(request,app_id):
- app=CharApp.objects.get(app_id=app_id)
- name=app.char_name
- background=app.background
- submitted=app.submitted
- p_id=request.user.id
- context={'name':name,'background':background,
- 'p_id':p_id,'submitted':submitted}
- returnrender(request,'chargen/detail.html',context)
-
-defcreating(request):
- user=request.user
- ifrequest.method=='POST':
- form=AppForm(request.POST)
- ifform.is_valid():
- name=form.cleaned_data['name']
- background=form.cleaned_data['background']
- applied_date=datetime.now()
- submitted=True
- if'save'inrequest.POST:
- submitted=False
- app=CharApp(char_name=name,background=background,
- date_applied=applied_date,account_id=user.id,
- submitted=submitted)
- app.save()
- ifsubmitted:
- # Create the actual character object
- typeclass=settings.BASE_CHARACTER_TYPECLASS
- home=ObjectDB.objects.get_id(settings.GUEST_HOME)
- # turn the permissionhandler to a string
- perms=str(user.permissions)
- # create the character
- char=create.create_object(typeclass=typeclass,key=name,
- home=home,permissions=perms)
- user.db._playable_characters.append(char)
- # add the right locks for the character so the account can
- # puppet it
- char.locks.add("puppet:id(%i) or pid(%i) or perm(Developers) "
- "or pperm(Developers)"%(char.id,user.id))
- char.db.background=background# set the character background
- returnHttpResponseRedirect('/chargen')
- else:
- form=AppForm()
- returnrender(request,'chargen/create.html',{'form':form})
-
You could change the format as you desire. To make it more secure, you could remove app_id from the
-“detail” url, and instead just fetch the account’s applications using a unifying field like
-account_id to find all the character application objects to display.
-
We must also update the main mygame/web/urls.py file (that is, one level up from our chargen app),
-so the main website knows where our app’s views are located. Find the patterns variable, and
-change it to include:
-
# in file mygame/web/urls.py
-
-fromdjango.conf.urlsimporturl,include
-
-# default evennia patterns
-fromevennia.web.urlsimporturlpatterns
-
-# eventual custom patterns
-custom_patterns=[
- # url(r'/desired/url/', view, name='example'),
-]
-
-# this is required by Django.
-urlpatterns+=[
- url(r'^chargen/',include('web.chargen.urls')),
-]
-
-urlpatterns=custom_patterns+urlpatterns
-
So we have our url patterns, views, and models defined. Now we must define our HTML templates that
-the actual user will see and interact with. For this tutorial we us the basic prosimii template
-that comes with Evennia.
-
Take note that we use user.is_authenticated to make sure that the user cannot create a character
-without logging in.
-
These files will all go into the /mygame/web/chargen/templates/chargen/ directory.
This HTML template should hold a list of all the applications the account currently has active. For
-this demonstration, we will only list the applications that the account has submitted. You could
-easily adjust this to include saved applications, or other types of applications if you have
-different kinds.
-
Please refer back to views.py to see where we define the variables these templates make use of.
This page should show a detailed character sheet of their application. This will only show their
-name and character background. You will likely want to extend this to show many more fields for your
-game. In a full-fledged character generation, you may want to extend the boolean attribute of
-submitted to allow accounts to save character applications and submit them later.
Our create HTML template will use the Django form we defined back in views.py/forms.py to drive the
-majority of the application process. There will be a form input for every field we defined in
-forms.py, which is handy. We have used POST as our method because we are sending information to the
-server that will update the database. As an alternative, GET would be much less secure. You can read
-up on documentation elsewhere on the web for GET vs. POST.
Once you have all these files stand in your mygame/folder and run:
-
evennia makemigrations
-evennia migrate
-
-
-
This will create and update the models. If you see any errors at this stage, read the traceback
-carefully, it should be relatively easy to figure out where the error is.
-
Login to the website (you need to have previously registered an Player account with the game to do
-this). Next you navigate to http://yourwebsite.com/chargen (if you are running locally this will
-be something like http://localhost:4001/chargen and you will see your new app in action.
-
This should hopefully give you a good starting point in figuring out how you’d like to approach your
-own web generation. The main difficulties are in setting the appropriate settings on your newly
-created character object. Thankfully, the Evennia API makes this easy.
-
-
-
Adding a no CAPCHA reCAPCHA on your character generation¶
-
As sad as it is, if your server is open to the web, bots might come to visit and take advantage of
-your open form to create hundreds, thousands, millions of characters if you give them the
-opportunity. This section shows you how to use the No CAPCHA reCAPCHA designed by Google. Not only is it
-easy to use, it is user-friendly… for humans. A simple checkbox to check, except if Google has
-some suspicion, in which case you will have a more difficult test with an image and the usual text
-inside. It’s worth pointing out that, as long as Google doesn’t suspect you of being a robot, this
-is quite useful, not only for common users, but to screen-reader users, to which reading inside of
-an image is pretty difficult, if not impossible. And to top it all, it will be so easy to add in
-your website.
The first thing is to ask Google for a way to safely authenticate your website to their service. To
-do it, we need to create a site key and a secret. Go to
-https://www.google.com/recaptcha/admin to create such a
-site key. It’s quite easy when you have a Google account.
-
When you have created your site key, save it safely. Also copy your secret key as well. You should
-find both information on the web page. Both would contain a lot of letters and figures.
-
-
-
Step 2: installing and configuring the dedicated Django app¶
-
Since Evennia runs on Django, the easiest way to add our CAPCHA and perform the proper check is to
-install the dedicated Django app. Quite easy:
-
pip install django-nocaptcha-recaptcha
-
-
-
And add it to the installed apps in your settings. In your mygame/server/conf/settings.py, you
-might have something like this:
Finally we have to add the CAPCHA to our form. It will be pretty easy too. First, open your
-web/chargen/forms.py file. We’re going to add a new field, but hopefully, all the hard work has
-been done for us. Update at your convenience, You might end up with something like this:
As you see, we added a line of import (line 2) and a field in our form.
-
And lastly, we need to update our HTML file to add in the Google library. You can open
-web/chargen/templates/chargen/create.html. There’s only one line to add:
And you should put it at the bottom of the page. Just before the closing body would be good, but
-for the time being, the base page doesn’t provide a footer block, so we’ll put it in the content
-block. Note that it’s not the best place, but it will work. In the end, your
-web/chargen/templates/chargen/create.html file should look like this:
Reload and open http://localhost:4001/chargen/create and
-you should see your beautiful CAPCHA just before the “submit” button. Try not to check the checkbox
-to see what happens. And do the same while checking the checkbox!
Before doing this tutorial you will probably want to read the intro in Basic Web tutorial.
-
In this tutorial we will create a web page that displays the stats of a game character. For this,
-and all other pages we want to make specific to our game, we’ll need to create our own Django “app”
-
We’ll call our app character, since it will be dealing with character information. From your game
-dir, run
-
evennia startapp character
-
-
-
This will create a directory named character in the root of your game dir. It contains all basic
-files that a Django app needs. To keep mygame well ordered, move it to your mygame/web/
-directory instead:
-
mv character web/
-
-
-
Note that we will not edit all files in this new directory, many of the generated files are outside
-the scope of this tutorial.
-
In order for Django to find our new web app, we’ll need to add it to the INSTALLED_APPS setting.
-Evennia’s default installed apps are already set, so in server/conf/settings.py, we’ll just extend
-them:
-
INSTALLED_APPS+=('web.character',)
-
-
-
-
Note: That end comma is important. It makes sure that Python interprets the addition as a tuple
-instead of a string.
-
-
The first thing we need to do is to create a view and an URL pattern to point to it. A view is a
-function that generates the web page that a visitor wants to see, while the URL pattern lets Django
-know what URL should trigger the view. The pattern may also provide some information of its own as
-we shall see.
-
Here is our character/urls.py file (Note: you may have to create this file if a blank one
-wasn’t generated for you):
-
# URL patterns for the character app
-
-fromdjango.conf.urlsimporturl
-fromweb.character.viewsimportsheet
-
-urlpatterns=[
- url(r'^sheet/(?P<object_id>\d+)/$',sheet,name="sheet")
-]
-
-
-
This file contains all of the URL patterns for the application. The url function in the
-urlpatterns list are given three arguments. The first argument is a pattern-string used to
-identify which URLs are valid. Patterns are specified as regular expressions. Regular expressions
-are used to match strings and are written in a special, very compact, syntax. A detailed description
-of regular expressions is beyond this tutorial but you can learn more about them
-here. For now, just accept that this regular
-expression requires that the visitor’s URL looks something like this:
-
sheet/123/
-
-
-
That is, sheet/ followed by a number, rather than some other possible URL pattern. We will
-interpret this number as object ID. Thanks to how the regular expression is formulated, the pattern
-recognizer stores the number in a variable called object_id. This will be passed to the view (see
-below). We add the imported view function (sheet) in the second argument. We also add the name
-keyword to identify the URL pattern itself. You should always name your URL patterns, this makes
-them easy to refer to in html templates using the {%url%} tag (but we won’t get more into that
-in this tutorial).
-
-
Security Note: Normally, users do not have the ability to see object IDs within the game (it’s
-restricted to superusers only). Exposing the game’s object IDs to the public like this enables
-griefers to perform what is known as an account enumeration
-attack in the efforts of
-hijacking your superuser account. Consider this: in every Evennia installation, there are two
-objects that we can always expect to exist and have the same object IDs– Limbo (#2) and the
-superuser you create in the beginning (#1). Thus, the griefer can get 50% of the information they
-need to hijack the admin account (the admin’s username) just by navigating to sheet/1!
-
-
Next we create views.py, the view file that urls.py refers to.
-
# Views for our character app
-
-fromdjango.httpimportHttp404
-fromdjango.shortcutsimportrender
-fromdjango.confimportsettings
-
-fromevennia.utils.searchimportobject_search
-fromevennia.utils.utilsimportinherits_from
-
-defsheet(request,object_id):
- object_id='#'+object_id
- try:
- character=object_search(object_id)[0]
- exceptIndexError:
- raiseHttp404("I couldn't find a character with that ID.")
- ifnotinherits_from(character,settings.BASE_CHARACTER_TYPECLASS):
- raiseHttp404("I couldn't find a character with that ID. "
- "Found something else instead.")
- returnrender(request,'character/sheet.html',{'character':character})
-
-
-
As explained earlier, the URL pattern parser in urls.py parses the URL and passes object_id to
-our view function sheet. We do a database search for the object using this number. We also make
-sure such an object exists and that it is actually a Character. The view function is also handed a
-request object. This gives us information about the request, such as if a logged-in user viewed it
-
-
we won’t use that information here but it is good to keep in mind.
-
-
On the last line, we call the render function. Apart from the request object, the render
-function takes a path to an html template and a dictionary with extra data you want to pass into
-said template. As extra data we pass the Character object we just found. In the template it will be
-available as the variable “character”.
-
The html template is created as templates/character/sheet.html under your character app folder.
-You may have to manually create both template and its subfolder character. Here’s the template
-to create:
In Django templates, {%...%} denotes special in-template “functions” that Django understands.
-The {{...}} blocks work as “slots”. They are replaced with whatever value the code inside the
-block returns.
-
The first line, {%extends"base.html"%}, tells Django that this template extends the base
-template that Evennia is using. The base template is provided by the theme. Evennia comes with the
-open-source third-party theme prosimii. You can find it and its base.html in
-evennia/web/templates/prosimii. Like other templates, these can be overwritten.
-
The next line is {%blockcontent%}. The base.html file has blocks, which are placeholders
-that templates can extend. The main block, and the one we use, is named content.
-
We can access the character variable anywhere in the template because we passed it in the render
-call at the end of view.py. That means we also have access to the Character’s db attributes,
-much like you would in normal Python code. You don’t have the ability to call functions with
-arguments in the template– in fact, if you need to do any complicated logic, you should do it in
-view.py and pass the results as more variables to the template. But you still have a great deal of
-flexibility in how you display the data.
-
We can do a little bit of logic here as well. We use the {%for%}...{%endfor%} and {%if%}...{%else%}...{%endif%} structures to change how the template renders depending on how many
-skills the user has, or if the user is approved (assuming your game has an approval system).
-
The last file we need to edit is the master URLs file. This is needed in order to smoothly integrate
-the URLs from your new character app with the URLs from Evennia’s existing pages. Find the file
-web/urls.py and update its patterns list as follows:
Now reload the server with evenniareload and visit the page in your browser. If you haven’t
-changed your defaults, you should be able to find the sheet for character #1 at
-http://localhost:4001/character/sheet/1/
-
Try updating the stats in-game and refresh the page in your browser. The results should show
-immediately.
-
As an optional final step, you can also change your character typeclass to have a method called
-‘get_absolute_url’.
Doing so will give you a ‘view on site’ button in the top right of the Django Admin Objects
-changepage that links to your new character sheet, and allow you to get the link to a character’s
-page by using in any template where you have a given object.
-
Now that you’ve made a basic page and app with Django, you may want to read the full Django
-tutorial to get a better idea of what it can do. You can find Django’s tutorial
-here.
The Evennia website is a Django application that ties in with the MUD database. Since the website
-shares this database you could, for example, tell website visitors how many accounts are logged into
-the game at the moment, how long the server has been up and any other database information you may
-want. During development you can access the website by pointing your browser to
-http://localhost:4001.
-
-
You may also want to set DEBUG=True in your settings file for debugging the website. You will
-then see proper tracebacks in the browser rather than just error codes. Note however that this will
-leak memory a lot (it stores everything all the time) and is not to be used in production. It’s
-recommended to only use DEBUG for active web development and to turn it off otherwise.
-
-
A Django (and thus Evennia) website basically consists of three parts, a
-view an associated
-template and an urls.py file. Think of
-the view as the Python back-end and the template as the HTML files you are served, optionally filled
-with data from the back-end. The urls file is a sort of mapping that tells Django that if a specific
-URL is given in the browser, a particular view should be triggered. You are wise to review the
-Django documentation for details on how to use these components.
-
Evennia’s default website is located in
-evennia/web/website. In this
-folder you’ll find the simple default view as well as subfolders templates and static. Static
-files are things like images, CSS files and Javascript.
You customize your website from your game directory. In the folder web you’ll find folders
-static, templates, static_overrides and templates_overrides. The first two of those are
-populated automatically by Django and used to serve the website. You should not edit anything in
-them - the change will be lost. To customize the website you’ll need to copy the file you want to
-change from the web/website/template/ or web/website/static/ path to the corresponding place
-under one of _overrides directories.
-
Example: To override or modify evennia/web/website/template/website/index.html you need to
-add/modify mygame/web/template_overrides/website/index.html.
-
The detailed description on how to customize the website is best described in tutorial form. See the
-Web Tutorial for more information.
The Python backend for every HTML page is called a Django
-view. A view can do all sorts of
-functions, but the main one is to update variables data that the page can display, like how your
-out-of-the-box website will display statistics about number of users and database objects.
-
To re-point a given page to a view.py of your own, you need to modify mygame/web/urls.py. An
-URL pattern is a regular
-expression that you need to enter in the address
-field of your web browser to get to the page in question. If you put your own URL pattern before
-the default ones, your own view will be used instead. The file urls.py even marks where you should
-put your change.
-
Here’s an example:
-
# mygame/web/urls.py
-
-fromdjango.conf.urlsimporturl,include
-# default patterns
-fromevennia.web.urlsimporturlpatterns
-
-# our own view to use as a replacement
-fromweb.myviewsimportmyview
-
-# custom patterns to add
-patterns=[
- # overload the main page view
- url(r'^',myview,name='mycustomview'),
-]
-
-urlpatterns=patterns+urlpatterns
-
-
-
-
Django will always look for a list named urlpatterns which consists of the results of url()
-calls. It will use the first match it finds in this list. Above, we add a new URL redirect from
-the root of the website. It will now our own function myview from a new module
-mygame/web/myviews.py.
-
-
If our game is found on http://mygame.com, the regular expression "^" means we just entered
-mygame.com in the address bar. If we had wanted to add a view for http://mygame.com/awesome, the
-regular expression would have been ^/awesome.
-
-
Look at evennia/web/website/views.py to see the inputs and outputs you must have to define a view. Easiest may be to
-copy the default file to mygame/web to have something to modify and expand on.
-
Restart the server and reload the page in the browser - the website will now use your custom view.
-If there are errors, consider turning on settings.DEBUG to see the full tracebacks - in debug mode
-you will also log all requests in mygame/server/logs/http_requests.log.
Evennia comes with a MUD client accessible from a normal web browser. During
-development you can try it at http://localhost:4001/webclient.
-See the Webclient page for more details.
Django comes with a built-in admin
-website. This is accessible by clicking
-the ‘admin’ button from your game website. The admin site allows you to see, edit and create objects
-in your database from a graphical interface.
-
The behavior of default Evennia models are controlled by files admin.py in the Evennia package.
-New database models you choose to add yourself (such as in the Web Character View Tutorial) can/will
-also have admin.py files. New models are registered to the admin website by a call of
-admin.site.register(modelclass,adminclass) inside an admin.py file. It is an error to attempt
-to register a model that has already been registered.
-
To overload Evennia’s admin files you don’t need to modify Evennia itself. To customize you can call
-admin.site.unregister(modelclass), then follow that with admin.site.register in one of your own
-admin.py files in a new app that you add.
Evennia relies on Django for its web features. For details on expanding your web experience, the
-Django documentation or the Django
-Book are the main resources to look into. In Django
-lingo, the Evennia is a django “project” that consists of Django “applications”. For the sake of web
-implementation, the relevant django “applications” in default Evennia are web/website or
-web/webclient.
Evennia uses the Django web framework as the basis of both its
-database configuration and the website it provides. While a full understanding of Django requires
-reading the Django documentation, we have provided this tutorial to get you running with the basics
-and how they pertain to Evennia. This text details getting everything set up. The Web-based
-Character view Tutorial gives a more explicit example of making a
-custom web page connected to your game, and you may want to read that after finishing this guide.
Django is a web framework. It gives you a set of development tools for building a website quickly
-and easily.
-
Django projects are split up into apps and these apps all contribute to one project. For instance,
-you might have an app for conducting polls, or an app for showing news posts or, like us, one for
-creating a web client.
-
Each of these applications has a urls.py file, which specifies what
-URLs are used by the app, a views.py file
-for the code that the URLs activate, a templates directory for displaying the results of that code
-in HTML for the user, and a static folder that holds assets
-like CSS, Javascript,
-and Image files (You may note your mygame/web folder does not have a static or template folder.
-This is intended and explained further below). Django applications may also have a models.py file
-for storing information in the database. We “cmd: attr(locktest, 80, compare=gt)”will not change any
-models here, take a look at the New Models page (as well as the Django
-docs on models) if you are interested.
-
There is also a root urls.py that determines the URL structure for the entire project. A starter
-urls.py is included in the default game template, and automatically imports all of Evennia’s
-default URLs for you. This is located in web/urls.py.
Evennia’s default logo is a fun little googly-eyed snake wrapped around a gear globe. As cute as it
-is, it probably doesn’t represent your game. So one of the first things you may wish to do is
-replace it with a logo of your own.
-
Django web apps all have static assets: CSS files, Javascript files, and Image files. In order to
-make sure the final project has all the static files it needs, the system collects the files from
-every app’s static folder and places it in the STATIC_ROOT defined in settings.py. By default,
-the Evennia STATIC_ROOT is in web/static.
-
Because Django pulls files from all of those separate places and puts them in one folder, it’s
-possible for one file to overwrite another. We will use this to plug in our own files without having
-to change anything in the Evennia itself.
-
By default, Evennia is configured to pull files you put in the web/static_overridesafter all
-other static files. That means that files in static_overrides folder will overwrite any previously
-loaded files having the same path under its static folder. This last part is important to repeat:
-To overload the static resource from a standard static folder you need to replicate the path of
-folders and file names from that static folder in exactly the same way inside static_overrides.
-
Let’s see how this works for our logo. The default web application is in the Evennia library itself,
-in evennia/web/. We can see that there is a static folder here. If we browse down, we’ll
-eventually find the full path to the Evennia logo file:
-evennia/web/website/static/website/images/evennia_logo.png.
-
Inside our static_overrides we must replicate the part of the path inside the website’s static
-folder, in other words, we must replicate website/images/evennia_logo.png.
-
So, to change the logo, we need to create the folder path website/images/ in static_overrides.
-You may already have this folder structure prepared for you. We then rename our own logo file to
-evennia_logo.png and copy it there. The final path for this file would thus be:
-mygame/web/static_overrides/website/images/evennia_logo.png.
-
To get this file pulled in, just change to your own game directory and reload the server:
-
evenniareload
-
-
-
This will reload the configuration and bring in the new static file(s). If you didn’t want to reload
-the server you could instead use
-
evenniacollectstatic
-
-
-
to only update the static files without any other changes.
-
-
Note: Evennia will collect static files automatically during startup. So if evenniacollectstatic reports finding 0 files to collect, make sure you didn’t start the engine at some
-point - if so the collector has already done its work! To make sure, connect to the website and
-check so the logo has actually changed to your own version.
-
-
-
Note: Sometimes the static asset collector can get confused. If no matter what you do, your
-overridden files aren’t getting copied over the defaults, try removing the target file (or
-everything) in the web/static directory, and re-running collectstatic to gather everything from
-scratch.
The default front page for Evennia contains information about the Evennia project. You’ll probably
-want to replace this information with information about your own project. Changing the page template
-is done in a similar way to changing static resources.
-
Like static files, Django looks through a series of template folders to find the file it wants. The
-difference is that Django does not copy all of the template files into one place, it just searches
-through the template folders until it finds a template that matches what it’s looking for. This
-means that when you edit a template, the changes are instant. You don’t have to reload the server or
-run any extra commands to see these changes - reloading the web page in your browser is enough.
-
To replace the index page’s text, we’ll need to find the template for it. We’ll go into more detail
-about how to determine which template is used for rendering a page in the Web-based Character view
-Tutorial. For now, you should know that the template we want to change
-is stored in evennia/web/website/templates/website/index.html.
-
To replace this template file, you will put your changed template inside the
-web/template_overrides/website directory in your game folder. In the same way as with static
-resources you must replicate the path inside the default template directory exactly. So we must
-copy our replacement template named index.html there (or create the website directory in
-web/template_overridesifitdoesnotexist,first).Thefinalpathtothefileshouldthusbe:web/template_overrides/website/index.html` within your game directory.
-
Note that it is usually easier to just copy the original template over and edit it in place. The
-original file already has all the markup and tags, ready for editing.
For further hints on working with the web presence, you could now continue to the Web-based
-Character view Tutorial where you learn to make a web page that
-displays in-game character stats. You can also look at Django’s own
-tutorial to get more insight in how Django works and what
-possibilities exist.
Griatch (IRC)APP @friarzen: Could one (with goldenlayout) programmatically provide pane positions
-and sizes?
-I recall it was not trivial for the old split.js solution.
- friarzen take a look at the goldenlayout_default_config.js
-It is kinda cryptic but that is the layout json.
- Griatch (IRC)APP @friarzen: Hm, so dynamically replacing the goldenlayout_config in the global
-scope at the right
- thing would do it?
- friarzen yep
- friarzen the biggest pain in the butt is that goldenlayout_config needs to be set before the
-goldenlayout init()
-is called, which isn't trivial with the current structure, but it isn't impossible.
- Griatch (IRC)APP One could in principle re-run init() at a later date though, right?
- friarzen Hmm...not sure I've ever tried it... seems doable off the top of my head...
-right now, that whole file exists to be overridden on page load as a separate <script> HTML tag, so
-it can get
-force-loaded early.
-you could just as easily call the server at that time for the proper config info and store it into
-the variable.
- Griatch (IRC)APP @friarzen: Right. I picture the default would either be modified directly in
-that file or even be
-in settings.
- friarzen And if you have it call with an authenticated session at that early point, you could
-even have the layout
-be defined as a per-user setting.
- Griatch (IRC)APP Yeah, I think that'd be a very important feature.
-So one part of the config would be the golden-layout config blob; then another section with custom
-per-pane
-settings. Things like which tag a pane filters on, its type, and options for that type (like
-replace/scroll for a
-text pane etc).
-And probably one general config section; things like notifications, sounds, popup settings and the
-like
- friarzen Actually, that information is already somewhat stored into the data as componentState {}
- Griatch (IRC)APP I imagine a pane would need to be identified uniquely for golden-layout to know
-which is which,
-so that'd need to be associated.
- friarzen see line 55.
-that's....where it gets tricky...
-goldenlayout is kinda dumb in that it treats the whole json blob as a state object.
- Griatch (IRC)APP componentState, is the idea that it takes arbitrary settings then?
- friarzen so each sub-dictionary with a type of "component" in it is a window and as you move
-stuff around it
-dynamically creates new dictionaries as needed to keep it all together.
-yep
-right now I'm storing the list of types as a string and the updateMethod type as a string, just to
-be as obvious as
-possible.
- Griatch (IRC)APP I wonder if one could populate componentState(s) just before initializing the
-system, and then
-extract them into a more Evennia-friendly storage when saving. Or maybe it's no big deal to just
-store that whole
-blob as-is, with some extra things added?
- friarzen if you want to see it in action, take a look at the values in your localstorage after
-you've moved stuff
-around, created new windows, etc.
-I think their preference is to just store the whole blob.
-which is what the localstorage save does, in fact.
- Griatch (IRC)APP Yes, I see.
-It allows you to retain the session within your browser as well
- friarzen One trick I've been thinking about for the whole interface is to have another pair of
-components, one
-being a "dynamic" config window that displays a series of dropdowns, one for each plugin that
-defines it's own
-settings.
-And another that has an embedded i-frame to allow a split-screen where half of the display is the
-text interface
-and the other loads the local main website page and allows further browsing.
- Griatch (IRC)APP I think ideally, each pane should handle its per-pane settings as a dropdown or
-popup triggered
-from its header.
- friarzen well, pane != plugin...
- Griatch (IRC)APP True, one tends to sometimes make the other available but they are not the same
- friarzen think of the history plugin for example...if you want to make keyboard keys dynamically
-configurable, you
-need some place to enter that information.
-yeah.
- Griatch (IRC)APP Yes, I buy that - a dynamical option window would be needed. I'd say the
-'global' settings should
-be treated as just another module in this regard though
-So that if you have no modules installed, there is one entry in that option pane, the one that opens
-the global
-options
- friarzen Yeah, so that is that component...one pane.
- Griatch (IRC)APP Another thing that the config should contain would be the available message
-tags. Waiting for
-them to actually show up before being able to use them is not that useful. This would likely have to
-be a setting
-though I imagine.
- friarzen we could remove the current pop-up and just open the new component pane instead, and
-then that pane would
-display the list of plugins that registered a config() callback.
- Griatch (IRC)APP Yes
- friarzen yeah, the server has to pre-define what tags it is going to send.
- Griatch (IRC)APP The process for adding a tag would be to adding to, say, a list in settings,
-restart and then .. profit
- friarzen yep, which is kind of how I see spawns working.
- Griatch (IRC)APP spawns, meaning stand-alone windows?
- friarzen we just have a plugin that defines it's config pane with a "match this text" box and a
-tag type to send
-the data to, or spawn a new pane with that tag type preselected to capture that text.
-wouldn't be stand alone windows. just new tabs, for now.
- Griatch (IRC)APP Ok, so a filter target that filters on text content and directs to tag
- friarzen yep.
- Griatch (IRC)APP (or re-tags it, I suppose)
- friarzen yeah, exactly.
-and opens a new window for that tag if one doesn't already exist.
- Griatch (IRC)APP A lot of complex effects could be achieved there. Should the filter extract the
-text and re-tag,
-or should it keep the text with its original tag and make a copy of it with the new tag? O_o;
- friarzen yet more options for the user. :slightly_smiling_face:
-baby steps first I think.
-"pages from bob should go to bob's window, not the general chat window"
- Griatch (IRC)APP It doesn't sound too hard to do - using tagging like that is really neat since
-then the rerouting
-can just work normally without knowing which pane you are actually rerouting too (and you could have
-multiple panes
-getting the same content too)
- friarzen yep.
-and the i-frame component, I think, provides just as much wow facter.
- Griatch (IRC)APP Yes, the setting would be something like: If text contains "bob" -> set tag
-"bob" (auto-create
-pane if not exist []?)
- friarzen just being able to load a character page from the wiki side in half the screen
-(including the pictures
-and whatnot) while playing/reading the text from the other half is really nice.
- Griatch (IRC)APP Could there not be some cross-scripting warnings in modern browsers if trying to
-load another
-website in an iframe?
-I doubt you could open Github like that, for example.
-friarzen well, assuming it is all from the same origin game server, it will all be from the same
-domain, so should
-avoid that, but it will take some testing to make sure. I want to get it to the point where you can
-click on
-somebody's name in the general terminal-style window and have it pop up that characters' wiki page.
- Griatch (IRC)APP That does sound nice :)
- friarzen i-frames are pretty much the only way to handle cross-domain content, but they are a
-pain in the butt to
-get that to work.
- Griatch (IRC)APP If it's on the same domain it would be fine, but it should probably give a "you
-are now leaving
-..." message and switch to a new window if going elsewhere.
- friarzen (without getting into modern CORS url blessings)
-yeah
- Griatch (IRC)APP Just to avoid headaches :)
- friarzen So, yeah, two new goldenlayout components that I am working on but they aren't ready
-yet. heh.
- Griatch (IRC)APP Oh, you are working on them already?
- friarzen yeah, some initial "will this work" structure.
-I haven't found anything yet that will not make it work.
-it's mostly just not going to look very pretty to start with. It'll take a few iterations I bet.
- Griatch (IRC)APP Sounds good! We should probably try to formalize our thoughts around a future
-config format as
-well. Depending just how and when the golden-layout blob needs to be in place and if we can
-construct it on-the-
-fly, it affects the format style chosen.
- friarzen Yeah, that's new from today's conversation, so I don't really have anything built for
-that.
- Griatch (IRC)APP I'm still unsure about how the Evennia/pane specific things (that'd need to be
-serialized to the
-database on a per-account basis) would interact with the golden-layout structure.
- friarzen maybe the place to start is to wipe-out/update the old
-https://github.com/evennia/evennia/wiki/Webclient-
-brainstorm entry?
- Griatch (IRC)APP I don't think we need to wipe the old, but we could certainly add a new section
-above the old and
-start some new discussions.
- friarzen It is really just that per componentState bit.
-anything in that json is treated as a blackbox that goldenlayout just passes around.
- Griatch (IRC)APP So would we save the whole blob then, componentState and all, and simply not
-worry about
-ourselves storing per-pane options?
-The drawback with that would be that a dev could not offer an exact default layout/setup for the
-user.
-... unless they set it up and saved the blob I guess
- friarzen Yeah, that's exactly how I built mine. :slightly_smiling_face:
-not the most dev-friendly way to do it, but it works.
- Griatch (IRC)APP Heh. Ok, so the config would be one section for the golden-layout-blob, one for
-module settings,
-one of which is the global settings.
-and a list of the available tags
- friarzen yep
- Griatch (IRC)APP And probably an empty misc section for future use ...
- friarzen seems reasonable.
- Griatch (IRC)APP So, that means that in the future one would need a procedure for the dev to
-easily create and
-save the player-wide default to central storage. Well, one step at a time.
-For now, good night!
- friarzen Yep, I expect that would be some kind of admin-approved api/command
- Griatch (IRC)APP And thanks for the discussion!
-
These are my ideas for the functionality of Evennia’s webclient in the (possibly distant) future. It
-assumes the webclient options have been implemented as per
-#1172.
-
-
The idea of the GUI is based around panes (a “window” feels like something that pops open). These
-are areas of the window of the type you would see from a javascript library like
-Split.js. Each pane has a separator with a handle for
-resizing it.
-
Each pane has an icon for opening a dropdown menu (see next mockup image).
-
Above image could show the default setup; mimicking the traditional telnet mud client setup. There
-should be at least one input pane (but you could add multiple). The Input pane has the icon for
-launching the main webclient options window. Alternatively the options icon could hover
-transparently in the upper left, “above” all panes.
-
The main webclient options window should have an option for hiding all GUI customization icons (the
-draggable handles of panes and the dropdown menu) for users who does not want to have to worry about
-changing things accidentally. Devs not wanting to offer players the ability to customize their
-client GUIs could maybe just set it up the way they want and then turn the gui controls off on the
-javascript level.
-
-
The dropdown menu allows for splitting each pane horizontally and vertically.
-
-
Each pane is “tagged” with what data will be sent to it. It could accept more than one type of
-tagged data. Just which tags are available is different for each game (and should be reported by the
-server).
-
For example, the server could send the return of the “look” command as
-
msg("you see ...",{"pane":"look"})
-
-
-
If one (or more) of the panes is set to receive “look” data, all such will go there. If no pane is
-assigned to “look”, this text will end up in the pane(s) with the “Unassigned” designation. It might
-be necessary to enforce that at least one window has the “unassigned” designation in order to not
-lose output without a clear pane target. By contrast the pane tagged with “All” receives all output
-regardless of if it is also being sent to other panes.
-
Another thing that came to mind is logging. It might be an idea to tie a given log file to a
-specific pane (panes?) to limit spam in the log (it might be one reason for having a pane with the
-“All” tag, for those wanting to log everything). This could also be set in the dropdown menu or in
-the webclient options window, maybe.
That way of splitting panes seems easy to manage while still being powerful at the same time.
-It’s certainly a lot more flexible than what I had in mind.
-
This is a quick sketch of what I was thinking about:
The panes themselves can work differently depending on the content though:
-
-
channel messages: when someone talks on a channel, the output text needs to be appended to the
-text already shown.
-
inventory: this pane should clear its output every time the inventory is displayed, so old
-inventory is not displayed. Also what about dropping items? As long as the player doesn’t check its
-inventory then that pane won’t be updated, unless more OOB data is added to track the inventory.
-
help/look: those pane should probably also clear their content before displaying new content.
-
-
logging: do you mean have a “save to log” item in the pane menu?
It makes sense that different types of panes would have different functionality. I was thinking that
-something like inventory would be very specific to a given game. But titeuf87 has a point - maybe
-you can get a rather generalized behavior just by defining if a pane should replace or append to the
-existing data.
-
As for the updating of inventory when dropping items, not sure if that can be generalized, but I
-guess drop/get commands could be wired to do that.
-
As for logging - yes I picture a “save to log” thing in the menu. The problem is just where to save
-the log. I think at least for an initial setup, one could maybe set the logging file path in the
-webclient options and just append logs from all the panes marked for logging to that same file.
I’ve been messing a little bit with split.js - I’ve managed to split a split dynamically with a data
-attribute on a button and jQuery. My current thinking on this issue is to use jQuery
-Templates to write the template for horizontal and
-vertical splits, then using jQuery to set up the initial split layout by inserting those templates
-as appropriate for user settings.
-
We would have a dropdown per split that decides what tag it’s meant to handle - perhaps a data
-attribute that we change in the template for easy selection through jQuery - and then send tags for
-messages to the webclient. This dropdown would also have a setting per split to either append new
-content or replace content - again, perhaps a data attribute.
-
We might also want to store “mobile” and “desktop” layouts separately - or just default to a mobile-
-friendly layout at a certain screen size, and turn off the splits.
-
Oh, and embedding Bootstrap containers in splits works perfectly - that could help us too.
I’ve got a demo of this working at my development box,
-if anyone wants to take a look. So far, I’ve got tag handling, dynamic splits, and the ability to
-swap splits’ contents. Since this doesn’t have a fancy interface yet, if you want to see a dynamic
-split, open your browser’s console and try these commands:
-
SplitHandler.split("#main","horizontal") will split the top-half into two 50-50 splits, both
-receiving content that’s not tagged or is tagged “all” from the server.
-
SplitHandler.swapContent("#input","#split_2") after doing so will swap the input into the top-
-left split.
-
I’m trying to figure out where to put each split’s configuration - maybe a dropdown button that’s
-semi-transparent until you hover over it? So far, if you edited the data attributes, you could
-change each split to receive only tagged messages ( data-tag=“all” ), and you could change them to
-overwrite instead of append data ( data-update-append or data-update-overwrite )
I would suggest that, assuming the game dev allows the user to modify their GUI in the first place,
-that modification is a “mode”. I picture that 99% of the time a user doesn’t need to modify their
-interface. They only want to click whatever game-related buttons etc are present in the pane without
-risk of resizing things accidentally.
-
So I’d say that normally the panes are all fixed, with minimal spacing between them, no handles etc.
-But you can enter the client settings window and choose Customize GUI mode (or something like
-that). When you do, then separators get handles and the dropdown menu markers appear (permanently)
-in the corner of each pane. This means that if a user wants to easily tweak their window they
-could stay in this mode, but they could also “lock” the gui layout at any time.
Evennia comes with a MUD client accessible from a normal web browser. During development you can try
-it at http://localhost:4001/webclient. The client consists of several parts, all under
-evennia/web/webclient/:
-
templates/webclient/webclient.html and templates/webclient/base.html are the very simplistic
-django html templates describing the webclient layout.
-
static/webclient/js/evennia.js is the main evennia javascript library. This handles all
-communication between Evennia and the client over websockets and via AJAX/COMET if the browser can’t
-handle websockets. It will make the Evennia object available to the javascript namespace, which
-offers methods for sending and receiving data to/from the server transparently. This is intended to
-be used also if swapping out the gui front end.
-
static/webclient/js/webclient_gui.js is the default plugin manager. It adds the plugins and
-plugin_manager objects to the javascript namespace, coordinates the GUI operations between the
-various plugins, and uses the Evennia object library for all in/out.
-
static/webclient/js/plugins provides a default set of plugins that implement a “telnet-like”
-interface.
-
static/webclient/css/webclient.css is the CSS file for the client; it also defines things like how
-to display ANSI/Xterm256 colors etc.
-
The server-side webclient protocols are found in evennia/server/portal/webclient.py and
-webclient_ajax.py for the two types of connections. You can’t (and should not need to) modify
-these.
Like was the case for the website, you override the webclient from your game directory. You need to
-add/modify a file in the matching directory location within one of the _overrides directories.
-These _override directories are NOT directly used by the web server when the game is running, the
-server copies everything web related in the Evennia folder over to mygame/web/static/ and then
-copies in all of your _overrides. This can cause some cases were you edit a file, but it doesn’t
-seem to make any difference in the servers behavior. Before doing anything else, try shutting
-down the game and running evenniacollectstatic from the command line then start it back up, clear
-your browser cache, and see if your edit shows up.
-
Example: To change the utilized plugin list, you need to override base.html by copying
-evennia/web/webclient/templates/webclient/base.html to
-mygame/web/template_overrides/webclient/base.html and editing it to add your new plugin.
booleanonKeydown(event) This plugin listens for Keydown events
-
onBeforeUnload() This plugin does something special just before the webclient page/tab is
-closed.
-
onLoggedIn(args,kwargs) This plugin does something when the webclient first logs in.
-
onGotOptions(args,kwargs) This plugin does something with options sent from the server.
-
booleanonText(args,kwargs) This plugin does something with messages sent from the server.
-
booleanonPrompt(args,kwargs) This plugin does something when the server sends a prompt.
-
booleanonUnknownCmd(cmdname,args,kwargs) This plugin does something with “unknown commands”.
-
onConnectionClose(args,kwargs) This plugin does something when the webclient disconnects from
-the server.
-
newstringonSend(string) This plugin examines/alters text that other plugins generate. Use
-with caution
-
-
The order of the plugins defined in base.html is important. All the callbacks for each plugin
-will be executed in that order. Functions marked “boolean” above must return true/false. Returning
-true will short-circuit the execution, so no other plugins lower in the base.html list will have
-their callback for this event called. This enables things like the up/down arrow keys for the
-history.js plugin to always occur before the default_in.js plugin adds that key to the current input
-buffer.
clienthelp.js Defines onOptionsUI from the options2 plugin. This is a mostly empty plugin to
-add some “How To” information for your game.
-
default_in.js Defines onKeydown. key or mouse clicking the arrow will send the currently
-typed text.
-
default_out.js Defines onText, onPrompt, and onUnknownCmd. Generates HTML output for the user.
-
default_unload.js Defines onBeforeUnload. Prompts the user to confirm that they meant to
-leave/close the game.
-
font.js Defines onOptionsUI. The plugin adds the ability to select your font and font size.
-
goldenlayout_default_config.js Not actually a plugin, defines a global variable that
-goldenlayout uses to determine its window layout, known tag routing, etc.
-
goldenlayout.js Defines onKeydown, onText and custom functions. A very powerful “tabbed” window
-manager for drag-n-drop windows, text routing and more.
-
history.js Defines onKeydown and onSend. Creates a history of past sent commands, and uses arrow
-keys to peruse.
-
hotbuttons.js Defines onGotOptions. A Disabled-by-default plugin that defines a button bar with
-user-assignable commands.
-
iframe.js Defines onOptionsUI. A goldenlayout-only plugin to create a restricted browsing sub-
-window for a side-by-side web/text interface, mostly an example of how to build new HTML
-“components” for goldenlayout.
-
message_routing.js Defines onOptionsUI, onText, onKeydown. This goldenlayout-only plugin
-implements regex matching to allow users to “tag” arbitrary text that matches, so that it gets
-routed to proper windows. Similar to “Spawn” functions for other clients.
-
multimedia.js An basic plugin to allow the client to handle “image” “audio” and “video” messages
-from the server and display them as inline HTML.
-
notifications.js Defines onText. Generates browser notification events for each new message
-while the tab is hidden.
-
oob.js Defines onSend. Allows the user to test/send Out Of Band json messages to the server.
-
options.js Defines most callbacks. Provides a popup-based UI to coordinate options settings with
-the server.
-
options2.js Defines most callbacks. Provides a goldenlayout-based version of the
-options/settings tab. Integrates with other plugins via the custom onOptionsUI callback.
-
popups.js Provides default popups/Dialog UI for other plugins to use.
So, you love the functionality of the webclient, but your game has specific types of text that need
-to be separated out into their own space, visually. The Goldenlayout plugin framework can help with
-this.
GoldenLayout is a web framework that allows web developers and their users to create their own
-tabbed/windowed layouts. Windows/tabs can be click-and-dragged from location to location by
-clicking on their titlebar and dragging until the “frame lines” appear. Dragging a window onto
-another window’s titlebar will create a tabbed “Stack”. The Evennia goldenlayout plugin defines 3
-basic types of window: The Main window, input windows and non-main text output windows. The Main
-window and the first input window are unique in that they can’t be “closed”.
-
The most basic customization is to provide your users with a default layout other than just one Main
-output and the one starting input window. This is done by modifying your server’s
-goldenlayout_default_config.js.
-
Start by creating a new
-mygame/web/static_overrides/webclient/js/plugins/goldenlayout_default_config.js file, and adding
-the following JSON variable:
This is a bit ugly, but hopefully, from the indentation, you can see that it creates a side-by-side
-(2-column) interface with 3 windows down the left side (The Main and 2 inputs) and a pair of windows
-on the right side for extra outputs. Any text tagged with “some-tag-here” will flow to the bottom
-of the “example” window, and any text tagged “sheet” will replace the text already in the “sheet”
-window.
-
Note: GoldenLayout gets VERY confused and will break if you create two windows with the “Main”
-componentName.
-
Now, let’s say you want to display text on each window using different CSS. This is where new
-goldenlayout “components” come in. Each component is like a blueprint that gets stamped out when
-you create a new instance of that component, once it is defined, it won’t be easily altered. You
-will need to define a new component, preferably in a new plugin file, and then add that into your
-page (either dynamically to the DOM via javascript, or by including the new plugin file into the
-base.html).
-
First up, follow the directions in Customizing the Web Client section above to override the
-base.html.
-
Next, add the new plugin to your copy of base.html:
[Parsing command arguments, theory and best practices](Parsing-command-arguments,-theory-and-best-
-practices) - Parsing-command-arguments-best-practices
-
Portal and server - The-two-processes-that-make-up-the-running-Evennia-
-server-are-described.
Say you create a room named Meadow in your nice big forest MUD. That’s all nice and dandy, but
-what if you, in the other end of that forest want another Meadow? As a game creator, this can
-cause all sorts of confusion. For example, teleporting to Meadow will now give you a warning that
-there are two Meadow s and you have to select which one. It’s no problem to do that, you just
-choose for example to go to 2-meadow, but unless you examine them you couldn’t be sure which of
-the two sat in the magical part of the forest and which didn’t.
-
Another issue is if you want to group rooms in geographic regions. Let’s say the “normal” part of
-the forest should have separate weather patterns from the magical part. Or maybe a magical
-disturbance echoes through all magical-forest rooms. It would then be convenient to be able to
-simply find all rooms that are “magical” so you could send messages to them.
Zones try to separate rooms by global location. In our example we would separate the forest into
-two parts - the magical and the non-magical part. Each have a Meadow and rooms belonging to each
-part should be easy to retrieve.
-
Many MUD codebases hardcode zones as part of the engine and database. Evennia does no such
-distinction due to the fact that rooms themselves are meant to be customized to any level anyway.
-Below is a suggestion for how to implement zones in Evennia.
-
All objects in Evennia can hold any number of Tags. Tags are short labels that you attach to
-objects. They make it very easy to retrieve groups of objects. An object can have any number of
-different tags. So let’s attach the relevant tag to our forest:
You could add this manually, or automatically during creation somehow (you’d need to modify your
-@dig command for this, most likely). You can also use the default @tag command during building:
-
@tag forestobj = magicalforest : zone
-
-
-
Henceforth you can then easily retrieve only objects with a given tag:
The tagging or aliasing systems above don’t instill any sort of functional difference between a
-magical forest room and a normal one - they are just arbitrary ways to mark objects for quick
-retrieval later. Any functional differences must be expressed using Typeclasses.
-
Of course, an alternative way to implement zones themselves is to have all rooms/objects in a zone
-inherit from a given typeclass parent - and then limit your searches to objects inheriting from that
-given parent. The effect would be similar but you’d need to expand the search functionality to
-properly search the inheritance tree.
-"""
-Settings and configuration for Django.
-
-Read values from the module specified by the DJANGO_SETTINGS_MODULE environment
-variable, and then from django.conf.global_settings; see the global_settings.py
-for a list of all possible variables.
-"""
-
-importimportlib
-importos
-importtime
-importtraceback
-importwarnings
-frompathlibimportPath
-
-importdjango
-fromdjango.confimportglobal_settings
-fromdjango.core.exceptionsimportImproperlyConfigured
-fromdjango.utils.deprecationimportRemovedInDjango40Warning
-fromdjango.utils.functionalimportLazyObject,empty
-
-ENVIRONMENT_VARIABLE="DJANGO_SETTINGS_MODULE"
-
-PASSWORD_RESET_TIMEOUT_DAYS_DEPRECATED_MSG=(
- 'The PASSWORD_RESET_TIMEOUT_DAYS setting is deprecated. Use '
- 'PASSWORD_RESET_TIMEOUT instead.'
-)
-
-DEFAULT_HASHING_ALGORITHM_DEPRECATED_MSG=(
- 'The DEFAULT_HASHING_ALGORITHM transitional setting is deprecated. '
- 'Support for it and tokens, cookies, sessions, and signatures that use '
- 'SHA-1 hashing algorithm will be removed in Django 4.0.'
-)
-
-
-classSettingsReference(str):
- """
- String subclass which references a current settings value. It's treated as
- the value in memory but serializes to a settings.NAME attribute reference.
- """
- def__new__(self,value,setting_name):
- returnstr.__new__(self,value)
-
- def__init__(self,value,setting_name):
- self.setting_name=setting_name
-
-
-classLazySettings(LazyObject):
- """
- A lazy proxy for either global Django settings or a custom settings object.
- The user can manually configure settings prior to using them. Otherwise,
- Django uses the settings module pointed to by DJANGO_SETTINGS_MODULE.
- """
- def_setup(self,name=None):
- """
- Load the settings module pointed to by the environment variable. This
- is used the first time settings are needed, if the user hasn't
- configured settings manually.
- """
- settings_module=os.environ.get(ENVIRONMENT_VARIABLE)
- ifnotsettings_module:
- desc=("setting %s"%name)ifnameelse"settings"
- raiseImproperlyConfigured(
- "Requested %s, but settings are not configured. "
- "You must either define the environment variable %s "
- "or call settings.configure() before accessing settings."
- %(desc,ENVIRONMENT_VARIABLE))
-
- self._wrapped=Settings(settings_module)
-
- def__repr__(self):
- # Hardcode the class name as otherwise it yields 'Settings'.
- ifself._wrappedisempty:
- return'<LazySettings [Unevaluated]>'
- return'<LazySettings "%(settings_module)s">'%{
- 'settings_module':self._wrapped.SETTINGS_MODULE,
- }
-
- def__getattr__(self,name):
- """Return the value of a setting and cache it in self.__dict__."""
- ifself._wrappedisempty:
- self._setup(name)
- val=getattr(self._wrapped,name)
-
- # Special case some settings which require further modification.
- # This is done here for performance reasons so the modified value is cached.
- ifnamein{'MEDIA_URL','STATIC_URL'}andvalisnotNone:
- val=self._add_script_prefix(val)
- elifname=='SECRET_KEY'andnotval:
- raiseImproperlyConfigured("The SECRET_KEY setting must not be empty.")
-
- self.__dict__[name]=val
- returnval
-
- def__setattr__(self,name,value):
- """
- Set the value of setting. Clear all cached values if _wrapped changes
- (@override_settings does this) or clear single values when set.
- """
- ifname=='_wrapped':
- self.__dict__.clear()
- else:
- self.__dict__.pop(name,None)
- super().__setattr__(name,value)
-
- def__delattr__(self,name):
- """Delete a setting and clear it from cache if needed."""
- super().__delattr__(name)
- self.__dict__.pop(name,None)
-
- defconfigure(self,default_settings=global_settings,**options):
- """
- Called to manually configure the settings. The 'default_settings'
- parameter sets where to retrieve any unspecified values from (its
- argument must support attribute access (__getattr__)).
- """
- ifself._wrappedisnotempty:
- raiseRuntimeError('Settings already configured.')
- holder=UserSettingsHolder(default_settings)
- forname,valueinoptions.items():
- ifnotname.isupper():
- raiseTypeError('Setting %r must be uppercase.'%name)
- setattr(holder,name,value)
- self._wrapped=holder
-
- @staticmethod
- def_add_script_prefix(value):
- """
- Add SCRIPT_NAME prefix to relative paths.
-
- Useful when the app is being served at a subpath and manually prefixing
- subpath to STATIC_URL and MEDIA_URL in settings is inconvenient.
- """
- # Don't apply prefix to absolute paths and URLs.
- ifvalue.startswith(('http://','https://','/')):
- returnvalue
- fromdjango.urlsimportget_script_prefix
- return'%s%s'%(get_script_prefix(),value)
-
- @property
- defconfigured(self):
- """Return True if the settings have already been configured."""
- returnself._wrappedisnotempty
-
- @property
- defPASSWORD_RESET_TIMEOUT_DAYS(self):
- stack=traceback.extract_stack()
- # Show a warning if the setting is used outside of Django.
- # Stack index: -1 this line, -2 the caller.
- filename,_,_,_=stack[-2]
- ifnotfilename.startswith(os.path.dirname(django.__file__)):
- warnings.warn(
- PASSWORD_RESET_TIMEOUT_DAYS_DEPRECATED_MSG,
- RemovedInDjango40Warning,
- stacklevel=2,
- )
- returnself.__getattr__('PASSWORD_RESET_TIMEOUT_DAYS')
-
-
-classSettings:
- def__init__(self,settings_module):
- # update this dict from global settings (but only for ALL_CAPS settings)
- forsettingindir(global_settings):
- ifsetting.isupper():
- setattr(self,setting,getattr(global_settings,setting))
-
- # store the settings module in case someone later cares
- self.SETTINGS_MODULE=settings_module
-
- mod=importlib.import_module(self.SETTINGS_MODULE)
-
- tuple_settings=(
- "INSTALLED_APPS",
- "TEMPLATE_DIRS",
- "LOCALE_PATHS",
- )
- self._explicit_settings=set()
- forsettingindir(mod):
- ifsetting.isupper():
- setting_value=getattr(mod,setting)
-
- if(settingintuple_settingsand
- notisinstance(setting_value,(list,tuple))):
- raiseImproperlyConfigured("The %s setting must be a list or a tuple. "%setting)
- setattr(self,setting,setting_value)
- self._explicit_settings.add(setting)
-
- ifself.is_overridden('PASSWORD_RESET_TIMEOUT_DAYS'):
- ifself.is_overridden('PASSWORD_RESET_TIMEOUT'):
- raiseImproperlyConfigured(
- 'PASSWORD_RESET_TIMEOUT_DAYS/PASSWORD_RESET_TIMEOUT are '
- 'mutually exclusive.'
- )
- setattr(self,'PASSWORD_RESET_TIMEOUT',self.PASSWORD_RESET_TIMEOUT_DAYS*60*60*24)
- warnings.warn(PASSWORD_RESET_TIMEOUT_DAYS_DEPRECATED_MSG,RemovedInDjango40Warning)
-
- ifself.is_overridden('DEFAULT_HASHING_ALGORITHM'):
- warnings.warn(DEFAULT_HASHING_ALGORITHM_DEPRECATED_MSG,RemovedInDjango40Warning)
-
- ifhasattr(time,'tzset')andself.TIME_ZONE:
- # When we can, attempt to validate the timezone. If we can't find
- # this file, no check happens and it's harmless.
- zoneinfo_root=Path('/usr/share/zoneinfo')
- zone_info_file=zoneinfo_root.joinpath(*self.TIME_ZONE.split('/'))
- ifzoneinfo_root.exists()andnotzone_info_file.exists():
- raiseValueError("Incorrect timezone setting: %s"%self.TIME_ZONE)
- # Move the time zone info into os.environ. See ticket #2315 for why
- # we don't do this unconditionally (breaks Windows).
- os.environ['TZ']=self.TIME_ZONE
- time.tzset()
-
- defis_overridden(self,setting):
- returnsettinginself._explicit_settings
-
- def__repr__(self):
- return'<%(cls)s "%(settings_module)s">'%{
- 'cls':self.__class__.__name__,
- 'settings_module':self.SETTINGS_MODULE,
- }
-
-
-classUserSettingsHolder:
- """Holder for user configured settings."""
- # SETTINGS_MODULE doesn't make much sense in the manually configured
- # (standalone) case.
- SETTINGS_MODULE=None
-
- def__init__(self,default_settings):
- """
- Requests for configuration variables not in this class are satisfied
- from the module specified in default_settings (if possible).
- """
- self.__dict__['_deleted']=set()
- self.default_settings=default_settings
-
- def__getattr__(self,name):
- ifnotname.isupper()ornameinself._deleted:
- raiseAttributeError
- returngetattr(self.default_settings,name)
-
- def__setattr__(self,name,value):
- self._deleted.discard(name)
- ifname=='PASSWORD_RESET_TIMEOUT_DAYS':
- setattr(self,'PASSWORD_RESET_TIMEOUT',value*60*60*24)
- warnings.warn(PASSWORD_RESET_TIMEOUT_DAYS_DEPRECATED_MSG,RemovedInDjango40Warning)
- ifname=='DEFAULT_HASHING_ALGORITHM':
- warnings.warn(DEFAULT_HASHING_ALGORITHM_DEPRECATED_MSG,RemovedInDjango40Warning)
- super().__setattr__(name,value)
-
- def__delattr__(self,name):
- self._deleted.add(name)
- ifhasattr(self,name):
- super().__delattr__(name)
-
- def__dir__(self):
- returnsorted(
- sforsin[*self.__dict__,*dir(self.default_settings)]
- ifsnotinself._deleted
- )
-
- defis_overridden(self,setting):
- deleted=(settinginself._deleted)
- set_locally=(settinginself.__dict__)
- set_on_default=getattr(self.default_settings,'is_overridden',lambdas:False)(setting)
- returndeletedorset_locallyorset_on_default
-
- def__repr__(self):
- return'<%(cls)s>'%{
- 'cls':self.__class__.__name__,
- }
-
-
-settings=LazySettings()
-
Source code for django.db.models.fields.related_descriptors
-"""
-Accessors for related objects.
-
-When a field defines a relation between two models, each model class provides
-an attribute to access related instances of the other model class (unless the
-reverse accessor has been disabled with related_name='+').
-
-Accessors are implemented as descriptors in order to customize access and
-assignment. This module defines the descriptor classes.
-
-Forward accessors follow foreign keys. Reverse accessors trace them back. For
-example, with the following models::
-
- class Parent(Model):
- pass
-
- class Child(Model):
- parent = ForeignKey(Parent, related_name='children')
-
- ``child.parent`` is a forward many-to-one relation. ``parent.children`` is a
-reverse many-to-one relation.
-
-There are three types of relations (many-to-one, one-to-one, and many-to-many)
-and two directions (forward and reverse) for a total of six combinations.
-
-1. Related instance on the forward side of a many-to-one relation:
- ``ForwardManyToOneDescriptor``.
-
- Uniqueness of foreign key values is irrelevant to accessing the related
- instance, making the many-to-one and one-to-one cases identical as far as
- the descriptor is concerned. The constraint is checked upstream (unicity
- validation in forms) or downstream (unique indexes in the database).
-
-2. Related instance on the forward side of a one-to-one
- relation: ``ForwardOneToOneDescriptor``.
-
- It avoids querying the database when accessing the parent link field in
- a multi-table inheritance scenario.
-
-3. Related instance on the reverse side of a one-to-one relation:
- ``ReverseOneToOneDescriptor``.
-
- One-to-one relations are asymmetrical, despite the apparent symmetry of the
- name, because they're implemented in the database with a foreign key from
- one table to another. As a consequence ``ReverseOneToOneDescriptor`` is
- slightly different from ``ForwardManyToOneDescriptor``.
-
-4. Related objects manager for related instances on the reverse side of a
- many-to-one relation: ``ReverseManyToOneDescriptor``.
-
- Unlike the previous two classes, this one provides access to a collection
- of objects. It returns a manager rather than an instance.
-
-5. Related objects manager for related instances on the forward or reverse
- sides of a many-to-many relation: ``ManyToManyDescriptor``.
-
- Many-to-many relations are symmetrical. The syntax of Django models
- requires declaring them on one side but that's an implementation detail.
- They could be declared on the other side without any change in behavior.
- Therefore the forward and reverse descriptors can be the same.
-
- If you're looking for ``ForwardManyToManyDescriptor`` or
- ``ReverseManyToManyDescriptor``, use ``ManyToManyDescriptor`` instead.
-"""
-
-fromdjango.core.exceptionsimportFieldError
-fromdjango.dbimportconnections,router,transaction
-fromdjango.db.modelsimportQ,signals
-fromdjango.db.models.queryimportQuerySet
-fromdjango.db.models.query_utilsimportDeferredAttribute
-fromdjango.db.models.utilsimportresolve_callables
-fromdjango.utils.functionalimportcached_property
-
-
-classForeignKeyDeferredAttribute(DeferredAttribute):
- def__set__(self,instance,value):
- ifinstance.__dict__.get(self.field.attname)!=valueandself.field.is_cached(instance):
- self.field.delete_cached_value(instance)
- instance.__dict__[self.field.attname]=value
-
-
-classForwardManyToOneDescriptor:
- """
- Accessor to the related object on the forward side of a many-to-one or
- one-to-one (via ForwardOneToOneDescriptor subclass) relation.
-
- In the example::
-
- class Child(Model):
- parent = ForeignKey(Parent, related_name='children')
-
- ``Child.parent`` is a ``ForwardManyToOneDescriptor`` instance.
- """
-
- def__init__(self,field_with_rel):
- self.field=field_with_rel
-
- @cached_property
- defRelatedObjectDoesNotExist(self):
- # The exception can't be created at initialization time since the
- # related model might not be resolved yet; `self.field.model` might
- # still be a string model reference.
- returntype(
- 'RelatedObjectDoesNotExist',
- (self.field.remote_field.model.DoesNotExist,AttributeError),{
- '__module__':self.field.model.__module__,
- '__qualname__':'%s.%s.RelatedObjectDoesNotExist'%(
- self.field.model.__qualname__,
- self.field.name,
- ),
- }
- )
-
- defis_cached(self,instance):
- returnself.field.is_cached(instance)
-
- defget_queryset(self,**hints):
- returnself.field.remote_field.model._base_manager.db_manager(hints=hints).all()
-
- defget_prefetch_queryset(self,instances,queryset=None):
- ifquerysetisNone:
- queryset=self.get_queryset()
- queryset._add_hints(instance=instances[0])
-
- rel_obj_attr=self.field.get_foreign_related_value
- instance_attr=self.field.get_local_related_value
- instances_dict={instance_attr(inst):instforinstininstances}
- related_field=self.field.foreign_related_fields[0]
- remote_field=self.field.remote_field
-
- # FIXME: This will need to be revisited when we introduce support for
- # composite fields. In the meantime we take this practical approach to
- # solve a regression on 1.6 when the reverse manager in hidden
- # (related_name ends with a '+'). Refs #21410.
- # The check for len(...) == 1 is a special case that allows the query
- # to be join-less and smaller. Refs #21760.
- ifremote_field.is_hidden()orlen(self.field.foreign_related_fields)==1:
- query={'%s__in'%related_field.name:{instance_attr(inst)[0]forinstininstances}}
- else:
- query={'%s__in'%self.field.related_query_name():instances}
- queryset=queryset.filter(**query)
-
- # Since we're going to assign directly in the cache,
- # we must manage the reverse relation cache manually.
- ifnotremote_field.multiple:
- forrel_objinqueryset:
- instance=instances_dict[rel_obj_attr(rel_obj)]
- remote_field.set_cached_value(rel_obj,instance)
- returnqueryset,rel_obj_attr,instance_attr,True,self.field.get_cache_name(),False
-
- defget_object(self,instance):
- qs=self.get_queryset(instance=instance)
- # Assuming the database enforces foreign keys, this won't fail.
- returnqs.get(self.field.get_reverse_related_filter(instance))
-
- def__get__(self,instance,cls=None):
- """
- Get the related instance through the forward relation.
-
- With the example above, when getting ``child.parent``:
-
- - ``self`` is the descriptor managing the ``parent`` attribute
- - ``instance`` is the ``child`` instance
- - ``cls`` is the ``Child`` class (we don't need it)
- """
- ifinstanceisNone:
- returnself
-
- # The related instance is loaded from the database and then cached
- # by the field on the model instance state. It can also be pre-cached
- # by the reverse accessor (ReverseOneToOneDescriptor).
- try:
- rel_obj=self.field.get_cached_value(instance)
- exceptKeyError:
- has_value=Nonenotinself.field.get_local_related_value(instance)
- ancestor_link=instance._meta.get_ancestor_link(self.field.model)ifhas_valueelseNone
- ifancestor_linkandancestor_link.is_cached(instance):
- # An ancestor link will exist if this field is defined on a
- # multi-table inheritance parent of the instance's class.
- ancestor=ancestor_link.get_cached_value(instance)
- # The value might be cached on an ancestor if the instance
- # originated from walking down the inheritance chain.
- rel_obj=self.field.get_cached_value(ancestor,default=None)
- else:
- rel_obj=None
- ifrel_objisNoneandhas_value:
- rel_obj=self.get_object(instance)
- remote_field=self.field.remote_field
- # If this is a one-to-one relation, set the reverse accessor
- # cache on the related object to the current instance to avoid
- # an extra SQL query if it's accessed later on.
- ifnotremote_field.multiple:
- remote_field.set_cached_value(rel_obj,instance)
- self.field.set_cached_value(instance,rel_obj)
-
- ifrel_objisNoneandnotself.field.null:
- raiseself.RelatedObjectDoesNotExist(
- "%s has no %s."%(self.field.model.__name__,self.field.name)
- )
- else:
- returnrel_obj
-
- def__set__(self,instance,value):
- """
- Set the related instance through the forward relation.
-
- With the example above, when setting ``child.parent = parent``:
-
- - ``self`` is the descriptor managing the ``parent`` attribute
- - ``instance`` is the ``child`` instance
- - ``value`` is the ``parent`` instance on the right of the equal sign
- """
- # An object must be an instance of the related class.
- ifvalueisnotNoneandnotisinstance(value,self.field.remote_field.model._meta.concrete_model):
- raiseValueError(
- 'Cannot assign "%r": "%s.%s" must be a "%s" instance.'%(
- value,
- instance._meta.object_name,
- self.field.name,
- self.field.remote_field.model._meta.object_name,
- )
- )
- elifvalueisnotNone:
- ifinstance._state.dbisNone:
- instance._state.db=router.db_for_write(instance.__class__,instance=value)
- ifvalue._state.dbisNone:
- value._state.db=router.db_for_write(value.__class__,instance=instance)
- ifnotrouter.allow_relation(value,instance):
- raiseValueError('Cannot assign "%r": the current database router prevents this relation.'%value)
-
- remote_field=self.field.remote_field
- # If we're setting the value of a OneToOneField to None, we need to clear
- # out the cache on any old related object. Otherwise, deleting the
- # previously-related object will also cause this object to be deleted,
- # which is wrong.
- ifvalueisNone:
- # Look up the previously-related object, which may still be available
- # since we've not yet cleared out the related field.
- # Use the cache directly, instead of the accessor; if we haven't
- # populated the cache, then we don't care - we're only accessing
- # the object to invalidate the accessor cache, so there's no
- # need to populate the cache just to expire it again.
- related=self.field.get_cached_value(instance,default=None)
-
- # If we've got an old related object, we need to clear out its
- # cache. This cache also might not exist if the related object
- # hasn't been accessed yet.
- ifrelatedisnotNone:
- remote_field.set_cached_value(related,None)
-
- forlh_field,rh_fieldinself.field.related_fields:
- setattr(instance,lh_field.attname,None)
-
- # Set the values of the related field.
- else:
- forlh_field,rh_fieldinself.field.related_fields:
- setattr(instance,lh_field.attname,getattr(value,rh_field.attname))
-
- # Set the related instance cache used by __get__ to avoid an SQL query
- # when accessing the attribute we just set.
- self.field.set_cached_value(instance,value)
-
- # If this is a one-to-one relation, set the reverse accessor cache on
- # the related object to the current instance to avoid an extra SQL
- # query if it's accessed later on.
- ifvalueisnotNoneandnotremote_field.multiple:
- remote_field.set_cached_value(value,instance)
-
- def__reduce__(self):
- """
- Pickling should return the instance attached by self.field on the
- model, not a new copy of that descriptor. Use getattr() to retrieve
- the instance directly from the model.
- """
- returngetattr,(self.field.model,self.field.name)
-
-
-classForwardOneToOneDescriptor(ForwardManyToOneDescriptor):
- """
- Accessor to the related object on the forward side of a one-to-one relation.
-
- In the example::
-
- class Restaurant(Model):
- place = OneToOneField(Place, related_name='restaurant')
-
- ``Restaurant.place`` is a ``ForwardOneToOneDescriptor`` instance.
- """
-
- defget_object(self,instance):
- ifself.field.remote_field.parent_link:
- deferred=instance.get_deferred_fields()
- # Because it's a parent link, all the data is available in the
- # instance, so populate the parent model with this data.
- rel_model=self.field.remote_field.model
- fields=[field.attnameforfieldinrel_model._meta.concrete_fields]
-
- # If any of the related model's fields are deferred, fallback to
- # fetching all fields from the related model. This avoids a query
- # on the related model for every deferred field.
- ifnotany(fieldinfieldsforfieldindeferred):
- kwargs={field:getattr(instance,field)forfieldinfields}
- obj=rel_model(**kwargs)
- obj._state.adding=instance._state.adding
- obj._state.db=instance._state.db
- returnobj
- returnsuper().get_object(instance)
-
- def__set__(self,instance,value):
- super().__set__(instance,value)
- # If the primary key is a link to a parent model and a parent instance
- # is being set, update the value of the inherited pk(s).
- ifself.field.primary_keyandself.field.remote_field.parent_link:
- opts=instance._meta
- # Inherited primary key fields from this object's base classes.
- inherited_pk_fields=[
- fieldforfieldinopts.concrete_fields
- iffield.primary_keyandfield.remote_field
- ]
- forfieldininherited_pk_fields:
- rel_model_pk_name=field.remote_field.model._meta.pk.attname
- raw_value=getattr(value,rel_model_pk_name)ifvalueisnotNoneelseNone
- setattr(instance,rel_model_pk_name,raw_value)
-
-
-classReverseOneToOneDescriptor:
- """
- Accessor to the related object on the reverse side of a one-to-one
- relation.
-
- In the example::
-
- class Restaurant(Model):
- place = OneToOneField(Place, related_name='restaurant')
-
- ``Place.restaurant`` is a ``ReverseOneToOneDescriptor`` instance.
- """
-
- def__init__(self,related):
- # Following the example above, `related` is an instance of OneToOneRel
- # which represents the reverse restaurant field (place.restaurant).
- self.related=related
-
- @cached_property
- defRelatedObjectDoesNotExist(self):
- # The exception isn't created at initialization time for the sake of
- # consistency with `ForwardManyToOneDescriptor`.
- returntype(
- 'RelatedObjectDoesNotExist',
- (self.related.related_model.DoesNotExist,AttributeError),{
- '__module__':self.related.model.__module__,
- '__qualname__':'%s.%s.RelatedObjectDoesNotExist'%(
- self.related.model.__qualname__,
- self.related.name,
- )
- },
- )
-
- defis_cached(self,instance):
- returnself.related.is_cached(instance)
-
- defget_queryset(self,**hints):
- returnself.related.related_model._base_manager.db_manager(hints=hints).all()
-
- defget_prefetch_queryset(self,instances,queryset=None):
- ifquerysetisNone:
- queryset=self.get_queryset()
- queryset._add_hints(instance=instances[0])
-
- rel_obj_attr=self.related.field.get_local_related_value
- instance_attr=self.related.field.get_foreign_related_value
- instances_dict={instance_attr(inst):instforinstininstances}
- query={'%s__in'%self.related.field.name:instances}
- queryset=queryset.filter(**query)
-
- # Since we're going to assign directly in the cache,
- # we must manage the reverse relation cache manually.
- forrel_objinqueryset:
- instance=instances_dict[rel_obj_attr(rel_obj)]
- self.related.field.set_cached_value(rel_obj,instance)
- returnqueryset,rel_obj_attr,instance_attr,True,self.related.get_cache_name(),False
-
- def__get__(self,instance,cls=None):
- """
- Get the related instance through the reverse relation.
-
- With the example above, when getting ``place.restaurant``:
-
- - ``self`` is the descriptor managing the ``restaurant`` attribute
- - ``instance`` is the ``place`` instance
- - ``cls`` is the ``Place`` class (unused)
-
- Keep in mind that ``Restaurant`` holds the foreign key to ``Place``.
- """
- ifinstanceisNone:
- returnself
-
- # The related instance is loaded from the database and then cached
- # by the field on the model instance state. It can also be pre-cached
- # by the forward accessor (ForwardManyToOneDescriptor).
- try:
- rel_obj=self.related.get_cached_value(instance)
- exceptKeyError:
- related_pk=instance.pk
- ifrelated_pkisNone:
- rel_obj=None
- else:
- filter_args=self.related.field.get_forward_related_filter(instance)
- try:
- rel_obj=self.get_queryset(instance=instance).get(**filter_args)
- exceptself.related.related_model.DoesNotExist:
- rel_obj=None
- else:
- # Set the forward accessor cache on the related object to
- # the current instance to avoid an extra SQL query if it's
- # accessed later on.
- self.related.field.set_cached_value(rel_obj,instance)
- self.related.set_cached_value(instance,rel_obj)
-
- ifrel_objisNone:
- raiseself.RelatedObjectDoesNotExist(
- "%s has no %s."%(
- instance.__class__.__name__,
- self.related.get_accessor_name()
- )
- )
- else:
- returnrel_obj
-
- def__set__(self,instance,value):
- """
- Set the related instance through the reverse relation.
-
- With the example above, when setting ``place.restaurant = restaurant``:
-
- - ``self`` is the descriptor managing the ``restaurant`` attribute
- - ``instance`` is the ``place`` instance
- - ``value`` is the ``restaurant`` instance on the right of the equal sign
-
- Keep in mind that ``Restaurant`` holds the foreign key to ``Place``.
- """
- # The similarity of the code below to the code in
- # ForwardManyToOneDescriptor is annoying, but there's a bunch
- # of small differences that would make a common base class convoluted.
-
- ifvalueisNone:
- # Update the cached related instance (if any) & clear the cache.
- # Following the example above, this would be the cached
- # ``restaurant`` instance (if any).
- rel_obj=self.related.get_cached_value(instance,default=None)
- ifrel_objisnotNone:
- # Remove the ``restaurant`` instance from the ``place``
- # instance cache.
- self.related.delete_cached_value(instance)
- # Set the ``place`` field on the ``restaurant``
- # instance to None.
- setattr(rel_obj,self.related.field.name,None)
- elifnotisinstance(value,self.related.related_model):
- # An object must be an instance of the related class.
- raiseValueError(
- 'Cannot assign "%r": "%s.%s" must be a "%s" instance.'%(
- value,
- instance._meta.object_name,
- self.related.get_accessor_name(),
- self.related.related_model._meta.object_name,
- )
- )
- else:
- ifinstance._state.dbisNone:
- instance._state.db=router.db_for_write(instance.__class__,instance=value)
- ifvalue._state.dbisNone:
- value._state.db=router.db_for_write(value.__class__,instance=instance)
- ifnotrouter.allow_relation(value,instance):
- raiseValueError('Cannot assign "%r": the current database router prevents this relation.'%value)
-
- related_pk=tuple(getattr(instance,field.attname)forfieldinself.related.field.foreign_related_fields)
- # Set the value of the related field to the value of the related object's related field
- forindex,fieldinenumerate(self.related.field.local_related_fields):
- setattr(value,field.attname,related_pk[index])
-
- # Set the related instance cache used by __get__ to avoid an SQL query
- # when accessing the attribute we just set.
- self.related.set_cached_value(instance,value)
-
- # Set the forward accessor cache on the related object to the current
- # instance to avoid an extra SQL query if it's accessed later on.
- self.related.field.set_cached_value(value,instance)
-
- def__reduce__(self):
- # Same purpose as ForwardManyToOneDescriptor.__reduce__().
- returngetattr,(self.related.model,self.related.name)
-
-
-classReverseManyToOneDescriptor:
- """
- Accessor to the related objects manager on the reverse side of a
- many-to-one relation.
-
- In the example::
-
- class Child(Model):
- parent = ForeignKey(Parent, related_name='children')
-
- ``Parent.children`` is a ``ReverseManyToOneDescriptor`` instance.
-
- Most of the implementation is delegated to a dynamically defined manager
- class built by ``create_forward_many_to_many_manager()`` defined below.
- """
-
- def__init__(self,rel):
- self.rel=rel
- self.field=rel.field
-
- @cached_property
- defrelated_manager_cls(self):
- related_model=self.rel.related_model
-
- returncreate_reverse_many_to_one_manager(
- related_model._default_manager.__class__,
- self.rel,
- )
-
- def__get__(self,instance,cls=None):
- """
- Get the related objects through the reverse relation.
-
- With the example above, when getting ``parent.children``:
-
- - ``self`` is the descriptor managing the ``children`` attribute
- - ``instance`` is the ``parent`` instance
- - ``cls`` is the ``Parent`` class (unused)
- """
- ifinstanceisNone:
- returnself
-
- returnself.related_manager_cls(instance)
-
- def_get_set_deprecation_msg_params(self):
- return(
- 'reverse side of a related set',
- self.rel.get_accessor_name(),
- )
-
- def__set__(self,instance,value):
- raiseTypeError(
- 'Direct assignment to the %s is prohibited. Use %s.set() instead.'
- %self._get_set_deprecation_msg_params(),
- )
-
-
-defcreate_reverse_many_to_one_manager(superclass,rel):
- """
- Create a manager for the reverse side of a many-to-one relation.
-
- This manager subclasses another manager, generally the default manager of
- the related model, and adds behaviors specific to many-to-one relations.
- """
-
- classRelatedManager(superclass):
- def__init__(self,instance):
- super().__init__()
-
- self.instance=instance
- self.model=rel.related_model
- self.field=rel.field
-
- self.core_filters={self.field.name:instance}
-
- def__call__(self,*,manager):
- manager=getattr(self.model,manager)
- manager_class=create_reverse_many_to_one_manager(manager.__class__,rel)
- returnmanager_class(self.instance)
- do_not_call_in_templates=True
-
- def_apply_rel_filters(self,queryset):
- """
- Filter the queryset for the instance this manager is bound to.
- """
- db=self._dborrouter.db_for_read(self.model,instance=self.instance)
- empty_strings_as_null=connections[db].features.interprets_empty_strings_as_nulls
- queryset._add_hints(instance=self.instance)
- ifself._db:
- queryset=queryset.using(self._db)
- queryset._defer_next_filter=True
- queryset=queryset.filter(**self.core_filters)
- forfieldinself.field.foreign_related_fields:
- val=getattr(self.instance,field.attname)
- ifvalisNoneor(val==''andempty_strings_as_null):
- returnqueryset.none()
- ifself.field.many_to_one:
- # Guard against field-like objects such as GenericRelation
- # that abuse create_reverse_many_to_one_manager() with reverse
- # one-to-many relationships instead and break known related
- # objects assignment.
- try:
- target_field=self.field.target_field
- exceptFieldError:
- # The relationship has multiple target fields. Use a tuple
- # for related object id.
- rel_obj_id=tuple([
- getattr(self.instance,target_field.attname)
- fortarget_fieldinself.field.get_path_info()[-1].target_fields
- ])
- else:
- rel_obj_id=getattr(self.instance,target_field.attname)
- queryset._known_related_objects={self.field:{rel_obj_id:self.instance}}
- returnqueryset
-
- def_remove_prefetched_objects(self):
- try:
- self.instance._prefetched_objects_cache.pop(self.field.remote_field.get_cache_name())
- except(AttributeError,KeyError):
- pass# nothing to clear from cache
-
- defget_queryset(self):
- try:
- returnself.instance._prefetched_objects_cache[self.field.remote_field.get_cache_name()]
- except(AttributeError,KeyError):
- queryset=super().get_queryset()
- returnself._apply_rel_filters(queryset)
-
- defget_prefetch_queryset(self,instances,queryset=None):
- ifquerysetisNone:
- queryset=super().get_queryset()
-
- queryset._add_hints(instance=instances[0])
- queryset=queryset.using(queryset._dborself._db)
-
- rel_obj_attr=self.field.get_local_related_value
- instance_attr=self.field.get_foreign_related_value
- instances_dict={instance_attr(inst):instforinstininstances}
- query={'%s__in'%self.field.name:instances}
- queryset=queryset.filter(**query)
-
- # Since we just bypassed this class' get_queryset(), we must manage
- # the reverse relation manually.
- forrel_objinqueryset:
- instance=instances_dict[rel_obj_attr(rel_obj)]
- setattr(rel_obj,self.field.name,instance)
- cache_name=self.field.remote_field.get_cache_name()
- returnqueryset,rel_obj_attr,instance_attr,False,cache_name,False
-
- defadd(self,*objs,bulk=True):
- self._remove_prefetched_objects()
- db=router.db_for_write(self.model,instance=self.instance)
-
- defcheck_and_update_obj(obj):
- ifnotisinstance(obj,self.model):
- raiseTypeError("'%s' instance expected, got %r"%(
- self.model._meta.object_name,obj,
- ))
- setattr(obj,self.field.name,self.instance)
-
- ifbulk:
- pks=[]
- forobjinobjs:
- check_and_update_obj(obj)
- ifobj._state.addingorobj._state.db!=db:
- raiseValueError(
- "%r instance isn't saved. Use bulk=False or save "
- "the object first."%obj
- )
- pks.append(obj.pk)
- self.model._base_manager.using(db).filter(pk__in=pks).update(**{
- self.field.name:self.instance,
- })
- else:
- withtransaction.atomic(using=db,savepoint=False):
- forobjinobjs:
- check_and_update_obj(obj)
- obj.save()
- add.alters_data=True
-
- defcreate(self,**kwargs):
- kwargs[self.field.name]=self.instance
- db=router.db_for_write(self.model,instance=self.instance)
- returnsuper(RelatedManager,self.db_manager(db)).create(**kwargs)
- create.alters_data=True
-
- defget_or_create(self,**kwargs):
- kwargs[self.field.name]=self.instance
- db=router.db_for_write(self.model,instance=self.instance)
- returnsuper(RelatedManager,self.db_manager(db)).get_or_create(**kwargs)
- get_or_create.alters_data=True
-
- defupdate_or_create(self,**kwargs):
- kwargs[self.field.name]=self.instance
- db=router.db_for_write(self.model,instance=self.instance)
- returnsuper(RelatedManager,self.db_manager(db)).update_or_create(**kwargs)
- update_or_create.alters_data=True
-
- # remove() and clear() are only provided if the ForeignKey can have a value of null.
- ifrel.field.null:
- defremove(self,*objs,bulk=True):
- ifnotobjs:
- return
- val=self.field.get_foreign_related_value(self.instance)
- old_ids=set()
- forobjinobjs:
- ifnotisinstance(obj,self.model):
- raiseTypeError("'%s' instance expected, got %r"%(
- self.model._meta.object_name,obj,
- ))
- # Is obj actually part of this descriptor set?
- ifself.field.get_local_related_value(obj)==val:
- old_ids.add(obj.pk)
- else:
- raiseself.field.remote_field.model.DoesNotExist(
- "%r is not related to %r."%(obj,self.instance)
- )
- self._clear(self.filter(pk__in=old_ids),bulk)
- remove.alters_data=True
-
- defclear(self,*,bulk=True):
- self._clear(self,bulk)
- clear.alters_data=True
-
- def_clear(self,queryset,bulk):
- self._remove_prefetched_objects()
- db=router.db_for_write(self.model,instance=self.instance)
- queryset=queryset.using(db)
- ifbulk:
- # `QuerySet.update()` is intrinsically atomic.
- queryset.update(**{self.field.name:None})
- else:
- withtransaction.atomic(using=db,savepoint=False):
- forobjinqueryset:
- setattr(obj,self.field.name,None)
- obj.save(update_fields=[self.field.name])
- _clear.alters_data=True
-
- defset(self,objs,*,bulk=True,clear=False):
- # Force evaluation of `objs` in case it's a queryset whose value
- # could be affected by `manager.clear()`. Refs #19816.
- objs=tuple(objs)
-
- ifself.field.null:
- db=router.db_for_write(self.model,instance=self.instance)
- withtransaction.atomic(using=db,savepoint=False):
- ifclear:
- self.clear(bulk=bulk)
- self.add(*objs,bulk=bulk)
- else:
- old_objs=set(self.using(db).all())
- new_objs=[]
- forobjinobjs:
- ifobjinold_objs:
- old_objs.remove(obj)
- else:
- new_objs.append(obj)
-
- self.remove(*old_objs,bulk=bulk)
- self.add(*new_objs,bulk=bulk)
- else:
- self.add(*objs,bulk=bulk)
- set.alters_data=True
-
- returnRelatedManager
-
-
-classManyToManyDescriptor(ReverseManyToOneDescriptor):
- """
- Accessor to the related objects manager on the forward and reverse sides of
- a many-to-many relation.
-
- In the example::
-
- class Pizza(Model):
- toppings = ManyToManyField(Topping, related_name='pizzas')
-
- ``Pizza.toppings`` and ``Topping.pizzas`` are ``ManyToManyDescriptor``
- instances.
-
- Most of the implementation is delegated to a dynamically defined manager
- class built by ``create_forward_many_to_many_manager()`` defined below.
- """
-
- def__init__(self,rel,reverse=False):
- super().__init__(rel)
-
- self.reverse=reverse
-
- @property
- defthrough(self):
- # through is provided so that you have easy access to the through
- # model (Book.authors.through) for inlines, etc. This is done as
- # a property to ensure that the fully resolved value is returned.
- returnself.rel.through
-
- @cached_property
- defrelated_manager_cls(self):
- related_model=self.rel.related_modelifself.reverseelseself.rel.model
-
- returncreate_forward_many_to_many_manager(
- related_model._default_manager.__class__,
- self.rel,
- reverse=self.reverse,
- )
-
- def_get_set_deprecation_msg_params(self):
- return(
- '%s side of a many-to-many set'%('reverse'ifself.reverseelse'forward'),
- self.rel.get_accessor_name()ifself.reverseelseself.field.name,
- )
-
-
-defcreate_forward_many_to_many_manager(superclass,rel,reverse):
- """
- Create a manager for the either side of a many-to-many relation.
-
- This manager subclasses another manager, generally the default manager of
- the related model, and adds behaviors specific to many-to-many relations.
- """
-
- classManyRelatedManager(superclass):
- def__init__(self,instance=None):
- super().__init__()
-
- self.instance=instance
-
- ifnotreverse:
- self.model=rel.model
- self.query_field_name=rel.field.related_query_name()
- self.prefetch_cache_name=rel.field.name
- self.source_field_name=rel.field.m2m_field_name()
- self.target_field_name=rel.field.m2m_reverse_field_name()
- self.symmetrical=rel.symmetrical
- else:
- self.model=rel.related_model
- self.query_field_name=rel.field.name
- self.prefetch_cache_name=rel.field.related_query_name()
- self.source_field_name=rel.field.m2m_reverse_field_name()
- self.target_field_name=rel.field.m2m_field_name()
- self.symmetrical=False
-
- self.through=rel.through
- self.reverse=reverse
-
- self.source_field=self.through._meta.get_field(self.source_field_name)
- self.target_field=self.through._meta.get_field(self.target_field_name)
-
- self.core_filters={}
- self.pk_field_names={}
- forlh_field,rh_fieldinself.source_field.related_fields:
- core_filter_key='%s__%s'%(self.query_field_name,rh_field.name)
- self.core_filters[core_filter_key]=getattr(instance,rh_field.attname)
- self.pk_field_names[lh_field.name]=rh_field.name
-
- self.related_val=self.source_field.get_foreign_related_value(instance)
- ifNoneinself.related_val:
- raiseValueError('"%r" needs to have a value for field "%s" before '
- 'this many-to-many relationship can be used.'%
- (instance,self.pk_field_names[self.source_field_name]))
- # Even if this relation is not to pk, we require still pk value.
- # The wish is that the instance has been already saved to DB,
- # although having a pk value isn't a guarantee of that.
- ifinstance.pkisNone:
- raiseValueError("%r instance needs to have a primary key value before "
- "a many-to-many relationship can be used."%
- instance.__class__.__name__)
-
- def__call__(self,*,manager):
- manager=getattr(self.model,manager)
- manager_class=create_forward_many_to_many_manager(manager.__class__,rel,reverse)
- returnmanager_class(instance=self.instance)
- do_not_call_in_templates=True
-
- def_build_remove_filters(self,removed_vals):
- filters=Q(**{self.source_field_name:self.related_val})
- # No need to add a subquery condition if removed_vals is a QuerySet without
- # filters.
- removed_vals_filters=(notisinstance(removed_vals,QuerySet)or
- removed_vals._has_filters())
- ifremoved_vals_filters:
- filters&=Q(**{'%s__in'%self.target_field_name:removed_vals})
- ifself.symmetrical:
- symmetrical_filters=Q(**{self.target_field_name:self.related_val})
- ifremoved_vals_filters:
- symmetrical_filters&=Q(
- **{'%s__in'%self.source_field_name:removed_vals})
- filters|=symmetrical_filters
- returnfilters
-
- def_apply_rel_filters(self,queryset):
- """
- Filter the queryset for the instance this manager is bound to.
- """
- queryset._add_hints(instance=self.instance)
- ifself._db:
- queryset=queryset.using(self._db)
- queryset._defer_next_filter=True
- returnqueryset._next_is_sticky().filter(**self.core_filters)
-
- def_remove_prefetched_objects(self):
- try:
- self.instance._prefetched_objects_cache.pop(self.prefetch_cache_name)
- except(AttributeError,KeyError):
- pass# nothing to clear from cache
-
- defget_queryset(self):
- try:
- returnself.instance._prefetched_objects_cache[self.prefetch_cache_name]
- except(AttributeError,KeyError):
- queryset=super().get_queryset()
- returnself._apply_rel_filters(queryset)
-
- defget_prefetch_queryset(self,instances,queryset=None):
- ifquerysetisNone:
- queryset=super().get_queryset()
-
- queryset._add_hints(instance=instances[0])
- queryset=queryset.using(queryset._dborself._db)
-
- query={'%s__in'%self.query_field_name:instances}
- queryset=queryset._next_is_sticky().filter(**query)
-
- # M2M: need to annotate the query in order to get the primary model
- # that the secondary model was actually related to. We know that
- # there will already be a join on the join table, so we can just add
- # the select.
-
- # For non-autocreated 'through' models, can't assume we are
- # dealing with PK values.
- fk=self.through._meta.get_field(self.source_field_name)
- join_table=fk.model._meta.db_table
- connection=connections[queryset.db]
- qn=connection.ops.quote_name
- queryset=queryset.extra(select={
- '_prefetch_related_val_%s'%f.attname:
- '%s.%s'%(qn(join_table),qn(f.column))forfinfk.local_related_fields})
- return(
- queryset,
- lambdaresult:tuple(
- getattr(result,'_prefetch_related_val_%s'%f.attname)
- forfinfk.local_related_fields
- ),
- lambdainst:tuple(
- f.get_db_prep_value(getattr(inst,f.attname),connection)
- forfinfk.foreign_related_fields
- ),
- False,
- self.prefetch_cache_name,
- False,
- )
-
- defadd(self,*objs,through_defaults=None):
- self._remove_prefetched_objects()
- db=router.db_for_write(self.through,instance=self.instance)
- withtransaction.atomic(using=db,savepoint=False):
- self._add_items(
- self.source_field_name,self.target_field_name,*objs,
- through_defaults=through_defaults,
- )
- # If this is a symmetrical m2m relation to self, add the mirror
- # entry in the m2m table.
- ifself.symmetrical:
- self._add_items(
- self.target_field_name,
- self.source_field_name,
- *objs,
- through_defaults=through_defaults,
- )
- add.alters_data=True
-
- defremove(self,*objs):
- self._remove_prefetched_objects()
- self._remove_items(self.source_field_name,self.target_field_name,*objs)
- remove.alters_data=True
-
- defclear(self):
- db=router.db_for_write(self.through,instance=self.instance)
- withtransaction.atomic(using=db,savepoint=False):
- signals.m2m_changed.send(
- sender=self.through,action="pre_clear",
- instance=self.instance,reverse=self.reverse,
- model=self.model,pk_set=None,using=db,
- )
- self._remove_prefetched_objects()
- filters=self._build_remove_filters(super().get_queryset().using(db))
- self.through._default_manager.using(db).filter(filters).delete()
-
- signals.m2m_changed.send(
- sender=self.through,action="post_clear",
- instance=self.instance,reverse=self.reverse,
- model=self.model,pk_set=None,using=db,
- )
- clear.alters_data=True
-
- defset(self,objs,*,clear=False,through_defaults=None):
- # Force evaluation of `objs` in case it's a queryset whose value
- # could be affected by `manager.clear()`. Refs #19816.
- objs=tuple(objs)
-
- db=router.db_for_write(self.through,instance=self.instance)
- withtransaction.atomic(using=db,savepoint=False):
- ifclear:
- self.clear()
- self.add(*objs,through_defaults=through_defaults)
- else:
- old_ids=set(self.using(db).values_list(self.target_field.target_field.attname,flat=True))
-
- new_objs=[]
- forobjinobjs:
- fk_val=(
- self.target_field.get_foreign_related_value(obj)[0]
- ifisinstance(obj,self.model)
- elseself.target_field.get_prep_value(obj)
- )
- iffk_valinold_ids:
- old_ids.remove(fk_val)
- else:
- new_objs.append(obj)
-
- self.remove(*old_ids)
- self.add(*new_objs,through_defaults=through_defaults)
- set.alters_data=True
-
- defcreate(self,*,through_defaults=None,**kwargs):
- db=router.db_for_write(self.instance.__class__,instance=self.instance)
- new_obj=super(ManyRelatedManager,self.db_manager(db)).create(**kwargs)
- self.add(new_obj,through_defaults=through_defaults)
- returnnew_obj
- create.alters_data=True
-
- defget_or_create(self,*,through_defaults=None,**kwargs):
- db=router.db_for_write(self.instance.__class__,instance=self.instance)
- obj,created=super(ManyRelatedManager,self.db_manager(db)).get_or_create(**kwargs)
- # We only need to add() if created because if we got an object back
- # from get() then the relationship already exists.
- ifcreated:
- self.add(obj,through_defaults=through_defaults)
- returnobj,created
- get_or_create.alters_data=True
-
- defupdate_or_create(self,*,through_defaults=None,**kwargs):
- db=router.db_for_write(self.instance.__class__,instance=self.instance)
- obj,created=super(ManyRelatedManager,self.db_manager(db)).update_or_create(**kwargs)
- # We only need to add() if created because if we got an object back
- # from get() then the relationship already exists.
- ifcreated:
- self.add(obj,through_defaults=through_defaults)
- returnobj,created
- update_or_create.alters_data=True
-
- def_get_target_ids(self,target_field_name,objs):
- """
- Return the set of ids of `objs` that the target field references.
- """
- fromdjango.db.modelsimportModel
- target_ids=set()
- target_field=self.through._meta.get_field(target_field_name)
- forobjinobjs:
- ifisinstance(obj,self.model):
- ifnotrouter.allow_relation(obj,self.instance):
- raiseValueError(
- 'Cannot add "%r": instance is on database "%s", '
- 'value is on database "%s"'%
- (obj,self.instance._state.db,obj._state.db)
- )
- target_id=target_field.get_foreign_related_value(obj)[0]
- iftarget_idisNone:
- raiseValueError(
- 'Cannot add "%r": the value for field "%s" is None'%
- (obj,target_field_name)
- )
- target_ids.add(target_id)
- elifisinstance(obj,Model):
- raiseTypeError(
- "'%s' instance expected, got %r"%
- (self.model._meta.object_name,obj)
- )
- else:
- target_ids.add(target_field.get_prep_value(obj))
- returntarget_ids
-
- def_get_missing_target_ids(self,source_field_name,target_field_name,db,target_ids):
- """
- Return the subset of ids of `objs` that aren't already assigned to
- this relationship.
- """
- vals=self.through._default_manager.using(db).values_list(
- target_field_name,flat=True
- ).filter(**{
- source_field_name:self.related_val[0],
- '%s__in'%target_field_name:target_ids,
- })
- returntarget_ids.difference(vals)
-
- def_get_add_plan(self,db,source_field_name):
- """
- Return a boolean triple of the way the add should be performed.
-
- The first element is whether or not bulk_create(ignore_conflicts)
- can be used, the second whether or not signals must be sent, and
- the third element is whether or not the immediate bulk insertion
- with conflicts ignored can be performed.
- """
- # Conflicts can be ignored when the intermediary model is
- # auto-created as the only possible collision is on the
- # (source_id, target_id) tuple. The same assertion doesn't hold for
- # user-defined intermediary models as they could have other fields
- # causing conflicts which must be surfaced.
- can_ignore_conflicts=(
- connections[db].features.supports_ignore_conflictsand
- self.through._meta.auto_createdisnotFalse
- )
- # Don't send the signal when inserting duplicate data row
- # for symmetrical reverse entries.
- must_send_signals=(self.reverseorsource_field_name==self.source_field_name)and(
- signals.m2m_changed.has_listeners(self.through)
- )
- # Fast addition through bulk insertion can only be performed
- # if no m2m_changed listeners are connected for self.through
- # as they require the added set of ids to be provided via
- # pk_set.
- returncan_ignore_conflicts,must_send_signals,(can_ignore_conflictsandnotmust_send_signals)
-
- def_add_items(self,source_field_name,target_field_name,*objs,through_defaults=None):
- # source_field_name: the PK fieldname in join table for the source object
- # target_field_name: the PK fieldname in join table for the target object
- # *objs - objects to add. Either object instances, or primary keys of object instances.
- ifnotobjs:
- return
-
- through_defaults=dict(resolve_callables(through_defaultsor{}))
- target_ids=self._get_target_ids(target_field_name,objs)
- db=router.db_for_write(self.through,instance=self.instance)
- can_ignore_conflicts,must_send_signals,can_fast_add=self._get_add_plan(db,source_field_name)
- ifcan_fast_add:
- self.through._default_manager.using(db).bulk_create([
- self.through(**{
- '%s_id'%source_field_name:self.related_val[0],
- '%s_id'%target_field_name:target_id,
- })
- fortarget_idintarget_ids
- ],ignore_conflicts=True)
- return
-
- missing_target_ids=self._get_missing_target_ids(
- source_field_name,target_field_name,db,target_ids
- )
- withtransaction.atomic(using=db,savepoint=False):
- ifmust_send_signals:
- signals.m2m_changed.send(
- sender=self.through,action='pre_add',
- instance=self.instance,reverse=self.reverse,
- model=self.model,pk_set=missing_target_ids,using=db,
- )
- # Add the ones that aren't there already.
- self.through._default_manager.using(db).bulk_create([
- self.through(**through_defaults,**{
- '%s_id'%source_field_name:self.related_val[0],
- '%s_id'%target_field_name:target_id,
- })
- fortarget_idinmissing_target_ids
- ],ignore_conflicts=can_ignore_conflicts)
-
- ifmust_send_signals:
- signals.m2m_changed.send(
- sender=self.through,action='post_add',
- instance=self.instance,reverse=self.reverse,
- model=self.model,pk_set=missing_target_ids,using=db,
- )
-
- def_remove_items(self,source_field_name,target_field_name,*objs):
- # source_field_name: the PK colname in join table for the source object
- # target_field_name: the PK colname in join table for the target object
- # *objs - objects to remove. Either object instances, or primary
- # keys of object instances.
- ifnotobjs:
- return
-
- # Check that all the objects are of the right type
- old_ids=set()
- forobjinobjs:
- ifisinstance(obj,self.model):
- fk_val=self.target_field.get_foreign_related_value(obj)[0]
- old_ids.add(fk_val)
- else:
- old_ids.add(obj)
-
- db=router.db_for_write(self.through,instance=self.instance)
- withtransaction.atomic(using=db,savepoint=False):
- # Send a signal to the other end if need be.
- signals.m2m_changed.send(
- sender=self.through,action="pre_remove",
- instance=self.instance,reverse=self.reverse,
- model=self.model,pk_set=old_ids,using=db,
- )
- target_model_qs=super().get_queryset()
- iftarget_model_qs._has_filters():
- old_vals=target_model_qs.using(db).filter(**{
- '%s__in'%self.target_field.target_field.attname:old_ids})
- else:
- old_vals=old_ids
- filters=self._build_remove_filters(old_vals)
- self.through._default_manager.using(db).filter(filters).delete()
-
- signals.m2m_changed.send(
- sender=self.through,action="post_remove",
- instance=self.instance,reverse=self.reverse,
- model=self.model,pk_set=old_ids,using=db,
- )
-
- returnManyRelatedManager
-
-"""
-Various data structures used in query construction.
-
-Factored out from django.db.models.query to avoid making the main module very
-large and/or so that they can be used by other modules without getting into
-circular import difficulties.
-"""
-importcopy
-importfunctools
-importinspect
-importwarnings
-fromcollectionsimportnamedtuple
-
-fromdjango.core.exceptionsimportFieldDoesNotExist,FieldError
-fromdjango.db.models.constantsimportLOOKUP_SEP
-fromdjango.utilsimporttree
-fromdjango.utils.deprecationimportRemovedInDjango40Warning
-
-# PathInfo is used when converting lookups (fk__somecol). The contents
-# describe the relation in Model terms (model Options and Fields for both
-# sides of the relation. The join_field is the field backing the relation.
-PathInfo=namedtuple('PathInfo','from_opts to_opts target_fields join_field m2m direct filtered_relation')
-
-
-classInvalidQueryType(type):
- @property
- def_subclasses(self):
- return(FieldDoesNotExist,FieldError)
-
- def__warn(self):
- warnings.warn(
- 'The InvalidQuery exception class is deprecated. Use '
- 'FieldDoesNotExist or FieldError instead.',
- category=RemovedInDjango40Warning,
- stacklevel=4,
- )
-
- def__instancecheck__(self,instance):
- self.__warn()
- returnisinstance(instance,self._subclasses)orsuper().__instancecheck__(instance)
-
- def__subclasscheck__(self,subclass):
- self.__warn()
- returnissubclass(subclass,self._subclasses)orsuper().__subclasscheck__(subclass)
-
-
-classInvalidQuery(Exception,metaclass=InvalidQueryType):
- pass
-
-
-defsubclasses(cls):
- yieldcls
- forsubclassincls.__subclasses__():
- yield fromsubclasses(subclass)
-
-
-classQ(tree.Node):
- """
- Encapsulate filters as objects that can then be combined logically (using
- `&` and `|`).
- """
- # Connection types
- AND='AND'
- OR='OR'
- default=AND
- conditional=True
-
- def__init__(self,*args,_connector=None,_negated=False,**kwargs):
- super().__init__(children=[*args,*sorted(kwargs.items())],connector=_connector,negated=_negated)
-
- def_combine(self,other,conn):
- ifnot(isinstance(other,Q)orgetattr(other,'conditional',False)isTrue):
- raiseTypeError(other)
-
- ifnotself:
- returnother.copy()ifhasattr(other,'copy')elsecopy.copy(other)
- elifisinstance(other,Q)andnotother:
- _,args,kwargs=self.deconstruct()
- returntype(self)(*args,**kwargs)
-
- obj=type(self)()
- obj.connector=conn
- obj.add(self,conn)
- obj.add(other,conn)
- returnobj
-
- def__or__(self,other):
- returnself._combine(other,self.OR)
-
- def__and__(self,other):
- returnself._combine(other,self.AND)
-
- def__invert__(self):
- obj=type(self)()
- obj.add(self,self.AND)
- obj.negate()
- returnobj
-
- defresolve_expression(self,query=None,allow_joins=True,reuse=None,summarize=False,for_save=False):
- # We must promote any new joins to left outer joins so that when Q is
- # used as an expression, rows aren't filtered due to joins.
- clause,joins=query._add_q(
- self,reuse,allow_joins=allow_joins,split_subq=False,
- check_filterable=False,
- )
- query.promote_joins(joins)
- returnclause
-
- defdeconstruct(self):
- path='%s.%s'%(self.__class__.__module__,self.__class__.__name__)
- ifpath.startswith('django.db.models.query_utils'):
- path=path.replace('django.db.models.query_utils','django.db.models')
- args=tuple(self.children)
- kwargs={}
- ifself.connector!=self.default:
- kwargs['_connector']=self.connector
- ifself.negated:
- kwargs['_negated']=True
- returnpath,args,kwargs
-
-
-classDeferredAttribute:
- """
- A wrapper for a deferred-loading field. When the value is read from this
- object the first time, the query is executed.
- """
- def__init__(self,field):
- self.field=field
-
- def__get__(self,instance,cls=None):
- """
- Retrieve and caches the value from the datastore on the first lookup.
- Return the cached value.
- """
- ifinstanceisNone:
- returnself
- data=instance.__dict__
- field_name=self.field.attname
- iffield_namenotindata:
- # Let's see if the field is part of the parent chain. If so we
- # might be able to reuse the already loaded value. Refs #18343.
- val=self._check_parent_chain(instance)
- ifvalisNone:
- instance.refresh_from_db(fields=[field_name])
- else:
- data[field_name]=val
- returndata[field_name]
-
- def_check_parent_chain(self,instance):
- """
- Check if the field value can be fetched from a parent field already
- loaded in the instance. This can be done if the to-be fetched
- field is a primary key field.
- """
- opts=instance._meta
- link_field=opts.get_ancestor_link(self.field.model)
- ifself.field.primary_keyandself.field!=link_field:
- returngetattr(instance,link_field.attname)
- returnNone
-
-
-classRegisterLookupMixin:
-
- @classmethod
- def_get_lookup(cls,lookup_name):
- returncls.get_lookups().get(lookup_name,None)
-
- @classmethod
- @functools.lru_cache(maxsize=None)
- defget_lookups(cls):
- class_lookups=[parent.__dict__.get('class_lookups',{})forparentininspect.getmro(cls)]
- returncls.merge_dicts(class_lookups)
-
- defget_lookup(self,lookup_name):
- fromdjango.db.models.lookupsimportLookup
- found=self._get_lookup(lookup_name)
- iffoundisNoneandhasattr(self,'output_field'):
- returnself.output_field.get_lookup(lookup_name)
- iffoundisnotNoneandnotissubclass(found,Lookup):
- returnNone
- returnfound
-
- defget_transform(self,lookup_name):
- fromdjango.db.models.lookupsimportTransform
- found=self._get_lookup(lookup_name)
- iffoundisNoneandhasattr(self,'output_field'):
- returnself.output_field.get_transform(lookup_name)
- iffoundisnotNoneandnotissubclass(found,Transform):
- returnNone
- returnfound
-
- @staticmethod
- defmerge_dicts(dicts):
- """
- Merge dicts in reverse to preference the order of the original list. e.g.,
- merge_dicts([a, b]) will preference the keys in 'a' over those in 'b'.
- """
- merged={}
- fordinreversed(dicts):
- merged.update(d)
- returnmerged
-
- @classmethod
- def_clear_cached_lookups(cls):
- forsubclassinsubclasses(cls):
- subclass.get_lookups.cache_clear()
-
- @classmethod
- defregister_lookup(cls,lookup,lookup_name=None):
- iflookup_nameisNone:
- lookup_name=lookup.lookup_name
- if'class_lookups'notincls.__dict__:
- cls.class_lookups={}
- cls.class_lookups[lookup_name]=lookup
- cls._clear_cached_lookups()
- returnlookup
-
- @classmethod
- def_unregister_lookup(cls,lookup,lookup_name=None):
- """
- Remove given lookup from cls lookups. For use in tests only as it's
- not thread-safe.
- """
- iflookup_nameisNone:
- lookup_name=lookup.lookup_name
- delcls.class_lookups[lookup_name]
-
-
-defselect_related_descend(field,restricted,requested,load_fields,reverse=False):
- """
- Return True if this field should be used to descend deeper for
- select_related() purposes. Used by both the query construction code
- (sql.query.fill_related_selections()) and the model instance creation code
- (query.get_klass_info()).
-
- Arguments:
- * field - the field to be checked
- * restricted - a boolean field, indicating if the field list has been
- manually restricted using a requested clause)
- * requested - The select_related() dictionary.
- * load_fields - the set of fields to be loaded on this model
- * reverse - boolean, True if we are checking a reverse select related
- """
- ifnotfield.remote_field:
- returnFalse
- iffield.remote_field.parent_linkandnotreverse:
- returnFalse
- ifrestricted:
- ifreverseandfield.related_query_name()notinrequested:
- returnFalse
- ifnotreverseandfield.namenotinrequested:
- returnFalse
- ifnotrestrictedandfield.null:
- returnFalse
- ifload_fields:
- iffield.attnamenotinload_fields:
- ifrestrictedandfield.nameinrequested:
- msg=(
- 'Field %s.%s cannot be both deferred and traversed using '
- 'select_related at the same time.'
- )%(field.model._meta.object_name,field.name)
- raiseFieldError(msg)
- returnTrue
-
-
-defrefs_expression(lookup_parts,annotations):
- """
- Check if the lookup_parts contains references to the given annotations set.
- Because the LOOKUP_SEP is contained in the default annotation names, check
- each prefix of the lookup_parts for a match.
- """
- forninrange(1,len(lookup_parts)+1):
- level_n_lookup=LOOKUP_SEP.join(lookup_parts[0:n])
- iflevel_n_lookupinannotationsandannotations[level_n_lookup]:
- returnannotations[level_n_lookup],lookup_parts[n:]
- returnFalse,()
-
-
-defcheck_rel_lookup_compatibility(model,target_opts,field):
- """
- Check that self.model is compatible with target_opts. Compatibility
- is OK if:
- 1) model and opts match (where proxy inheritance is removed)
- 2) model is parent of opts' model or the other way around
- """
- defcheck(opts):
- return(
- model._meta.concrete_model==opts.concrete_modelor
- opts.concrete_modelinmodel._meta.get_parent_list()or
- modelinopts.get_parent_list()
- )
- # If the field is a primary key, then doing a query against the field's
- # model is ok, too. Consider the case:
- # class Restaurant(models.Model):
- # place = OneToOneField(Place, primary_key=True):
- # Restaurant.objects.filter(pk__in=Restaurant.objects.all()).
- # If we didn't have the primary key check, then pk__in (== place__in) would
- # give Place's opts as the target opts, but Restaurant isn't compatible
- # with that. This logic applies only to primary keys, as when doing __in=qs,
- # we are going to turn this into __in=qs.values('pk') later on.
- return(
- check(target_opts)or
- (getattr(field,'primary_key',False)andcheck(field.model._meta))
- )
-
-
-classFilteredRelation:
- """Specify custom filtering in the ON clause of SQL joins."""
-
- def__init__(self,relation_name,*,condition=Q()):
- ifnotrelation_name:
- raiseValueError('relation_name cannot be empty.')
- self.relation_name=relation_name
- self.alias=None
- ifnotisinstance(condition,Q):
- raiseValueError('condition argument must be a Q() instance.')
- self.condition=condition
- self.path=[]
-
- def__eq__(self,other):
- ifnotisinstance(other,self.__class__):
- returnNotImplemented
- return(
- self.relation_name==other.relation_nameand
- self.alias==other.aliasand
- self.condition==other.condition
- )
-
- defclone(self):
- clone=FilteredRelation(self.relation_name,condition=self.condition)
- clone.alias=self.alias
- clone.path=self.path[:]
- returnclone
-
- defresolve_expression(self,*args,**kwargs):
- """
- QuerySet.annotate() only accepts expression-like arguments
- (with a resolve_expression() method).
- """
- raiseNotImplementedError('FilteredRelation.resolve_expression() is unused.')
-
- defas_sql(self,compiler,connection):
- # Resolve the condition in Join.filtered_relation.
- query=compiler.query
- where=query.build_filtered_relation_q(self.condition,reuse=set(self.path))
- returncompiler.compile(where)
-
-"""
-Evennia MUD/MUX/MU* creation system
-
-This is the main top-level API for Evennia. You can explore the evennia library
-by accessing evennia.<subpackage> directly. From inside the game you can read
-docs of all object by viewing its `__doc__` string, such as through
-
- py evennia.ObjectDB.__doc__
-
-For full functionality you should explore this module via a django-
-aware shell. Go to your game directory and use the command
-
- evennia shell
-
-to launch such a shell (using python or ipython depending on your install).
-See www.evennia.com for full documentation.
-
-"""
-
-# docstring header
-
-DOCSTRING="""
-Evennia MU* creation system.
-
-Online manual and API docs are found at http://www.evennia.com.
-
-Flat-API shortcut names:
-{}
-"""
-
-# Delayed loading of properties
-
-# Typeclasses
-
-DefaultAccount=None
-DefaultGuest=None
-DefaultObject=None
-DefaultCharacter=None
-DefaultRoom=None
-DefaultExit=None
-DefaultChannel=None
-DefaultScript=None
-
-# Database models
-ObjectDB=None
-AccountDB=None
-ScriptDB=None
-ChannelDB=None
-Msg=None
-
-# commands
-Command=None
-CmdSet=None
-default_cmds=None
-syscmdkeys=None
-InterruptCommand=None
-
-# search functions
-search_object=None
-search_script=None
-search_account=None
-search_channel=None
-search_message=None
-search_help=None
-search_tag=None
-
-# create functions
-create_object=None
-create_script=None
-create_account=None
-create_channel=None
-create_message=None
-create_help_entry=None
-
-# utilities
-settings=None
-lockfuncs=None
-inputhandler=None
-logger=None
-gametime=None
-ansi=None
-spawn=None
-managers=None
-contrib=None
-EvMenu=None
-EvTable=None
-EvForm=None
-EvEditor=None
-EvMore=None
-ANSIString=None
-signals=None
-
-# Handlers
-SESSION_HANDLER=None
-TASK_HANDLER=None
-TICKER_HANDLER=None
-MONITOR_HANDLER=None
-
-# Containers
-GLOBAL_SCRIPTS=None
-OPTION_CLASSES=None
-
-def_create_version():
- """
- Helper function for building the version string
- """
- importos
- fromsubprocessimportcheck_output,CalledProcessError,STDOUT
-
- version="Unknown"
- root=os.path.dirname(os.path.abspath(__file__))
- try:
- withopen(os.path.join(root,"VERSION.txt"),"r")asf:
- version=f.read().strip()
- exceptIOErroraserr:
- print(err)
- try:
- rev=(
- check_output("git rev-parse --short HEAD",shell=True,cwd=root,stderr=STDOUT)
- .strip()
- .decode()
- )
- version="%s (rev %s)"%(version,rev)
- except(IOError,CalledProcessError,OSError):
- # ignore if we cannot get to git
- pass
- returnversion
-
-
-__version__=_create_version()
-del_create_version
-
-
-def_init():
- """
- This function is called automatically by the launcher only after
- Evennia has fully initialized all its models. It sets up the API
- in a safe environment where all models are available already.
- """
- globalDefaultAccount,DefaultObject,DefaultGuest,DefaultCharacter
- globalDefaultRoom,DefaultExit,DefaultChannel,DefaultScript
- globalObjectDB,AccountDB,ScriptDB,ChannelDB,Msg
- globalCommand,CmdSet,default_cmds,syscmdkeys,InterruptCommand
- globalsearch_object,search_script,search_account,search_channel
- globalsearch_help,search_tag,search_message
- globalcreate_object,create_script,create_account,create_channel
- globalcreate_message,create_help_entry
- globalsignals
- globalsettings,lockfuncs,logger,utils,gametime,ansi,spawn,managers
- globalcontrib,TICKER_HANDLER,MONITOR_HANDLER,SESSION_HANDLER
- globalTASK_HANDLER
- globalGLOBAL_SCRIPTS,OPTION_CLASSES
- globalEvMenu,EvTable,EvForm,EvMore,EvEditor
- globalANSIString
-
- # Parent typeclasses
- from.accounts.accountsimportDefaultAccount
- from.accounts.accountsimportDefaultGuest
- from.objects.objectsimportDefaultObject
- from.objects.objectsimportDefaultCharacter
- from.objects.objectsimportDefaultRoom
- from.objects.objectsimportDefaultExit
- from.comms.commsimportDefaultChannel
- from.scripts.scriptsimportDefaultScript
-
- # Database models
- from.objects.modelsimportObjectDB
- from.accounts.modelsimportAccountDB
- from.scripts.modelsimportScriptDB
- from.comms.modelsimportChannelDB
- from.comms.modelsimportMsg
-
- # commands
- from.commands.commandimportCommand,InterruptCommand
- from.commands.cmdsetimportCmdSet
-
- # search functions
- from.utils.searchimportsearch_object
- from.utils.searchimportsearch_script
- from.utils.searchimportsearch_account
- from.utils.searchimportsearch_message
- from.utils.searchimportsearch_channel
- from.utils.searchimportsearch_help
- from.utils.searchimportsearch_tag
-
- # create functions
- from.utils.createimportcreate_object
- from.utils.createimportcreate_script
- from.utils.createimportcreate_account
- from.utils.createimportcreate_channel
- from.utils.createimportcreate_message
- from.utils.createimportcreate_help_entry
-
- # utilities
- fromdjango.confimportsettings
- from.locksimportlockfuncs
- from.utilsimportlogger
- from.utilsimportgametime
- from.utilsimportansi
- from.prototypes.spawnerimportspawn
- from.importcontrib
- from.utils.evmenuimportEvMenu
- from.utils.evtableimportEvTable
- from.utils.evmoreimportEvMore
- from.utils.evformimportEvForm
- from.utils.eveditorimportEvEditor
- from.utils.ansiimportANSIString
- from.serverimportsignals
-
- # handlers
- from.scripts.tickerhandlerimportTICKER_HANDLER
- from.scripts.taskhandlerimportTASK_HANDLER
- from.server.sessionhandlerimportSESSION_HANDLER
- from.scripts.monitorhandlerimportMONITOR_HANDLER
-
- # containers
- from.utils.containersimportGLOBAL_SCRIPTS
- from.utils.containersimportOPTION_CLASSES
-
- # API containers
-
- class_EvContainer(object):
- """
- Parent for other containers
-
- """
-
- def_help(self):
- "Returns list of contents"
- names=[namefornameinself.__class__.__dict__ifnotname.startswith("_")]
- names+=[namefornameinself.__dict__ifnotname.startswith("_")]
- print(self.__doc__+"-"*60+"\n"+", ".join(names))
-
- help=property(_help)
-
- classDBmanagers(_EvContainer):
- """
- Links to instantiated Django database managers. These are used
- to perform more advanced custom database queries than the standard
- search functions allow.
-
- helpentries - HelpEntry.objects
- accounts - AccountDB.objects
- scripts - ScriptDB.objects
- msgs - Msg.objects
- channels - Channel.objects
- objects - ObjectDB.objects
- serverconfigs - ServerConfig.objects
- tags - Tags.objects
- attributes - Attributes.objects
-
- """
-
- from.help.modelsimportHelpEntry
- from.accounts.modelsimportAccountDB
- from.scripts.modelsimportScriptDB
- from.comms.modelsimportMsg,ChannelDB
- from.objects.modelsimportObjectDB
- from.server.modelsimportServerConfig
- from.typeclasses.attributesimportAttribute
- from.typeclasses.tagsimportTag
-
- # create container's properties
- helpentries=HelpEntry.objects
- accounts=AccountDB.objects
- scripts=ScriptDB.objects
- msgs=Msg.objects
- channels=ChannelDB.objects
- objects=ObjectDB.objects
- serverconfigs=ServerConfig.objects
- attributes=Attribute.objects
- tags=Tag.objects
- # remove these so they are not visible as properties
- delHelpEntry,AccountDB,ScriptDB,Msg,ChannelDB
- # del ExternalChannelConnection
- delObjectDB,ServerConfig,Tag,Attribute
-
- managers=DBmanagers()
- delDBmanagers
-
- classDefaultCmds(_EvContainer):
- """
- This container holds direct shortcuts to all default commands in Evennia.
-
- To access in code, do 'from evennia import default_cmds' then
- access the properties on the imported default_cmds object.
-
- """
-
- from.commands.default.cmdset_characterimportCharacterCmdSet
- from.commands.default.cmdset_accountimportAccountCmdSet
- from.commands.default.cmdset_unloggedinimportUnloggedinCmdSet
- from.commands.default.cmdset_sessionimportSessionCmdSet
- from.commands.default.muxcommandimportMuxCommand,MuxAccountCommand
-
- def__init__(self):
- "populate the object with commands"
-
- defadd_cmds(module):
- "helper method for populating this object with cmds"
- fromevennia.utilsimportutils
-
- cmdlist=utils.variable_from_module(module,module.__all__)
- self.__dict__.update(dict([(c.__name__,c)forcincmdlist]))
-
- from.commands.defaultimport(
- admin,
- batchprocess,
- building,
- comms,
- general,
- account,
- help,
- system,
- unloggedin,
- )
-
- add_cmds(admin)
- add_cmds(building)
- add_cmds(batchprocess)
- add_cmds(building)
- add_cmds(comms)
- add_cmds(general)
- add_cmds(account)
- add_cmds(help)
- add_cmds(system)
- add_cmds(unloggedin)
-
- default_cmds=DefaultCmds()
- delDefaultCmds
-
- classSystemCmds(_EvContainer):
- """
- Creating commands with keys set to these constants will make
- them system commands called as a replacement by the parser when
- special situations occur. If not defined, the hard-coded
- responses in the server are used.
-
- 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_LOGINSTART - this command will be called as the very
- first command when an account connects to
- the server.
-
- To access in code, do 'from evennia import syscmdkeys' then
- access the properties on the imported syscmdkeys object.
-
- """
-
- from.commandsimportcmdhandler
-
- CMD_NOINPUT=cmdhandler.CMD_NOINPUT
- CMD_NOMATCH=cmdhandler.CMD_NOMATCH
- CMD_MULTIMATCH=cmdhandler.CMD_MULTIMATCH
- CMD_LOGINSTART=cmdhandler.CMD_LOGINSTART
- delcmdhandler
-
- syscmdkeys=SystemCmds()
- delSystemCmds
- del_EvContainer
-
- # delayed starts - important so as to not back-access evennia before it has
- # finished initializing
- GLOBAL_SCRIPTS.start()
-
-
-
[docs]defset_trace(term_size=(140,80),debugger="auto"):
- """
- Helper function for running a debugger inside the Evennia event loop.
-
- Args:
- term_size (tuple, optional): Only used for Pudb and defines the size of the terminal
- (width, height) in number of characters.
- debugger (str, optional): One of 'auto', 'pdb' or 'pudb'. Pdb is the standard debugger. Pudb
- is an external package with a different, more 'graphical', ncurses-based UI. With
- 'auto', will use pudb if possible, otherwise fall back to pdb. Pudb is available through
- `pip install pudb`.
-
- Notes:
- To use:
-
- 1) add this to a line to act as a breakpoint for entering the debugger:
-
- from evennia import set_trace; set_trace()
-
- 2) restart evennia in interactive mode
-
- evennia istart
-
- 3) debugger will appear in the interactive terminal when breakpoint is reached. Exit
- with 'q', remove the break line and restart server when finished.
-
- """
- importsys
-
- dbg=None
-
- ifdebuggerin("auto","pudb"):
- try:
- frompudbimportdebugger
-
- dbg=debugger.Debugger(stdout=sys.__stdout__,term_size=term_size)
- exceptImportError:
- ifdebugger=="pudb":
- raise
- pass
-
- ifnotdbg:
- importpdb
-
- dbg=pdb.Pdb(stdout=sys.__stdout__)
-
- try:
- # Start debugger, forcing it up one stack frame (otherwise `set_trace`
- # will start debugger this point, not the actual code location)
- dbg.set_trace(sys._getframe().f_back)
- exceptException:
- # Stopped at breakpoint. Press 'n' to continue into the code.
- dbg.set_trace()
-"""
-Typeclass for Account objects.
-
-Note that this object is primarily intended to
-store OOC information, not game info! This
-object represents the actual user (not their
-character) and has NO actual presence in the
-game world (this is handled by the associated
-character object, so you should customize that
-instead for most things).
-
-"""
-importre
-importtime
-fromdjango.confimportsettings
-fromdjango.contrib.authimportauthenticate,password_validation
-fromdjango.core.exceptionsimportImproperlyConfigured,ValidationError
-fromdjango.utilsimporttimezone
-fromdjango.utils.module_loadingimportimport_string
-fromevennia.typeclasses.modelsimportTypeclassBase
-fromevennia.accounts.managerimportAccountManager
-fromevennia.accounts.modelsimportAccountDB
-fromevennia.objects.modelsimportObjectDB
-fromevennia.comms.modelsimportChannelDB
-fromevennia.server.modelsimportServerConfig
-fromevennia.server.throttleimportThrottle
-fromevennia.utilsimportclass_from_module,create,logger
-fromevennia.utils.utilsimportlazy_property,to_str,make_iter,is_iter,variable_from_module
-fromevennia.server.signalsimport(
- SIGNAL_ACCOUNT_POST_CREATE,
- SIGNAL_OBJECT_POST_PUPPET,
- SIGNAL_OBJECT_POST_UNPUPPET,
-)
-fromevennia.typeclasses.attributesimportNickHandler,ModelAttributeBackend
-fromevennia.scripts.scripthandlerimportScriptHandler
-fromevennia.commands.cmdsethandlerimportCmdSetHandler
-fromevennia.utils.optionhandlerimportOptionHandler
-
-fromdjango.utils.translationimportgettextas_
-fromrandomimportgetrandbits
-
-__all__=("DefaultAccount","DefaultGuest")
-
-_SESSIONS=None
-
-_AT_SEARCH_RESULT=variable_from_module(*settings.SEARCH_AT_RESULT.rsplit(".",1))
-_MULTISESSION_MODE=settings.MULTISESSION_MODE
-_MAX_NR_CHARACTERS=settings.MAX_NR_CHARACTERS
-_CMDSET_ACCOUNT=settings.CMDSET_ACCOUNT
-_MUDINFO_CHANNEL=None
-_CONNECT_CHANNEL=None
-_CMDHANDLER=None
-
-
-# Create throttles for too many account-creations and login attempts
-CREATION_THROTTLE=Throttle(
- name='creation',limit=settings.CREATION_THROTTLE_LIMIT,
- timeout=settings.CREATION_THROTTLE_TIMEOUT
-)
-LOGIN_THROTTLE=Throttle(
- name='login',limit=settings.LOGIN_THROTTLE_LIMIT,timeout=settings.LOGIN_THROTTLE_TIMEOUT
-)
-
-
-classAccountSessionHandler(object):
- """
- Manages the session(s) attached to an account.
-
- """
-
- def__init__(self,account):
- """
- Initializes the handler.
-
- Args:
- account (Account): The Account on which this handler is defined.
-
- """
- self.account=account
-
- defget(self,sessid=None):
- """
- Get the sessions linked to this object.
-
- Args:
- sessid (int, optional): Specify a given session by
- session id.
-
- Returns:
- sessions (list): A list of Session objects. If `sessid`
- is given, this is a list with one (or zero) elements.
-
- """
- global_SESSIONS
- ifnot_SESSIONS:
- fromevennia.server.sessionhandlerimportSESSIONSas_SESSIONS
- ifsessid:
- returnmake_iter(_SESSIONS.session_from_account(self.account,sessid))
- else:
- return_SESSIONS.sessions_from_account(self.account)
-
- defall(self):
- """
- Alias to get(), returning all sessions.
-
- Returns:
- sessions (list): All sessions.
-
- """
- returnself.get()
-
- defcount(self):
- """
- Get amount of sessions connected.
-
- Returns:
- sesslen (int): Number of sessions handled.
-
- """
- returnlen(self.get())
-
-
-
[docs]classDefaultAccount(AccountDB,metaclass=TypeclassBase):
- """
- This is the base Typeclass for all Accounts. Accounts represent
- the person playing the game and tracks account info, password
- etc. They are OOC entities without presence in-game. An Account
- can connect to a Character Object in order to "enter" the
- game.
-
- Account Typeclass API:
-
- * Available properties (only available on initiated typeclass objects)
-
- - key (string) - name of account
- - name (string)- wrapper for user.username
- - aliases (list of strings) - aliases to the object. Will be saved to
- database as AliasDB entries but returned as strings.
- - dbref (int, read-only) - unique #id-number. Also "id" can be used.
- - date_created (string) - time stamp of object creation
- - permissions (list of strings) - list of permission strings
- - user (User, read-only) - django User authorization object
- - obj (Object) - game object controlled by account. 'character' can also
- be used.
- - sessions (list of Sessions) - sessions connected to this account
- - is_superuser (bool, read-only) - if the connected user is a superuser
-
- * Handlers
-
- - locks - lock-handler: use locks.add() to add new lock strings
- - db - attribute-handler: store/retrieve database attributes on this
- self.db.myattr=val, val=self.db.myattr
- - ndb - non-persistent attribute handler: same as db but does not
- create a database entry when storing data
- - scripts - script-handler. Add new scripts to object with scripts.add()
- - cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object
- - nicks - nick-handler. New nicks with nicks.add().
-
- * Helper methods
-
- - msg(text=None, from_obj=None, session=None, options=None, **kwargs)
- - execute_cmd(raw_string)
- - search(ostring, global_search=False, attribute_name=None,
- use_nicks=False, location=None,
- ignore_errors=False, account=False)
- - is_typeclass(typeclass, exact=False)
- - swap_typeclass(new_typeclass, clean_attributes=False, no_default=True)
- - access(accessing_obj, access_type='read', default=False, no_superuser_bypass=False)
- - check_permstring(permstring)
-
- * Hook methods
-
- basetype_setup()
- at_account_creation()
-
- > note that the following hooks are also found on Objects and are
- usually handled on the character level:
-
- - at_init()
- - at_access()
- - at_cmdset_get(**kwargs)
- - at_first_login()
- - at_post_login(session=None)
- - at_disconnect()
- - at_message_receive()
- - at_message_send()
- - at_server_reload()
- - at_server_shutdown()
-
- """
-
- objects=AccountManager()
-
- # properties
-
-
- # Do not make this a lazy property; the web UI will not refresh it!
- @property
- defcharacters(self):
- # Get playable characters list
- objs=self.db._playable_charactersor[]
-
- # Rebuild the list if legacy code left null values after deletion
- try:
- ifNoneinobjs:
- objs=[xforxinself.db._playable_charactersifx]
- self.db._playable_characters=objs
- exceptExceptionase:
- logger.log_trace(e)
- logger.log_err(e)
-
- returnobjs
-
-
[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):
- """
- Access method for disconnecting a given session from the
- account (connection happens automatically in the
- sessionhandler)
-
- Args:
- session (Session): Session to disconnect.
- reason (str, optional): Eventual reason for the disconnect.
-
- """
- global_SESSIONS
- ifnot_SESSIONS:
- fromevennia.server.sessionhandlerimportSESSIONSas_SESSIONS
- _SESSIONS.disconnect(session,reason)
-
- # puppeting operations
-
-
[docs]defpuppet_object(self,session,obj):
- """
- Use the given session to control (puppet) the given object (usually
- a Character type).
-
- Args:
- session (Session): session to use for puppeting
- obj (Object): the object to start puppeting
-
- Raises:
- RuntimeError: If puppeting is not possible, the
- `exception.msg` will contain the reason.
-
-
- """
- # safety checks
- ifnotobj:
- raiseRuntimeError("Object not found")
- ifnotsession:
- raiseRuntimeError("Session not found")
- ifself.get_puppet(session)==obj:
- # already puppeting this object
- self.msg("You are already puppeting this object.")
- return
- ifnotobj.access(self,"puppet"):
- # no access
- self.msg("You don't have permission to puppet '{obj.key}'.")
- return
- ifobj.account:
- # object already puppeted
- ifobj.account==self:
- ifobj.sessions.count():
- # we may take over another of our sessions
- # output messages to the affected sessions
- if_MULTISESSION_MODEin(1,3):
- txt1=f"Sharing |c{obj.name}|n with another of your sessions."
- txt2=f"|c{obj.name}|n|G is now shared from another of your sessions.|n"
- self.msg(txt1,session=session)
- self.msg(txt2,session=obj.sessions.all())
- 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.unpuppet_object(obj.sessions.get())
- elifobj.account.is_connected:
- # controlled by another account
- self.msg(_("|c{key}|R is already puppeted by another Account.").format(key=obj.key))
- return
-
- # do the puppeting
- ifsession.puppet:
- # cleanly unpuppet eventual previous object puppeted by this session
- self.unpuppet_object(session)
- # if we get to this point the character is ready to puppet or it
- # was left with a lingering account/session reference from an unclean
- # server kill or similar
-
- obj.at_pre_puppet(self,session=session)
-
- # do the connection
- obj.sessions.add(session)
- obj.account=self
- session.puid=obj.id
- session.puppet=obj
-
- # re-cache locks to make sure superuser bypass is updated
- obj.locks.cache_lock_bypass(obj)
- # final hook
- obj.at_post_puppet()
- SIGNAL_OBJECT_POST_PUPPET.send(sender=obj,account=self,session=session)
-
-
[docs]defunpuppet_object(self,session):
- """
- Disengage control over an object.
-
- Args:
- session (Session or list): The session or a list of
- sessions to disengage from their puppets.
-
- Raises:
- RuntimeError With message about error.
-
- """
- forsessioninmake_iter(session):
- obj=session.puppet
- ifobj:
- # do the disconnect, but only if we are the last session to puppet
- obj.at_pre_unpuppet()
- obj.sessions.remove(session)
- ifnotobj.sessions.count():
- delobj.account
- obj.at_post_unpuppet(self,session=session)
- SIGNAL_OBJECT_POST_UNPUPPET.send(sender=obj,session=session,account=self)
- # Just to be sure we're always clear.
- session.puppet=None
- session.puid=None
-
-
[docs]defunpuppet_all(self):
- """
- Disconnect all puppets. This is called by server before a
- reset/shutdown.
- """
- self.unpuppet_object(self.sessions.all())
-
-
[docs]defget_puppet(self,session):
- """
- Get an object puppeted by this session through this account. This is
- the main method for retrieving the puppeted object from the
- account's end.
-
- Args:
- session (Session): Find puppeted object based on this session
-
- Returns:
- puppet (Object): The matching puppeted object, if any.
-
- """
- returnsession.puppetifsessionelseNone
-
-
[docs]defget_all_puppets(self):
- """
- Get all currently puppeted objects.
-
- Returns:
- puppets (list): All puppeted objects currently controlled
- by this Account.
-
- """
- returnlist(set(session.puppetforsessioninself.sessions.all()ifsession.puppet))
-
- def__get_single_puppet(self):
- """
- This is a legacy convenience link for use with `MULTISESSION_MODE`.
-
- Returns:
- puppets (Object or list): Users of `MULTISESSION_MODE` 0 or 1 will
- always get the first puppet back. Users of higher `MULTISESSION_MODE`s will
- get a list of all puppeted objects.
-
- """
- puppets=self.get_all_puppets()
- if_MULTISESSION_MODEin(0,1):
- returnpuppetsandpuppets[0]orNone
- returnpuppets
-
- character=property(__get_single_puppet)
- puppet=property(__get_single_puppet)
-
- # utility methods
-
[docs]@classmethod
- defis_banned(cls,**kwargs):
- """
- Checks if a given username or IP is banned.
-
- Keyword Args:
- ip (str, optional): IP address.
- username (str, optional): Username.
-
- Returns:
- is_banned (bool): Whether either is banned or not.
-
- """
-
- ip=kwargs.get("ip","").strip()
- username=kwargs.get("username","").lower().strip()
-
- # Check IP and/or name bans
- bans=ServerConfig.objects.conf("server_bans")
- ifbansand(
- any(tup[0]==usernamefortupinbansifusername)
- orany(tup[2].match(ip)fortupinbansifipandtup[2])
- ):
- returnTrue
-
- returnFalse
-
-
[docs]@classmethod
- defget_username_validators(
- cls,validator_config=getattr(settings,"AUTH_USERNAME_VALIDATORS",[])
- ):
- """
- Retrieves and instantiates validators for usernames.
-
- Args:
- validator_config (list): List of dicts comprising the battery of
- validators to apply to a username.
-
- Returns:
- validators (list): List of instantiated Validator objects.
- """
-
- objs=[]
- forvalidatorinvalidator_config:
- try:
- klass=import_string(validator["NAME"])
- exceptImportError:
- msg=(
- f"The module in NAME could not be imported: {validator['NAME']}. "
- "Check your AUTH_USERNAME_VALIDATORS setting."
- )
- raiseImproperlyConfigured(msg)
- objs.append(klass(**validator.get("OPTIONS",{})))
- returnobjs
-
-
[docs]@classmethod
- defauthenticate(cls,username,password,ip="",**kwargs):
- """
- Checks the given username/password against the database to see if the
- credentials are valid.
-
- Note that this simply checks credentials and returns a valid reference
- to the user-- it does not log them in!
-
- To finish the job:
- After calling this from a Command, associate the account with a Session:
- - session.sessionhandler.login(session, account)
-
- ...or after calling this from a View, associate it with an HttpRequest:
- - django.contrib.auth.login(account, request)
-
- Args:
- username (str): Username of account
- password (str): Password of account
- ip (str, optional): IP address of client
-
- Keyword Args:
- session (Session, optional): Session requesting authentication
-
- Returns:
- account (DefaultAccount, None): Account whose credentials were
- provided if not banned.
- errors (list): Error messages of any failures.
-
- """
- errors=[]
- ifip:
- ip=str(ip)
-
- # See if authentication is currently being throttled
- ifipandLOGIN_THROTTLE.check(ip):
- errors.append(_("Too many login failures; please try again in a few minutes."))
-
- # With throttle active, do not log continued hits-- it is a
- # waste of storage and can be abused to make your logs harder to
- # read and/or fill up your disk.
- returnNone,errors
-
- # Check IP and/or name bans
- banned=cls.is_banned(username=username,ip=ip)
- ifbanned:
- # this is a banned IP or name!
- errors.append(
- _(
- "|rYou have been banned and cannot continue from here."
- "\nIf you feel this ban is in error, please email an admin.|x"
- )
- )
- logger.log_sec(f"Authentication Denied (Banned): {username} (IP: {ip}).")
- LOGIN_THROTTLE.update(ip,"Too many sightings of banned artifact.")
- returnNone,errors
-
- # Authenticate and get Account object
- account=authenticate(username=username,password=password)
- ifnotaccount:
- # User-facing message
- errors.append(_("Username and/or password is incorrect."))
-
- # Log auth failures while throttle is inactive
- logger.log_sec(f"Authentication Failure: {username} (IP: {ip}).")
-
- # Update throttle
- ifip:
- LOGIN_THROTTLE.update(ip,_("Too many authentication failures."))
-
- # Try to call post-failure hook
- session=kwargs.get("session",None)
- ifsession:
- account=AccountDB.objects.get_account_from_name(username)
- ifaccount:
- account.at_failed_login(session)
-
- returnNone,errors
-
- # Account successfully authenticated
- logger.log_sec(f"Authentication Success: {account} (IP: {ip}).")
- returnaccount,errors
-
-
[docs]@classmethod
- defnormalize_username(cls,username):
- """
- Django: Applies NFKC Unicode normalization to usernames so that visually
- identical characters with different Unicode code points are considered
- identical.
-
- (This deals with the Turkish "i" problem and similar
- annoyances. Only relevant if you go out of your way to allow Unicode
- usernames though-- Evennia accepts ASCII by default.)
-
- In this case we're simply piggybacking on this feature to apply
- additional normalization per Evennia's standards.
- """
- username=super(DefaultAccount,cls).normalize_username(username)
-
- # strip excessive spaces in accountname
- username=re.sub(r"\s+"," ",username).strip()
-
- returnusername
-
-
[docs]@classmethod
- defvalidate_username(cls,username):
- """
- Checks the given username against the username validator associated with
- Account objects, and also checks the database to make sure it is unique.
-
- Args:
- username (str): Username to validate
-
- Returns:
- valid (bool): Whether or not the password passed validation
- errors (list): Error messages of any failures
-
- """
- valid=[]
- errors=[]
-
- # Make sure we're at least using the default validator
- validators=cls.get_username_validators()
- ifnotvalidators:
- validators=[cls.username_validator]
-
- # Try username against all enabled validators
- forvalidatorinvalidators:
- try:
- valid.append(notvalidator(username))
- exceptValidationErrorase:
- valid.append(False)
- errors.extend(e.messages)
-
- # Disqualify if any check failed
- ifFalseinvalid:
- valid=False
- else:
- valid=True
-
- returnvalid,errors
-
-
[docs]@classmethod
- defvalidate_password(cls,password,account=None):
- """
- Checks the given password against the list of Django validators enabled
- in the server.conf file.
-
- Args:
- password (str): Password to validate
-
- Keyword Args:
- account (DefaultAccount, optional): Account object to validate the
- password for. Optional, but Django includes some validators to
- do things like making sure users aren't setting passwords to the
- same value as their username. If left blank, these user-specific
- checks are skipped.
-
- Returns:
- valid (bool): Whether or not the password passed validation
- error (ValidationError, None): Any validation error(s) raised. Multiple
- errors can be nested within a single object.
-
- """
- valid=False
- error=None
-
- # Validation returns None on success; invert it and return a more sensible bool
- try:
- valid=notpassword_validation.validate_password(password,user=account)
- exceptValidationErrorase:
- error=e
-
- returnvalid,error
-
-
[docs]defset_password(self,password,**kwargs):
- """
- Applies the given password to the account. Logs and triggers the `at_password_change` hook.
-
- Args:
- 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.
-
- """
- super(DefaultAccount,self).set_password(password)
- logger.log_sec(f"Password successfully changed for {self}.")
- self.at_password_change()
-
-
[docs]defcreate_character(self,*args,**kwargs):
- """
- Create a character linked to this account.
-
- Args:
- key (str, optional): If not given, use the same name as the account.
- typeclass (str, optional): Typeclass to use for this character. If
- not given, use settings.BASE_CHARACTER_TYPECLASS.
- permissions (list, optional): If not given, use the account's permissions.
- ip (str, optiona): The client IP creating this character. Will fall back to the
- one stored for the account if not given.
- kwargs (any): Other kwargs will be used in the create_call.
- Returns:
- Object: A new character of the `character_typeclass` type. None on an error.
- list or None: A list of errors, or None.
-
- """
- # parse inputs
- character_key=kwargs.pop("key",self.key)
- character_ip=kwargs.pop("ip",self.db.creator_ip)
- character_permissions=kwargs.pop("permissions",self.permissions)
-
- # Load the appropriate Character class
- character_typeclass=kwargs.pop("typeclass",None)
- character_typeclass=(
- character_typeclassifcharacter_typeclasselsesettings.BASE_CHARACTER_TYPECLASS
- )
- Character=class_from_module(character_typeclass)
-
- if"location"notinkwargs:
- kwargs["location"]=ObjectDB.objects.get_id(settings.START_LOCATION)
-
- # Create the character
- character,errs=Character.create(
- character_key,
- self,
- ip=character_ip,
- typeclass=character_typeclass,
- permissions=character_permissions,
- **kwargs,
- )
- ifcharacter:
- # Update playable character list
- ifcharacternotinself.characters:
- self.db._playable_characters.append(character)
-
- # We need to set this to have @ic auto-connect to this character
- self.db._last_puppet=character
- returncharacter,errs
-
-
[docs]@classmethod
- defcreate(cls,*args,**kwargs):
- """
- Creates an Account (or Account/Character pair for MULTISESSION_MODE<2)
- with default (or overridden) permissions and having joined them to the
- appropriate default channels.
-
- Keyword Args:
- username (str): Username of Account owner
- password (str): Password of Account owner
- email (str, optional): Email address of Account owner
- ip (str, optional): IP address of requesting connection
- guest (bool, optional): Whether or not this is to be a Guest account
-
- permissions (str, optional): Default permissions for the Account
- typeclass (str, optional): Typeclass to use for new Account
- character_typeclass (str, optional): Typeclass to use for new char
- when applicable.
-
- Returns:
- account (Account): Account if successfully created; None if not
- errors (list): List of error messages in string form
-
- """
-
- account=None
- errors=[]
-
- username=kwargs.get("username")
- password=kwargs.get("password")
- email=kwargs.get("email","").strip()
- guest=kwargs.get("guest",False)
-
- permissions=kwargs.get("permissions",settings.PERMISSION_ACCOUNT_DEFAULT)
- typeclass=kwargs.get("typeclass",cls)
-
- ip=kwargs.get("ip","")
- ifipandCREATION_THROTTLE.check(ip):
- errors.append(
- _("You are creating too many accounts. Please log into an existing account.")
- )
- returnNone,errors
-
- # Normalize username
- username=cls.normalize_username(username)
-
- # Validate username
- ifnotguest:
- valid,errs=cls.validate_username(username)
- ifnotvalid:
- # this echoes the restrictions made by django's auth
- # module (except not allowing spaces, for convenience of
- # logging in).
- errors.extend(errs)
- returnNone,errors
-
- # Validate password
- # Have to create a dummy Account object to check username similarity
- valid,errs=cls.validate_password(password,account=cls(username=username))
- ifnotvalid:
- errors.extend(errs)
- returnNone,errors
-
- # Check IP and/or name bans
- banned=cls.is_banned(username=username,ip=ip)
- ifbanned:
- # this is a banned IP or name!
- string=_(
- "|rYou have been banned and cannot continue from here."
- "\nIf you feel this ban is in error, please email an admin.|x"
- )
- errors.append(string)
- returnNone,errors
-
- # everything's ok. Create the new account.
- try:
- try:
- account=create.create_account(
- username,email,password,permissions=permissions,typeclass=typeclass
- )
- logger.log_sec(f"Account Created: {account} (IP: {ip}).")
-
- exceptException:
- errors.append(
- _("There was an error creating the Account. "
- "If this problem persists, contact an admin."))
- logger.log_trace()
- returnNone,errors
-
- # This needs to be set so the engine knows this account is
- # logging in for the first time. (so it knows to call the right
- # hooks during login later)
- account.db.FIRST_LOGIN=True
-
- # Record IP address of creation, if available
- ifip:
- account.db.creator_ip=ip
-
- # join the new account to the public channel
- pchannel=ChannelDB.objects.get_channel(settings.DEFAULT_CHANNELS[0]["key"])
- ifnotpchannelornotpchannel.connect(account):
- string="New account '{account.key}' could not connect to public channel!"
- errors.append(string)
- logger.log_err(string)
-
- ifaccountandsettings.MULTISESSION_MODE<2:
- # Auto-create a character to go with this account
-
- character,errs=account.create_character(
- typeclass=kwargs.get("character_typeclass")
- )
- iferrs:
- errors.extend(errs)
-
- 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.
- errors.append(_("An error occurred. Please e-mail an admin if the problem persists."))
- logger.log_trace()
-
- # Update the throttle to indicate a new account was created from this IP
- ifipandnotguest:
- CREATION_THROTTLE.update(ip,"Too many accounts being created.")
- SIGNAL_ACCOUNT_POST_CREATE.send(sender=account,ip=ip)
- returnaccount,errors
-
-
[docs]defdelete(self,*args,**kwargs):
- """
- Deletes the account persistently.
-
- Notes:
- `*args` and `**kwargs` are passed on to the base delete
- mechanism (these are usually not used).
-
- Return:
- bool: If deletion was successful. Only time it fails would be
- if the Account was already deleted. Note that even on a failure,
- connected resources (nicks/aliases etc) will still have been
- deleted.
-
- """
- forsessioninself.sessions.all():
- # unpuppeting all objects and disconnecting the user, if any
- # sessions remain (should usually be handled from the
- # deleting command)
- try:
- self.unpuppet_object(session)
- exceptRuntimeError:
- # no puppet to disconnect from
- pass
- session.sessionhandler.disconnect(session,reason=_("Account being deleted."))
- self.scripts.stop()
- self.attributes.clear()
- self.nicks.clear()
- self.aliases.clear()
- ifnotself.pk:
- returnFalse
- super().delete(*args,**kwargs)
- returnTrue
-
-
- # methods inherited from database model
-
-
[docs]defmsg(self,text=None,from_obj=None,session=None,options=None,**kwargs):
- """
- Evennia -> User
- This is the main route for sending data back to the user from the
- server.
-
- Args:
- text (str or tuple, optional): The message to send. This
- is treated internally like any send-command, so its
- value can be a tuple if sending multiple arguments to
- the `text` oob command.
- from_obj (Object or Account or list, optional): Object sending. If given, its
- at_msg_send() hook will be called. If iterable, call on all entities.
- session (Session or list, optional): Session object or a list of
- Sessions to receive this send. If given, overrules the
- default send behavior for the current
- MULTISESSION_MODE.
- options (list): Protocol-specific options. Passed on to the protocol.
- Keyword Args:
- any (dict): All other keywords are passed on to the protocol.
-
- """
- iffrom_obj:
- # call hook
- forobjinmake_iter(from_obj):
- try:
- obj.at_msg_send(text=text,to_obj=self,**kwargs)
- exceptException:
- # this may not be assigned.
- logger.log_trace()
- try:
- ifnotself.at_msg_receive(text=text,**kwargs):
- # abort message to this account
- return
- exceptException:
- # this may not be assigned.
- pass
-
- kwargs["options"]=options
-
- iftextisnotNone:
- ifnot(isinstance(text,str)orisinstance(text,tuple)):
- # sanitize text before sending across the wire
- try:
- text=to_str(text)
- exceptException:
- text=repr(text)
- kwargs["text"]=text
-
- # session relay
- sessions=make_iter(session)ifsessionelseself.sessions.all()
- forsessioninsessions:
- session.data_out(**kwargs)
-
-
[docs]defexecute_cmd(self,raw_string,session=None,**kwargs):
- """
- Do something as this account. This method is never called normally,
- but only when the account object itself is supposed to execute the
- command. It takes account nicks into account, but not nicks of
- eventual puppets.
-
- Args:
- raw_string (str): Raw command input coming from the command line.
- session (Session, optional): The session to be responsible
- for the command-send
-
- Keyword Args:
- kwargs (any): Other keyword arguments will be added to the
- found command object instance as variables before it
- executes. This is unused by default Evennia but may be
- used to set flags and change operating paramaters for
- commands at run-time.
-
- """
- # break circular import issues
- global_CMDHANDLER
- ifnot_CMDHANDLER:
- fromevennia.commands.cmdhandlerimportcmdhandleras_CMDHANDLER
- raw_string=self.nicks.nickreplace(
- raw_string,categories=("inputline","channel"),include_account=False
- )
- ifnotsessionand_MULTISESSION_MODEin(0,1):
- # for these modes we use the first/only session
- sessions=self.sessions.get()
- session=sessions[0]ifsessionselseNone
-
- return_CMDHANDLER(
- 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,
- return_puppet=False,
- search_object=False,
- typeclass=None,
- nofound_string=None,
- multimatch_string=None,
- use_nicks=True,
- quiet=False,
- **kwargs,
- ):
- """
- This is similar to `DefaultObject.search` but defaults to searching
- for Accounts only.
-
- Args:
- searchdata (str or int): Search criterion, the Account's
- key or dbref to search for.
- return_puppet (bool, optional): Instructs the method to
- return matches as the object the Account controls rather
- than the Account itself (or None) if nothing is puppeted).
- search_object (bool, optional): Search for Objects instead of
- Accounts. This is used by e.g. the @examine command when
- wanting to examine Objects while OOC.
- typeclass (Account typeclass, optional): Limit the search
- only to this particular typeclass. This can be used to
- limit to specific account typeclasses or to limit the search
- to a particular Object typeclass if `search_object` is True.
- nofound_string (str, optional): A one-time error message
- to echo if `searchdata` leads to no matches. If not given,
- will fall back to the default handler.
- multimatch_string (str, optional): A one-time error
- message to echo if `searchdata` leads to multiple matches.
- If not given, will fall back to the default handler.
- use_nicks (bool, optional): Use account-level nick replacement.
- quiet (bool, optional): If set, will not show any error to the user,
- and will also lead to returning a list of matches.
-
- Return:
- match (Account, Object or None): A single Account or Object match.
- list: If `quiet=True` this is a list of 0, 1 or more Account or Object matches.
-
- Notes:
- Extra keywords are ignored, but are allowed in call in
- order to make API more consistent with
- objects.objects.DefaultObject.search.
-
- """
- # handle me, self and *me, *self
- ifisinstance(searchdata,str):
- # handle wrapping of common terms
- ifsearchdata.lower()in("me","*me","self","*self"):
- returnself
- searchdata=self.nicks.nickreplace(
- searchdata,categories=("account",),include_account=False
- )
- ifsearch_object:
- matches=ObjectDB.objects.object_search(searchdata,typeclass=typeclass)
- else:
- matches=AccountDB.objects.account_search(searchdata,typeclass=typeclass)
-
- ifquiet:
- matches=list(matches)
- ifreturn_puppet:
- matches=[match.puppetformatchinmatches]
- else:
- matches=_AT_SEARCH_RESULT(
- matches,
- self,
- query=searchdata,
- nofound_string=nofound_string,
- multimatch_string=multimatch_string,
- )
- ifmatchesandreturn_puppet:
- try:
- matches=matches.puppet
- exceptAttributeError:
- returnNone
- returnmatches
-
-
[docs]defaccess(
- self,accessing_obj,access_type="read",default=False,no_superuser_bypass=False,**kwargs
- ):
- """
- Determines if another object has permission to access this
- object in whatever way.
-
- Args:
- accessing_obj (Object): Object trying to access this one.
- access_type (str, optional): Type of access sought.
- default (bool, optional): What to return if no lock of
- access_type was found
- no_superuser_bypass (bool, optional): Turn off superuser
- lock bypassing. Be careful with this one.
-
- Keyword Args:
- kwargs (any): Passed to the at_access hook along with the result.
-
- Returns:
- result (bool): Result of access check.
-
- """
- result=super().access(
- accessing_obj,
- access_type=access_type,
- default=default,
- no_superuser_bypass=no_superuser_bypass,
- )
- self.at_access(result,accessing_obj,access_type,**kwargs)
- returnresult
-
- @property
- defidle_time(self):
- """
- 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:
- returntime.time()-float(max(idle))
- returnNone
-
- @property
- defconnection_time(self):
- """
- 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:
- returntime.time()-float(min(conn))
- returnNone
-
- # account hooks
-
-
[docs]defbasetype_setup(self):
- """
- This sets up the basic properties for an account. Overload this
- with at_account_creation rather than changing this method.
-
- """
- # A basic security setup
- lockstring=(
- "examine:perm(Admin);edit:perm(Admin);"
- "delete:perm(Admin);boot:perm(Admin);msg:all();"
- "noidletimeout:perm(Builder) or perm(noidletimeout)"
- )
- self.locks.add(lockstring)
-
- # The ooc account cmdset
- self.cmdset.add_default(_CMDSET_ACCOUNT,persistent=True)
-
-
[docs]defat_account_creation(self):
- """
- This is called once, the very first time the account is created
- (i.e. first time they register with the game). It's a good
- place to store attributes all accounts should have, like
- configuration values etc.
-
- """
- # set an (empty) attribute holding the characters this account has
- lockstring="attrread:perm(Admins);attredit:perm(Admins);""attrcreate:perm(Admins);"
- self.attributes.add("_playable_characters",[],lockstring=lockstring)
- self.attributes.add("_saved_protocol_flags",{},lockstring=lockstring)
-
-
[docs]defat_init(self):
- """
- This is always called whenever this object is initiated --
- that is, whenever it its typeclass is cached from memory. This
- happens on-demand first time the object is used or activated
- in some way after being created but also after each server
- restart or reload. In the case of account objects, this usually
- happens the moment the account logs in or reconnects after a
- reload.
-
- """
- pass
-
- # Note that the hooks below also exist in the character object's
- # typeclass. You can often ignore these and rely on the character
- # ones instead, unless you are implementing a multi-character game
- # and have some things that should be done regardless of which
- # character is currently connected to this account.
-
-
[docs]defat_first_save(self):
- """
- This is a generic hook called by Evennia when this object is
- saved to the database the very first time. You generally
- don't override this method but the hooks called by it.
-
- """
- self.basetype_setup()
- self.at_account_creation()
-
- permissions=[settings.PERMISSION_ACCOUNT_DEFAULT]
- ifhasattr(self,"_createdict"):
- # this will only be set if the utils.create_account
- # function was used to create the object.
- cdict=self._createdict
- updates=[]
- ifnotcdict.get("key"):
- ifnotself.db_key:
- self.db_key=f"#{self.dbid}"
- updates.append("db_key")
- elifself.key!=cdict.get("key"):
- updates.append("db_key")
- self.db_key=cdict["key"]
- ifupdates:
- self.save(update_fields=updates)
-
- ifcdict.get("locks"):
- self.locks.add(cdict["locks"])
- ifcdict.get("permissions"):
- permissions=cdict["permissions"]
- ifcdict.get("tags"):
- # this should be a list of tags, tuples (key, category) or (key, category, data)
- self.tags.batch_add(*cdict["tags"])
- ifcdict.get("attributes"):
- # this should be tuples (key, val, ...)
- self.attributes.batch_add(*cdict["attributes"])
- ifcdict.get("nattributes"):
- # this should be a dict of nattrname:value
- forkey,valueincdict["nattributes"]:
- self.nattributes.add(key,value)
- delself._createdict
-
- self.permissions.batch_add(*permissions)
-
-
[docs]defat_access(self,result,accessing_obj,access_type,**kwargs):
- """
- This is triggered after an access-call on this Account has
- completed.
-
- Args:
- result (bool): The result of the access check.
- accessing_obj (any): The object requesting the access
- check.
- access_type (str): The type of access checked.
-
- Keyword Args:
- kwargs (any): These are passed on from the access check
- and can be used to relay custom instructions from the
- check mechanism.
-
- Notes:
- This method cannot affect the result of the lock check and
- its return value is not used in any way. It can be used
- e.g. to customize error messages in a central location or
- create other effects based on the access result.
-
- """
- pass
-
-
[docs]defat_cmdset_get(self,**kwargs):
- """
- Called just *before* cmdsets on this account are requested by
- the command handler. The cmdsets are available as
- `self.cmdset`. If changes need to be done on the fly to the
- cmdset before passing them on to the cmdhandler, this is the
- place to do it. This is called also if the account currently
- have no cmdsets. kwargs are usually not used unless the
- cmdset is generated dynamically.
-
- """
- pass
-
-
[docs]defat_first_login(self,**kwargs):
- """
- Called the very first time this account logs into the game.
- Note that this is called *before* at_pre_login, so no session
- is established and usually no character is yet assigned at
- this point. This hook is intended for account-specific setup
- like configurations.
-
- Args:
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- """
- pass
-
-
[docs]defat_password_change(self,**kwargs):
- """
- Called after a successful password set/modify.
-
- Args:
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- """
- pass
-
-
[docs]defat_pre_login(self,**kwargs):
- """
- Called every time the user logs in, just before the actual
- login-state is set.
-
- Args:
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- """
- pass
-
- def_send_to_connect_channel(self,message):
- """
- 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,_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.msg(f"[{now}]: {message}")
- if_CONNECT_CHANNEL:
- _CONNECT_CHANNEL.msg(f"[{now}]: {message}")
-
-
[docs]defat_post_login(self,session=None,**kwargs):
- """
- Called at the end of the login process, just before letting
- the account loose.
-
- Args:
- session (Session, optional): Session logging in, if any.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- Notes:
- This is called *before* an eventual Character's
- `at_post_login` hook. By default it is used to set up
- auto-puppeting based on `MULTISESSION_MODE`.
-
- """
- # if we have saved protocol flags on ourselves, load them here.
- protocol_flags=self.attributes.get("_saved_protocol_flags",{})
- ifsessionandprotocol_flags:
- session.update_flags(**protocol_flags)
-
- # inform the client that we logged in through an OOB message
- ifsession:
- session.msg(logged_in={})
-
- self._send_to_connect_channel(_("|G{key} connected|n").format(key=self.key))
- if_MULTISESSION_MODE==0:
- # in this mode we should have only one character available. We
- # try to auto-connect to our last conneted object, if any
- try:
- self.puppet_object(session,self.db._last_puppet)
- exceptRuntimeError:
- self.msg(_("The Character does not exist."))
- return
- elif_MULTISESSION_MODE==1:
- # in this mode all sessions connect to the same puppet.
- try:
- self.puppet_object(session,self.db._last_puppet)
- exceptRuntimeError:
- self.msg(_("The Character does not exist."))
- return
- elif_MULTISESSION_MODEin(2,3):
- # In this mode we by default end up at a character selection
- # screen. We execute look on the account.
- # we make sure to clean up the _playable_characters list in case
- # any was deleted in the interim.
- self.db._playable_characters=[charforcharinself.db._playable_charactersifchar]
- self.msg(
- self.at_look(target=self.db._playable_characters,session=session),session=session
- )
-
-
[docs]defat_failed_login(self,session,**kwargs):
- """
- Called by the login process if a user account is targeted correctly
- but provided with an invalid password. By default it does nothing,
- but exists to be overriden.
-
- Args:
- session (session): Session logging in.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
- """
- pass
-
-
[docs]defat_disconnect(self,reason=None,**kwargs):
- """
- Called just before user is disconnected.
-
- Args:
- reason (str, optional): The reason given for the disconnect,
- (echoed to the connection channel by default).
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
-
- """
- reason=f" ({reasonifreasonelse''})"
- self._send_to_connect_channel(
- _("|R{key} disconnected{reason}|n").format(key=self.key,reason=reason)
- )
-
-
[docs]defat_post_disconnect(self,**kwargs):
- """
- This is called *after* disconnection is complete. No messages
- can be relayed to the account from here. After this call, the
- account should not be accessed any more, making this a good
- spot for deleting it (in the case of a guest account account,
- for example).
-
- Args:
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- """
- pass
-
-
[docs]defat_msg_receive(self,text=None,from_obj=None,**kwargs):
- """
- This hook is called whenever someone sends a message to this
- object using the `msg` method.
-
- Note that from_obj may be None if the sender did not include
- itself as an argument to the obj.msg() call - so you have to
- check for this. .
-
- Consider this a pre-processing method before msg is passed on
- to the user session. If this method returns False, the msg
- will not be passed on.
-
- Args:
- text (str, optional): The message received.
- from_obj (any, optional): The object sending the message.
-
- Keyword Args:
- This includes any keywords sent to the `msg` method.
-
- Returns:
- receive (bool): If this message should be received.
-
- Notes:
- If this method returns False, the `msg` operation
- will abort without sending the message.
-
- """
- returnTrue
-
-
[docs]defat_msg_send(self,text=None,to_obj=None,**kwargs):
- """
- This is a hook that is called when *this* object sends a
- message to another object with `obj.msg(text, to_obj=obj)`.
-
- Args:
- text (str, optional): Text to send.
- to_obj (any, optional): The object to send to.
-
- Keyword Args:
- Keywords passed from msg()
-
- Notes:
- Since this method is executed by `from_obj`, if no `from_obj`
- was passed to `DefaultCharacter.msg` this hook will never
- get called.
-
- """
- pass
-
-
[docs]defat_server_reload(self):
- """
- This hook is called whenever the server is shutting down for
- restart/reboot. If you want to, for example, save
- non-persistent properties across a restart, this is the place
- to do it.
- """
- pass
-
-
[docs]defat_server_shutdown(self):
- """
- This hook is called whenever the server is shutting down fully
- (i.e. not for a restart).
- """
- pass
-
-
[docs]defat_look(self,target=None,session=None,**kwargs):
- """
- Called when this object executes a look. It allows to customize
- just what this means.
-
- Args:
- target (Object or list, optional): An object or a list
- objects to inspect.
- session (Session, optional): The session doing this look.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- Returns:
- look_string (str): A prepared look string, ready to send
- off to any recipient (usually to ourselves)
-
- """
-
- iftargetandnotis_iter(target):
- # single target - just show it
- ifhasattr(target,"return_appearance"):
- returntarget.return_appearance(self)
- else:
- returnf"{target} has no in-game appearance."
- else:
- # list of targets - make list to disconnect from db
- characters=list(tarfortarintargetiftar)iftargetelse[]
- sessions=self.sessions.all()
- ifnotsessions:
- # no sessions, nothing to report
- return""
- is_su=self.is_superuser
-
- # text shown when looking in the ooc area
- result=[f"Account |g{self.key}|n (you are Out-of-Character)"]
-
- nsess=len(sessions)
- result.append(
- nsess==1
- and"\n\n|wConnected session:|n"
- orf"\n\n|wConnected sessions ({nsess}):|n"
- )
- forisess,sessinenumerate(sessions):
- csessid=sess.sessid
- addr="%s (%s)"%(
- sess.protocol_key,
- isinstance(sess.address,tuple)andstr(sess.address[0])orstr(sess.address),
- )
- result.append(
- "\n%s%s"
- %(
- session
- andsession.sessid==csessid
- and"|w* %s|n"%(isess+1)
- or" %s"%(isess+1),
- addr,
- )
- )
- result.append("\n\n |whelp|n - more commands")
- result.append("\n |wpublic <Text>|n - talk on public channel")
-
- charmax=_MAX_NR_CHARACTERS
-
- ifis_suorlen(characters)<charmax:
- ifnotcharacters:
- result.append(
- "\n\n You don't have any characters yet. See |whelp charcreate|n for "
- "creating one."
- )
- else:
- result.append("\n |wcharcreate <name> [=description]|n - create new character")
- result.append(
- "\n |wchardelete <name>|n - delete a character (cannot be undone!)"
- )
-
- ifcharacters:
- string_s_ending=len(characters)>1and"s"or""
- 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):"
- )
- else:
- result.append(
- "\n\nAvailable character%s%s:"
- %(
- string_s_ending,
- charmax>1and" (%i/%i)"%(len(characters),charmax)or"",
- )
- )
-
- forcharincharacters:
- csessions=char.sessions.all()
- ifcsessions:
- forsessincsessions:
- # character is already puppeted
- sid=sessinsessionsandsessions.index(sess)+1
- ifsessandsid:
- result.append(
- 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)"
- )
- else:
- # character is "free to puppet"
- result.append(f"\n - {char.key} [{', '.join(char.permissions.all())}]")
- look_string=("-"*68)+"\n"+"".join(result)+"\n"+("-"*68)
- returnlook_string
-
-
-
[docs]classDefaultGuest(DefaultAccount):
- """
- This class is used for guest logins. Unlike Accounts, Guests and
- their characters are deleted after disconnection.
-
- """
-
-
[docs]@classmethod
- defcreate(cls,**kwargs):
- """
- Forwards request to cls.authenticate(); returns a DefaultGuest object
- if one is available for use.
-
- """
- returncls.authenticate(**kwargs)
-
-
[docs]@classmethod
- defauthenticate(cls,**kwargs):
- """
- Gets or creates a Guest account object.
-
- Keyword Args:
- ip (str, optional): IP address of requestor; used for ban checking,
- throttling and logging
-
- Returns:
- account (Object): Guest account object, if available
- errors (list): List of error messages accrued during this request.
-
- """
- errors=[]
- account=None
- username=None
- ip=kwargs.get("ip","").strip()
-
- # check if guests are enabled.
- ifnotsettings.GUEST_ENABLED:
- errors.append(_("Guest accounts are not enabled on this server."))
- returnNone,errors
-
- try:
- # Find an available guest name.
- fornameinsettings.GUEST_LIST:
- ifnotAccountDB.objects.filter(username__iexact=name).exists():
- username=name
- break
- ifnotusername:
- errors.append(_("All guest accounts are in use. Please try again later."))
- ifip:
- LOGIN_THROTTLE.update(ip,"Too many requests for Guest access.")
- returnNone,errors
- else:
- # build a new account with the found guest username
- password="%016x"%getrandbits(64)
- home=settings.GUEST_HOME
- permissions=settings.PERMISSION_GUEST_DEFAULT
- typeclass=settings.BASE_GUEST_TYPECLASS
-
- # Call parent class creator
- account,errs=super(DefaultGuest,cls).create(
- guest=True,
- username=username,
- password=password,
- permissions=permissions,
- typeclass=typeclass,
- home=home,
- ip=ip,
- )
- errors.extend(errs)
-
- ifnotaccount.characters:
- # this can happen for multisession_mode > 1. For guests we
- # always auto-create a character, regardless of multi-session-mode.
- character,errs=account.create_character()
-
- iferrs:
- errors.extend(errs)
-
- returnaccount,errors
-
- 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.
- errors.append(_("An error occurred. Please e-mail an admin if the problem persists."))
- logger.log_trace()
- returnNone,errors
-
- returnaccount,errors
-
-
[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.
-
- Args:
- session (Session, optional): Session connecting.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- """
- self._send_to_connect_channel(_("|G{key} connected|n").format(key=self.key))
- self.puppet_object(session,self.db._last_puppet)
-
-
[docs]defat_server_shutdown(self):
- """
- We repeat the functionality of `at_disconnect()` here just to
- be on the safe side.
- """
- super().at_server_shutdown()
- characters=self.db._playable_characters
- ifcharacters:
- forcharacterincharacters:
- ifcharacter:
- character.delete()
-
-
[docs]defat_post_disconnect(self,**kwargs):
- """
- Once having disconnected, destroy the guest's characters and
-
- Args:
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- """
- super().at_post_disconnect()
- characters=self.db._playable_characters
- forcharacterincharacters:
- ifcharacter:
- character.delete()
- self.delete()
-"""
-Bots are a special child typeclasses of
-Account that are controlled by the server.
-
-"""
-
-importtime
-fromdjango.confimportsettings
-fromevennia.accounts.accountsimportDefaultAccount
-fromevennia.scripts.scriptsimportDefaultScript
-fromevennia.utilsimportsearch
-fromevennia.utilsimportutils
-fromdjango.utils.translationimportgettextas_
-
-_IDLE_TIMEOUT=settings.IDLE_TIMEOUT
-
-_IRC_ENABLED=settings.IRC_ENABLED
-_RSS_ENABLED=settings.RSS_ENABLED
-_GRAPEVINE_ENABLED=settings.GRAPEVINE_ENABLED
-
-
-_SESSIONS=None
-
-
-# Bot helper utilities
-
-
-
[docs]classBotStarter(DefaultScript):
- """
- This non-repeating script has the
- sole purpose of kicking its bot
- into gear when it is initialized.
-
- """
-
-
[docs]defat_script_creation(self):
- """
- Called once, when script is created.
-
- """
- self.key="botstarter"
- self.desc="bot start/keepalive"
- self.persistent=True
- self.db.started=False
[docs]defat_repeat(self):
- """
- Called self.interval seconds to keep connection. We cannot use
- the IDLE command from inside the game since the system will
- not catch it (commands executed from the server side usually
- has no sessions). So we update the idle counter manually here
- instead. This keeps the bot getting hit by IDLE_TIMEOUT.
-
- """
- global_SESSIONS
- ifnot_SESSIONS:
- fromevennia.server.sessionhandlerimportSESSIONSas_SESSIONS
- forsessionin_SESSIONS.sessions_from_account(self.account):
- session.update_session_counters(idle=True)
-
-
[docs]defat_server_reload(self):
- """
- If server reloads we don't need to reconnect the protocol
- again, this is handled by the portal reconnect mechanism.
-
- """
- self.db.started=True
-
-
[docs]defat_server_shutdown(self):
- """
- Make sure we are shutdown.
-
- """
- self.db.started=False
-
-
-#
-# Bot base class
-
-
-
[docs]classBot(DefaultAccount):
- """
- A Bot will start itself when the server starts (it will generally
- not do so on a reload - that will be handled by the normal Portal
- session resync)
-
- """
-
-
[docs]defbasetype_setup(self):
- """
- This sets up the basic properties for the bot.
-
- """
- # the text encoding to use.
- self.db.encoding="utf-8"
- # A basic security setup (also avoid idle disconnects)
- lockstring=(
- "examine:perm(Admin);edit:perm(Admin);delete:perm(Admin);"
- "boot:perm(Admin);msg:false();noidletimeout:true()"
- )
- self.locks.add(lockstring)
- # set the basics of being a bot
- script_key=str(self.key)
- self.scripts.add(BotStarter,key=script_key)
- self.is_bot=True
-
-
[docs]defstart(self,**kwargs):
- """
- This starts the bot, whatever that may mean.
-
- """
- pass
[docs]defat_server_shutdown(self):
- """
- We need to handle this case manually since the shutdown may be
- a reset.
-
- """
- forsessioninself.sessions.all():
- session.sessionhandler.disconnect(session)
-
-
-# Bot implementations
-
-# IRC
-
-
-
[docs]classIRCBot(Bot):
- """
- Bot for handling IRC connections.
-
- """
-
- # override this on a child class to use custom factory
- factory_path="evennia.server.portal.irc.IRCBotFactory"
-
-
[docs]defstart(
- self,
- ev_channel=None,
- irc_botname=None,
- irc_channel=None,
- irc_network=None,
- irc_port=None,
- irc_ssl=None,
- ):
- """
- Start by telling the portal to start a new session.
-
- Args:
- ev_channel (str): Key of the Evennia channel to connect to.
- irc_botname (str): Name of bot to connect to irc channel. If
- not set, use `self.key`.
- irc_channel (str): Name of channel on the form `#channelname`.
- irc_network (str): URL of the IRC network, like `irc.freenode.net`.
- irc_port (str): Port number of the irc network, like `6667`.
- irc_ssl (bool): Indicates whether to use SSL connection.
-
- """
- ifnot_IRC_ENABLED:
- # the bot was created, then IRC was turned off. We delete
- # ourselves (this will also kill the start script)
- self.delete()
- return
-
- global_SESSIONS
- ifnot_SESSIONS:
- fromevennia.server.sessionhandlerimportSESSIONSas_SESSIONS
-
- # if keywords are given, store (the BotStarter script
- # will not give any keywords, so this should normally only
- # happen at initialization)
- ifirc_botname:
- self.db.irc_botname=irc_botname
- elifnotself.db.irc_botname:
- self.db.irc_botname=self.key
- ifev_channel:
- # connect to Evennia channel
- channel=search.channel_search(ev_channel)
- ifnotchannel:
- raiseRuntimeError(f"Evennia Channel '{ev_channel}' not found.")
- channel=channel[0]
- channel.connect(self)
- self.db.ev_channel=channel
- ifirc_channel:
- self.db.irc_channel=irc_channel
- ifirc_network:
- self.db.irc_network=irc_network
- ifirc_port:
- self.db.irc_port=irc_port
- ifirc_ssl:
- self.db.irc_ssl=irc_ssl
-
- # instruct the server and portal to create a new session with
- # the stored configuration
- configdict={
- "uid":self.dbid,
- "botname":self.db.irc_botname,
- "channel":self.db.irc_channel,
- "network":self.db.irc_network,
- "port":self.db.irc_port,
- "ssl":self.db.irc_ssl,
- }
- _SESSIONS.start_bot_session(self.factory_path,configdict)
-
-
[docs]defat_msg_send(self,**kwargs):
- "Shortcut here or we can end up in infinite loop"
- pass
-
-
[docs]defget_nicklist(self,caller):
- """
- Retrive the nick list from the connected channel.
-
- Args:
- caller (Object or Account): The requester of the list. This will
- be stored and echoed to when the irc network replies with the
- requested info.
-
- Notes: Since the return is asynchronous, the caller is stored internally
- in a list; all callers in this list will get the nick info once it
- returns (it is a custom OOB inputfunc option). The callback will not
- survive a reload (which should be fine, it's very quick).
- """
- ifnothasattr(self,"_nicklist_callers"):
- self._nicklist_callers=[]
- self._nicklist_callers.append(caller)
- super().msg(request_nicklist="")
- return
-
-
[docs]defping(self,caller):
- """
- Fire a ping to the IRC server.
-
- Args:
- caller (Object or Account): The requester of the ping.
-
- """
- ifnothasattr(self,"_ping_callers"):
- self._ping_callers=[]
- self._ping_callers.append(caller)
- super().msg(ping="")
-
-
[docs]defreconnect(self):
- """
- Force a protocol-side reconnect of the client without
- having to destroy/recreate the bot "account".
-
- """
- super().msg(reconnect="")
-
-
[docs]defmsg(self,text=None,**kwargs):
- """
- Takes text from connected channel (only).
-
- Args:
- text (str, optional): Incoming text from channel.
-
- Keyword Args:
- options (dict): Options dict with the following allowed keys:
- - from_channel (str): dbid of a channel this text originated from.
- - from_obj (list): list of objects sending this text.
-
- """
- from_obj=kwargs.get("from_obj",None)
- options=kwargs.get("options",None)or{}
-
- ifnotself.ndb.ev_channelandself.db.ev_channel:
- # cache channel lookup
- self.ndb.ev_channel=self.db.ev_channel
-
- if(
- "from_channel"inoptions
- andtext
- andself.ndb.ev_channel.dbid==options["from_channel"]
- ):
- ifnotfrom_objorfrom_obj!=[self]:
- super().msg(channel=text)
-
-
[docs]defexecute_cmd(self,session=None,txt=None,**kwargs):
- """
- Take incoming data and send it to connected channel. This is
- triggered by the bot_data_in Inputfunc.
-
- Args:
- session (Session, optional): Session responsible for this
- command. Note that this is the bot.
- txt (str, optional): Command string.
- Keyword Args:
- user (str): The name of the user who sent the message.
- channel (str): The name of channel the message was sent to.
- type (str): Nature of message. Either 'msg', 'action', 'nicklist'
- or 'ping'.
- nicklist (list, optional): Set if `type='nicklist'`. This is a list
- of nicks returned by calling the `self.get_nicklist`. It must look
- for a list `self._nicklist_callers` which will contain all callers
- waiting for the nicklist.
- timings (float, optional): Set if `type='ping'`. This is the return
- (in seconds) of a ping request triggered with `self.ping`. The
- return must look for a list `self._ping_callers` which will contain
- all callers waiting for the ping return.
-
- """
- ifkwargs["type"]=="nicklist":
- # the return of a nicklist request
- ifhasattr(self,"_nicklist_callers")andself._nicklist_callers:
- chstr=f"{self.db.irc_channel} ({self.db.irc_network}:{self.db.irc_port})"
- nicklist=", ".join(sorted(kwargs["nicklist"],key=lambdan:n.lower()))
- forobjinself._nicklist_callers:
- obj.msg(
- "Nicks at {chstr}:\n{nicklist}".format(chstr=chstr,nicklist=nicklist)
- )
- self._nicklist_callers=[]
- return
-
- elifkwargs["type"]=="ping":
- # the return of a ping
- ifhasattr(self,"_ping_callers")andself._ping_callers:
- chstr=f"{self.db.irc_channel} ({self.db.irc_network}:{self.db.irc_port})"
- forobjinself._ping_callers:
- obj.msg(
- "IRC ping return from {chstr} took {time}s.".format(
- chstr=chstr,time=kwargs["timing"]
- )
- )
- self._ping_callers=[]
- return
-
- elifkwargs["type"]=="privmsg":
- # A private message to the bot - a command.
- user=kwargs["user"]
-
- iftxt.lower().startswith("who"):
- # return server WHO list (abbreviated for IRC)
- global_SESSIONS
- ifnot_SESSIONS:
- fromevennia.server.sessionhandlerimportSESSIONSas_SESSIONS
- whos=[]
- t0=time.time()
- forsessin_SESSIONS.get_sessions():
- delta_cmd=t0-sess.cmd_last_visible
- delta_conn=t0-session.conn_time
- account=sess.get_account()
- whos.append(
- "%s (%s/%s)"
- %(
- utils.crop("|w%s|n"%account.name,width=25),
- utils.time_format(delta_conn,0),
- utils.time_format(delta_cmd,1),
- )
- )
- text=f"Who list (online/idle): {', '.join(sorted(whos,key=lambdaw:w.lower()))}"
- eliftxt.lower().startswith("about"):
- # some bot info
- text=f"This is an Evennia IRC bot connecting from '{settings.SERVERNAME}'."
- else:
- text="I understand 'who' and 'about'."
- super().msg(privmsg=((text,),{"user":user}))
- else:
- # something to send to the main channel
- ifkwargs["type"]=="action":
- # An action (irc pose)
- text=f"{kwargs['user']}@{kwargs['channel']}{txt}"
- else:
- # msg - A normal channel message
- text=f"{kwargs['user']}@{kwargs['channel']}: {txt}"
-
- ifnotself.ndb.ev_channelandself.db.ev_channel:
- # cache channel lookup
- self.ndb.ev_channel=self.db.ev_channel
-
- ifself.ndb.ev_channel:
- self.ndb.ev_channel.msg(text,senders=self)
-
-
-#
-# RSS
-#
-
-
-
[docs]classRSSBot(Bot):
- """
- An RSS relayer. The RSS protocol itself runs a ticker to update
- its feed at regular intervals.
-
- """
-
-
[docs]defstart(self,ev_channel=None,rss_url=None,rss_rate=None):
- """
- Start by telling the portal to start a new RSS session
-
- Args:
- ev_channel (str): Key of the Evennia channel to connect to.
- rss_url (str): Full URL to the RSS feed to subscribe to.
- rss_rate (int): How often for the feedreader to update.
-
- Raises:
- RuntimeError: If `ev_channel` does not exist.
-
- """
- ifnot_RSS_ENABLED:
- # The bot was created, then RSS was turned off. Delete ourselves.
- self.delete()
- return
-
- global_SESSIONS
- ifnot_SESSIONS:
- fromevennia.server.sessionhandlerimportSESSIONSas_SESSIONS
-
- ifev_channel:
- # connect to Evennia channel
- channel=search.channel_search(ev_channel)
- ifnotchannel:
- raiseRuntimeError(f"Evennia Channel '{ev_channel}' not found.")
- channel=channel[0]
- self.db.ev_channel=channel
- ifrss_url:
- self.db.rss_url=rss_url
- ifrss_rate:
- self.db.rss_rate=rss_rate
- # instruct the server and portal to create a new session with
- # the stored configuration
- configdict={"uid":self.dbid,"url":self.db.rss_url,"rate":self.db.rss_rate}
- _SESSIONS.start_bot_session("evennia.server.portal.rss.RSSBotFactory",configdict)
-
-
[docs]defexecute_cmd(self,txt=None,session=None,**kwargs):
- """
- Take incoming data and send it to connected channel. This is
- triggered by the bot_data_in Inputfunc.
-
- Args:
- session (Session, optional): Session responsible for this
- command.
- txt (str, optional): Command string.
- kwargs (dict, optional): Additional Information passed from bot.
- Not used by the RSSbot by default.
-
- """
- ifnotself.ndb.ev_channelandself.db.ev_channel:
- # cache channel lookup
- self.ndb.ev_channel=self.db.ev_channel
- ifself.ndb.ev_channel:
- self.ndb.ev_channel.msg(txt,senders=self.id)
-
-
-# Grapevine bot
-
-
-
[docs]classGrapevineBot(Bot):
- """
- g Grapevine (https://grapevine.haus) relayer. The channel to connect to is the first
- name in the settings.GRAPEVINE_CHANNELS list.
-
- """
-
- factory_path="evennia.server.portal.grapevine.RestartingWebsocketServerFactory"
-
-
[docs]defstart(self,ev_channel=None,grapevine_channel=None):
- """
- Start by telling the portal to connect to the grapevine network.
-
- """
- ifnot_GRAPEVINE_ENABLED:
- self.delete()
- return
-
- global_SESSIONS
- ifnot_SESSIONS:
- fromevennia.server.sessionhandlerimportSESSIONSas_SESSIONS
-
- # connect to Evennia channel
- ifev_channel:
- # connect to Evennia channel
- channel=search.channel_search(ev_channel)
- ifnotchannel:
- raiseRuntimeError(f"Evennia Channel '{ev_channel}' not found.")
- channel=channel[0]
- channel.connect(self)
- self.db.ev_channel=channel
-
- ifgrapevine_channel:
- self.db.grapevine_channel=grapevine_channel
-
- # these will be made available as properties on the protocol factory
- configdict={"uid":self.dbid,"grapevine_channel":self.db.grapevine_channel}
-
- _SESSIONS.start_bot_session(self.factory_path,configdict)
-
-
[docs]defat_msg_send(self,**kwargs):
- "Shortcut here or we can end up in infinite loop"
- pass
-
-
[docs]defmsg(self,text=None,**kwargs):
- """
- Takes text from connected channel (only).
-
- Args:
- text (str, optional): Incoming text from channel.
-
- Keyword Args:
- options (dict): Options dict with the following allowed keys:
- - from_channel (str): dbid of a channel this text originated from.
- - from_obj (list): list of objects sending this text.
-
- """
- from_obj=kwargs.get("from_obj",None)
- options=kwargs.get("options",None)or{}
-
- ifnotself.ndb.ev_channelandself.db.ev_channel:
- # cache channel lookup
- self.ndb.ev_channel=self.db.ev_channel
-
- if(
- "from_channel"inoptions
- andtext
- andself.ndb.ev_channel.dbid==options["from_channel"]
- ):
- ifnotfrom_objorfrom_obj!=[self]:
- # send outputfunc channel(msg, chan, sender)
-
- # TODO we should refactor channel formatting to operate on the
- # account/object level instead. For now, remove the channel/name
- # prefix since we pass that explicitly anyway
- prefix,text=text.split(":",1)
-
- super().msg(
- channel=(
- text.strip(),
- self.db.grapevine_channel,
- ", ".join(obj.keyforobjinfrom_obj),
- {},
- )
- )
-
-
[docs]defexecute_cmd(
- self,
- txt=None,
- session=None,
- event=None,
- grapevine_channel=None,
- sender=None,
- game=None,
- **kwargs,
- ):
- """
- Take incoming data from protocol and send it to connected channel. This is
- triggered by the bot_data_in Inputfunc.
- """
- ifevent=="channels/broadcast":
- # A private message to the bot - a command.
-
- text=f"{sender}@{game}: {txt}"
-
- ifnotself.ndb.ev_channelandself.db.ev_channel:
- # simple cache of channel lookup
- self.ndb.ev_channel=self.db.ev_channel
- ifself.ndb.ev_channel:
- self.ndb.ev_channel.msg(text,senders=self)
[docs]classAccountDBManager(TypedObjectManager,UserManager):
- """
- This AccountManager implements methods for searching
- and manipulating Accounts directly from the database.
-
- Evennia-specific search methods (will return Characters if
- possible or a Typeclass/list of Typeclassed objects, whereas
- Django-general methods will return Querysets or database objects):
-
- dbref (converter)
- dbref_search
- get_dbref_range
- object_totals
- typeclass_search
- num_total_accounts
- get_connected_accounts
- get_recently_created_accounts
- get_recently_connected_accounts
- get_account_from_email
- get_account_from_uid
- get_account_from_name
- account_search (equivalent to evennia.search_account)
-
- """
-
-
[docs]defnum_total_accounts(self):
- """
- Get total number of accounts.
-
- Returns:
- count (int): The total number of registered accounts.
-
- """
- returnself.count()
-
-
[docs]defget_connected_accounts(self):
- """
- Get all currently connected accounts.
-
- Returns:
- count (list): Account objects with currently
- connected sessions.
-
- """
- returnself.filter(db_is_connected=True)
-
-
[docs]defget_recently_created_accounts(self,days=7):
- """
- Get accounts recently created.
-
- Args:
- days (int, optional): How many days in the past "recently" means.
-
- Returns:
- accounts (list): The Accounts created the last `days` interval.
-
- """
- end_date=timezone.now()
- tdelta=datetime.timedelta(days)
- start_date=end_date-tdelta
- returnself.filter(date_joined__range=(start_date,end_date))
-
-
[docs]defget_recently_connected_accounts(self,days=7):
- """
- Get accounts recently connected to the game.
-
- Args:
- days (int, optional): Number of days backwards to check
-
- Returns:
- accounts (list): The Accounts connected to the game in the
- last `days` interval.
-
- """
- end_date=timezone.now()
- tdelta=datetime.timedelta(days)
- start_date=end_date-tdelta
- returnself.filter(last_login__range=(start_date,end_date)).order_by("-last_login")
-
-
[docs]defget_account_from_email(self,uemail):
- """
- Search account by
- Returns an account object based on email address.
-
- Args:
- uemail (str): An email address to search for.
-
- Returns:
- account (Account): A found account, if found.
-
- """
- returnself.filter(email__iexact=uemail)
-
-
[docs]defget_account_from_uid(self,uid):
- """
- Get an account by id.
-
- Args:
- uid (int): Account database id.
-
- Returns:
- account (Account): The result.
-
- """
- try:
- returnself.get(id=uid)
- exceptself.model.DoesNotExist:
- returnNone
-
-
[docs]defget_account_from_name(self,uname):
- """
- Get account object based on name.
-
- Args:
- uname (str): The Account name to search for.
-
- Returns:
- account (Account): The found account.
-
- """
- try:
- returnself.get(username__iexact=uname)
- exceptself.model.DoesNotExist:
- returnNone
-
-
[docs]defsearch_account(self,ostring,exact=True,typeclass=None):
- """
- Searches for a particular account by name or
- database id.
-
- Args:
- ostring (str or int): A key string or database id.
- exact (bool, optional): Only valid for string matches. If
- `True`, requires exact (non-case-sensitive) match,
- otherwise also match also keys containing the `ostring`
- (non-case-sensitive fuzzy match).
- typeclass (str or Typeclass, optional): Limit the search only to
- accounts of this typeclass.
- Returns:
- Queryset: A queryset (an iterable) with 0, 1 or more matches.
-
- """
- dbref=self.dbref(ostring)
- ifdbrefordbref==0:
- # dbref search is always exact
- dbref_match=self.search_dbref(dbref)
- ifdbref_match:
- returndbref_match
-
- query={"username__iexact"ifexactelse"username__icontains":ostring}
- iftypeclass:
- # we accept both strings and actual typeclasses
- ifcallable(typeclass):
- typeclass=f"{typeclass.__module__}.{typeclass.__name__}"
- else:
- typeclass=str(typeclass)
- query["db_typeclass_path"]=typeclass
- ifexact:
- matches=self.filter(**query)
- else:
- matches=self.filter(**query)
- ifnotmatches:
- # try alias match
- matches=self.filter(
- db_tags__db_tagtype__iexact="alias",
- **{"db_tags__db_key__iexact"ifexactelse"db_tags__db_key__icontains":ostring},
- )
- returnmatches
-
-
[docs]defcreate_account(
- self,
- key,
- email,
- password,
- typeclass=None,
- is_superuser=False,
- locks=None,
- permissions=None,
- tags=None,
- attributes=None,
- report_to=None,
- ):
- """
- This creates a new account.
-
- Args:
- key (str): The account's name. This should be unique.
- email (str or None): Email on valid addr@addr.domain form. If
- the empty string, will be set to None.
- password (str): Password in cleartext.
-
- Keyword Args:
- typeclass (str): The typeclass to use for the account.
- is_superuser (bool): Wether or not this account is to be a superuser
- locks (str): Lockstring.
- permission (list): List of permission strings.
- tags (list): List of Tags on form `(key, category[, data])`
- attributes (list): List of Attributes on form
- `(key, value [, category, [,lockstring [, default_pass]]])`
- report_to (Object): An object with a msg() method to report
- errors to. If not given, errors will be logged.
-
- Returns:
- Account: The newly created Account.
- Raises:
- ValueError: If `key` already exists in database.
-
-
- Notes:
- Usually only the server admin should need to be superuser, all
- other access levels can be handled with more fine-grained
- permissions or groups. A superuser bypasses all lock checking
- operations and is thus not suitable for play-testing the game.
-
- """
- typeclass=typeclassiftypeclasselsesettings.BASE_ACCOUNT_TYPECLASS
- locks=make_iter(locks)iflocksisnotNoneelseNone
- permissions=make_iter(permissions)ifpermissionsisnotNoneelseNone
- tags=make_iter(tags)iftagsisnotNoneelseNone
- attributes=make_iter(attributes)ifattributesisnotNoneelseNone
-
- ifisinstance(typeclass,str):
- # a path is given. Load the actual typeclass.
- typeclass=class_from_module(typeclass,settings.TYPECLASS_PATHS)
-
- # setup input for the create command. We use AccountDB as baseclass
- # here to give us maximum freedom (the typeclasses will load
- # correctly when each object is recovered).
-
- ifnotemail:
- email=None
- ifself.model.objects.filter(username__iexact=key):
- raiseValueError("An Account with the name '%s' already exists."%key)
-
- # this handles a given dbref-relocate to an account.
- report_to=dbid_to_obj(report_to,self.model)
-
- # create the correct account entity, using the setup from
- # base django auth.
- now=timezone.now()
- email=typeclass.objects.normalize_email(email)
- new_account=typeclass(
- username=key,
- email=email,
- is_staff=is_superuser,
- is_superuser=is_superuser,
- last_login=now,
- date_joined=now,
- )
- ifpasswordisnotNone:
- # the password may be None for 'fake' accounts, like bots
- valid,error=new_account.validate_password(password,new_account)
- ifnotvalid:
- raiseerror
-
- new_account.set_password(password)
-
- new_account._createdict=dict(
- locks=locks,permissions=permissions,
- report_to=report_to,tags=tags,attributes=attributes
- )
- # saving will trigger the signal that calls the
- # at_first_save hook on the typeclass, where the _createdict
- # can be used.
- new_account.save()
-
- # note that we don't send a signal here, that is sent from the Account.create helper method
- # instead.
-
- returnnew_account
-
- # back-compatibility alias
- account_search=search_account
-"""
-Account
-
-The account class is an extension of the default Django user class,
-and is customized for the needs of Evennia.
-
-We use the Account to store a more mud-friendly style of permission
-system as well as to allow the admin more flexibility by storing
-attributes on the Account. Within the game we should normally use the
-Account manager's methods to create users so that permissions are set
-correctly.
-
-To make the Account model more flexible for your own game, it can also
-persistently store attributes of its own. This is ideal for extra
-account info and OOC account configuration variables etc.
-
-"""
-fromdjango.confimportsettings
-fromdjango.dbimportmodels
-fromdjango.contrib.auth.modelsimportAbstractUser
-fromdjango.utils.encodingimportsmart_str
-
-fromevennia.accounts.managerimportAccountDBManager
-fromevennia.typeclasses.modelsimportTypedObject
-fromevennia.utils.utilsimportmake_iter
-fromevennia.server.signalsimportSIGNAL_ACCOUNT_POST_RENAME
-
-__all__=("AccountDB",)
-
-# _ME = _("me")
-# _SELF = _("self")
-
-_MULTISESSION_MODE=settings.MULTISESSION_MODE
-
-_GA=object.__getattribute__
-_SA=object.__setattr__
-_DA=object.__delattr__
-
-_TYPECLASS=None
-
-
-# ------------------------------------------------------------
-#
-# AccountDB
-#
-# ------------------------------------------------------------
-
-
-
[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
- by use of the variable AUTH_PROFILE_MODULE in the settings.
- One accesses the fields/methods. We try use this model as much
- as possible rather than User, since we can customize this to
- our liking.
-
- The TypedObject supplies the following (inherited) properties:
-
- - key - main name
- - 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 AccountDB adds the following properties:
-
- - is_connected - If any Session is currently connected to this Account
- - name - alias for user.username
- - sessions - sessions connected to this account
- - is_superuser - bool if this account is a superuser
- - is_bot - bool if this account is a bot and not a real account
-
- """
-
- #
- # AccountDB Database model setup
- #
- # inherited fields (from TypedObject):
- # db_key, db_typeclass_path, db_date_created, db_permissions
-
- # store a connected flag here too, not just in sessionhandler.
- # This makes it easier to track from various out-of-process locations
- db_is_connected=models.BooleanField(
- default=False,
- verbose_name="is_connected",
- help_text="If player is connected to game or not",
- )
- # database storage of persistant cmdsets.
- db_cmdset_storage=models.CharField(
- "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.",
- )
- # marks if this is a "virtual" bot account object
- db_is_bot=models.BooleanField(
- default=False,verbose_name="is_bot",help_text="Used to identify irc/rss bots"
- )
-
- # Database manager
- objects=AccountDBManager()
-
- # defaults
- __defaultclasspath__="evennia.accounts.accounts.DefaultAccount"
- __applabel__="accounts"
- __settingsclasspath__=settings.BASE_SCRIPT_TYPECLASS
-
- classMeta:
- verbose_name="Account"
-
- # cmdset_storage property
- # This seems very sensitive to caching, so leaving it be for now /Griatch
- # @property
- def__cmdset_storage_get(self):
- """
- Getter. Allows for value = self.name. Returns a list of cmdset_storage.
- """
- storage=self.db_cmdset_storage
- # we need to check so storage is not None
- return[path.strip()forpathinstorage.split(",")]ifstorageelse[]
-
- # @cmdset_storage.setter
- def__cmdset_storage_set(self,value):
- """
- Setter. Allows for self.name = value. Stores as a comma-separated
- string.
- """
- _SA(self,"db_cmdset_storage",",".join(str(val).strip()forvalinmake_iter(value)))
- _GA(self,"save")()
-
- # @cmdset_storage.deleter
- def__cmdset_storage_del(self):
- "Deleter. Allows for del self.name"
- _SA(self,"db_cmdset_storage",None)
- _GA(self,"save")()
-
- cmdset_storage=property(__cmdset_storage_get,__cmdset_storage_set,__cmdset_storage_del)
-
- #
- # property/field access
- #
-
- def__str__(self):
- returnsmart_str(f"{self.name}(account {self.dbid})")
-
- def__repr__(self):
- returnf"{self.name}(account#{self.dbid})"
-
- # @property
- def__username_get(self):
- returnself.username
-
- def__username_set(self,value):
- old_name=self.username
- self.username=value
- self.save(update_fields=["username"])
- SIGNAL_ACCOUNT_POST_RENAME.send(self,old_name=old_name,new_name=value)
-
- def__username_del(self):
- delself.username
-
- # aliases
- name=property(__username_get,__username_set,__username_del)
- key=property(__username_get,__username_set,__username_del)
-
- # @property
- def__uid_get(self):
- "Getter. Retrieves the user id"
- returnself.id
-
- def__uid_set(self,value):
- raiseException("User id cannot be set!")
-
- def__uid_del(self):
- raiseException("User id cannot be deleted!")
-
- uid=property(__uid_get,__uid_set,__uid_del)
-"""
-Command handler
-
-This module contains the infrastructure for accepting commands on the
-command line. The processing of a command works as follows:
-
-1. The calling object (caller) is analyzed based on its callertype.
-2. Cmdsets are gathered from different sources:
- - 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.
-3. The collected cmdsets are merged together to a combined, current cmdset.
-4. If the input string is empty -> check for CMD_NOINPUT command in
- current cmdset or fallback to error message. Exit.
-5. The Command Parser is triggered, using the current cmdset to analyze the
- input string for possible command matches.
-6. If multiple matches are found -> check for CMD_MULTIMATCH in current
- 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. 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.
-9. We have a unique cmdobject, primed for use. Call all hooks:
- `at_pre_cmd()`, `cmdobj.parse()`, `cmdobj.func()` and finally `at_post_cmd()`.
-10. Return deferred that will fire with the return from `cmdobj.func()` (unused by default).
-
-"""
-
-fromcollectionsimportdefaultdict
-fromweakrefimportWeakValueDictionary
-fromtracebackimportformat_exc
-fromitertoolsimportchain
-fromcopyimportcopy
-importtypes
-fromtwisted.internetimportreactor
-fromtwisted.internet.taskimportdeferLater
-fromtwisted.internet.deferimportinlineCallbacks,returnValue
-fromdjango.confimportsettings
-fromevennia.commands.commandimportInterruptCommand
-fromevennia.utilsimportlogger,utils
-fromevennia.utils.utilsimportstring_suggestions
-
-fromdjango.utils.translationimportgettextas_
-
-_IN_GAME_ERRORS=settings.IN_GAME_ERRORS
-
-__all__=("cmdhandler","InterruptCommand")
-_GA=object.__getattribute__
-_CMDSET_MERGE_CACHE=WeakValueDictionary()
-
-# tracks recursive calls by each caller
-# to avoid infinite loops (commands calling themselves)
-_COMMAND_NESTING=defaultdict(lambda:0)
-_COMMAND_RECURSION_LIMIT=10
-
-# This decides which command parser is to be used.
-# You have to restart the server for changes to take effect.
-_COMMAND_PARSER=utils.variable_from_module(*settings.COMMAND_PARSER.rsplit(".",1))
-
-# System command names - import these variables rather than trying to
-# remember the actual string constants. If not defined, Evennia
-# hard-coded defaults are used instead.
-
-# command to call if user just presses <return> with no input
-CMD_NOINPUT="__noinput_command"
-# command to call if no command match was found
-CMD_NOMATCH="__nomatch_command"
-# command to call if multiple command matches were found
-CMD_MULTIMATCH="__multimatch_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))
-
-# Output strings. The first is the IN_GAME_ERRORS return, the second
-# 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.
-"""),
-)
-
-_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.
-"""),
-)
-
-_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.
-"""),
-)
-
-_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.
-"""),
-)
-
-_ERROR_RECURSION_LIMIT=_(
- "Command recursion limit ({recursion_limit}) ""reached for '{raw_cmdname}' ({cmdclass})."
-)
-
-
-# delayed imports
-_GET_INPUT=None
-
-
-# helper functions
-
-
-def_msg_err(receiver,stringtuple):
- """
- Helper function for returning an error to the caller.
-
- Args:
- receiver (Object): object to get the error message.
- stringtuple (tuple): tuple with two strings - one for the
- _IN_GAME_ERRORS mode (with the traceback) and one with the
- production string (with a timestamp) to be shown to the user.
-
- """
- string=_("{traceback}\n{errmsg}\n(Traceback was logged {timestamp}).")
- timestamp=logger.timeformat()
- tracestring=format_exc()
- logger.log_trace()
- if_IN_GAME_ERRORS:
- receiver.msg(
- string.format(
- traceback=tracestring,errmsg=stringtuple[0].strip(),timestamp=timestamp
- ).strip()
- )
- else:
- receiver.msg(
- string.format(
- traceback=tracestring.splitlines()[-1],
- errmsg=stringtuple[1].strip(),
- timestamp=timestamp,
- ).strip()
- )
-
-
-def_process_input(caller,prompt,result,cmd,generator):
- """
- Specifically handle the get_input value to send to _progressive_cmd_run as
- part of yielding from a Command's `func`.
-
- Args:
- caller (Character, Account or Session): the caller.
- prompt (str): The sent prompt.
- result (str): The unprocessed answer.
- cmd (Command): The command itself.
- generator (GeneratorType): The generator.
-
- Returns:
- result (bool): Always `False` (stop processing).
-
- """
- # We call it using a Twisted deferLater to make sure the input is properly closed.
- deferLater(reactor,0,_progressive_cmd_run,cmd,generator,response=result)
- returnFalse
-
-
-def_progressive_cmd_run(cmd,generator,response=None):
- """
- Progressively call the command that was given in argument. Used
- when `yield` is present in the Command's `func()` method.
-
- Args:
- cmd (Command): the command itself.
- generator (GeneratorType): the generator describing the processing.
- reponse (str, optional): the response to send to the generator.
-
- Raises:
- ValueError: If the func call yields something not identifiable as a
- time-delay or a string prompt.
-
- Note:
- This function is responsible for executing the command, if
- the func() method contains 'yield' instructions. The yielded
- value will be accessible at each step and will affect the
- process. If the value is a number, just delay the execution
- of the command. If it's a string, wait for the user input.
-
- """
- global_GET_INPUT
- ifnot_GET_INPUT:
- fromevennia.utils.evmenuimportget_inputas_GET_INPUT
-
- try:
- ifresponseisNone:
- value=next(generator)
- else:
- value=generator.send(response)
- exceptStopIteration:
- # duplicated from cmdhandler._run_command, to have these
- # run in the right order while staying inside the deferred
- cmd.at_post_cmd()
- ifcmd.save_for_next:
- # store a reference to this command, possibly
- # accessible by the next command.
- cmd.caller.ndb.last_cmd=copy(cmd)
- else:
- cmd.caller.ndb.last_cmd=None
- else:
- ifisinstance(value,(int,float)):
- utils.delay(value,_progressive_cmd_run,cmd,generator)
- elifisinstance(value,str):
- _GET_INPUT(cmd.caller,value,_process_input,cmd=cmd,generator=generator)
- else:
- raiseValueError("unknown type for a yielded value in command: {}".format(type(value)))
-
-
-# custom Exceptions
-
-
-classNoCmdSets(Exception):
- "No cmdsets found. Critical error."
- pass
-
-
-classExecSystemCommand(Exception):
- "Run a system command"
-
- def__init__(self,syscmd,sysarg):
- self.args=(syscmd,sysarg)# needed by exception error handling
- self.syscmd=syscmd
- self.sysarg=sysarg
-
-
-classErrorReported(Exception):
- "Re-raised when a subsructure already reported the error"
-
- def__init__(self,raw_string):
- self.args=(raw_string,)
- self.raw_string=raw_string
-
-
-# Helper function
-
-
-@inlineCallbacks
-defget_and_merge_cmdsets(caller,session,account,obj,callertype,raw_string):
- """
- Gather all relevant cmdsets and merge them.
-
- Args:
- caller (Session, Account or Object): The entity executing the command. Which
- type of object this is depends on the current game state; for example
- when the user is not logged in, this will be a Session, when being OOC
- it will be an Account and when puppeting an object this will (often) be
- a Character Object. In the end it depends on where the cmdset is stored.
- session (Session or None): The Session associated with caller, if any.
- account (Account or None): The calling Account associated with caller, if any.
- obj (Object or None): The Object associated with caller, if any.
- callertype (str): This identifies caller as either "account", "object" or "session"
- to avoid having to do this check internally.
- raw_string (str): The input string. This is only used for error reporting.
-
- Returns:
- cmdset (Deferred): This deferred fires with the merged cmdset
- result once merger finishes.
-
- Notes:
- The cdmsets are merged in order or generality, so that the
- Object's cmdset is merged last (and will thus take precedence
- over same-named and same-prio commands on Account and Session).
-
- """
- try:
-
- @inlineCallbacks
- def_get_local_obj_cmdsets(obj):
- """
- Helper-method; Get Object-level cmdsets
-
- """
- # Gather cmdsets from location, objects in location or carried
- try:
- local_obj_cmdsets=[None]
- try:
- location=obj.location
- exceptException:
- location=None
- iflocation:
- # Gather all cmdsets stored on objects in the room and
- # also in the caller's inventory and the location itself
- local_objlist=yield(
- location.contents_get(exclude=obj)+obj.contents_get()+[location]
- )
- local_objlist=[oforoinlocal_objlistifnoto._is_deleted]
- forlobjinlocal_objlist:
- try:
- # call hook in case we need to do dynamic changing to cmdset
- _GA(lobj,"at_cmdset_get")(caller=caller)
- exceptException:
- logger.log_trace()
- # the call-type lock is checked here, it makes sure an account
- # is not seeing e.g. the commands on a fellow account (which is why
- # the no_superuser_bypass must be True)
- local_obj_cmdsets=yieldlist(
- chain.from_iterable(
- lobj.cmdset.cmdset_stack
- forlobjinlocal_objlist
- if(
- lobj.cmdset.current
- andlobj.access(
- caller,access_type="call",no_superuser_bypass=True
- )
- )
- )
- )
- forcsetinlocal_obj_cmdsets:
- # This is necessary for object sets, or we won't be able to
- # separate the command sets from each other in a busy room. We
- # only keep the setting if duplicates were set to False/True
- # explicitly.
- cset.old_duplicates=cset.duplicates
- cset.duplicates=Trueifcset.duplicatesisNoneelsecset.duplicates
- returnValue(local_obj_cmdsets)
- exceptException:
- _msg_err(caller,_ERROR_CMDSETS)
- raiseErrorReported(raw_string)
-
- @inlineCallbacks
- def_get_cmdsets(obj):
- """
- Helper method; Get cmdset while making sure to trigger all
- hooks safely. Returns the stack and the valid options.
-
- """
- try:
- yieldobj.at_cmdset_get()
- exceptException:
- _msg_err(caller,_ERROR_CMDSETS)
- raiseErrorReported(raw_string)
- try:
- returnValue((obj.cmdset.current,list(obj.cmdset.cmdset_stack)))
- exceptAttributeError:
- returnValue(((None,None,None),[]))
-
- local_obj_cmdsets=[]
- ifcallertype=="session":
- # we are calling the command from the session level
- report_to=session
- current,cmdsets=yield_get_cmdsets(session)
- ifaccount:# this automatically implies logged-in
- pcurrent,account_cmdsets=yield_get_cmdsets(account)
- cmdsets+=account_cmdsets
- current=current+pcurrent
- ifobj:
- ocurrent,obj_cmdsets=yield_get_cmdsets(obj)
- current=current+ocurrent
- cmdsets+=obj_cmdsets
- ifnotcurrent.no_objs:
- local_obj_cmdsets=yield_get_local_obj_cmdsets(obj)
- ifcurrent.no_exits:
- # filter out all exits
- local_obj_cmdsets=[
- cmdsetforcmdsetinlocal_obj_cmdsetsifcmdset.key!="ExitCmdSet"
- ]
- cmdsets+=local_obj_cmdsets
-
- elifcallertype=="account":
- # we are calling the command from the account level
- report_to=account
- current,cmdsets=yield_get_cmdsets(account)
- ifobj:
- ocurrent,obj_cmdsets=yield_get_cmdsets(obj)
- current=current+ocurrent
- cmdsets+=obj_cmdsets
- ifnotcurrent.no_objs:
- local_obj_cmdsets=yield_get_local_obj_cmdsets(obj)
- ifcurrent.no_exits:
- # filter out all exits
- local_obj_cmdsets=[
- cmdsetforcmdsetinlocal_obj_cmdsetsifcmdset.key!="ExitCmdSet"
- ]
- cmdsets+=local_obj_cmdsets
-
- elifcallertype=="object":
- # we are calling the command from the object level
- report_to=obj
- current,cmdsets=yield_get_cmdsets(obj)
- ifnotcurrent.no_objs:
- local_obj_cmdsets=yield_get_local_obj_cmdsets(obj)
- ifcurrent.no_exits:
- # filter out all exits
- local_obj_cmdsets=[
- cmdsetforcmdsetinlocal_obj_cmdsetsifcmdset.key!="ExitCmdSet"
- ]
- cmdsets+=yieldlocal_obj_cmdsets
- else:
- raiseException("get_and_merge_cmdsets: callertype %s is not valid."%callertype)
-
- # weed out all non-found sets
- cmdsets=yield[cmdsetforcmdsetincmdsetsifcmdsetandcmdset.key!="_EMPTY_CMDSET"]
- # report cmdset errors to user (these should already have been logged)
- yield[
- report_to.msg(cmdset.errmessage)forcmdsetincmdsetsifcmdset.key=="_CMDSET_ERROR"
- ]
-
- ifcmdsets:
- # faster to do tuple on list than to build tuple directly
- mergehash=tuple([id(cmdset)forcmdsetincmdsets])
- ifmergehashin_CMDSET_MERGE_CACHE:
- # cached merge exist; use that
- cmdset=_CMDSET_MERGE_CACHE[mergehash]
- else:
- # we group and merge all same-prio cmdsets separately (this avoids
- # order-dependent clashes in certain cases, such as
- # when duplicates=True)
- tempmergers={}
- forcmdsetincmdsets:
- prio=cmdset.priority
- ifpriointempmergers:
- # merge same-prio cmdset together separately
- tempmergers[prio]=yieldtempmergers[prio]+cmdset
- else:
- tempmergers[prio]=cmdset
-
- # sort cmdsets after reverse priority (highest prio are merged in last)
- sorted_cmdsets=yieldsorted(list(tempmergers.values()),key=lambdax:x.priority)
-
- # Merge all command sets into one, beginning with the lowest-prio one
- cmdset=sorted_cmdsets[0]
- formerging_cmdsetinsorted_cmdsets[1:]:
- cmdset=yieldcmdset+merging_cmdset
- # store the original, ungrouped set for diagnosis
- cmdset.merged_from=cmdsets
- # cache
- _CMDSET_MERGE_CACHE[mergehash]=cmdset
- else:
- cmdset=None
- forcsetin(csetforcsetinlocal_obj_cmdsetsifcset):
- cset.duplicates=cset.old_duplicates
- # important - this syncs the CmdSetHandler's .current field with the
- # true current cmdset!
- ifcmdset:
- caller.cmdset.current=cmdset
-
- returnValue(cmdset)
- exceptErrorReported:
- raise
- exceptException:
- _msg_err(caller,_ERROR_CMDSETS)
- raise
- # raise ErrorReported
-
-
-# Main command-handler function
-
-
-
[docs]@inlineCallbacks
-defcmdhandler(
- called_by,
- raw_string,
- _testing=False,
- callertype="session",
- session=None,
- cmdobj=None,
- cmdobj_key=None,
- **kwargs,
-):
- """
- This is the main mechanism that handles any string sent to the engine.
-
- Args:
- called_by (Session, Account or Object): Object from which this
- command was called. which this was called from. What this is
- depends on the game state.
- raw_string (str): The command string as given on the command line.
- _testing (bool, optional): Used for debug purposes and decides if we
- should actually execute the command or not. If True, the
- command instance will be returned.
- callertype (str, optional): One of "session", "account" or
- "object". These are treated in decending order, so when the
- Session is the caller, it will merge its own cmdset into
- cmdsets from both Account and eventual puppeted Object (and
- cmdsets in its room etc). An Account will only include its own
- cmdset and the Objects and so on. Merge order is the same
- order, so that Object cmdsets are merged in last, giving them
- precendence for same-name and same-prio commands.
- session (Session, optional): Relevant if callertype is "account" - the session will help
- retrieve the correct cmdsets from puppeted objects.
- cmdobj (Command, optional): If given a command instance, this will be executed using
- `called_by` as the caller, `raw_string` representing its arguments and (optionally)
- `cmdobj_key` as its input command name. No cmdset lookup will be performed but
- all other options apply as normal. This allows for running a specific Command
- within the command system mechanism.
- cmdobj_key (string, optional): Used together with `cmdobj` keyword to specify
- which cmdname should be assigned when calling the specified Command instance. This
- is made available as `self.cmdstring` when the Command runs.
- If not given, the command will be assumed to be called as `cmdobj.key`.
-
- Keyword Args:
- kwargs (any): other keyword arguments will be assigned as named variables on the
- retrieved command object *before* it is executed. This is unused
- in default Evennia but may be used by code to set custom flags or
- special operating conditions for a command as it executes.
-
- Returns:
- deferred (Deferred): This deferred is fired with the return
- value of the command's `func` method. This is not used in
- default Evennia.
-
- """
-
- @inlineCallbacks
- def_run_command(cmd,cmdname,args,raw_cmdname,cmdset,session,account):
- """
- Helper function: This initializes and runs the Command
- instance once the parser has identified it as either a normal
- command or one of the system commands.
-
- Args:
- cmd (Command): Command object
- cmdname (str): Name of command
- args (str): extra text entered after the identified command
- raw_cmdname (str): Name of Command, unaffected by eventual
- prefix-stripping (if no prefix-stripping, this is the same
- as cmdname).
- cmdset (CmdSet): Command sert the command belongs to (if any)..
- session (Session): Session of caller (if any).
- account (Account): Account of caller (if any).
-
- Returns:
- deferred (Deferred): this will fire with the return of the
- command's `func` method.
-
- Raises:
- RuntimeError: If command recursion limit was reached.
-
- """
- global_COMMAND_NESTING
- try:
- # Assign useful variables to the instance
- cmd.caller=caller
- cmd.cmdname=cmdname
- cmd.raw_cmdname=raw_cmdname
- cmd.cmdstring=cmdname# deprecated
- cmd.args=args
- cmd.cmdset=cmdset
- cmd.session=session
- cmd.account=account
- cmd.raw_string=unformatted_raw_string
- # cmd.obj # set via on-object cmdset handler for each command,
- # since this may be different for every command when
- # merging multiple cmdsets
-
- if_testing:
- # only return the command instance
- returnValue(cmd)
-
- # assign custom kwargs to found cmd object
- forkey,valinkwargs.items():
- setattr(cmd,key,val)
-
- _COMMAND_NESTING[called_by]+=1
- if_COMMAND_NESTING[called_by]>_COMMAND_RECURSION_LIMIT:
- err=_ERROR_RECURSION_LIMIT.format(
- recursion_limit=_COMMAND_RECURSION_LIMIT,
- raw_cmdname=raw_cmdname,
- cmdclass=cmd.__class__,
- )
- raiseRuntimeError(err)
-
- # pre-command hook
- abort=yieldcmd.at_pre_cmd()
- ifabort:
- # abort sequence
- returnValue(abort)
-
- # Parse and execute
- yieldcmd.parse()
-
- # main command code
- # (return value is normally None)
- ret=cmd.func()
- ifisinstance(ret,types.GeneratorType):
- # cmd.func() is a generator, execute progressively
- _progressive_cmd_run(cmd,ret)
- ret=yieldret
- # note that the _progressive_cmd_run will itself run
- # the at_post_cmd etc as it finishes; this is a bit of
- # code duplication but there seems to be no way to
- # catch the StopIteration here (it's not in the same
- # frame since this is in a deferred chain)
- else:
- ret=yieldret
- # post-command hook
- yieldcmd.at_post_cmd()
-
- ifcmd.save_for_next:
- # store a reference to this command, possibly
- # accessible by the next command.
- caller.ndb.last_cmd=yieldcopy(cmd)
- else:
- caller.ndb.last_cmd=None
-
- # return result to the deferred
- returnValue(ret)
-
- exceptInterruptCommand:
- # Do nothing, clean exit
- pass
- exceptException:
- _msg_err(caller,_ERROR_UNTRAPPED)
- raiseErrorReported(raw_string)
- finally:
- _COMMAND_NESTING[called_by]-=1
-
- session,account,obj=session,None,None
- ifcallertype=="session":
- session=called_by
- account=session.account
- obj=session.puppet
- elifcallertype=="account":
- account=called_by
- ifsession:
- obj=yieldsession.puppet
- elifcallertype=="object":
- obj=called_by
- else:
- raiseRuntimeError("cmdhandler: callertype %s is not valid."%callertype)
- # the caller will be the one to receive messages and excert its permissions.
- # we assign the caller with preference 'bottom up'
- caller=objoraccountorsession
- # The error_to is the default recipient for errors. Tries to make sure an account
- # does not get spammed for errors while preserving character mirroring.
- error_to=objorsessionoraccount
-
- try:# catch bugs in cmdhandler itself
- try:# catch special-type commands
- ifcmdobj:
- # the command object is already given
- cmd=cmdobj()ifcallable(cmdobj)elsecmdobj
- cmdname=cmdobj_keyifcmdobj_keyelsecmd.key
- args=raw_string
- unformatted_raw_string="%s%s"%(cmdname,args)
- cmdset=None
- raw_cmdname=cmdname
- # session = session
- # account = account
-
- else:
- # no explicit cmdobject given, figure it out
- cmdset=yieldget_and_merge_cmdsets(
- caller,session,account,obj,callertype,raw_string
- )
- ifnotcmdset:
- # this is bad and shouldn't happen.
- raiseNoCmdSets
- # store the completely unmodified raw string - including
- # whitespace and eventual prefixes-to-be-stripped.
- unformatted_raw_string=raw_string
- raw_string=raw_string.strip()
- ifnotraw_string:
- # Empty input. Test for system command instead.
- syscmd=yieldcmdset.get(CMD_NOINPUT)
- sysarg=""
- raiseExecSystemCommand(syscmd,sysarg)
- # Parse the input string and match to available cmdset.
- # This also checks for permissions, so all commands in match
- # are commands the caller is allowed to call.
- matches=yield_COMMAND_PARSER(raw_string,cmdset,caller)
-
- # Deal with matches
-
- iflen(matches)>1:
- # We have a multiple-match
- syscmd=yieldcmdset.get(CMD_MULTIMATCH)
- sysarg=_("There were multiple matches.")
- ifsyscmd:
- # use custom CMD_MULTIMATCH
- syscmd.matches=matches
- else:
- # fall back to default error handling
- sysarg=yield_SEARCH_AT_RESULT(
- [match[2]formatchinmatches],caller,query=matches[0][0]
- )
- raiseExecSystemCommand(syscmd,sysarg)
-
- cmdname,args,cmd,raw_cmdname="","",None,""
- iflen(matches)==1:
- # We have a unique command match. But it may still be invalid.
- match=matches[0]
- cmdname,args,cmd,raw_cmdname=(match[0],match[1],match[2],match[5])
-
- ifnotmatches:
- # No commands match our entered command
- syscmd=yieldcmdset.get(CMD_NOMATCH)
- ifsyscmd:
- # use custom CMD_NOMATCH command
- sysarg=raw_string
- else:
- # fallback to default error text
- sysarg=_("Command '{command}' is not available.").format(
- command=raw_string
- )
- suggestions=string_suggestions(
- raw_string,
- cmdset.get_all_cmd_keys_and_aliases(caller),
- cutoff=0.7,
- maxnum=3,
- )
- ifsuggestions:
- sysarg+=_(" Maybe you meant {command}?").format(
- command=utils.list_to_string(suggestions,_("or"),addquote=True)
- )
- else:
- sysarg+=_(' Type "help" for help.')
- raiseExecSystemCommand(syscmd,sysarg)
-
- ifnotcmd.retain_instance:
- # making a copy allows multiple users to share the command also when yield is used
- cmd=copy(cmd)
-
- # A normal command.
- ret=yield_run_command(cmd,cmdname,args,raw_cmdname,cmdset,session,account)
- returnValue(ret)
-
- exceptErrorReportedasexc:
- # this error was already reported, so we
- # catch it here and don't pass it on.
- logger.log_err("User input was: '%s'."%exc.raw_string)
-
- exceptExecSystemCommandasexc:
- # Not a normal command: run a system command, if available,
- # or fall back to a return string.
- syscmd=exc.syscmd
- sysarg=exc.sysarg
-
- ifsyscmd:
- ret=yield_run_command(
- syscmd,syscmd.key,sysarg,unformatted_raw_string,cmdset,session,account
- )
- returnValue(ret)
- elifsysarg:
- # return system arg
- error_to.msg(exc.sysarg)
-
- exceptNoCmdSets:
- # Critical error.
- logger.log_err("No cmdsets found: %s"%caller)
- error_to.msg(_ERROR_NOCMDSETS)
-
- exceptException:
- # We should not end up here. If we do, it's a programming bug.
- _msg_err(error_to,_ERROR_UNTRAPPED)
-
- exceptException:
- # This catches exceptions in cmdhandler exceptions themselves
- _msg_err(error_to,_ERROR_CMDHANDLER)
-"""
-The default command parser. Use your own by assigning
-`settings.COMMAND_PARSER` to a Python path to a module containing the
-replacing cmdparser function. The replacement parser must accept the
-same inputs as the default one.
-
-"""
-
-
-importre
-fromdjango.confimportsettings
-fromevennia.utils.loggerimportlog_trace
-
-_MULTIMATCH_REGEX=re.compile(settings.SEARCH_MULTIMATCH_REGEX,re.I+re.U)
-_CMD_IGNORE_PREFIXES=settings.CMD_IGNORE_PREFIXES
-
-
-
[docs]defcreate_match(cmdname,string,cmdobj,raw_cmdname):
- """
- Builds a command match by splitting the incoming string and
- evaluating the quality of the match.
-
- Args:
- cmdname (str): Name of command to check for.
- string (str): The string to match against.
- cmdobj (str): The full Command instance.
- raw_cmdname (str, optional): If CMD_IGNORE_PREFIX is set and the cmdname starts with
- one of the prefixes to ignore, this contains the raw, unstripped cmdname,
- otherwise it is None.
-
- Returns:
- match (tuple): This is on the form (cmdname, args, cmdobj, cmdlen, mratio, raw_cmdname),
- where `cmdname` is the command's name and `args` is the rest of the incoming
- string, without said command name. `cmdobj` is
- the Command instance, the cmdlen is the same as len(cmdname) and mratio
- is a measure of how big a part of the full input string the cmdname
- takes up - an exact match would be 1.0. Finally, the `raw_cmdname` is
- the cmdname unmodified by eventual prefix-stripping.
-
- """
- cmdlen,strlen=len(str(cmdname)),len(str(string))
- mratio=1-(strlen-cmdlen)/(1.0*strlen)
- args=string[cmdlen:]
- return(cmdname,args,cmdobj,cmdlen,mratio,raw_cmdname)
-
-
-
[docs]defbuild_matches(raw_string,cmdset,include_prefixes=False):
- """
- Build match tuples by matching raw_string against available commands.
-
- Args:
- raw_string (str): Input string that can look in any way; the only assumption is
- that the sought command's name/alias must be *first* in the string.
- cmdset (CmdSet): The current cmdset to pick Commands from.
- include_prefixes (bool): If set, include prefixes like @, ! etc (specified in settings)
- in the match, otherwise strip them before matching.
-
- Returns:
- matches (list) A list of match tuples created by `cmdparser.create_match`.
-
- """
- matches=[]
- try:
- ifinclude_prefixes:
- # use the cmdname as-is
- l_raw_string=raw_string.lower()
- forcmdincmdset:
- matches.extend(
- [
- create_match(cmdname,raw_string,cmd,cmdname)
- forcmdnamein[cmd.key]+cmd.aliases
- ifcmdname
- andl_raw_string.startswith(cmdname.lower())
- and(notcmd.arg_regexorcmd.arg_regex.match(l_raw_string[len(cmdname):]))
- ]
- )
- else:
- # strip prefixes set in settings
- raw_string=(
- raw_string.lstrip(_CMD_IGNORE_PREFIXES)iflen(raw_string)>1elseraw_string
- )
- l_raw_string=raw_string.lower()
- forcmdincmdset:
- forraw_cmdnamein[cmd.key]+cmd.aliases:
- cmdname=(
- raw_cmdname.lstrip(_CMD_IGNORE_PREFIXES)
- iflen(raw_cmdname)>1
- elseraw_cmdname
- )
- if(
- cmdname
- andl_raw_string.startswith(cmdname.lower())
- and(notcmd.arg_regexorcmd.arg_regex.match(l_raw_string[len(cmdname):]))
- ):
- matches.append(create_match(cmdname,raw_string,cmd,raw_cmdname))
- exceptException:
- log_trace("cmdhandler error. raw_input:%s"%raw_string)
- returnmatches
-
-
-
[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
- match was found.
-
- Args:
- raw_string (str): The user input to parse.
-
- Returns:
- mindex, new_raw_string (tuple): If a multimatch-separator was detected,
- this is stripped out as an integer to separate between the matches. The
- new_raw_string is the result of stripping out that identifier. If no
- such form was found, returns (None, None).
-
- Example:
- In the default configuration, entering 2-ball (e.g. in a room will more
- than one 'ball' object), will lead to a multimatch and this function
- will parse `"2-ball"` and return `(2, "ball")`.
-
- """
- # no matches found
- num_ref_match=_MULTIMATCH_REGEX.match(raw_string)
- ifnum_ref_match:
- # the user might be trying to identify the command
- # with a #num-command style syntax. We expect the regex to
- # contain the groups "number" and "name".
- mindex,new_raw_string=(num_ref_match.group("number"),num_ref_match.group("name")+num_ref_match.group("args"))
- returnint(mindex),new_raw_string
- else:
- returnNone,None
-
-
-
[docs]defcmdparser(raw_string,cmdset,caller,match_index=None):
- """
- This function is called by the cmdhandler once it has
- gathered and merged all valid cmdsets valid for this particular parsing.
-
- Args:
- raw_string (str): The unparsed text entered by the caller.
- cmdset (CmdSet): The merged, currently valid cmdset
- caller (Session, Account or Object): The caller triggering this parsing.
- match_index (int, optional): Index to pick a given match in a
- list of same-named command matches. If this is given, it suggests
- this is not the first time this function was called: normally
- the first run resulted in a multimatch, and the index is given
- to select between the results for the second run.
-
- Returns:
- matches (list): This is a list of match-tuples as returned by `create_match`.
- If no matches were found, this is an empty list.
-
- Notes:
- The cmdparser understand the following command combinations (where
- [] marks optional parts.
-
- ```
- [cmdname[ cmdname2 cmdname3 ...] [the rest]
- ```
-
- A command may consist of any number of space-separated words of any
- length, and contain any character. It may also be empty.
-
- The parser makes use of the cmdset to find command candidates. The
- parser return a list of matches. Each match is a tuple with its
- first three elements being the parsed cmdname (lower case),
- the remaining arguments, and the matched cmdobject from the cmdset.
-
- """
- ifnotraw_string:
- return[]
-
- # find matches, first using the full name
- matches=build_matches(raw_string,cmdset,include_prefixes=True)
-
- 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")]
-
- # try to bring the number of matches down to 1
- iflen(matches)>1:
- # See if it helps to analyze the match with preserved case but only if
- # it leaves at least one match.
- trimmed=[matchformatchinmatchesifraw_string.startswith(match[0])]
- iftrimmed:
- matches=trimmed
-
- iflen(matches)>1:
- # we still have multiple matches. Sort them by count quality.
- matches=sorted(matches,key=lambdam:m[3])
- # only pick the matches with highest count quality
- quality=[mat[3]formatinmatches]
- matches=matches[-quality.count(quality[-1]):]
-
- iflen(matches)>1:
- # still multiple matches. Fall back to ratio-based quality.
- matches=sorted(matches,key=lambdam:m[4])
- # only pick the highest rated ratio match
- quality=[mat[4]formatinmatches]
- matches=matches[-quality.count(quality[-1]):]
-
- iflen(matches)>1andmatch_indexisnotNone:
- # We couldn't separate match by quality, but we have an
- # index argument to tell us which match to use.
- if0<match_index<=len(matches):
- matches=[matches[match_index-1]]
- else:
- # we tried to give an index outside of the range - this means
- # a no-match
- matches=[]
-
- # no matter what we have at this point, we have to return it.
- returnmatches
-"""
-
-A Command Set (CmdSet) holds a set of commands. The Cmdsets can be
-merged and combined to create new sets of commands in a
-non-destructive way. This makes them very powerful for implementing
-custom game states where different commands (or different variations
-of commands) are available to the accounts depending on circumstance.
-
-The available merge operations are partly borrowed from mathematical
-Set theory.
-
-
-* Union The two command sets are merged so that as many commands as
- possible of each cmdset ends up in the merged cmdset. Same-name
- commands are merged by priority. This is the most common default.
- Ex: A1,A3 + B1,B2,B4,B5 = A1,B2,A3,B4,B5
-* Intersect - Only commands found in *both* cmdsets (i.e. which have
- same names) end up in the merged cmdset, with the higher-priority
- cmdset replacing the lower one. Ex: A1,A3 + B1,B2,B4,B5 = A1
-* Replace - The commands of this cmdset completely replaces the
- lower-priority cmdset's commands, regardless of if same-name commands
- exist. Ex: A1,A3 + B1,B2,B4,B5 = A1,A3
-* Remove - This removes the relevant commands from the
- lower-priority cmdset completely. They are not replaced with
- anything, so this in effects uses the high-priority cmdset as a filter
- to affect the low-priority cmdset. Ex: A1,A3 + B1,B2,B4,B5 = B2,B4,B5
-
-"""
-fromweakrefimportWeakKeyDictionary
-fromdjango.utils.translationimportgettextas_
-fromevennia.utils.utilsimportinherits_from,is_iter
-
-__all__=("CmdSet",)
-
-
-class_CmdSetMeta(type):
- """
- This metaclass makes some minor on-the-fly convenience fixes to
- the cmdset class.
-
- """
-
- def__init__(cls,*args,**kwargs):
- """
- Fixes some things in the cmdclass
-
- """
- # by default we key the cmdset the same as the
- # name of its class.
- ifnothasattr(cls,"key")ornotcls.key:
- cls.key=cls.__name__
- cls.path="%s.%s"%(cls.__module__,cls.__name__)
-
- ifnotisinstance(cls.key_mergetypes,dict):
- cls.key_mergetypes={}
-
- super().__init__(*args,**kwargs)
-
-
-
[docs]classCmdSet(object,metaclass=_CmdSetMeta):
- """
- This class describes a unique cmdset that understands priorities.
- CmdSets can be merged and made to perform various set operations
- on each other. CmdSets have priorities that affect which of their
- ingoing commands gets used.
-
- In the examples, cmdset A always have higher priority than cmdset B.
-
- key - the name of the cmdset. This can be used on its own for game
- operations
-
- mergetype (partly from Set theory):
-
- Union - The two command sets are merged so that as many
- commands as possible of each cmdset ends up in the
- merged cmdset. Same-name commands are merged by
- priority. This is the most common default.
- Ex: A1,A3 + B1,B2,B4,B5 = A1,B2,A3,B4,B5
- Intersect - Only commands found in *both* cmdsets
- (i.e. which have same names) end up in the merged
- cmdset, with the higher-priority cmdset replacing the
- lower one. Ex: A1,A3 + B1,B2,B4,B5 = A1
- Replace - The commands of this cmdset completely replaces
- the lower-priority cmdset's commands, regardless
- of if same-name commands exist.
- Ex: A1,A3 + B1,B2,B4,B5 = A1,A3
- Remove - This removes the relevant commands from the
- lower-priority cmdset completely. They are not
- replaced with anything, so this in effects uses the
- high-priority cmdset as a filter to affect the
- low-priority cmdset.
- Ex: A1,A3 + B1,B2,B4,B5 = B2,B4,B5
-
- Note: Commands longer than 2 characters and starting
- with double underscrores, like '__noinput_command'
- are considered 'system commands' and are
- excempt from all merge operations - they are
- ALWAYS included across mergers and only affected
- if same-named system commands replace them.
-
- priority- All cmdsets are always merged in pairs of two so that
- the higher set's mergetype is applied to the
- lower-priority cmdset. Default commands have priority 0,
- high-priority ones like Exits and Channels have 10 and 9.
- Priorities can be negative as well to give default
- commands preference.
-
- duplicates - determines what happens when two sets of equal
- priority merge (only). Defaults to None and has the first of them in the
- merger (i.e. A above) automatically taking
- precedence. But if `duplicates` is true, the
- result will be a merger with more than one of each
- name match. This will usually lead to the account
- receiving a multiple-match error higher up the road,
- but can be good for things like cmdsets on non-account
- objects in a room, to allow the system to warn that
- more than one 'ball' in the room has the same 'kick'
- command defined on it, so it may offer a chance to
- select which ball to kick ... Allowing duplicates
- only makes sense for Union and Intersect, the setting
- is ignored for the other mergetypes.
- Note that the `duplicates` flag is *not* propagated in
- a cmdset merger. So `A + B = C` will result in
- a cmdset with duplicate commands, but C.duplicates will
- be `None`. For duplication to apply to a whole cmdset
- stack merge, _all_ cmdsets in the stack must have
- `.duplicates=True` set.
- Finally, if a final cmdset has `.duplicates=None` (the normal
- unless created alone with another value), the cmdhandler
- will assume True for object-based cmdsets and False for
- all other. This is usually the most intuitive outcome.
-
- key_mergetype (dict) - allows the cmdset to define a unique
- mergetype for particular cmdsets. Format is
- {CmdSetkeystring:mergetype}. Priorities still apply.
- Example: {'Myevilcmdset','Replace'} which would make
- sure for this set to always use 'Replace' on
- Myevilcmdset no matter what overall mergetype this set
- has.
-
- no_objs - don't include any commands from nearby objects
- when searching for suitable commands
- no_exits - ignore the names of exits when matching against
- commands
- no_channels - ignore the name of channels when matching against
- commands (WARNING- this is dangerous since the
- account can then not even ask staff for help if
- something goes wrong)
-
-
- """
-
- key="Unnamed CmdSet"
- mergetype="Union"
- priority=0
-
- # These flags, if set to None should be interpreted as 'I don't care' and,
- # will allow "pass-through" even of lower-prio cmdsets' explicitly True/False
- # options. If this is set to True/False however, priority matters.
- no_exits=None
- no_objs=None
- no_channels=None
- # The .duplicates setting does not propagate and since duplicates can only happen
- # on same-prio cmdsets, there is no concept of passthrough on `None`.
- # The merger of two cmdsets always return in a cmdset with `duplicates=None`
- # (even if the result may have duplicated commands).
- # If a final cmdset has `duplicates=None` (normal, unless the cmdset is
- # created on its own with the flag set), the cmdhandler will auto-assume it to be
- # True for Object-based cmdsets and stay None/False for all other entities.
- #
- # Example:
- # A and C has .duplicates=True, B has .duplicates=None (or False)
- # B + A = BA, where BA will have duplicate cmds, but BA.duplicates = None
- # BA + C = BAC, where BAC will have more duplication, but BAC.duplicates = None
- #
- # Basically, for the `.duplicate` setting to survive throughout a
- # merge-stack, every cmdset in the stack must have `duplicates` set explicitly.
- duplicates=None
-
- persistent=False
- key_mergetypes={}
- errmessage=""
- # pre-store properties to duplicate straight off
- to_duplicate=(
- "key",
- "cmdsetobj",
- "no_exits",
- "no_objs",
- "no_channels",
- "persistent",
- "mergetype",
- "priority",
- "duplicates",
- "errmessage",
- )
-
-
[docs]def__init__(self,cmdsetobj=None,key=None):
- """
- Creates a new CmdSet instance.
-
- Args:
- cmdsetobj (Session, Account, Object, optional): This is the database object
- to which this particular instance of cmdset is related. It
- is often a character but may also be a regular object, Account
- or Session.
- key (str, optional): The idenfier for this cmdset. This
- helps if wanting to selectively remov cmdsets.
-
- """
-
- ifkey:
- self.key=key
- self.commands=[]
- self.system_commands=[]
- self.actual_mergetype=self.mergetype
- self.cmdsetobj=cmdsetobj
- # this is set only on merged sets, in cmdhandler.py, in order to
- # track, list and debug mergers correctly.
- self.merged_from=[]
-
- # initialize system
- self.at_cmdset_creation()
- self._contains_cache=WeakKeyDictionary()# {}
-
- # Priority-sensitive merge operations for cmdsets
-
- def_union(self,cmdset_a,cmdset_b):
- """
- Merge two sets using union merger
-
- Args:
- cmdset_a (Cmdset): Cmdset given higher priority in the case of a tie.
- cmdset_b (Cmdset): Cmdset given lower priority in the case of a tie.
-
- Returns:
- cmdset_c (Cmdset): The result of A U B operation.
-
- Notes:
- Union, C = A U B, means that C gets all elements from both A and B.
-
- """
- cmdset_c=cmdset_a._duplicate()
- # we make copies, not refs by use of [:]
- cmdset_c.commands=cmdset_a.commands[:]
- ifcmdset_a.duplicatesandcmdset_a.priority==cmdset_b.priority:
- cmdset_c.commands.extend(cmdset_b.commands)
- else:
- cmdset_c.commands.extend([cmdforcmdincmdset_bifcmdnotincmdset_a])
- returncmdset_c
-
- def_intersect(self,cmdset_a,cmdset_b):
- """
- Merge two sets using intersection merger
-
- Args:
- cmdset_a (Cmdset): Cmdset given higher priority in the case of a tie.
- cmdset_b (Cmdset): Cmdset given lower priority in the case of a tie.
-
- Returns:
- cmdset_c (Cmdset): The result of A (intersect) B operation.
-
- Notes:
- Intersection, C = A (intersect) B, means that C only gets the
- parts of A and B that are the same (that is, the commands
- of each set having the same name. Only the one of these
- having the higher prio ends up in C).
-
- """
- cmdset_c=cmdset_a._duplicate()
- ifcmdset_a.duplicatesandcmdset_a.priority==cmdset_b.priority:
- forcmdin[cmdforcmdincmdset_aifcmdincmdset_b]:
- cmdset_c.add(cmd)
- cmdset_c.add(cmdset_b.get(cmd))
- else:
- cmdset_c.commands=[cmdforcmdincmdset_aifcmdincmdset_b]
- returncmdset_c
-
- def_replace(self,cmdset_a,cmdset_b):
- """
- Replace the contents of one set with another
-
- Args:
- cmdset_a (Cmdset): Cmdset replacing
- cmdset_b (Cmdset): Cmdset to replace
-
- Returns:
- cmdset_c (Cmdset): This is indentical to cmdset_a.
-
- Notes:
- C = A, where B is ignored.
-
- """
- cmdset_c=cmdset_a._duplicate()
- cmdset_c.commands=cmdset_a.commands[:]
- returncmdset_c
-
- def_remove(self,cmdset_a,cmdset_b):
- """
- Filter a set by another.
-
- Args:
- cmdset_a (Cmdset): Cmdset acting as a removal filter.
- cmdset_b (Cmdset): Cmdset to filter
-
- Returns:
- cmdset_c (Cmdset): B, with all matching commands from A removed.
-
- Notes:
- C = B - A, where A is used to remove the commands of B.
-
- """
-
- cmdset_c=cmdset_a._duplicate()
- cmdset_c.commands=[cmdforcmdincmdset_bifcmdnotincmdset_a]
- returncmdset_c
-
- def_instantiate(self,cmd):
- """
- checks so that object is an instantiated command and not, say
- a cmdclass. If it is, instantiate it. Other types, like
- strings, are passed through.
-
- Args:
- cmd (any): Entity to analyze.
-
- Returns:
- result (any): An instantiated Command or the input unmodified.
-
- """
- ifcallable(cmd):
- returncmd()
- else:
- returncmd
-
- def_duplicate(self):
- """
- Returns a new cmdset with the same settings as this one (no
- actual commands are copied over)
-
- Returns:
- cmdset (Cmdset): A copy of the current cmdset.
- """
- cmdset=CmdSet()
- forkey,valin((key,getattr(self,key))forkeyinself.to_duplicate):
- ifval!=getattr(cmdset,key):
- # only copy if different from default; avoid turning
- # class-vars into instance vars
- setattr(cmdset,key,val)
- cmdset.key_mergetypes=self.key_mergetypes.copy()
- returncmdset
-
- def__str__(self):
- """
- Show all commands in cmdset when printing it.
-
- Returns:
- commands (str): Representation of commands in Cmdset.
-
- """
- perm="perm"ifself.persistentelse"non-perm"
- options=", ".join([
- "{}:{}".format(opt,"T"ifgetattr(self,opt)else"F")
- foroptin("no_exits","no_objs","no_channels","duplicates")
- ifgetattr(self,opt)isnotNone
- ])
- options=(", "+options)ifoptionselse""
- return(f"<CmdSet {self.key}, {self.mergetype}, {perm}, prio {self.priority}{options}>: "
- +", ".join([str(cmd)forcmdinsorted(self.commands,key=lambdao:o.key)]))
-
- def__iter__(self):
- """
- Allows for things like 'for cmd in cmdset':
-
- Returns:
- iterable (iter): Commands in Cmdset.
-
- """
- returniter(self.commands)
-
- def__contains__(self,othercmd):
- """
- Returns True if this cmdset contains the given command (as
- defined by command name and aliases). This allows for things
- like 'if cmd in cmdset'
-
- """
- ret=self._contains_cache.get(othercmd)
- ifretisNone:
- ret=othercmdinself.commands
- self._contains_cache[othercmd]=ret
- returnret
-
- def__add__(self,cmdset_a):
- """
- Merge this cmdset (B) with another cmdset (A) using the + operator,
-
- C = B + A
-
- Here, we (by convention) say that 'A is merged onto B to form
- C'. The actual merge operation used in the 'addition' depends
- on which priorities A and B have. The one of the two with the
- highest priority will apply and give its properties to C. In
- the case of a tie, A takes priority and replaces the
- same-named commands in B unless A has the 'duplicate' variable
- set (which means both sets' commands are kept).
- """
-
- # It's okay to merge with None
- ifnotcmdset_a:
- returnself
-
- sys_commands_a=cmdset_a.get_system_cmds()
- sys_commands_b=self.get_system_cmds()
-
- ifself.priority<=cmdset_a.priority:
- # A higher or equal priority to B
-
- # preserve system __commands
- sys_commands=sys_commands_a+[
- cmdforcmdinsys_commands_bifcmdnotinsys_commands_a
- ]
-
- mergetype=cmdset_a.key_mergetypes.get(self.key,cmdset_a.mergetype)
- ifmergetype=="Intersect":
- cmdset_c=self._intersect(cmdset_a,self)
- elifmergetype=="Replace":
- cmdset_c=self._replace(cmdset_a,self)
- elifmergetype=="Remove":
- cmdset_c=self._remove(cmdset_a,self)
- else:# Union
- cmdset_c=self._union(cmdset_a,self)
-
- # pass through options whenever they are set, unless the merging or higher-prio
- # set changes the setting (i.e. has a non-None value). We don't pass through
- # the duplicates setting; that is per-merge; the resulting .duplicates value
- # is always None (so merging cmdsets must all have explicit values if wanting
- # to cause duplicates).
- cmdset_c.no_channels=(
- self.no_channelsifcmdset_a.no_channelsisNoneelsecmdset_a.no_channels
- )
- cmdset_c.no_exits=self.no_exitsifcmdset_a.no_exitsisNoneelsecmdset_a.no_exits
- cmdset_c.no_objs=self.no_objsifcmdset_a.no_objsisNoneelsecmdset_a.no_objs
- cmdset_c.duplicates=None
-
- else:
- # B higher priority than A
-
- # preserver system __commands
- sys_commands=sys_commands_b+[
- cmdforcmdinsys_commands_aifcmdnotinsys_commands_b
- ]
-
- mergetype=self.key_mergetypes.get(cmdset_a.key,self.mergetype)
- ifmergetype=="Intersect":
- cmdset_c=self._intersect(self,cmdset_a)
- elifmergetype=="Replace":
- cmdset_c=self._replace(self,cmdset_a)
- elifmergetype=="Remove":
- cmdset_c=self._remove(self,cmdset_a)
- else:# Union
- cmdset_c=self._union(self,cmdset_a)
-
- # pass through options whenever they are set, unless the higher-prio
- # set changes the setting (i.e. has a non-None value). We don't pass through
- # the duplicates setting; that is per-merge; the resulting .duplicates value#
- # is always None (so merging cmdsets must all have explicit values if wanting
- # to cause duplicates).
- cmdset_c.no_channels=(
- cmdset_a.no_channelsifself.no_channelsisNoneelseself.no_channels
- )
- cmdset_c.no_exits=cmdset_a.no_exitsifself.no_exitsisNoneelseself.no_exits
- cmdset_c.no_objs=cmdset_a.no_objsifself.no_objsisNoneelseself.no_objs
- cmdset_c.duplicates=None
-
- # we store actual_mergetype since key_mergetypes
- # might be different from the main mergetype.
- # This is used for diagnosis.
- cmdset_c.actual_mergetype=mergetype
-
- # print "__add__ for %s (prio %i) called with %s (prio %i)." % (self.key, self.priority,
- # cmdset_a.key, cmdset_a.priority)
-
- # return the system commands to the cmdset
- cmdset_c.add(sys_commands,allow_duplicates=True)
- returncmdset_c
-
-
[docs]defadd(self,cmd,allow_duplicates=False):
- """
- Add a new command or commands to this CmdSet, a list of
- commands or a cmdset to this cmdset. Note that this is *not*
- a merge operation (that is handled by the + operator).
-
- Args:
- cmd (Command, list, Cmdset): This allows for adding one or
- more commands to this Cmdset in one go. If another Cmdset
- is given, all its commands will be added.
- allow_duplicates (bool, optional): If set, will not try to remove
- duplicate cmds in the set. This is needed during the merge process
- to avoid wiping commands coming from cmdsets with duplicate=True.
-
- Notes:
- If cmd already exists in set, it will replace the old one
- (no priority checking etc happens here). This is very useful
- when overloading default commands).
-
- If cmd is another cmdset class or -instance, the commands of
- that command set is added to this one, as if they were part of
- the original cmdset definition. No merging or priority checks
- are made, rather later added commands will simply replace
- existing ones to make a unique set.
-
- """
-
- ifinherits_from(cmd,"evennia.commands.cmdset.CmdSet"):
- # cmd is a command set so merge all commands in that set
- # to this one. We raise a visible error if we created
- # an infinite loop (adding cmdset to itself somehow)
- cmdset=cmd
- try:
- cmdset=self._instantiate(cmdset)
- exceptRuntimeError:
- err=("Adding cmdset {cmdset} to {cls} lead to an "
- "infinite loop. When adding a cmdset to another, "
- "make sure they are not themself cyclically added to "
- "the new cmdset somewhere in the chain.")
- raiseRuntimeError(_(err.format(cmdset=cmdset,cls=self.__class__)))
- cmds=cmdset.commands
- elifis_iter(cmd):
- cmds=[self._instantiate(c)forcincmd]
- else:
- cmds=[self._instantiate(cmd)]
- commands=self.commands
- system_commands=self.system_commands
- forcmdincmds:
- # add all commands
- ifnothasattr(cmd,"obj")orcmd.objisNone:
- cmd.obj=self.cmdsetobj
- try:
- ic=commands.index(cmd)
- commands[ic]=cmd# replace
- exceptValueError:
- commands.append(cmd)
- self.commands=commands
- ifnotallow_duplicates:
- # extra run to make sure to avoid doublets
- self.commands=list(set(self.commands))
- # add system_command to separate list as well,
- # for quick look-up
- ifcmd.key.startswith("__"):
- try:
- ic=system_commands.index(cmd)
- system_commands[ic]=cmd# replace
- exceptValueError:
- system_commands.append(cmd)
-
-
[docs]defremove(self,cmd):
- """
- Remove a command instance from the cmdset.
-
- Args:
- cmd (Command or str): Either the Command object to remove
- or the key of such a command.
-
- """
- cmd=self._instantiate(cmd)
- ifcmd.key.startswith("__"):
- try:
- ic=self.system_commands.index(cmd)
- delself.system_commands[ic]
- exceptValueError:
- # ignore error
- pass
- else:
- self.commands=[oldcmdforoldcmdinself.commandsifoldcmd!=cmd]
-
-
[docs]defget(self,cmd):
- """
- Get a command from the cmdset. This is mostly useful to
- check if the command is part of this cmdset or not.
-
- Args:
- cmd (Command or str): Either the Command object or its key.
-
- Returns:
- cmd (Command): The first matching Command in the set.
-
- """
- cmd=self._instantiate(cmd)
- forthiscmdinself.commands:
- ifthiscmd==cmd:
- returnthiscmd
- returnNone
-
-
[docs]defcount(self):
- """
- Number of commands in set.
-
- Returns:
- N (int): Number of commands in this Cmdset.
-
- """
- returnlen(self.commands)
-
-
[docs]defget_system_cmds(self):
- """
- Get system commands in cmdset
-
- Returns:
- sys_cmds (list): The system commands in the set.
-
- Notes:
- As far as the Cmdset is concerned, system commands are any
- commands with a key starting with double underscore __.
- These are excempt from merge operations.
-
- """
- returnself.system_commands
-
-
[docs]defmake_unique(self,caller):
- """
- Remove duplicate command-keys (unsafe)
-
- Args:
- caller (object): Commands on this object will
- get preference in the duplicate removal.
-
- Notes:
- This is an unsafe command meant to clean out a cmdset of
- doublet commands after it has been created. It is useful
- for commands inheriting cmdsets from the cmdhandler where
- obj-based cmdsets always are added double. Doublets will
- be weeded out with preference to commands defined on
- caller, otherwise just by first-come-first-served.
-
- """
- unique={}
- forcmdinself.commands:
- ifcmd.keyinunique:
- ocmd=unique[cmd.key]
- if(hasattr(cmd,"obj")andcmd.obj==caller)andnot(
- hasattr(ocmd,"obj")andocmd.obj==caller
- ):
- unique[cmd.key]=cmd
- else:
- unique[cmd.key]=cmd
- self.commands=list(unique.values())
-
-
[docs]defget_all_cmd_keys_and_aliases(self,caller=None):
- """
- Collects keys/aliases from commands
-
- Args:
- caller (Object, optional): If set, this is used to check access permissions
- on each command. Only commands that pass are returned.
-
- Returns:
- names (list): A list of all command keys and aliases in this cmdset. If `caller`
- was given, this list will only contain commands to which `caller` passed
- the `call` locktype check.
-
- """
- names=[]
- ifcaller:
- [names.extend(cmd._keyaliases)forcmdinself.commandsifcmd.access(caller)]
- else:
- [names.extend(cmd._keyaliases)forcmdinself.commands]
- returnnames
-
-
[docs]defat_cmdset_creation(self):
- """
- Hook method - this should be overloaded in the inheriting
- class, and should take care of populating the cmdset by use of
- self.add().
-
- """
- pass
-"""
-CmdSethandler
-
-The Cmdsethandler tracks an object's 'Current CmdSet', which is the
-current merged sum of all CmdSets added to it.
-
-A CmdSet constitues a set of commands. The CmdSet works as a special
-intelligent container that, when added to other CmdSet make sure that
-same-name commands are treated correctly (usually so there are no
-doublets). This temporary but up-to-date merger of CmdSet is jointly
-called the Current Cmset. It is this Current CmdSet that the
-commandhandler looks through whenever an account enters a command (it
-also adds CmdSets from objects in the room in real-time). All account
-objects have a 'default cmdset' containing all the normal in-game mud
-commands (look etc).
-
-So what is all this cmdset complexity good for?
-
-In its simplest form, a CmdSet has no commands, only a key name. In
-this case the cmdset's use is up to each individual game - it can be
-used by an AI module for example (mobs in cmdset 'roam' move from room
-to room, in cmdset 'attack' they enter combat with accounts).
-
-Defining commands in cmdsets offer some further powerful game-design
-consequences however. Here are some examples:
-
-As mentioned above, all accounts always have at least the Default
-CmdSet. This contains the set of all normal-use commands in-game,
-stuff like look and @desc etc. Now assume our players end up in a dark
-room. You don't want the player to be able to do much in that dark
-room unless they light a candle. You could handle this by changing all
-your normal commands to check if the player is in a dark room. This
-rapidly goes unwieldly and error prone. Instead you just define a
-cmdset with only those commands you want to be available in the 'dark'
-cmdset - maybe a modified look command and a 'light candle' command -
-and have this completely replace the default cmdset.
-
-Another example: Say you want your players to be able to go
-fishing. You could implement this as a 'fish' command that fails
-whenever the account has no fishing rod. Easy enough. But what if you
-want to make fishing more complex - maybe you want four-five different
-commands for throwing your line, reeling in, etc? Most players won't
-(we assume) have fishing gear, and having all those detailed commands
-is cluttering up the command list. And what if you want to use the
-'throw' command also for throwing rocks etc instead of 'using it up'
-for a minor thing like fishing?
-
-So instead you put all those detailed fishing commands into their own
-CommandSet called 'Fishing'. Whenever the player gives the command
-'fish' (presumably the code checks there is also water nearby), only
-THEN this CommandSet is added to the Cmdhandler of the account. The
-'throw' command (which normally throws rocks) is replaced by the
-custom 'fishing variant' of throw. What has happened is that the
-Fishing CommandSet was merged on top of the Default ones, and due to
-how we defined it, its command overrules the default ones.
-
-When we are tired of fishing, we give the 'go home' command (or
-whatever) and the Cmdhandler simply removes the fishing CommandSet
-so that we are back at defaults (and can throw rocks again).
-
-Since any number of CommandSets can be piled on top of each other, you
-can then implement separate sets for different situations. For
-example, you can have a 'On a boat' set, onto which you then tack on
-the 'Fishing' set. Fishing from a boat? No problem!
-
-"""
-importsys
-fromtracebackimportformat_exc
-fromimportlibimportimport_module
-frominspectimporttrace
-fromdjango.confimportsettings
-fromevennia.utilsimportlogger,utils
-fromevennia.commands.cmdsetimportCmdSet
-fromevennia.server.modelsimportServerConfig
-
-fromdjango.utils.translationimportgettextas_
-
-__all__=("import_cmdset","CmdSetHandler")
-
-_CACHED_CMDSETS={}
-_CMDSET_PATHS=utils.make_iter(settings.CMDSET_PATHS)
-_IN_GAME_ERRORS=settings.IN_GAME_ERRORS
-_CMDSET_FALLBACKS=settings.CMDSET_FALLBACKS
-
-
-# Output strings
-
-_ERROR_CMDSET_IMPORT=_(
- """{traceback}
-Error loading cmdset '{path}'
-(Traceback was logged {timestamp})"""
-)
-
-_ERROR_CMDSET_KEYERROR=_(
- """Error loading cmdset: No cmdset class '{classname}' in '{path}'.
-(Traceback was logged {timestamp})"""
-)
-
-_ERROR_CMDSET_SYNTAXERROR=_(
- """{traceback}
-SyntaxError encountered when loading cmdset '{path}'.
-(Traceback was logged {timestamp})"""
-)
-
-_ERROR_CMDSET_EXCEPTION=_(
- """{traceback}
-Compile/Run error when loading cmdset '{path}'.
-(Traceback was logged {timestamp})"""
-)
-
-_ERROR_CMDSET_FALLBACK=_(
- """
-Error encountered for cmdset at path '{path}'.
-Replacing with fallback '{fallback_path}'.
-"""
-)
-
-_ERROR_CMDSET_NO_FALLBACK=_("""Fallback path '{fallback_path}' failed to generate a cmdset.""")
-
-
-class_ErrorCmdSet(CmdSet):
- """
- This is a special cmdset used to report errors.
- """
-
- key="_CMDSET_ERROR"
- errmessage="Error when loading cmdset."
-
-
-class_EmptyCmdSet(CmdSet):
- """
- This cmdset represents an empty cmdset
- """
-
- key="_EMPTY_CMDSET"
- priority=-101
- mergetype="Union"
-
-
-
[docs]defimport_cmdset(path,cmdsetobj,emit_to_obj=None,no_logging=False):
- """
- This helper function is used by the cmdsethandler to load a cmdset
- instance from a python module, given a python_path. It's usually accessed
- through the cmdsethandler's add() and add_default() methods.
- path - This is the full path to the cmdset object on python dot-form
-
- Args:
- path (str): The path to the command set to load.
- cmdsetobj (CmdSet): The database object/typeclass on which this cmdset is to be
- assigned (this can be also channels and exits, as well as accounts
- but there will always be such an object)
- emit_to_obj (Object, optional): If given, error is emitted to
- this object (in addition to logging)
- no_logging (bool, optional): Don't log/send error messages.
- This can be useful if import_cmdset is just used to check if
- this is a valid python path or not.
- Returns:
- cmdset (CmdSet): The imported command set. If an error was
- encountered, `commands.cmdsethandler._ErrorCmdSet` is returned
- for the benefit of the handler.
-
- """
- python_paths=[path]+[
- "%s.%s"%(prefix,path)forprefixin_CMDSET_PATHSifnotpath.startswith(prefix)
- ]
- errstring=""
- forpython_pathinpython_paths:
-
- if"."inpath:
- modpath,classname=python_path.rsplit(".",1)
- else:
- raiseImportError(f"The path '{path}' is not on the form modulepath.ClassName")
-
- try:
- # first try to get from cache
- cmdsetclass=_CACHED_CMDSETS.get(python_path,None)
-
- ifnotcmdsetclass:
- try:
- module=import_module(modpath,package="evennia")
- exceptImportErrorasexc:
- iflen(trace())>2:
- # error in module, make sure to not hide it.
- dum,dum,tb=sys.exc_info()
- raiseexc.with_traceback(tb)
- else:
- # try next suggested path
- errstring+=_("\n(Unsuccessfully tried '{path}').").format(
- path=python_path
- )
- continue
- try:
- cmdsetclass=getattr(module,classname)
- exceptAttributeErrorasexc:
- iflen(trace())>2:
- # Attribute error within module, don't hide it
- dum,dum,tb=sys.exc_info()
- raiseexc.with_traceback(tb)
- else:
- errstring+=_("\n(Unsuccessfully tried '{path}').").format(
- path=python_path
- )
- continue
- _CACHED_CMDSETS[python_path]=cmdsetclass
-
- # instantiate the cmdset (and catch its errors)
- ifcallable(cmdsetclass):
- cmdsetclass=cmdsetclass(cmdsetobj)
- returncmdsetclass
- exceptImportErroraserr:
- logger.log_trace()
- errstring+=_ERROR_CMDSET_IMPORT
- if_IN_GAME_ERRORS:
- errstring=errstring.format(
- path=python_path,traceback=format_exc(),timestamp=logger.timeformat()
- )
- else:
- errstring=errstring.format(
- path=python_path,traceback=err,timestamp=logger.timeformat()
- )
- break
- exceptKeyError:
- logger.log_trace()
- errstring+=_ERROR_CMDSET_KEYERROR
- errstring=errstring.format(
- classname=classname,path=python_path,timestamp=logger.timeformat()
- )
- break
- exceptSyntaxErroraserr:
- logger.log_trace()
- errstring+=_ERROR_CMDSET_SYNTAXERROR
- if_IN_GAME_ERRORS:
- errstring=errstring.format(
- path=python_path,traceback=format_exc(),timestamp=logger.timeformat()
- )
- else:
- errstring=errstring.format(
- path=python_path,traceback=err,timestamp=logger.timeformat()
- )
- break
- exceptExceptionaserr:
- logger.log_trace()
- errstring+=_ERROR_CMDSET_EXCEPTION
- if_IN_GAME_ERRORS:
- errstring=errstring.format(
- path=python_path,traceback=format_exc(),timestamp=logger.timeformat()
- )
- else:
- errstring=errstring.format(
- path=python_path,traceback=err,timestamp=logger.timeformat()
- )
- break
-
- iferrstring:
- # returning an empty error cmdset
- errstring=errstring.strip()
- ifnotno_logging:
- logger.log_err(errstring)
- ifemit_to_objandnotServerConfig.objects.conf("server_starting_mode"):
- emit_to_obj.msg(errstring)
- err_cmdset=_ErrorCmdSet()
- err_cmdset.errmessage=errstring
- returnerr_cmdset
- returnNone# undefined error
-
-
-# classes
-
-
-
[docs]classCmdSetHandler(object):
- """
- The CmdSetHandler is always stored on an object, this object is supplied
- as an argument.
-
- The 'current' cmdset is the merged set currently active for this object.
- This is the set the game engine will retrieve when determining which
- commands are available to the object. The cmdset_stack holds a history of
- all CmdSets to allow the handler to remove/add cmdsets at will. Doing so
- will re-calculate the 'current' cmdset.
- """
-
-
[docs]def__init__(self,obj,init_true=True):
- """
- This method is called whenever an object is recreated.
-
- Args:
- obj (Object): An reference to the game object this handler
- belongs to.
- init_true (bool, optional): Set when the handler is initializing
- and loads the current cmdset.
-
- """
- self.obj=obj
-
- # the id of the "merged" current cmdset for easy access.
- self.key=None
- # this holds the "merged" current command set. Note that while the .update
- # method updates this field in order to have it synced when operating on
- # cmdsets in-code, when the game runs, this field is kept up-to-date by
- # the cmdsethandler's get_and_merge_cmdsets!
- self.current=None
- # this holds a history of CommandSets
- self.cmdset_stack=[_EmptyCmdSet(cmdsetobj=self.obj)]
- # this tracks which mergetypes are actually in play in the stack
- self.mergetype_stack=["Union"]
-
- # the subset of the cmdset_paths that are to be stored in the database
- self.persistent_paths=[""]
-
- ifinit_true:
- self.update(init_mode=True)# is then called from the object __init__.
-
- def__str__(self):
- """
- Display current commands
-
- """
-
- strings=["<CmdSetHandler> stack:"]
- mergelist=[]
- iflen(self.cmdset_stack)>1:
- # We have more than one cmdset in stack; list them all
- forsnum,cmdsetinenumerate(self.cmdset_stack):
- mergelist.append(str(snum+1))
- strings.append(f" {snum+1}: {cmdset}")
-
- # Display the currently active cmdset, limited by self.obj's permissions
- mergetype=self.mergetype_stack[-1]
- ifmergetype!=self.current.mergetype:
- merged_on=self.cmdset_stack[-2].key
- mergetype=_("custom {mergetype} on cmdset '{cmdset}'")
- mergetype=mergetype.format(mergetype=mergetype,cmdset=merged_on)
-
- ifmergelist:
- # current is a result of mergers
- mergelist="+".join(mergelist)
- strings.append(f" <Merged {mergelist}>: {self.current}")
- else:
- # current is a single cmdset
- strings.append(" "+str(self.current))
- return"\n".join(strings).rstrip()
-
- def_import_cmdset(self,cmdset_path,emit_to_obj=None):
- """
- Method wrapper for import_cmdset; Loads a cmdset from a
- module.
-
- Args:
- cmdset_path (str): The python path to an cmdset object.
- emit_to_obj (Object): The object to send error messages to
-
- Returns:
- cmdset (Cmdset): The imported cmdset.
-
- """
- ifnotemit_to_obj:
- emit_to_obj=self.obj
- returnimport_cmdset(cmdset_path,self.obj,emit_to_obj)
-
-
[docs]defupdate(self,init_mode=False):
- """
- Re-adds all sets in the handler to have an updated current
-
- Args:
- init_mode (bool, optional): Used automatically right after
- this handler was created; it imports all persistent cmdsets
- from the database.
-
- Notes:
- This method is necessary in order to always have a `.current`
- cmdset when working with the cmdsethandler in code. But the
- CmdSetHandler doesn't (cannot) consider external cmdsets and game
- state. This means that the .current calculated from this method
- will likely not match the true current cmdset as determined at
- run-time by `cmdhandler.get_and_merge_cmdsets()`. So in a running
- game the responsibility of keeping `.current` upt-to-date belongs
- to the central `cmdhandler.get_and_merge_cmdsets()`!
-
- """
- ifinit_mode:
- # reimport all persistent cmdsets
- storage=self.obj.cmdset_storage
- ifstorage:
- self.cmdset_stack=[]
- forpos,pathinenumerate(storage):
- ifpos==0andnotpath:
- self.cmdset_stack=[_EmptyCmdSet(cmdsetobj=self.obj)]
- elifpath:
- cmdset=self._import_cmdset(path)
- ifcmdset:
- ifcmdset.key=="_CMDSET_ERROR":
- # If a cmdset fails to load, check if we have a fallback path to use
- fallback_path=_CMDSET_FALLBACKS.get(path,None)
- iffallback_path:
- err=_ERROR_CMDSET_FALLBACK.format(
- path=path,fallback_path=fallback_path
- )
- logger.log_err(err)
- if_IN_GAME_ERRORS:
- self.obj.msg(err)
- cmdset=self._import_cmdset(fallback_path)
- # If no cmdset is returned from the fallback, we can't go further
- ifnotcmdset:
- err=_ERROR_CMDSET_NO_FALLBACK.format(
- fallback_path=fallback_path
- )
- logger.log_err(err)
- if_IN_GAME_ERRORS:
- self.obj.msg(err)
- continue
- cmdset.persistent=cmdset.key!="_CMDSET_ERROR"
- self.cmdset_stack.append(cmdset)
-
- # merge the stack into a new merged cmdset
- new_current=None
- self.mergetype_stack=[]
- forcmdsetinself.cmdset_stack:
- try:
- # for cmdset's '+' operator, order matters.
- new_current=cmdset+new_current
- exceptTypeError:
- continue
- self.mergetype_stack.append(new_current.actual_mergetype)
- self.current=new_current
-
-
[docs]defadd(self,cmdset,emit_to_obj=None,persistent=False,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)
-
- Args:
- 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.
- 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).
-
- Notes:
- An interesting feature of this method is if you were to send
- it an *already instantiated cmdset* (i.e. not a class), the
- current cmdsethandler's obj attribute will then *not* be
- transferred over to this already instantiated set (this is
- because it might be used elsewhere and can cause strange
- effects). This means you could in principle have the
- handler launch command sets tied to a *different* object
- than the handler. Not sure when this would be useful, but
- 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)
-
- ifcallable(cmdset):
- cmdset=cmdset(self.obj)
- elifisinstance(cmdset,str):
- # this is (maybe) a python path. Try to import from cache.
- cmdset=self._import_cmdset(cmdset)
- ifcmdsetandcmdset.key!="_CMDSET_ERROR":
- cmdset.persistent=persistent
- ifpersistentandcmdset.key!="_CMDSET_ERROR":
- # store the path permanently
- storage=self.obj.cmdset_storageor[""]
- ifdefault_cmdset:
- storage[0]=cmdset.path
- else:
- storage.append(cmdset.path)
- self.obj.cmdset_storage=storage
- ifdefault_cmdset:
- self.cmdset_stack[0]=cmdset
- else:
- 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
- persistent (bool, optional): The new Cmdset should survive a server reboot.
-
- """
- 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]defremove(self,cmdset=None,default_cmdset=False):
- """
- Remove a cmdset from the handler.
-
- Args:
- cmdset (CommandSet or str, optional): This can can be supplied either as a cmdset-key,
- an instance of the CmdSet or a python path to the cmdset.
- If no key is given, the last cmdset in the stack is
- removed. Whenever the cmdset_stack changes, the cmdset is
- updated. If default_cmdset is set, this argument is ignored.
- default_cmdset (bool, optional): If set, this will remove the
- default cmdset (at the bottom of the stack).
-
- """
- ifdefault_cmdset:
- # remove the default cmdset only
- ifself.cmdset_stack:
- cmdset=self.cmdset_stack[0]
- ifcmdset.persistent:
- storage=self.obj.cmdset_storageor[""]
- storage[0]=""
- self.obj.cmdset_storage=storage
- self.cmdset_stack[0]=_EmptyCmdSet(cmdsetobj=self.obj)
- else:
- self.cmdset_stack=[_EmptyCmdSet(cmdsetobj=self.obj)]
- self.update()
- return
-
- iflen(self.cmdset_stack)<2:
- # don't allow deleting default cmdsets here.
- return
-
- ifnotcmdset:
- # remove the last one in the stack
- cmdset=self.cmdset_stack.pop()
- ifcmdset.persistent:
- storage=self.obj.cmdset_storage
- storage.pop()
- self.obj.cmdset_storage=storage
- else:
- # try it as a callable
- ifcallable(cmdset)andhasattr(cmdset,"path"):
- delcmdsets=[csetforcsetinself.cmdset_stack[1:]ifcset.path==cmdset.path]
- else:
- # try it as a path or key
- delcmdsets=[
- cset
- forcsetinself.cmdset_stack[1:]
- ifcset.path==cmdsetorcset.key==cmdset
- ]
- storage=[]
-
- ifany(cset.persistentforcsetindelcmdsets):
- # only hit database if there's need to
- storage=self.obj.cmdset_storage
- updated=False
- forcsetindelcmdsets:
- ifcset.persistent:
- try:
- storage.remove(cset.path)
- updated=True
- exceptValueError:
- # nothing to remove
- pass
- ifupdated:
- self.obj.cmdset_storage=storage
- forcsetindelcmdsets:
- # clean the in-memory stack
- try:
- self.cmdset_stack.remove(cset)
- exceptValueError:
- # nothing to remove
- pass
- # re-sync the cmdsethandler.
- self.update()
-
- # legacy alias
- delete=remove
-
-
[docs]defremove_default(self):
- """
- This explicitly deletes only the default cmdset.
-
- """
- self.remove(default_cmdset=True)
-
- # legacy alias
- delete_default=remove_default
-
-
[docs]defget(self):
- """
- Get all cmdsets.
-
- Returns:
- cmdsets (list): All the command sets currently in the handler.
-
- """
- returnself.cmdset_stack
-
- # backwards-compatible alias
- all=get
-
-
[docs]defclear(self):
- """
- Removes all Command Sets from the handler except the default one
- (use `self.remove_default` to remove that).
-
- """
- self.cmdset_stack=[self.cmdset_stack[0]]
- storage=self.obj.cmdset_storage
- ifstorage:
- storage=storage[0]
- self.obj.cmdset_storage=storage
- self.update()
-
-
[docs]defhas(self,cmdset,must_be_default=False):
- """
- checks so the cmdsethandler contains a given cmdset
-
- Args:
- cmdset (str or Cmdset): Cmdset key, pythonpath or
- Cmdset to check the existence for.
- must_be_default (bool, optional): Only return True if
- the checked cmdset is the default one.
-
- Returns:
- has_cmdset (bool): Whether or not the cmdset is in the handler.
-
- """
- ifcallable(cmdset)andhasattr(cmdset,"path"):
- # try it as a callable
- ifmust_be_default:
- returnself.cmdset_stackand(self.cmdset_stack[0].path==cmdset.path)
- else:
- returnany([csetforcsetinself.cmdset_stackifcset.path==cmdset.path])
- else:
- # try it as a path or key
- ifmust_be_default:
- returnself.cmdset_stackand(
- self.cmdset_stack[0].key==cmdsetorself.cmdset_stack[0].path==cmdset
- )
- else:
- returnany(
- [
- cset
- forcsetinself.cmdset_stack
- ifcset.path==cmdsetorcset.key==cmdset
- ]
- )
-
- # backwards-compatability alias
- has_cmdset=has
-
-
[docs]defreset(self):
- """
- Force reload of all cmdsets in handler. This should be called
- after _CACHED_CMDSETS have been cleared (normally this is
- handled automatically by @reload).
-
- """
- new_cmdset_stack=[]
- forcmdsetinself.cmdset_stack:
- ifcmdset.key=="_EMPTY_CMDSET":
- new_cmdset_stack.append(cmdset)
- else:
- new_cmdset_stack.append(self._import_cmdset(cmdset.path))
- self.cmdset_stack=new_cmdset_stack
- self.update()
-"""
-The base Command class.
-
-All commands in Evennia inherit from the 'Command' class in this module.
-
-"""
-importre
-importmath
-importinspect
-
-fromdjango.confimportsettings
-fromdjango.urlsimportreverse
-fromdjango.utils.textimportslugify
-
-fromevennia.locks.lockhandlerimportLockHandler
-fromevennia.utils.utilsimportis_iter,fill,lazy_property,make_iter
-fromevennia.utils.evtableimportEvTable
-fromevennia.utils.ansiimportANSIString
-
-
-CMD_IGNORE_PREFIXES=settings.CMD_IGNORE_PREFIXES
-
-
-
[docs]classInterruptCommand(Exception):
-
- """Cleanly interrupt a command."""
-
- pass
-
-
-def_init_command(cls,**kwargs):
- """
- Helper command.
- Makes sure all data are stored as lowercase and
- do checking on all properties that should be in list form.
- Sets up locks to be more forgiving. This is used both by the metaclass
- and (optionally) at instantiation time.
-
- If kwargs are given, these are set as instance-specific properties
- on the command - but note that the Command instance is *re-used* on a given
- host object, so a kwarg value set on the instance will *remain* on the instance
- for subsequent uses of that Command on that particular object.
-
- """
- foriinrange(len(kwargs)):
- # used for dynamic creation of commands
- key,value=kwargs.popitem()
- setattr(cls,key,value)
-
- cls.key=cls.key.lower()
- ifcls.aliasesandnotis_iter(cls.aliases):
- try:
- cls.aliases=[str(alias).strip().lower()foraliasincls.aliases.split(",")]
- exceptException:
- cls.aliases=[]
- cls.aliases=list(set(aliasforaliasincls.aliasesifaliasandalias!=cls.key))
-
- # optimization - a set is much faster to match against than a list
- cls._matchset=set([cls.key]+cls.aliases)
- # optimization for looping over keys+aliases
- cls._keyaliases=tuple(cls._matchset)
-
- # by default we don't save the command between runs
- ifnothasattr(cls,"save_for_next"):
- cls.save_for_next=False
-
- # pre-process locks as defined in class definition
- temp=[]
- ifhasattr(cls,"permissions"):
- cls.locks=cls.permissions
- ifnothasattr(cls,"locks"):
- # default if one forgets to define completely
- cls.locks="cmd:all()"
- if"cmd:"notincls.locks:
- cls.locks="cmd:all();"+cls.locks
- forlockstringincls.locks.split(";"):
- iflockstringand":"notinlockstring:
- lockstring="cmd:%s"%lockstring
- temp.append(lockstring)
- cls.lock_storage=";".join(temp)
-
- ifhasattr(cls,"arg_regex")andisinstance(cls.arg_regex,str):
- cls.arg_regex=re.compile(r"%s"%cls.arg_regex,re.I+re.UNICODE)
- ifnothasattr(cls,"auto_help"):
- cls.auto_help=True
- ifnothasattr(cls,"is_exit"):
- cls.is_exit=False
- ifnothasattr(cls,"help_category"):
- cls.help_category="general"
- ifnothasattr(cls,"retain_instance"):
- cls.retain_instance=False
-
- # make sure to pick up the parent's docstring if the child class is
- # missing one (important for auto-help)
- ifcls.__doc__isNone:
- forparent_classininspect.getmro(cls):
- ifparent_class.__doc__isnotNone:
- cls.__doc__=parent_class.__doc__
- break
- cls.help_category=cls.help_category.lower()
-
- # pre-prepare a help index entry for quicker lookup
- # strip the @- etc to allow help to be agnostic
- stripped_key=cls.key[1:]ifcls.keyandcls.key[0]inCMD_IGNORE_PREFIXESelse""
- stripped_aliases=(
- " ".join(al[1:]ifalandal[0]inCMD_IGNORE_PREFIXESelseal
- foralincls.aliases))
- cls.search_index_entry={
- "key":cls.key,
- "aliases":" ".join(cls.aliases),
- "no_prefix":f"{stripped_key}{stripped_aliases}",
- "category":cls.help_category,
- "text":cls.__doc__,
- "tags":""
- }
-
-
-
[docs]classCommandMeta(type):
- """
- The metaclass cleans up all properties on the class
- """
-
-
-
-
-# The Command class is the basic unit of an Evennia command; when
-# defining new commands, the admin subclass this class and
-# define their own parser method to handle the input. The
-# advantage of this is inheritage; commands that have similar
-# structure can parse the input string the same way, minimizing
-# parsing errors.
-
-
-
[docs]classCommand(metaclass=CommandMeta):
- """
- ## Base command
-
- (you may see this if a child command had no help text defined)
-
- Usage:
- command [args]
-
- This is the base command class. Inherit from this
- to create new commands.
-
- The cmdhandler makes the following variables available to the
- command methods (so you can always assume them to be there):
- self.caller - the game object calling the command
- self.cmdstring - the command name used to trigger this command (allows
- you to know which alias was used, for example)
- cmd.args - everything supplied to the command following the cmdstring
- (this is usually what is parsed in self.parse())
- cmd.cmdset - the cmdset from which this command was matched (useful only
- seldomly, notably for help-type commands, to create dynamic
- help entries and lists)
- cmd.obj - the object on which this command is defined. If a default command,
- this is usually the same as caller.
- cmd.rawstring - the full raw string input, including any args and no parsing.
-
- The following class properties can/should be defined on your child class:
-
- key - identifier for command (e.g. "look")
- aliases - (optional) list of aliases (e.g. ["l", "loo"])
- locks - lock string (default is "cmd:all()")
- help_category - how to organize this help entry in help system
- (default is "General")
- auto_help - defaults to True. Allows for turning off auto-help generation
- arg_regex - (optional) raw string regex defining how the argument part of
- the command should look in order to match for this command
- (e.g. must it be a space between cmdname and arg?)
- auto_help_display_key - (optional) if given, this replaces the string shown
- in the auto-help listing. This is particularly useful for system-commands
- whose actual key is not really meaningful.
-
- (Note that if auto_help is on, this initial string is also used by the
- system to create the help entry for the command, so it's a good idea to
- format it similar to this one). This behavior can be changed by
- overriding the method 'get_help' of a command: by default, this
- method returns cmd.__doc__ (that is, this very docstring, or
- the docstring of your command). You can, however, extend or
- replace this without disabling auto_help.
- """
-
- # the main way to call this command (e.g. 'look')
- key="command"
- # alternative ways to call the command (e.g. 'l', 'glance', 'examine')
- aliases=[]
- # a list of lock definitions on the form
- # cmd:[NOT] func(args) [ AND|OR][ NOT] func2(args)
- locks=settings.COMMAND_DEFAULT_LOCKS
- # used by the help system to group commands in lists.
- help_category=settings.COMMAND_DEFAULT_HELP_CATEGORY
- # This allows to turn off auto-help entry creation for individual commands.
- auto_help=True
- # optimization for quickly separating exit-commands from normal commands
- is_exit=False
- # define the command not only by key but by the regex form of its arguments
- arg_regex=settings.COMMAND_DEFAULT_ARG_REGEX
- # whether self.msg sends to all sessions of a related account/object (default
- # is to only send to the session sending the command).
- msg_all_sessions=settings.COMMAND_DEFAULT_MSG_ALL_SESSIONS
- # whether the exact command instance should be retained between command calls.
- # By default it's False; this allows for retaining state and saves some CPU, but
- # can cause cross-talk between users if multiple users access the same command
- # (especially if the command is using yield)
- retain_instance=False
-
- # auto-set (by Evennia on command instantiation) are:
- # obj - which object this command is defined on
- # session - which session is responsible for triggering this command. Only set
- # if triggered by an account.
-
-
[docs]def__init__(self,**kwargs):
- """
- The lockhandler works the same as for objects.
- optional kwargs will be set as properties on the Command at runtime,
- overloading evential same-named class properties.
-
- """
- ifkwargs:
- _init_command(self,**kwargs)
-
- def__str__(self):
- """
- Print the command key
- """
- returnself.key
-
- def__eq__(self,cmd):
- """
- Compare two command instances to each other by matching their
- key and aliases.
-
- Args:
- cmd (Command or str): Allows for equating both Command
- objects and their keys.
-
- Returns:
- equal (bool): If the commands are equal or not.
-
- """
- try:
- # first assume input is a command (the most common case)
- returnself._matchset.intersection(cmd._matchset)
- exceptAttributeError:
- # probably got a string
- returncmdinself._matchset
-
- def__hash__(self):
- """
- Python 3 requires that any class which implements __eq__ must also
- implement __hash__ and that the corresponding hashes for equivalent
- instances are themselves equivalent.
-
- Technically, the following implementation is only valid for comparison
- against other Commands, as our __eq__ supports comparison against
- str, too.
-
- """
- returnhash("\n".join(self._matchset))
-
- def__ne__(self,cmd):
- """
- The logical negation of __eq__. Since this is one of the most
- called methods in Evennia (along with __eq__) we do some
- code-duplication here rather than issuing a method-lookup to
- __eq__.
- """
- try:
- returnself._matchset.isdisjoint(cmd._matchset)
- exceptAttributeError:
- returncmdnotinself._matchset
-
- def__contains__(self,query):
- """
- This implements searches like 'if query in cmd'. It's a fuzzy
- matching used by the help system, returning True if query can
- be found as a substring of the commands key or its aliases.
-
- Args:
- query (str): query to match against. Should be lower case.
-
- Returns:
- result (bool): Fuzzy matching result.
-
- """
- returnany(queryinkeyaliasforkeyaliasinself._keyaliases)
-
- def_optimize(self):
- """
- Optimize the key and aliases for lookups.
- """
- # optimization - a set is much faster to match against than a list
- self._matchset=set([self.key]+self.aliases)
- # optimization for looping over keys+aliases
- self._keyaliases=tuple(self._matchset)
-
-
[docs]defset_key(self,new_key):
- """
- Update key.
-
- Args:
- new_key (str): The new key.
-
- Notes:
- This is necessary to use to make sure the optimization
- caches are properly updated as well.
-
- """
- self.key=new_key.lower()
- self._optimize()
-
-
[docs]defset_aliases(self,new_aliases):
- """
- Replace aliases with new ones.
-
- Args:
- new_aliases (str or list): Either a ;-separated string
- or a list of aliases. These aliases will replace the
- existing ones, if any.
-
- Notes:
- This is necessary to use to make sure the optimization
- caches are properly updated as well.
-
- """
- ifisinstance(new_aliases,str):
- new_aliases=new_aliases.split(";")
- aliases=(str(alias).strip().lower()foraliasinmake_iter(new_aliases))
- self.aliases=list(set(aliasforaliasinaliasesifalias!=self.key))
- self._optimize()
-
-
[docs]defmatch(self,cmdname):
- """
- This is called by the system when searching the available commands,
- in order to determine if this is the one we wanted. cmdname was
- previously extracted from the raw string by the system.
-
- Args:
- cmdname (str): Always lowercase when reaching this point.
-
- Returns:
- result (bool): Match result.
-
- """
- returncmdnameinself._matchset
-
-
[docs]defaccess(self,srcobj,access_type="cmd",default=False):
- """
- This hook is called by the cmdhandler to determine if srcobj
- is allowed to execute this command. It should return a boolean
- value and is not normally something that need to be changed since
- it's using the Evennia permission system directly.
-
- Args:
- srcobj (Object): Object trying to gain permission
- access_type (str, optional): The lock type to check.
- default (bool, optional): The fallback result if no lock
- of matching `access_type` is found on this Command.
-
- """
- returnself.lockhandler.check(srcobj,access_type,default=default)
-
-
[docs]defmsg(self,text=None,to_obj=None,from_obj=None,session=None,**kwargs):
- """
- This is a shortcut instead of calling msg() directly on an
- object - it will detect if caller is an Object or an Account and
- also appends self.session automatically if self.msg_all_sessions is False.
-
- Args:
- text (str, optional): Text string of message to send.
- to_obj (Object, optional): Target object of message. Defaults to self.caller.
- from_obj (Object, optional): Source of message. Defaults to to_obj.
- session (Session, optional): Supply data only to a unique
- session (ignores the value of `self.msg_all_sessions`).
-
- Keyword Args:
- options (dict): Options to the protocol.
- any (any): All other keywords are interpreted as th
- name of send-instructions.
-
- """
- from_obj=from_objorself.caller
- to_obj=to_objorfrom_obj
- ifnotsessionandnotself.msg_all_sessions:
- ifto_obj==self.caller:
- session=self.session
- else:
- session=to_obj.sessions.get()
- to_obj.msg(text=text,from_obj=from_obj,session=session,**kwargs)
-
-
[docs]defexecute_cmd(self,raw_string,session=None,obj=None,**kwargs):
- """
- A shortcut of execute_cmd on the caller. It appends the
- session automatically.
-
- Args:
- raw_string (str): Execute this string as a command input.
- session (Session, optional): If not given, the current command's Session will be used.
- obj (Object or Account, optional): Object or Account on which to call the execute_cmd.
- If not given, self.caller will be used.
-
- Keyword Args:
- Other keyword arguments will be added to the found command
- object instace as variables before it executes. This is
- unused by default Evennia but may be used to set flags and
- change operating paramaters for commands at run-time.
-
- """
- obj=self.callerifobjisNoneelseobj
- session=self.sessionifsessionisNoneelsesession
- obj.execute_cmd(raw_string,session=session,**kwargs)
-
- # Common Command hooks
-
-
[docs]defat_pre_cmd(self):
- """
- This hook is called before self.parse() on all commands. If
- this hook returns anything but False/None, the command
- sequence is aborted.
-
- """
- pass
-
-
[docs]defat_post_cmd(self):
- """
- This hook is called after the command has finished executing
- (after self.func()).
-
- """
- pass
-
-
[docs]defparse(self):
- """
- Once the cmdhandler has identified this as the command we
- want, this function is run. If many of your commands have a
- similar syntax (for example 'cmd arg1 = arg2') you should
- simply define this once and just let other commands of the
- same form inherit from this. See the docstring of this module
- for which object properties are available to use (notably
- self.args).
-
- """
- pass
-
-
[docs]defget_command_info(self):
- """
- This is the default output of func() if no func() overload is done.
- Provided here as a separate method so that it can be called for debugging
- purposes when making commands.
-
- """
- variables="\n".join(
- " |w{}|n ({}): {}".format(key,type(val),val)forkey,valinself.__dict__.items()
- )
- string=f"""
-Command {self} has no defined `func()` - showing on-command variables:
-{variables}
- """
- # a simple test command to show the available properties
- string+="-"*50
- string+="\n|w%s|n - Command variables from evennia:\n"%self.key
- string+="-"*50
- string+="\nname of cmd (self.key): |w%s|n\n"%self.key
- string+="cmd aliases (self.aliases): |w%s|n\n"%self.aliases
- string+="cmd locks (self.locks): |w%s|n\n"%self.locks
- string+="help category (self.help_category): |w%s|n\n"%self.help_category.capitalize()
- string+="object calling (self.caller): |w%s|n\n"%self.caller
- string+="object storing cmdset (self.obj): |w%s|n\n"%self.obj
- string+="command string given (self.cmdstring): |w%s|n\n"%self.cmdstring
- # show cmdset.key instead of cmdset to shorten output
- string+=fill(
- "current cmdset (self.cmdset): |w%s|n\n"
- %(self.cmdset.keyifself.cmdset.keyelseself.cmdset.__class__)
- )
-
- self.caller.msg(string)
-
-
[docs]deffunc(self):
- """
- 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())
-
- """
- self.get_command_info()
-
-
[docs]defget_extra_info(self,caller,**kwargs):
- """
- Display some extra information that may help distinguish this
- command from others, for instance, in a disambiguity prompt.
-
- If this command is a potential match in an ambiguous
- situation, one distinguishing feature may be its attachment to
- a nearby object, so we include this if available.
-
- Args:
- caller (TypedObject): The caller who typed an ambiguous
- term handed to the search function.
-
- Returns:
- A string with identifying information to disambiguate the
- object, conventionally with a preceding space.
-
- """
- ifhasattr(self,"obj")andself.objandself.obj!=caller:
- return" (%s)"%self.obj.get_display_name(caller).strip()
- return""
-
-
[docs]defget_help(self,caller,cmdset):
- """
- Return the help message for this command and this caller.
-
- By default, return self.__doc__ (the docstring just under
- the class definition). You can override this behavior,
- though, and even customize it depending on the caller, or other
- commands the caller can use.
-
- Args:
- caller (Object or Account): the caller asking for help on the command.
- cmdset (CmdSet): the command set (if you need additional commands).
-
- Returns:
- docstring (str): the help text to provide the caller for this command.
-
- """
- returnself.__doc__
-
-
[docs]defweb_get_detail_url(self):
- """
- Returns the URI path for a View that allows users to view details for
- this object.
-
- ex. 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.
-
- ex.
- ::
- 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.
-
- 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:
- path (str): URI path to object detail page, if defined.
-
- """
- try:
- returnreverse(
- 'help-entry-detail',
- kwargs={"category":slugify(self.help_category),"topic":slugify(self.key)},
- )
- exceptExceptionase:
- return"#"
-
-
[docs]defweb_get_admin_url(self):
- """
- Returns the URI path for the Django Admin page for this object.
-
- ex. Account#1 = '/admin/accounts/accountdb/1/change/'
-
- Returns:
- path (str): URI path to Django Admin page for object.
-
- """
- returnFalse
-
-
[docs]defclient_width(self):
- """
- Get the client screenwidth for the session using this command.
-
- Returns:
- client width (int): The width (in characters) of the client window.
-
- """
- ifself.session:
- returnself.session.protocol_flags.get(
- "SCREENWIDTH",{0:settings.CLIENT_DEFAULT_WIDTH}
- )[0]
- returnsettings.CLIENT_DEFAULT_WIDTH
-
-
[docs]defstyled_table(self,*args,**kwargs):
- """
- Create an EvTable styled by on user preferences.
-
- Args:
- *args (str): Column headers. If not colored explicitly, these will get colors
- from user options.
- Keyword Args:
- any (str, int or dict): EvTable options, including, optionally a `table` dict
- detailing the contents of the table.
- Returns:
- table (EvTable): An initialized evtable entity, either complete (if using `table` kwarg)
- or incomplete and ready for use with `.add_row` or `.add_collumn`.
-
- """
- border_color=self.account.options.get("border_color")
- column_color=self.account.options.get("column_names_color")
-
- colornames=["|%s%s|n"%(column_color,col)forcolinargs]
-
- h_line_char=kwargs.pop("header_line_char","~")
- header_line_char=ANSIString(f"|{border_color}{h_line_char}|n")
- c_char=kwargs.pop("corner_char","+")
- corner_char=ANSIString(f"|{border_color}{c_char}|n")
-
- b_left_char=kwargs.pop("border_left_char","||")
- border_left_char=ANSIString(f"|{border_color}{b_left_char}|n")
-
- b_right_char=kwargs.pop("border_right_char","||")
- border_right_char=ANSIString(f"|{border_color}{b_right_char}|n")
-
- b_bottom_char=kwargs.pop("border_bottom_char","-")
- border_bottom_char=ANSIString(f"|{border_color}{b_bottom_char}|n")
-
- b_top_char=kwargs.pop("border_top_char","-")
- border_top_char=ANSIString(f"|{border_color}{b_top_char}|n")
-
- table=EvTable(
- *colornames,
- header_line_char=header_line_char,
- corner_char=corner_char,
- 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
-
- def_render_decoration(
- self,
- header_text=None,
- fill_character=None,
- edge_character=None,
- mode="header",
- color_header=True,
- width=None,
- ):
- """
- Helper for formatting a string into a pretty display, for a header, separator or footer.
-
- Keyword Args:
- header_text (str): Text to include in header.
- fill_character (str): This single character will be used to fill the width of the
- display.
- edge_character (str): This character caps the edges of the display.
- mode(str): One of 'header', 'separator' or 'footer'.
- color_header (bool): If the header should be colorized based on user options.
- width (int): If not given, the client's width will be used if available.
-
- Returns:
- string (str): The decorated and formatted text.
-
- """
-
- colors=dict()
- colors["border"]=self.account.options.get("border_color")
- colors["headertext"]=self.account.options.get("%s_text_color"%mode)
- colors["headerstar"]=self.account.options.get("%s_star_color"%mode)
-
- width=widthorself.client_width()
- ifedge_character:
- width-=2
-
- ifheader_text:
- ifcolor_header:
- header_text=ANSIString(header_text).clean()
- header_text=ANSIString("|n|%s%s|n"%(colors["headertext"],header_text))
- ifmode=="header":
- begin_center=ANSIString(
- "|n|%s<|%s* |n"%(colors["border"],colors["headerstar"])
- )
- end_center=ANSIString("|n |%s*|%s>|n"%(colors["headerstar"],colors["border"]))
- center_string=ANSIString(begin_center+header_text+end_center)
- else:
- center_string=ANSIString("|n |%s%s |n"%(colors["headertext"],header_text))
- else:
- center_string=""
-
- fill_character=self.account.options.get("%s_fill"%mode)
-
- remain_fill=width-len(center_string)
- ifremain_fill%2==0:
- right_width=remain_fill/2
- left_width=remain_fill/2
- else:
- right_width=math.floor(remain_fill/2)
- left_width=math.ceil(remain_fill/2)
-
- right_fill=ANSIString("|n|%s%s|n"%(colors["border"],fill_character*int(right_width)))
- left_fill=ANSIString("|n|%s%s|n"%(colors["border"],fill_character*int(left_width)))
-
- ifedge_character:
- edge_fill=ANSIString("|n|%s%s|n"%(colors["border"],edge_character))
- main_string=ANSIString(center_string)
- final_send=(
- ANSIString(edge_fill)+left_fill+main_string+right_fill+ANSIString(edge_fill)
- )
- else:
- final_send=left_fill+ANSIString(center_string)+right_fill
- returnfinal_send
-
-
-"""
-Account (OOC) commands. These are stored on the Account object
-and self.caller is thus always an Account, not an Object/Character.
-
-These commands go in the AccountCmdset and are accessible also
-when puppeting a Character (although with lower priority)
-
-These commands use the account_caller property which tells the command
-parent (MuxCommand, usually) to setup caller correctly. They use
-self.account to make sure to always use the account object rather than
-self.caller (which change depending on the level you are calling from)
-The property self.character can be used to access the character when
-these commands are triggered with a connected character (such as the
-case of the `ooc` command), it is None if we are OOC.
-
-Note that under MULTISESSION_MODE > 2, Account commands should use
-self.msg() and similar methods to reroute returns to the correct
-method. Otherwise all text will be returned to all connected sessions.
-
-"""
-importtime
-fromcodecsimportlookupascodecs_lookup
-fromdjango.confimportsettings
-fromevennia.server.sessionhandlerimportSESSIONS
-fromevennia.utilsimportutils,create,logger,search
-
-COMMAND_DEFAULT_CLASS=utils.class_from_module(settings.COMMAND_DEFAULT_CLASS)
-
-_MAX_NR_CHARACTERS=settings.MAX_NR_CHARACTERS
-_MULTISESSION_MODE=settings.MULTISESSION_MODE
-
-# limit symbol import for API
-__all__=(
- "CmdOOCLook",
- "CmdIC",
- "CmdOOC",
- "CmdPassword",
- "CmdQuit",
- "CmdCharCreate",
- "CmdOption",
- "CmdSessions",
- "CmdWho",
- "CmdColorTest",
- "CmdQuell",
- "CmdCharDelete",
- "CmdStyle",
-)
-
-
-classMuxAccountLookCommand(COMMAND_DEFAULT_CLASS):
- """
- Custom parent (only) parsing for OOC looking, sets a "playable"
- property on the command based on the parsing.
-
- """
-
- defparse(self):
- """Custom parsing"""
-
- super().parse()
-
- if_MULTISESSION_MODE<2:
- # only one character allowed - not used in this mode
- self.playable=None
- return
-
- playable=self.account.db._playable_characters
- ifplayableisnotNone:
- # clean up list if character object was deleted in between
- ifNoneinplayable:
- playable=[characterforcharacterinplayableifcharacter]
- self.account.db._playable_characters=playable
- # store playable property
- ifself.args:
- self.playable=dict((utils.to_str(char.key.lower()),char)forcharinplayable).get(
- self.args.lower(),None
- )
- else:
- self.playable=playable
-
-
-# Obs - these are all intended to be stored on the Account, and as such,
-# use self.account instead of self.caller, just to be sure. Also self.msg()
-# is used to make sure returns go to the right session
-
-# note that this is inheriting from MuxAccountLookCommand,
-# and has the .playable property.
-
[docs]classCmdOOCLook(MuxAccountLookCommand):
- """
- look while out-of-character
-
- Usage:
- look
-
- Look in the ooc state.
- """
-
- # This is an OOC version of the look command. Since a
- # Account doesn't have an in-game existence, there is no
- # concept of location or "self". If we are controlling
- # a character, pass control over to normal look.
-
- key="look"
- aliases=["l","ls"]
- locks="cmd:all()"
- help_category="General"
-
- # this is used by the parent
- account_caller=True
-
-
[docs]deffunc(self):
- """implement the ooc look command"""
-
- if_MULTISESSION_MODE<2:
- # only one character allowed
- self.msg("You are out-of-character (OOC).\nUse |wic|n to get back into the game.")
- return
-
- # call on-account look helper method
- self.msg(self.account.at_look(target=self.playable,session=self.session))
-
-
-
[docs]classCmdCharCreate(COMMAND_DEFAULT_CLASS):
- """
- create a new character
-
- Usage:
- charcreate <charname> [= desc]
-
- Create a new character, optionally giving it a description. You
- may use upper-case letters in the name - you will nevertheless
- always be able to access your character using lower-case letters
- if you want.
- """
-
- key="charcreate"
- locks="cmd:pperm(Player)"
- help_category="General"
-
- # this is used by the parent
- account_caller=True
-
-
[docs]deffunc(self):
- """create the new character"""
- account=self.account
- ifnotself.args:
- self.msg("Usage: charcreate <charname> [= description]")
- return
- key=self.lhs
- desc=self.rhs
-
- charmax=_MAX_NR_CHARACTERS
-
- ifnotaccount.is_superuserand(
- account.db._playable_charactersandlen(account.db._playable_characters)>=charmax
- ):
- plural=""ifcharmax==1else"s"
- self.msg(f"You may only create a maximum of {charmax} character{plural}.")
- return
- fromevennia.objects.modelsimportObjectDB
-
- typeclass=settings.BASE_CHARACTER_TYPECLASS
-
- ifObjectDB.objects.filter(db_typeclass_path=typeclass,db_key__iexact=key):
- # check if this Character already exists. Note that we are only
- # searching the base character typeclass here, not any child
- # classes.
- self.msg("|rA character named '|w%s|r' already exists.|n"%key)
- return
-
- # create the character
- start_location=ObjectDB.objects.get_id(settings.START_LOCATION)
- default_home=ObjectDB.objects.get_id(settings.DEFAULT_HOME)
- permissions=settings.PERMISSION_ACCOUNT_DEFAULT
- new_character=create.create_object(
- typeclass,key=key,location=start_location,home=default_home,permissions=permissions
- )
- # only allow creator (and developers) to puppet this char
- new_character.locks.add(
- "puppet:id(%i) or pid(%i) or perm(Developer) or pperm(Developer);delete:id(%i) or perm(Admin)"
- %(new_character.id,account.id,account.id)
- )
- account.db._playable_characters.append(new_character)
- ifdesc:
- new_character.db.desc=desc
- elifnotnew_character.db.desc:
- new_character.db.desc="This is a character."
- self.msg(
- "Created new character %s. Use |wic %s|n to enter the game as this character."
- %(new_character.key,new_character.key)
- )
- logger.log_sec(
- "Character Created: %s (Caller: %s, IP: %s)."
- %(new_character,account,self.session.address)
- )
-
-
-
[docs]classCmdCharDelete(COMMAND_DEFAULT_CLASS):
- """
- delete a character - this cannot be undone!
-
- Usage:
- chardelete <charname>
-
- Permanently deletes one of your characters.
- """
-
- key="chardelete"
- locks="cmd:pperm(Player)"
- help_category="General"
-
-
[docs]deffunc(self):
- """delete the character"""
- account=self.account
-
- ifnotself.args:
- self.msg("Usage: chardelete <charactername>")
- return
-
- # use the playable_characters list to search
- match=[
- char
- forcharinutils.make_iter(account.db._playable_characters)
- ifchar.key.lower()==self.args.lower()
- ]
- ifnotmatch:
- self.msg("You have no such character to delete.")
- return
- eliflen(match)>1:
- self.msg(
- "Aborting - there are two characters with the same name. Ask an admin to delete the right one."
- )
- return
- else:# one match
- fromevennia.utils.evmenuimportget_input
-
- def_callback(caller,callback_prompt,result):
- ifresult.lower()=="yes":
- # only take action
- delobj=caller.ndb._char_to_delete
- key=delobj.key
- caller.db._playable_characters=[
- pcforpcincaller.db._playable_charactersifpc!=delobj
- ]
- delobj.delete()
- self.msg("Character '%s' was permanently deleted."%key)
- logger.log_sec(
- "Character Deleted: %s (Caller: %s, IP: %s)."
- %(key,account,self.session.address)
- )
- else:
- self.msg("Deletion was aborted.")
- delcaller.ndb._char_to_delete
-
- match=match[0]
- account.ndb._char_to_delete=match
-
- # Return if caller has no permission to delete this
- ifnotmatch.access(account,"delete"):
- self.msg("You do not have permission to delete this character.")
- return
-
- prompt=(
- "|rThis will permanently destroy '%s'. This cannot be undone.|n Continue yes/[no]?"
- )
- get_input(account,prompt%match.key,_callback)
-
-
-
[docs]classCmdIC(COMMAND_DEFAULT_CLASS):
- """
- control an object you have permission to puppet
-
- Usage:
- ic <character>
-
- Go in-character (IC) as a given Character.
-
- This will attempt to "become" a different object assuming you have
- the right to do so. Note that it's the ACCOUNT character that puppets
- characters/objects and which needs to have the correct permission!
-
- You cannot become an object that is already controlled by another
- account. In principle <character> can be any in-game object as long
- as you the account have access right to puppet it.
- """
-
- key="ic"
- # lock must be all() for different puppeted objects to access it.
- locks="cmd:all()"
- aliases="puppet"
- help_category="General"
-
- # this is used by the parent
- account_caller=True
-
-
[docs]deffunc(self):
- """
- Main puppet method
- """
- account=self.account
- session=self.session
-
- new_character=None
- character_candidates=[]
-
- ifnotself.args:
- character_candidates=[account.db._last_puppet]ifaccount.db._last_puppetelse[]
- ifnotcharacter_candidates:
- self.msg("Usage: ic <character>")
- return
- else:
- # argument given
-
- ifaccount.db._playable_characters:
- # look at the playable_characters list first
- character_candidates.extend(
- account.search(
- self.args,
- candidates=account.db._playable_characters,
- search_object=True,
- quiet=True,
- )
- )
-
- ifaccount.locks.check_lockstring(account,"perm(Builder)"):
- # builders and higher should be able to puppet more than their
- # playable characters.
- ifsession.puppet:
- # start by local search - this helps to avoid the user
- # getting locked into their playable characters should one
- # happen to be named the same as another. We replace the suggestion
- # from playable_characters here - this allows builders to puppet objects
- # with the same name as their playable chars should it be necessary
- # (by going to the same location).
- character_candidates=[
- char
- forcharinsession.puppet.search(self.args,quiet=True)
- ifchar.access(account,"puppet")
- ]
- ifnotcharacter_candidates:
- # fall back to global search only if Builder+ has no
- # playable_characers in list and is not standing in a room
- # with a matching char.
- character_candidates.extend(
- [
- char
- forcharinsearch.object_search(self.args)
- ifchar.access(account,"puppet")
- ]
- )
-
- # handle possible candidates
- ifnotcharacter_candidates:
- self.msg("That is not a valid character choice.")
- return
- iflen(character_candidates)>1:
- self.msg(
- "Multiple targets with the same name:\n%s"
- %", ".join("%s(#%s)"%(obj.key,obj.id)forobjincharacter_candidates)
- )
- return
- else:
- new_character=character_candidates[0]
-
- # do the puppet puppet
- try:
- account.puppet_object(session,new_character)
- account.db._last_puppet=new_character
- logger.log_sec(
- "Puppet Success: (Caller: %s, Target: %s, IP: %s)."
- %(account,new_character,self.session.address)
- )
- exceptRuntimeErrorasexc:
- self.msg("|rYou cannot become |C%s|n: %s"%(new_character.name,exc))
- logger.log_sec(
- "Puppet Failed: %s (Caller: %s, Target: %s, IP: %s)."
- %(exc,account,new_character,self.session.address)
- )
-
-
-# note that this is inheriting from MuxAccountLookCommand,
-# and as such has the .playable property.
-
[docs]classCmdOOC(MuxAccountLookCommand):
- """
- stop puppeting and go ooc
-
- Usage:
- ooc
-
- Go out-of-character (OOC).
-
- This will leave your current character and put you in a incorporeal OOC state.
- """
-
- key="ooc"
- locks="cmd:pperm(Player)"
- aliases="unpuppet"
- help_category="General"
-
- # this is used by the parent
- account_caller=True
-
-
[docs]deffunc(self):
- """Implement function"""
-
- account=self.account
- session=self.session
-
- old_char=account.get_puppet(session)
- ifnotold_char:
- string="You are already OOC."
- self.msg(string)
- return
-
- account.db._last_puppet=old_char
-
- # disconnect
- try:
- account.unpuppet_object(session)
- self.msg("\n|GYou go OOC.|n\n")
-
- if_MULTISESSION_MODE<2:
- # only one character allowed
- self.msg("You are out-of-character (OOC).\nUse |wic|n to get back into the game.")
- return
-
- self.msg(account.at_look(target=self.playable,session=session))
-
- exceptRuntimeErrorasexc:
- self.msg("|rCould not unpuppet from |c%s|n: %s"%(old_char,exc))
-
-
-
[docs]classCmdSessions(COMMAND_DEFAULT_CLASS):
- """
- check your connected session(s)
-
- Usage:
- sessions
-
- Lists the sessions currently connected to your account.
-
- """
-
- key="sessions"
- locks="cmd:all()"
- help_category="General"
-
- # this is used by the parent
- account_caller=True
-
-
[docs]classCmdWho(COMMAND_DEFAULT_CLASS):
- """
- list who is currently online
-
- Usage:
- who
- doing
-
- Shows who is currently online. Doing is an alias that limits info
- also for those with all permissions.
- """
-
- key="who"
- aliases="doing"
- locks="cmd:all()"
-
- # this is used by the parent
- account_caller=True
-
-
[docs]classCmdOption(COMMAND_DEFAULT_CLASS):
- """
- Set an account option
-
- Usage:
- option[/save] [name = value]
-
- Switches:
- save - Save the current option settings for future logins.
- clear - Clear the saved options.
-
- This command allows for viewing and setting client interface
- settings. Note that saved options may not be able to be used if
- later connecting with a client with different capabilities.
-
-
- """
-
- key="option"
- aliases="options"
- switch_options=("save","clear")
- locks="cmd:all()"
-
- # this is used by the parent
- account_caller=True
-
-
[docs]classCmdPassword(COMMAND_DEFAULT_CLASS):
- """
- change your password
-
- Usage:
- password <old password> = <new password>
-
- Changes your password. Make sure to pick a safe one.
- """
-
- key="password"
- locks="cmd:pperm(Player)"
-
- # this is used by the parent
- account_caller=True
-
-
[docs]classCmdQuit(COMMAND_DEFAULT_CLASS):
- """
- quit the game
-
- Usage:
- quit
-
- Switch:
- all - disconnect all connected sessions
-
- Gracefully disconnect your current session from the
- game. Use the /all switch to disconnect from all sessions.
- """
-
- key="quit"
- switch_options=("all",)
- locks="cmd:all()"
-
- # this is used by the parent
- account_caller=True
-
-
[docs]deffunc(self):
- """hook function"""
- account=self.account
-
- if"all"inself.switches:
- account.msg(
- "|RQuitting|n all sessions. Hope to see you soon again.",session=self.session
- )
- reason="quit/all"
- forsessioninaccount.sessions.all():
- account.disconnect_session_from_account(session,reason)
- else:
- nsess=len(account.sessions.all())
- reason="quit"
- ifnsess==2:
- account.msg("|RQuitting|n. One session is still connected.",session=self.session)
- elifnsess>2:
- account.msg(
- "|RQuitting|n. %i sessions are still connected."%(nsess-1),
- session=self.session,
- )
- else:
- # we are quitting the last available session
- account.msg("|RQuitting|n. Hope to see you again, soon.",session=self.session)
- account.disconnect_session_from_account(self.session,reason)
-
-
-
[docs]classCmdColorTest(COMMAND_DEFAULT_CLASS):
- """
- testing which colors your client support
-
- Usage:
- 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
- 16-color ansi (supported in most muds) or the 256-color xterm256
- standard. No checking is done to determine your client supports
- color - if not you will see rubbish appear.
- """
-
- key="color"
- locks="cmd:all()"
- help_category="General"
-
- # this is used by the parent
- account_caller=True
-
- # the slices of the ANSI_PARSER lists to use for retrieving the
- # relevant color tags to display. Replace if using another schema.
- # This command can only show one set of markup.
- slice_bright_fg=slice(7,15)# from ANSI_PARSER.ansi_map
- slice_dark_fg=slice(15,23)# from ANSI_PARSER.ansi_map
- slice_dark_bg=slice(-8,None)# from ANSI_PARSER.ansi_map
- slice_bright_bg=slice(None,None)# from ANSI_PARSER.ansi_xterm256_bright_bg_map
-
-
[docs]deftable_format(self,table):
- """
- Helper method to format the ansi/xterm256 tables.
- Takes a table of columns [[val,val,...],[val,val,...],...]
- """
- ifnottable:
- return[[]]
-
- extra_space=1
- max_widths=[max([len(str(val))forvalincol])forcolintable]
- ftable=[]
- forirowinrange(len(table[0])):
- ftable.append(
- [
- str(col[irow]).ljust(max_widths[icol])+" "*extra_space
- foricol,colinenumerate(table)
- ]
- )
- returnftable
[docs]classCmdQuell(COMMAND_DEFAULT_CLASS):
- """
- use character's permissions instead of account's
-
- Usage:
- quell
- unquell
-
- Normally the permission level of the Account is used when puppeting a
- Character/Object to determine access. This command will switch the lock
- system to make use of the puppeted Object's permissions instead. This is
- useful mainly for testing.
- Hierarchical permission quelling only work downwards, thus an Account cannot
- use a higher-permission Character to escalate their permission level.
- Use the unquell command to revert back to normal operation.
- """
-
- key="quell"
- aliases=["unquell"]
- locks="cmd:pperm(Player)"
- help_category="General"
-
- # this is used by the parent
- account_caller=True
-
- def_recache_locks(self,account):
- """Helper method to reset the lockhandler on an already puppeted object"""
- ifself.session:
- char=self.session.puppet
- ifchar:
- # we are already puppeting an object. We need to reset
- # the lock caches (otherwise the superuser status change
- # won't be visible until repuppet)
- char.locks.reset()
- account.locks.reset()
-
-
[docs]deffunc(self):
- """Perform the command"""
- account=self.account
- permstr=(
- account.is_superuser
- and" (superuser)"
- or"(%s)"%(", ".join(account.permissions.all()))
- )
- ifself.cmdstringin("unquell","unquell"):
- ifnotaccount.attributes.get("_quell"):
- self.msg("Already using normal Account permissions %s."%permstr)
- else:
- account.attributes.remove("_quell")
- self.msg("Account permissions %s restored."%permstr)
- else:
- ifaccount.attributes.get("_quell"):
- self.msg("Already quelling Account %s permissions."%permstr)
- return
- account.attributes.add("_quell",True)
- puppet=self.session.puppetifself.sessionelseNone
- ifpuppet:
- cpermstr="(%s)"%", ".join(puppet.permissions.all())
- cpermstr="Quelling to current puppet's permissions %s."%cpermstr
- cpermstr+=(
- "\n(Note: If this is higher than Account permissions %s,"
- " the lowest of the two will be used.)"%permstr
- )
- cpermstr+="\nUse unquell to return to normal permission usage."
- self.msg(cpermstr)
- else:
- self.msg("Quelling Account permissions%s. Use unquell to get them back."%permstr)
- self._recache_locks(account)
-
-
-
[docs]classCmdStyle(COMMAND_DEFAULT_CLASS):
- """
- In-game style options
-
- Usage:
- style
- style <option> = <value>
-
- Configure stylings for in-game display elements like table borders, help
- entriest etc. Use without arguments to see all available options.
-
- """
-
- key="style"
- switch_options=["clear"]
-
-
[docs]classCmdBoot(COMMAND_DEFAULT_CLASS):
- """
- kick an account from the server.
-
- Usage
- boot[/switches] <account obj> [: reason]
-
- Switches:
- quiet - Silently boot without informing account
- sid - boot by session id instead of name or dbref
-
- Boot an account object from the server. If a reason is
- supplied it will be echoed to the user unless /quiet is set.
- """
-
- key="boot"
- switch_options=("quiet","sid")
- locks="cmd:perm(boot) or perm(Admin)"
- help_category="Admin"
-
-
[docs]deffunc(self):
- """Implementing the function"""
- caller=self.caller
- args=self.args
-
- ifnotargs:
- caller.msg("Usage: boot[/switches] <account> [:reason]")
- return
-
- if":"inargs:
- args,reason=[a.strip()forainargs.split(":",1)]
- else:
- args,reason=args,""
-
- boot_list=[]
-
- if"sid"inself.switches:
- # Boot a particular session id.
- sessions=SESSIONS.get_sessions(True)
- forsessinsessions:
- # Find the session with the matching session id.
- ifsess.sessid==int(args):
- boot_list.append(sess)
- break
- else:
- # Boot by account object
- pobj=search.account_search(args)
- ifnotpobj:
- caller.msg("Account %s was not found."%args)
- return
- pobj=pobj[0]
- ifnotpobj.access(caller,"boot"):
- string="You don't have the permission to boot %s."%(pobj.key,)
- caller.msg(string)
- return
- # we have a bootable object with a connected user
- matches=SESSIONS.sessions_from_account(pobj)
- formatchinmatches:
- boot_list.append(match)
-
- ifnotboot_list:
- caller.msg("No matching sessions found. The Account does not seem to be online.")
- return
-
- # Carry out the booting of the sessions in the boot list.
-
- feedback=None
- if"quiet"notinself.switches:
- feedback="You have been disconnected by %s.\n"%caller.name
- ifreason:
- feedback+="\nReason given: %s"%reason
-
- forsessioninboot_list:
- session.msg(feedback)
- session.account.disconnect_session_from_account(session)
-
- ifpobjandboot_list:
- logger.log_sec(
- "Booted: %s (Reason: %s, Caller: %s, IP: %s)."
- %(pobj,reason,caller,self.session.address)
- )
-
-
-# regex matching IP addresses with wildcards, eg. 233.122.4.*
-IPREGEX=re.compile(r"[0-9*]{1,3}\.[0-9*]{1,3}\.[0-9*]{1,3}\.[0-9*]{1,3}")
-
-
-deflist_bans(cmd,banlist):
- """
- Helper function to display a list of active bans. Input argument
- is the banlist read into the two commands ban and unban below.
-
- Args:
- cmd (Command): Instance of the Ban command.
- banlist (list): List of bans to list.
- """
- ifnotbanlist:
- return"No active bans were found."
-
- table=cmd.styled_table("|wid","|wname/ip","|wdate","|wreason")
- forinum,baninenumerate(banlist):
- table.add_row(str(inum+1),ban[0]andban[0]orban[1],ban[3],ban[4])
- return"|wActive bans:|n\n%s"%table
-
-
-
[docs]classCmdBan(COMMAND_DEFAULT_CLASS):
- """
- ban an account from the server
-
- Usage:
- ban [<name or ip> [: reason]]
-
- Without any arguments, shows numbered list of active bans.
-
- This command bans a user from accessing the game. Supply an optional
- reason to be able to later remember why the ban was put in place.
-
- It is often preferable to ban an account from the server than to
- delete an account with accounts/delete. If banned by name, that account
- account can no longer be logged into.
-
- IP (Internet Protocol) address banning allows blocking all access
- from a specific address or subnet. Use an asterisk (*) as a
- wildcard.
-
- Examples:
- ban thomas - ban account 'thomas'
- ban/ip 134.233.2.111 - ban specific ip address
- ban/ip 134.233.2.* - ban all in a subnet
- ban/ip 134.233.*.* - even wider ban
-
- A single IP filter can be easy to circumvent by changing computers
- or requesting a new IP address. Setting a wide IP block filter with
- wildcards might be tempting, but remember that it may also
- accidentally block innocent users connecting from the same country
- or region.
-
- """
-
- key="ban"
- aliases=["bans"]
- locks="cmd:perm(ban) or perm(Developer)"
- help_category="Admin"
-
-
[docs]deffunc(self):
- """
- Bans are stored in a serverconf db object as a list of
- dictionaries:
- [ (name, ip, ipregex, date, reason),
- (name, ip, ipregex, date, reason),... ]
- where name and ip are set by the user and are shown in
- lists. ipregex is a converted form of ip where the * is
- replaced by an appropriate regex pattern for fast
- matching. date is the time stamp the ban was instigated and
- 'reason' is any optional info given to the command. Unset
- values in each tuple is set to the empty string.
- """
- banlist=ServerConfig.objects.conf("server_bans")
- ifnotbanlist:
- banlist=[]
-
- ifnotself.argsor(
- self.switchesandnotany(switchin("ip","name")forswitchinself.switches)
- ):
- self.caller.msg(list_bans(self,banlist))
- return
-
- now=time.ctime()
- reason=""
- if":"inself.args:
- ban,reason=self.args.rsplit(":",1)
- else:
- ban=self.args
- ban=ban.lower()
- ipban=IPREGEX.findall(ban)
- ifnotipban:
- # store as name
- typ="Name"
- bantup=(ban,"","",now,reason)
- else:
- # an ip address.
- typ="IP"
- ban=ipban[0]
- # replace * with regex form and compile it
- ipregex=ban.replace(".","\.")
- ipregex=ipregex.replace("*","[0-9]{1,3}")
- ipregex=re.compile(r"%s"%ipregex)
- bantup=("",ban,ipregex,now,reason)
-
- ret=yield(f"Are you sure you want to {typ}-ban '|w{ban}|n' [Y]/N?")
- ifstr(ret).lower()in("no","n"):
- self.caller.msg("Aborted.")
- return
-
- # save updated banlist
- banlist.append(bantup)
- ServerConfig.objects.conf("server_bans",banlist)
- self.caller.msg(f"{typ}-ban '|w{ban}|n' was added. Use |wunban|n to reinstate.")
- logger.log_sec(
- "Banned {typ}: {ban.strip()} (Caller: {self.caller}, IP: {self.session.address})."
- )
-
-
-
[docs]classCmdUnban(COMMAND_DEFAULT_CLASS):
- """
- remove a ban from an account
-
- Usage:
- unban <banid>
-
- This will clear an account name/ip ban previously set with the ban
- command. Use this command without an argument to view a numbered
- list of bans. Use the numbers in this list to select which one to
- unban.
-
- """
-
- key="unban"
- locks="cmd:perm(unban) or perm(Developer)"
- help_category="Admin"
-
-
[docs]deffunc(self):
- """Implement unbanning"""
-
- banlist=ServerConfig.objects.conf("server_bans")
-
- ifnotself.args:
- self.caller.msg(list_bans(self,banlist))
- return
-
- try:
- num=int(self.args)
- exceptException:
- self.caller.msg("You must supply a valid ban id to clear.")
- return
-
- ifnotbanlist:
- self.caller.msg("There are no bans to clear.")
- elifnot(0<num<len(banlist)+1):
- self.caller.msg("Ban id |w%s|x was not found."%self.args)
- else:
- # all is ok, ask, then clear ban
- ban=banlist[num-1]
- value=(" ".join([sforsinban[:2]])).strip()
-
- ret=yield(f"Are you sure you want to unban {num}: '|w{value}|n' [Y]/N?")
- ifstr(ret).lower()in("n","no"):
- self.caller.msg("Aborted.")
- return
-
- delbanlist[num-1]
- ServerConfig.objects.conf("server_bans",banlist)
- self.caller.msg(f"Cleared ban {num}: '{value}'")
- logger.log_sec(
- "Unbanned: {value.strip()} (Caller: {self.caller}, IP: {self.session.address})."
- )
-
-
-
[docs]classCmdEmit(COMMAND_DEFAULT_CLASS):
- """
- admin command for emitting message to multiple objects
-
- Usage:
- emit[/switches] [<obj>, <obj>, ... =] <message>
- remit [<obj>, <obj>, ... =] <message>
- pemit [<obj>, <obj>, ... =] <message>
-
- Switches:
- room - limit emits to rooms only (default)
- accounts - limit emits to accounts only
- contents - send to the contents of matched objects too
-
- Emits a message to the selected objects or to
- your immediate surroundings. If the object is a room,
- send to its contents. remit and pemit are just
- limited forms of emit, for sending to rooms and
- to accounts respectively.
- """
-
- key="emit"
- aliases=["pemit","remit"]
- switch_options=("room","accounts","contents")
- locks="cmd:perm(emit) or perm(Builder)"
- help_category="Admin"
-
-
[docs]deffunc(self):
- """Implement the command"""
-
- caller=self.caller
- args=self.args
-
- ifnotargs:
- string="Usage: "
- string+="\nemit[/switches] [<obj>, <obj>, ... =] <message>"
- string+="\nremit [<obj>, <obj>, ... =] <message>"
- string+="\npemit [<obj>, <obj>, ... =] <message>"
- caller.msg(string)
- return
-
- rooms_only="rooms"inself.switches
- accounts_only="accounts"inself.switches
- send_to_contents="contents"inself.switches
-
- # we check which command was used to force the switches
- ifself.cmdstring=="remit":
- rooms_only=True
- send_to_contents=True
- elifself.cmdstring=="pemit":
- accounts_only=True
-
- ifnotself.rhs:
- message=self.args
- objnames=[caller.location.key]
- else:
- message=self.rhs
- objnames=self.lhslist
-
- # send to all objects
- forobjnameinobjnames:
- obj=caller.search(objname,global_search=True)
- ifnotobj:
- return
- ifrooms_onlyandobj.locationisnotNone:
- caller.msg("%s is not a room. Ignored."%objname)
- continue
- ifaccounts_onlyandnotobj.has_account:
- caller.msg("%s has no active account. Ignored."%objname)
- continue
- ifobj.access(caller,"tell"):
- obj.msg(message)
- ifsend_to_contentsandhasattr(obj,"msg_contents"):
- obj.msg_contents(message)
- caller.msg("Emitted to %s and contents:\n%s"%(objname,message))
- else:
- caller.msg("Emitted to %s:\n%s"%(objname,message))
- else:
- caller.msg("You are not allowed to emit to %s."%objname)
-
-
-
[docs]classCmdNewPassword(COMMAND_DEFAULT_CLASS):
- """
- change the password of an account
-
- Usage:
- userpassword <user obj> = <new password>
-
- Set an account's password.
- """
-
- key="userpassword"
- locks="cmd:perm(newpassword) or perm(Admin)"
- help_category="Admin"
-
-
[docs]deffunc(self):
- """Implement the function."""
-
- caller=self.caller
-
- ifnotself.rhs:
- self.msg("Usage: userpassword <user obj> = <new password>")
- return
-
- # the account search also matches 'me' etc.
- account=caller.search_account(self.lhs)
- ifnotaccount:
- return
-
- newpass=self.rhs
-
- # Validate password
- validated,error=account.validate_password(newpass)
- ifnotvalidated:
- errors=[eforsuberrorinerror.messagesforeinerror.messages]
- string="\n".join(errors)
- caller.msg(string)
- return
-
- account.set_password(newpass)
- account.save()
- self.msg("%s - new password set to '%s'."%(account.name,newpass))
- ifaccount.character!=caller:
- account.msg("%s has changed your password to '%s'."%(caller.name,newpass))
- logger.log_sec(
- "Password Changed: %s (Caller: %s, IP: %s)."%(account,caller,self.session.address)
- )
-
-
-
[docs]classCmdPerm(COMMAND_DEFAULT_CLASS):
- """
- set the permissions of an account/object
-
- Usage:
- perm[/switch] <object> [= <permission>[,<permission>,...]]
- perm[/switch] *<account> [= <permission>[,<permission>,...]]
-
- Switches:
- del - delete the given permission from <object> or <account>.
- account - set permission on an account (same as adding * to name)
-
- This command sets/clears individual permission strings on an object
- or account. If no permission is given, list all permissions on <object>.
- """
-
- key="perm"
- aliases="setperm"
- switch_options=("del","account")
- locks="cmd:perm(perm) or perm(Developer)"
- help_category="Admin"
-
-
[docs]deffunc(self):
- """Implement function"""
-
- caller=self.caller
- switches=self.switches
- lhs,rhs=self.lhs,self.rhs
-
- ifnotself.args:
- string="Usage: perm[/switch] object [ = permission, permission, ...]"
- caller.msg(string)
- return
-
- accountmode="account"inself.switchesorlhs.startswith("*")
- lhs=lhs.lstrip("*")
-
- ifaccountmode:
- obj=caller.search_account(lhs)
- else:
- obj=caller.search(lhs,global_search=True)
- ifnotobj:
- return
-
- ifnotrhs:
- ifnotobj.access(caller,"examine"):
- caller.msg("You are not allowed to examine this object.")
- return
-
- string="Permissions on |w%s|n: "%obj.key
- ifnotobj.permissions.all():
- string+="<None>"
- else:
- string+=", ".join(obj.permissions.all())
- if(
- hasattr(obj,"account")
- andhasattr(obj.account,"is_superuser")
- andobj.account.is_superuser
- ):
- string+="\n(... but this object is currently controlled by a SUPERUSER! "
- string+="All access checks are passed automatically.)"
- caller.msg(string)
- return
-
- # we supplied an argument on the form obj = perm
- locktype="edit"ifaccountmodeelse"control"
- ifnotobj.access(caller,locktype):
- caller.msg(
- "You are not allowed to edit this %s's permissions."
- %("account"ifaccountmodeelse"object")
- )
- return
-
- caller_result=[]
- target_result=[]
- if"del"inswitches:
- # delete the given permission(s) from object.
- forperminself.rhslist:
- obj.permissions.remove(perm)
- ifobj.permissions.get(perm):
- caller_result.append(
- "\nPermissions %s could not be removed from %s."%(perm,obj.name)
- )
- else:
- caller_result.append(
- "\nPermission %s removed from %s (if they existed)."%(perm,obj.name)
- )
- target_result.append(
- "\n%s revokes the permission(s) %s from you."%(caller.name,perm)
- )
- logger.log_sec(
- "Permissions Deleted: %s, %s (Caller: %s, IP: %s)."
- %(perm,obj,caller,self.session.address)
- )
- else:
- # add a new permission
- permissions=obj.permissions.all()
-
- forperminself.rhslist:
-
- # don't allow to set a permission higher in the hierarchy than
- # the one the caller has (to prevent self-escalation)
- ifperm.lower()inPERMISSION_HIERARCHYandnotobj.locks.check_lockstring(
- caller,"dummy:perm(%s)"%perm
- ):
- caller.msg(
- "You cannot assign a permission higher than the one you have yourself."
- )
- return
-
- ifperminpermissions:
- caller_result.append(
- "\nPermission '%s' is already defined on %s."%(perm,obj.name)
- )
- else:
- obj.permissions.add(perm)
- plystring="the Account"ifaccountmodeelse"the Object/Character"
- caller_result.append(
- "\nPermission '%s' given to %s (%s)."%(perm,obj.name,plystring)
- )
- target_result.append(
- "\n%s gives you (%s, %s) the permission '%s'."
- %(caller.name,obj.name,plystring,perm)
- )
- logger.log_sec(
- "Permissions Added: %s, %s (Caller: %s, IP: %s)."
- %(obj,perm,caller,self.session.address)
- )
-
- caller.msg("".join(caller_result).strip())
- iftarget_result:
- obj.msg("".join(target_result).strip())
-
-
-
[docs]classCmdWall(COMMAND_DEFAULT_CLASS):
- """
- make an announcement to all
-
- Usage:
- wall <message>
-
- Announces a message to all connected sessions
- including all currently unlogged in.
- """
-
- key="wall"
- locks="cmd:perm(wall) or perm(Admin)"
- help_category="Admin"
-
-
[docs]classCmdForce(COMMAND_DEFAULT_CLASS):
- """
- forces an object to execute a command
-
- Usage:
- force <object>=<command string>
-
- Example:
- force bob=get stick
- """
-
- key="force"
- locks="cmd:perm(spawn) or perm(Builder)"
- help_category="Building"
- perm_used="edit"
-
-
[docs]deffunc(self):
- """Implements the force command"""
- ifnotself.lhsornotself.rhs:
- self.caller.msg("You must provide a target and a command string to execute.")
- return
- targ=self.caller.search(self.lhs)
- ifnottarg:
- return
- ifnottarg.access(self.caller,self.perm_used):
- self.caller.msg("You don't have permission to force them to execute commands.")
- return
- targ.execute_cmd(self.rhs)
- self.caller.msg("You have forced %s to: %s"%(targ,self.rhs))
Source code for evennia.commands.default.batchprocess
-"""
-Batch processors
-
-These commands implements the 'batch-command' and 'batch-code'
-processors, using the functionality in evennia.utils.batchprocessors.
-They allow for offline world-building.
-
-Batch-command is the simpler system. This reads a file (*.ev)
-containing a list of in-game commands and executes them in sequence as
-if they had been entered in the game (including permission checks
-etc).
-
-Batch-code is a full-fledged python code interpreter that reads blocks
-of python code (*.py) and executes them in sequence. This allows for
-much more power than Batch-command, but requires knowing Python and
-the Evennia API. It is also a severe security risk and should
-therefore always be limited to superusers only.
-
-"""
-importre
-
-fromdjango.confimportsettings
-fromevennia.utils.batchprocessorsimportBATCHCMD,BATCHCODE
-fromevennia.commands.cmdsetimportCmdSet
-fromevennia.utilsimportlogger,utils
-
-
-_RE_COMMENT=re.compile(r"^#.*?$",re.MULTILINE+re.DOTALL)
-_RE_CODE_START=re.compile(r"^# batchcode code:",re.MULTILINE)
-_COMMAND_DEFAULT_CLASS=utils.class_from_module(settings.COMMAND_DEFAULT_CLASS)
-
-# limit symbols for API inclusion
-__all__=("CmdBatchCommands","CmdBatchCode")
-
-_HEADER_WIDTH=70
-_UTF8_ERROR="""
- |rDecode error in '%s'.|n
-
- This file contains non-ascii character(s). This is common if you
- wrote some input in a language that has more letters and special
- symbols than English; such as accents or umlauts. This is usually
- fine and fully supported! But for Evennia to know how to decode such
- characters in a universal way, the batchfile must be saved with the
- international 'UTF-8' encoding. This file is not.
-
- Please re-save the batchfile with the UTF-8 encoding (refer to the
- documentation of your text editor on how to do this, or switch to a
- better featured one) and try again.
-
- Error reported was: '%s'
-"""
-
-
-# -------------------------------------------------------------
-# Helper functions
-# -------------------------------------------------------------
-
-
-defformat_header(caller,entry):
- """
- Formats a header
- """
- width=_HEADER_WIDTH-10
- # strip all comments for the header
- ifcaller.ndb.batch_batchmode!="batch_commands":
- # only do cleanup for batchcode
- entry=_RE_CODE_START.split(entry,1)[1]
- entry=_RE_COMMENT.sub("",entry).strip()
- header=utils.crop(entry,width=width)
- ptr=caller.ndb.batch_stackptr+1
- stacklen=len(caller.ndb.batch_stack)
- header="|w%02i/%02i|G: %s|n"%(ptr,stacklen,header)
- # add extra space to the side for padding.
- header="%s%s"%(header," "*(width-len(header)))
- header=header.replace("\n","\\n")
-
- returnheader
-
-
-defformat_code(entry):
- """
- Formats the viewing of code and errors
- """
- code=""
- forlineinentry.split("\n"):
- code+="\n|G>>>|n %s"%line
- returncode.strip()
-
-
-defbatch_cmd_exec(caller):
- """
- Helper function for executing a single batch-command entry
- """
- ptr=caller.ndb.batch_stackptr
- stack=caller.ndb.batch_stack
- command=stack[ptr]
- caller.msg(format_header(caller,command))
- try:
- caller.execute_cmd(command)
- exceptException:
- logger.log_trace()
- returnFalse
- returnTrue
-
-
-defbatch_code_exec(caller):
- """
- Helper function for executing a single batch-code entry
- """
- ptr=caller.ndb.batch_stackptr
- stack=caller.ndb.batch_stack
- debug=caller.ndb.batch_debug
- code=stack[ptr]
-
- caller.msg(format_header(caller,code))
- err=BATCHCODE.code_exec(code,extra_environ={"caller":caller},debug=debug)
- iferr:
- caller.msg(format_code(err))
- returnFalse
- returnTrue
-
-
-defstep_pointer(caller,step=1):
- """
- Step in stack, returning the item located.
-
- stackptr - current position in stack
- stack - the stack of units
- step - how many steps to move from stackptr
- """
- ptr=caller.ndb.batch_stackptr
- stack=caller.ndb.batch_stack
- nstack=len(stack)
- ifptr+step<=0:
- caller.msg("|RBeginning of batch file.")
- ifptr+step>=nstack:
- caller.msg("|REnd of batch file.")
- caller.ndb.batch_stackptr=max(0,min(nstack-1,ptr+step))
-
-
-defshow_curr(caller,showall=False):
- """
- Show the current position in stack
- """
- stackptr=caller.ndb.batch_stackptr
- stack=caller.ndb.batch_stack
-
- ifstackptr>=len(stack):
- caller.ndb.batch_stackptr=len(stack)-1
- show_curr(caller,showall)
- return
-
- entry=stack[stackptr]
-
- string=format_header(caller,entry)
- codeall=entry.strip()
- string+="|G(hh for help)"
- ifshowall:
- forlineincodeall.split("\n"):
- string+="\n|G||n %s"%line
- caller.msg(string)
-
-
-defpurge_processor(caller):
- """
- This purges all effects running
- on the caller.
- """
- try:
- delcaller.ndb.batch_stack
- delcaller.ndb.batch_stackptr
- delcaller.ndb.batch_pythonpath
- delcaller.ndb.batch_batchmode
- exceptException:
- # something might have already been erased; it's not critical
- pass
- # clear everything back to the state before the batch call
- ifcaller.ndb.batch_cmdset_backup:
- caller.cmdset.cmdset_stack=caller.ndb.batch_cmdset_backup
- caller.cmdset.update()
- delcaller.ndb.batch_cmdset_backup
- else:
- # something went wrong. Purge cmdset except default
- caller.cmdset.clear()
-
- # caller.scripts.validate() # this will purge interactive mode
-
-
-# -------------------------------------------------------------
-# main access commands
-# -------------------------------------------------------------
-
-
-
[docs]classCmdBatchCommands(_COMMAND_DEFAULT_CLASS):
- """
- build from batch-command file
-
- Usage:
- batchcommands[/interactive] <python.path.to.file>
-
- Switch:
- interactive - this mode will offer more control when
- executing the batch file, like stepping,
- skipping, reloading etc.
-
- Runs batches of commands from a batch-cmd text file (*.ev).
-
- """
-
- key="batchcommands"
- aliases=["batchcommand","batchcmd"]
- switch_options=("interactive",)
- locks="cmd:perm(batchcommands) or perm(Developer)"
- help_category="Building"
-
-
[docs]deffunc(self):
- """Starts the processor."""
-
- caller=self.caller
-
- args=self.args
- ifnotargs:
- caller.msg("Usage: batchcommands[/interactive] <path.to.file>")
- return
- python_path=self.args
-
- # parse indata file
-
- try:
- commands=BATCHCMD.parse_file(python_path)
- exceptUnicodeDecodeErroraserr:
- caller.msg(_UTF8_ERROR%(python_path,err))
- return
- exceptIOErroraserr:
- iferr:
- err="{}\n".format(str(err))
- else:
- err=""
- string=(
- "%s'%s' could not load. You have to supply python paths "
- "from one of the defined batch-file directories\n (%s)."
- )
- caller.msg(string%(err,python_path,", ".join(settings.BASE_BATCHPROCESS_PATHS)))
- return
- ifnotcommands:
- caller.msg("File %s seems empty of valid commands."%python_path)
- return
-
- switches=self.switches
-
- # Store work data in cache
- caller.ndb.batch_stack=commands
- caller.ndb.batch_stackptr=0
- caller.ndb.batch_pythonpath=python_path
- caller.ndb.batch_batchmode="batch_commands"
- # we use list() here to create a new copy of the cmdset stack
- caller.ndb.batch_cmdset_backup=list(caller.cmdset.cmdset_stack)
- caller.cmdset.add(BatchSafeCmdSet)
-
- if"inter"inswitchesor"interactive"inswitches:
- # Allow more control over how batch file is executed
-
- # Set interactive state directly
- caller.cmdset.add(BatchInteractiveCmdSet)
-
- caller.msg("\nBatch-command processor - Interactive mode for %s ..."%python_path)
- show_curr(caller)
- else:
- caller.msg(
- "Running Batch-command processor - Automatic mode "
- "for %s (this might take some time) ..."%python_path
- )
-
- # run in-process (might block)
- for_inrange(len(commands)):
- # loop through the batch file
- ifnotbatch_cmd_exec(caller):
- return
- step_pointer(caller,1)
- # clean out the safety cmdset and clean out all other
- # temporary attrs.
- string=" Batchfile '%s' applied."%python_path
- caller.msg("|G%s"%string)
- purge_processor(caller)
-
-
-
[docs]classCmdBatchCode(_COMMAND_DEFAULT_CLASS):
- """
- build from batch-code file
-
- Usage:
- batchcode[/interactive] <python path to file>
-
- Switch:
- interactive - this mode will offer more control when
- executing the batch file, like stepping,
- skipping, reloading etc.
- debug - auto-delete all objects that has been marked as
- deletable in the script file (see example files for
- syntax). This is useful so as to to not leave multiple
- object copies behind when testing out the script.
-
- Runs batches of commands from a batch-code text file (*.py).
-
- """
-
- key="batchcode"
- aliases=["batchcodes"]
- switch_options=("interactive","debug")
- locks="cmd:superuser()"
- help_category="Building"
-
-
[docs]deffunc(self):
- """Starts the processor."""
-
- caller=self.caller
-
- args=self.args
- ifnotargs:
- caller.msg("Usage: batchcode[/interactive/debug] <path.to.file>")
- return
- python_path=self.args
- debug="debug"inself.switches
-
- # parse indata file
- try:
- codes=BATCHCODE.parse_file(python_path)
- exceptUnicodeDecodeErroraserr:
- caller.msg(_UTF8_ERROR%(python_path,err))
- return
- exceptIOErroraserr:
- iferr:
- err="{}\n".format(str(err))
- else:
- err=""
- string=(
- "%s'%s' could not load. You have to supply python paths "
- "from one of the defined batch-file directories\n (%s)."
- )
- caller.msg(string%(err,python_path,", ".join(settings.BASE_BATCHPROCESS_PATHS)))
- return
- ifnotcodes:
- caller.msg("File %s seems empty of functional code."%python_path)
- return
-
- switches=self.switches
-
- # Store work data in cache
- caller.ndb.batch_stack=codes
- caller.ndb.batch_stackptr=0
- caller.ndb.batch_pythonpath=python_path
- caller.ndb.batch_batchmode="batch_code"
- caller.ndb.batch_debug=debug
- # we use list() here to create a new copy of cmdset_stack
- caller.ndb.batch_cmdset_backup=list(caller.cmdset.cmdset_stack)
- caller.cmdset.add(BatchSafeCmdSet)
-
- if"inter"inswitchesor"interactive"inswitches:
- # Allow more control over how batch file is executed
-
- # Set interactive state directly
- caller.cmdset.add(BatchInteractiveCmdSet)
-
- caller.msg("\nBatch-code processor - Interactive mode for %s ..."%python_path)
- show_curr(caller)
- else:
- caller.msg("Running Batch-code processor - Automatic mode for %s ..."%python_path)
-
- for_inrange(len(codes)):
- # loop through the batch file
- ifnotbatch_code_exec(caller):
- return
- step_pointer(caller,1)
- # clean out the safety cmdset and clean out all other
- # temporary attrs.
- string=" Batchfile '%s' applied."%python_path
- caller.msg("|G%s"%string)
- purge_processor(caller)
-
-
-# -------------------------------------------------------------
-# State-commands for the interactive batch processor modes
-# (these are the same for both processors)
-# -------------------------------------------------------------
-
-
-classCmdStateAbort(_COMMAND_DEFAULT_CLASS):
- """
- abort
-
- This is a safety feature. It force-ejects us out of the processor and to
- the default cmdset, regardless of what current cmdset the processor might
- have put us in (e.g. when testing buggy scripts etc).
- """
-
- key="abort"
- help_category="BatchProcess"
- locks="cmd:perm(batchcommands)"
-
- deffunc(self):
- """Exit back to default."""
- purge_processor(self.caller)
- self.caller.msg("Exited processor and reset out active cmdset back to the default one.")
-
-
-classCmdStateLL(_COMMAND_DEFAULT_CLASS):
- """
- ll
-
- Look at the full source for the current
- command definition.
- """
-
- key="ll"
- help_category="BatchProcess"
- locks="cmd:perm(batchcommands)"
-
- deffunc(self):
- show_curr(self.caller,showall=True)
-
-
-classCmdStatePP(_COMMAND_DEFAULT_CLASS):
- """
- pp
-
- Process the currently shown command definition.
- """
-
- key="pp"
- help_category="BatchProcess"
- locks="cmd:perm(batchcommands)"
-
- deffunc(self):
- """
- This checks which type of processor we are running.
- """
- caller=self.caller
- ifcaller.ndb.batch_batchmode=="batch_code":
- batch_code_exec(caller)
- else:
- batch_cmd_exec(caller)
-
-
-classCmdStateRR(_COMMAND_DEFAULT_CLASS):
- """
- rr
-
- Reload the batch file, keeping the current
- position in it.
- """
-
- key="rr"
- help_category="BatchProcess"
- locks="cmd:perm(batchcommands)"
-
- deffunc(self):
- caller=self.caller
- ifcaller.ndb.batch_batchmode=="batch_code":
- new_data=BATCHCODE.parse_file(caller.ndb.batch_pythonpath)
- else:
- new_data=BATCHCMD.parse_file(caller.ndb.batch_pythonpath)
- caller.ndb.batch_stack=new_data
- caller.msg(format_code("File reloaded. Staying on same command."))
- show_curr(caller)
-
-
-classCmdStateRRR(_COMMAND_DEFAULT_CLASS):
- """
- rrr
-
- Reload the batch file, starting over
- from the beginning.
- """
-
- key="rrr"
- help_category="BatchProcess"
- locks="cmd:perm(batchcommands)"
-
- deffunc(self):
- caller=self.caller
- ifcaller.ndb.batch_batchmode=="batch_code":
- BATCHCODE.parse_file(caller.ndb.batch_pythonpath)
- else:
- BATCHCMD.parse_file(caller.ndb.batch_pythonpath)
- caller.ndb.batch_stackptr=0
- caller.msg(format_code("File reloaded. Restarting from top."))
- show_curr(caller)
-
-
-classCmdStateNN(_COMMAND_DEFAULT_CLASS):
- """
- nn
-
- Go to next command. No commands are executed.
- """
-
- key="nn"
- help_category="BatchProcess"
- locks="cmd:perm(batchcommands)"
-
- deffunc(self):
- caller=self.caller
- arg=self.args
- ifargandarg.isdigit():
- step=int(self.args)
- else:
- step=1
- step_pointer(caller,step)
- show_curr(caller)
-
-
-classCmdStateNL(_COMMAND_DEFAULT_CLASS):
- """
- nl
-
- Go to next command, viewing its full source.
- No commands are executed.
- """
-
- key="nl"
- help_category="BatchProcess"
- locks="cmd:perm(batchcommands)"
-
- deffunc(self):
- caller=self.caller
- arg=self.args
- ifargandarg.isdigit():
- step=int(self.args)
- else:
- step=1
- step_pointer(caller,step)
- show_curr(caller,showall=True)
-
-
-classCmdStateBB(_COMMAND_DEFAULT_CLASS):
- """
- bb
-
- Backwards to previous command. No commands
- are executed.
- """
-
- key="bb"
- help_category="BatchProcess"
- locks="cmd:perm(batchcommands)"
-
- deffunc(self):
- caller=self.caller
- arg=self.args
- ifargandarg.isdigit():
- step=-int(self.args)
- else:
- step=-1
- step_pointer(caller,step)
- show_curr(caller)
-
-
-classCmdStateBL(_COMMAND_DEFAULT_CLASS):
- """
- bl
-
- Backwards to previous command, viewing its full
- source. No commands are executed.
- """
-
- key="bl"
- help_category="BatchProcess"
- locks="cmd:perm(batchcommands)"
-
- deffunc(self):
- caller=self.caller
- arg=self.args
- ifargandarg.isdigit():
- step=-int(self.args)
- else:
- step=-1
- step_pointer(caller,step)
- show_curr(caller,showall=True)
-
-
-classCmdStateSS(_COMMAND_DEFAULT_CLASS):
- """
- ss [steps]
-
- Process current command, then step to the next
- one. If steps is given,
- process this many commands.
- """
-
- key="ss"
- help_category="BatchProcess"
- locks="cmd:perm(batchcommands)"
-
- deffunc(self):
- caller=self.caller
- arg=self.args
- ifargandarg.isdigit():
- step=int(self.args)
- else:
- step=1
-
- for_inrange(step):
- ifcaller.ndb.batch_batchmode=="batch_code":
- batch_code_exec(caller)
- else:
- batch_cmd_exec(caller)
- step_pointer(caller,1)
- show_curr(caller)
-
-
-classCmdStateSL(_COMMAND_DEFAULT_CLASS):
- """
- sl [steps]
-
- Process current command, then step to the next
- one, viewing its full source. If steps is given,
- process this many commands.
- """
-
- key="sl"
- help_category="BatchProcess"
- locks="cmd:perm(batchcommands)"
-
- deffunc(self):
- caller=self.caller
- arg=self.args
- ifargandarg.isdigit():
- step=int(self.args)
- else:
- step=1
-
- for_inrange(step):
- ifcaller.ndb.batch_batchmode=="batch_code":
- batch_code_exec(caller)
- else:
- batch_cmd_exec(caller)
- step_pointer(caller,1)
- show_curr(caller)
-
-
-classCmdStateCC(_COMMAND_DEFAULT_CLASS):
- """
- cc
-
- Continue to process all remaining
- commands.
- """
-
- key="cc"
- help_category="BatchProcess"
- locks="cmd:perm(batchcommands)"
-
- deffunc(self):
- caller=self.caller
- nstack=len(caller.ndb.batch_stack)
- ptr=caller.ndb.batch_stackptr
- step=nstack-ptr
-
- for_inrange(step):
- ifcaller.ndb.batch_batchmode=="batch_code":
- batch_code_exec(caller)
- else:
- batch_cmd_exec(caller)
- step_pointer(caller,1)
- show_curr(caller)
-
- purge_processor(self)
- caller.msg(format_code("Finished processing batch file."))
-
-
-classCmdStateJJ(_COMMAND_DEFAULT_CLASS):
- """
- jj <command number>
-
- Jump to specific command number
- """
-
- key="jj"
- help_category="BatchProcess"
- locks="cmd:perm(batchcommands)"
-
- deffunc(self):
- caller=self.caller
- arg=self.args
- ifargandarg.isdigit():
- number=int(self.args)-1
- else:
- caller.msg(format_code("You must give a number index."))
- return
- ptr=caller.ndb.batch_stackptr
- step=number-ptr
- step_pointer(caller,step)
- show_curr(caller)
-
-
-classCmdStateJL(_COMMAND_DEFAULT_CLASS):
- """
- jl <command number>
-
- Jump to specific command number and view its full source.
- """
-
- key="jl"
- help_category="BatchProcess"
- locks="cmd:perm(batchcommands)"
-
- deffunc(self):
- caller=self.caller
- arg=self.args
- ifargandarg.isdigit():
- number=int(self.args)-1
- else:
- caller.msg(format_code("You must give a number index."))
- return
- ptr=caller.ndb.batch_stackptr
- step=number-ptr
- step_pointer(caller,step)
- show_curr(caller,showall=True)
-
-
-classCmdStateQQ(_COMMAND_DEFAULT_CLASS):
- """
- qq
-
- Quit the batchprocessor.
- """
-
- key="qq"
- help_category="BatchProcess"
- locks="cmd:perm(batchcommands)"
-
- deffunc(self):
- purge_processor(self.caller)
- self.caller.msg("Aborted interactive batch mode.")
-
-
-classCmdStateHH(_COMMAND_DEFAULT_CLASS):
- """Help command"""
-
- key="hh"
- help_category="BatchProcess"
- locks="cmd:perm(batchcommands)"
-
- deffunc(self):
- string="""
- Interactive batch processing commands:
-
- nn [steps] - next command (no processing)
- nl [steps] - next & look
- bb [steps] - back to previous command (no processing)
- bl [steps] - back & look
- jj <N> - jump to command nr N (no processing)
- jl <N> - jump & look
- pp - process currently shown command (no step)
- ss [steps] - process & step
- sl [steps] - process & step & look
- ll - look at full definition of current command
- rr - reload batch file (stay on current)
- rrr - reload batch file (start from first)
- hh - this help list
-
- cc - continue processing to end, then quit.
- qq - quit (abort all remaining commands)
-
- abort - this is a safety command that always is available
- regardless of what cmdsets gets added to us during
- batch-command processing. It immediately shuts down
- the processor and returns us to the default cmdset.
- """
- self.caller.msg(string)
-
-
-# -------------------------------------------------------------
-#
-# Defining the cmdsets for the interactive batchprocessor
-# mode (same for both processors)
-#
-# -------------------------------------------------------------
-
-
-classBatchSafeCmdSet(CmdSet):
- """
- The base cmdset for the batch processor.
- This sets a 'safe' abort command that will
- always be available to get out of everything.
- """
-
- key="Batch_default"
- priority=150# override other cmdsets.
-
- defat_cmdset_creation(self):
- """Init the cmdset"""
- self.add(CmdStateAbort())
-
-
-classBatchInteractiveCmdSet(CmdSet):
- """
- The cmdset for the interactive batch processor mode.
- """
-
- key="Batch_interactive"
- priority=104
-
- defat_cmdset_creation(self):
- """init the cmdset"""
- self.add(CmdStateAbort())
- self.add(CmdStateLL())
- self.add(CmdStatePP())
- self.add(CmdStateRR())
- self.add(CmdStateRRR())
- self.add(CmdStateNN())
- self.add(CmdStateNL())
- self.add(CmdStateBB())
- self.add(CmdStateBL())
- self.add(CmdStateSS())
- self.add(CmdStateSL())
- self.add(CmdStateCC())
- self.add(CmdStateJJ())
- self.add(CmdStateJL())
- self.add(CmdStateQQ())
- self.add(CmdStateHH())
-
[docs]classObjManipCommand(COMMAND_DEFAULT_CLASS):
- """
- This is a parent class for some of the defining objmanip commands
- since they tend to have some more variables to define new objects.
-
- Each object definition can have several components. First is
- always a name, followed by an optional alias list and finally an
- some optional data, such as a typeclass or a location. A comma ','
- separates different objects. Like this:
-
- name1;alias;alias;alias:option, name2;alias;alias ...
-
- Spaces between all components are stripped.
-
- A second situation is attribute manipulation. Such commands
- are simpler and offer combinations
-
- objname/attr/attr/attr, objname/attr, ...
-
- """
-
- # OBS - this is just a parent - it's not intended to actually be
- # included in a commandset on its own!
-
-
[docs]defparse(self):
- """
- We need to expand the default parsing to get all
- the cases, see the module doc.
- """
- # get all the normal parsing done (switches etc)
- super().parse()
-
- obj_defs=([],[])# stores left- and right-hand side of '='
- obj_attrs=([],[])# "
-
- foriside,arglistinenumerate((self.lhslist,self.rhslist)):
- # lhslist/rhslist is already split by ',' at this point
- forobjdefinarglist:
- aliases,option,attrs=[],None,[]
- if":"inobjdef:
- objdef,option=[part.strip()forpartinobjdef.rsplit(":",1)]
- if";"inobjdef:
- objdef,aliases=[part.strip()forpartinobjdef.split(";",1)]
- aliases=[alias.strip()foraliasinaliases.split(";")ifalias.strip()]
- if"/"inobjdef:
- objdef,attrs=[part.strip()forpartinobjdef.split("/",1)]
- attrs=[part.strip().lower()forpartinattrs.split("/")ifpart.strip()]
- # store data
- obj_defs[iside].append({"name":objdef,"option":option,"aliases":aliases})
- obj_attrs[iside].append({"name":objdef,"attrs":attrs})
-
- # store for future access
- self.lhs_objs=obj_defs[0]
- self.rhs_objs=obj_defs[1]
- self.lhs_objattr=obj_attrs[0]
- self.rhs_objattr=obj_attrs[1]
-
-
-
[docs]classCmdSetObjAlias(COMMAND_DEFAULT_CLASS):
- """
- adding permanent aliases for object
-
- Usage:
- alias <obj> [= [alias[,alias,alias,...]]]
- alias <obj> =
- alias/category <obj> = [alias[,alias,...]:<category>
-
- Switches:
- category - requires ending input with :category, to store the
- given aliases with the given category.
-
- Assigns aliases to an object so it can be referenced by more
- than one name. Assign empty to remove all aliases from object. If
- assigning a category, all aliases given will be using this category.
-
- Observe that this is not the same thing as personal aliases
- created with the 'nick' command! Aliases set with alias are
- changing the object in question, making those aliases usable
- by everyone.
- """
-
- key="@alias"
- aliases="setobjalias"
- switch_options=("category",)
- locks="cmd:perm(setobjalias) or perm(Builder)"
- help_category="Building"
-
-
[docs]deffunc(self):
- """Set the aliases."""
-
- caller=self.caller
-
- ifnotself.lhs:
- string="Usage: alias <obj> [= [alias[,alias ...]]]"
- self.caller.msg(string)
- return
- objname=self.lhs
-
- # Find the object to receive aliases
- obj=caller.search(objname)
- ifnotobj:
- return
- ifself.rhsisNone:
- # no =, so we just list aliases on object.
- aliases=obj.aliases.all(return_key_and_category=True)
- ifaliases:
- caller.msg(
- "Aliases for %s: %s"
- %(
- obj.get_display_name(caller),
- ", ".join(
- "'%s'%s"
- %(alias,""ifcategoryisNoneelse"[category:'%s']"%category)
- for(alias,category)inaliases
- ),
- )
- )
- else:
- caller.msg("No aliases exist for '%s'."%obj.get_display_name(caller))
- return
-
- ifnot(obj.access(caller,"control")orobj.access(caller,"edit")):
- caller.msg("You don't have permission to do that.")
- return
-
- ifnotself.rhs:
- # we have given an empty =, so delete aliases
- old_aliases=obj.aliases.all()
- ifold_aliases:
- caller.msg(
- "Cleared aliases from %s: %s"
- %(obj.get_display_name(caller),", ".join(old_aliases))
- )
- obj.aliases.clear()
- else:
- caller.msg("No aliases to clear.")
- return
-
- category=None
- if"category"inself.switches:
- if":"inself.rhs:
- rhs,category=self.rhs.rsplit(":",1)
- category=category.strip()
- else:
- caller.msg(
- "If specifying the /category switch, the category must be given "
- "as :category at the end."
- )
- else:
- rhs=self.rhs
-
- # merge the old and new aliases (if any)
- old_aliases=obj.aliases.get(category=category,return_list=True)
- new_aliases=[alias.strip().lower()foraliasinrhs.split(",")ifalias.strip()]
-
- # make the aliases only appear once
- old_aliases.extend(new_aliases)
- aliases=list(set(old_aliases))
-
- # save back to object.
- obj.aliases.add(aliases,category=category)
-
- # we need to trigger this here, since this will force
- # (default) Exits to rebuild their Exit commands with the new
- # aliases
- obj.at_cmdset_get(force_init=True)
-
- # report all aliases on the object
- caller.msg(
- "Alias(es) for '%s' set to '%s'%s."
- %(
- obj.get_display_name(caller),
- str(obj.aliases),
- " (category: '%s')"%categoryifcategoryelse"",
- )
- )
-
-
-
[docs]classCmdCopy(ObjManipCommand):
- """
- copy an object and its properties
-
- Usage:
- copy <original obj> [= <new_name>][;alias;alias..]
- [:<new_location>] [,<new_name2> ...]
-
- Create one or more copies of an object. If you don't supply any targets,
- one exact copy of the original object will be created with the name *_copy.
- """
-
- key="@copy"
- locks="cmd:perm(copy) or perm(Builder)"
- help_category="Building"
-
-
[docs]deffunc(self):
- """Uses ObjManipCommand.parse()"""
-
- caller=self.caller
- args=self.args
- ifnotargs:
- caller.msg(
- "Usage: copy <obj> [=<new_name>[;alias;alias..]]"
- "[:<new_location>] [, <new_name2>...]"
- )
- return
-
- ifnotself.rhs:
- # this has no target =, so an identical new object is created.
- from_obj_name=self.args
- from_obj=caller.search(from_obj_name)
- ifnotfrom_obj:
- return
- to_obj_name="%s_copy"%from_obj_name
- to_obj_aliases=["%s_copy"%aliasforaliasinfrom_obj.aliases.all()]
- copiedobj=ObjectDB.objects.copy_object(
- from_obj,new_key=to_obj_name,new_aliases=to_obj_aliases
- )
- ifcopiedobj:
- string="Identical copy of %s, named '%s' was created."%(
- from_obj_name,
- to_obj_name,
- )
- else:
- string="There was an error copying %s."
- else:
- # we have specified =. This might mean many object targets
- from_obj_name=self.lhs_objs[0]["name"]
- from_obj=caller.search(from_obj_name)
- ifnotfrom_obj:
- return
- forobjdefinself.rhs_objs:
- # loop through all possible copy-to targets
- to_obj_name=objdef["name"]
- to_obj_aliases=objdef["aliases"]
- to_obj_location=objdef["option"]
- ifto_obj_location:
- to_obj_location=caller.search(to_obj_location,global_search=True)
- ifnotto_obj_location:
- return
-
- copiedobj=ObjectDB.objects.copy_object(
- from_obj,
- new_key=to_obj_name,
- new_location=to_obj_location,
- new_aliases=to_obj_aliases,
- )
- ifcopiedobj:
- string="Copied %s to '%s' (aliases: %s)."%(
- from_obj_name,
- to_obj_name,
- to_obj_aliases,
- )
- else:
- string="There was an error copying %s to '%s'."%(from_obj_name,to_obj_name)
- # we are done, echo to user
- caller.msg(string)
-
-
-
[docs]classCmdCpAttr(ObjManipCommand):
- """
- copy attributes between objects
-
- Usage:
- cpattr[/switch] <obj>/<attr> = <obj1>/<attr1> [,<obj2>/<attr2>,<obj3>/<attr3>,...]
- cpattr[/switch] <obj>/<attr> = <obj1> [,<obj2>,<obj3>,...]
- cpattr[/switch] <attr> = <obj1>/<attr1> [,<obj2>/<attr2>,<obj3>/<attr3>,...]
- cpattr[/switch] <attr> = <obj1>[,<obj2>,<obj3>,...]
-
- Switches:
- move - delete the attribute from the source object after copying.
-
- Example:
- cpattr coolness = Anna/chillout, Anna/nicety, Tom/nicety
- ->
- copies the coolness attribute (defined on yourself), to attributes
- on Anna and Tom.
-
- Copy the attribute one object to one or more attributes on another object.
- If you don't supply a source object, yourself is used.
- """
-
- key="@cpattr"
- switch_options=("move",)
- locks="cmd:perm(cpattr) or perm(Builder)"
- help_category="Building"
-
-
[docs]defcheck_from_attr(self,obj,attr,clear=False):
- """
- Hook for overriding on subclassed commands. Checks to make sure a
- caller can copy the attr from the object in question. If not, return a
- false value and the command will abort. An error message should be
- provided by this function.
-
- If clear is True, user is attempting to move the attribute.
- """
- returnTrue
-
-
[docs]defcheck_to_attr(self,obj,attr):
- """
- Hook for overriding on subclassed commands. Checks to make sure a
- caller can write to the specified attribute on the specified object.
- If not, return a false value and the attribute will be skipped. An
- error message should be provided by this function.
- """
- returnTrue
-
-
[docs]defcheck_has_attr(self,obj,attr):
- """
- Hook for overriding on subclassed commands. Do any preprocessing
- required and verify an object has an attribute.
- """
- ifnotobj.attributes.has(attr):
- self.caller.msg("%s doesn't have an attribute %s."%(obj.name,attr))
- returnFalse
- returnTrue
-
-
[docs]defget_attr(self,obj,attr):
- """
- Hook for overriding on subclassed commands. Do any preprocessing
- required and get the attribute from the object.
- """
- returnobj.attributes.get(attr)
-
-
[docs]deffunc(self):
- """
- Do the copying.
- """
- caller=self.caller
-
- ifnotself.rhs:
- string="""Usage:
- cpattr[/switch] <obj>/<attr> = <obj1>/<attr1> [,<obj2>/<attr2>,<obj3>/<attr3>,...]
- cpattr[/switch] <obj>/<attr> = <obj1> [,<obj2>,<obj3>,...]
- cpattr[/switch] <attr> = <obj1>/<attr1> [,<obj2>/<attr2>,<obj3>/<attr3>,...]
- cpattr[/switch] <attr> = <obj1>[,<obj2>,<obj3>,...]"""
- caller.msg(string)
- return
-
- lhs_objattr=self.lhs_objattr
- to_objs=self.rhs_objattr
- from_obj_name=lhs_objattr[0]["name"]
- from_obj_attrs=lhs_objattr[0]["attrs"]
-
- ifnotfrom_obj_attrs:
- # this means the from_obj_name is actually an attribute
- # name on self.
- from_obj_attrs=[from_obj_name]
- from_obj=self.caller
- else:
- from_obj=caller.search(from_obj_name)
- ifnotfrom_objornotto_objs:
- caller.msg("You have to supply both source object and target(s).")
- return
- # copy to all to_obj:ects
- if"move"inself.switches:
- clear=True
- else:
- clear=False
- ifnotself.check_from_attr(from_obj,from_obj_attrs[0],clear=clear):
- return
-
- forattrinfrom_obj_attrs:
- ifnotself.check_has_attr(from_obj,attr):
- return
-
- if(len(from_obj_attrs)!=len(set(from_obj_attrs)))andclear:
- self.caller.msg("|RCannot have duplicate source names when moving!")
- return
-
- result=[]
-
- forto_objinto_objs:
- to_obj_name=to_obj["name"]
- to_obj_attrs=to_obj["attrs"]
- to_obj=caller.search(to_obj_name)
- ifnotto_obj:
- result.append("\nCould not find object '%s'"%to_obj_name)
- continue
- forinum,from_attrinenumerate(from_obj_attrs):
- try:
- to_attr=to_obj_attrs[inum]
- exceptIndexError:
- # if there are too few attributes given
- # on the to_obj, we copy the original name instead.
- to_attr=from_attr
- ifnotself.check_to_attr(to_obj,to_attr):
- continue
- value=self.get_attr(from_obj,from_attr)
- to_obj.attributes.add(to_attr,value)
- ifclearandnot(from_obj==to_objandfrom_attr==to_attr):
- from_obj.attributes.remove(from_attr)
- result.append(
- "\nMoved %s.%s -> %s.%s. (value: %s)"
- %(from_obj.name,from_attr,to_obj_name,to_attr,repr(value))
- )
- else:
- result.append(
- "\nCopied %s.%s -> %s.%s. (value: %s)"
- %(from_obj.name,from_attr,to_obj_name,to_attr,repr(value))
- )
- caller.msg("".join(result))
-
-
-
[docs]classCmdMvAttr(ObjManipCommand):
- """
- move attributes between objects
-
- Usage:
- mvattr[/switch] <obj>/<attr> = <obj1>/<attr1> [,<obj2>/<attr2>,<obj3>/<attr3>,...]
- mvattr[/switch] <obj>/<attr> = <obj1> [,<obj2>,<obj3>,...]
- mvattr[/switch] <attr> = <obj1>/<attr1> [,<obj2>/<attr2>,<obj3>/<attr3>,...]
- mvattr[/switch] <attr> = <obj1>[,<obj2>,<obj3>,...]
-
- Switches:
- copy - Don't delete the original after moving.
-
- Move an attribute from one object to one or more attributes on another
- object. If you don't supply a source object, yourself is used.
- """
-
- key="@mvattr"
- switch_options=("copy",)
- locks="cmd:perm(mvattr) or perm(Builder)"
- help_category="Building"
-
-
[docs]deffunc(self):
- """
- Do the moving
- """
- ifnotself.rhs:
- string="""Usage:
- mvattr[/switch] <obj>/<attr> = <obj1>/<attr1> [,<obj2>/<attr2>,<obj3>/<attr3>,...]
- mvattr[/switch] <obj>/<attr> = <obj1> [,<obj2>,<obj3>,...]
- mvattr[/switch] <attr> = <obj1>/<attr1> [,<obj2>/<attr2>,<obj3>/<attr3>,...]
- mvattr[/switch] <attr> = <obj1>[,<obj2>,<obj3>,...]"""
- self.caller.msg(string)
- return
-
- # simply use cpattr for all the functionality
- if"copy"inself.switches:
- self.execute_cmd("cpattr %s"%self.args)
- else:
- self.execute_cmd("cpattr/move %s"%self.args)
-
-
-
[docs]classCmdCreate(ObjManipCommand):
- """
- create new objects
-
- Usage:
- create[/drop] <objname>[;alias;alias...][:typeclass], <objname>...
-
- switch:
- drop - automatically drop the new object into your current
- location (this is not echoed). This also sets the new
- object's home to the current location rather than to you.
-
- Creates one or more new objects. If typeclass is given, the object
- is created as a child of this typeclass. The typeclass script is
- assumed to be located under types/ and any further
- directory structure is given in Python notation. So if you have a
- correct typeclass 'RedButton' defined in
- types/examples/red_button.py, you could create a new
- object of this type like this:
-
- create/drop button;red : examples.red_button.RedButton
-
- """
-
- key="@create"
- switch_options=("drop",)
- locks="cmd:perm(create) or perm(Builder)"
- help_category="Building"
-
- # lockstring of newly created objects, for easy overloading.
- # Will be formatted with the {id} of the creating object.
- new_obj_lockstring="control:id({id}) or perm(Admin);delete:id({id}) or perm(Admin)"
-
-
[docs]deffunc(self):
- """
- Creates the object.
- """
-
- caller=self.caller
-
- ifnotself.args:
- string="Usage: create[/drop] <newname>[;alias;alias...] [:typeclass.path]"
- caller.msg(string)
- return
-
- # create the objects
- forobjdefinself.lhs_objs:
- string=""
- name=objdef["name"]
- aliases=objdef["aliases"]
- typeclass=objdef["option"]
-
- # create object (if not a valid typeclass, the default
- # object typeclass will automatically be used)
- lockstring=self.new_obj_lockstring.format(id=caller.id)
- obj=create.create_object(
- typeclass,
- name,
- caller,
- home=caller,
- aliases=aliases,
- locks=lockstring,
- report_to=caller,
- )
- ifnotobj:
- continue
- ifaliases:
- string="You create a new %s: %s (aliases: %s)."
- string=string%(obj.typename,obj.name,", ".join(aliases))
- else:
- string="You create a new %s: %s."
- string=string%(obj.typename,obj.name)
- # set a default desc
- ifnotobj.db.desc:
- obj.db.desc="You see nothing special."
- if"drop"inself.switches:
- ifcaller.location:
- obj.home=caller.location
- obj.move_to(caller.location,quiet=True)
- ifstring:
- caller.msg(string)
-
-
-def_desc_load(caller):
- returncaller.db.evmenu_target.db.descor""
-
-
-def_desc_save(caller,buf):
- """
- Save line buffer to the desc prop. This should
- return True if successful and also report its status to the user.
- """
- caller.db.evmenu_target.db.desc=buf
- caller.msg("Saved.")
- returnTrue
-
-
-def_desc_quit(caller):
- caller.attributes.remove("evmenu_target")
- caller.msg("Exited editor.")
-
-
-
[docs]classCmdDesc(COMMAND_DEFAULT_CLASS):
- """
- describe an object or the current room.
-
- Usage:
- desc [<obj> =] <description>
-
- Switches:
- edit - Open up a line editor for more advanced editing.
-
- Sets the "desc" attribute on an object. If an object is not given,
- describe the current room.
- """
-
- key="@desc"
- switch_options=("edit",)
- locks="cmd:perm(desc) or perm(Builder)"
- help_category="Building"
-
-
[docs]defedit_handler(self):
- ifself.rhs:
- self.msg("|rYou may specify a value, or use the edit switch, but not both.|n")
- return
- ifself.args:
- obj=self.caller.search(self.args)
- else:
- obj=self.caller.locationorself.msg("|rYou can't describe oblivion.|n")
- ifnotobj:
- return
-
- ifnot(obj.access(self.caller,"control")orobj.access(self.caller,"edit")):
- self.caller.msg("You don't have permission to edit the description of %s."%obj.key)
- return
-
- self.caller.db.evmenu_target=obj
- # launch the editor
- EvEditor(
- self.caller,
- loadfunc=_desc_load,
- savefunc=_desc_save,
- quitfunc=_desc_quit,
- key="desc",
- persistent=True,
- )
- return
-
-
[docs]deffunc(self):
- """Define command"""
-
- caller=self.caller
- ifnotself.argsand"edit"notinself.switches:
- caller.msg("Usage: desc [<obj> =] <description>")
- return
-
- if"edit"inself.switches:
- self.edit_handler()
- return
-
- if"="inself.args:
- # We have an =
- obj=caller.search(self.lhs)
- ifnotobj:
- return
- desc=self.rhsor""
- else:
- obj=caller.locationorself.msg("|rYou don't have a location to describe.|n")
- ifnotobj:
- return
- desc=self.args
- ifobj.access(self.caller,"control")orobj.access(self.caller,"edit"):
- obj.db.desc=desc
- caller.msg("The description was set on %s."%obj.get_display_name(caller))
- else:
- caller.msg("You don't have permission to edit the description of %s."%obj.key)
-
-
-
[docs]classCmdDestroy(COMMAND_DEFAULT_CLASS):
- """
- permanently delete objects
-
- Usage:
- destroy[/switches] [obj, obj2, obj3, [dbref-dbref], ...]
-
- Switches:
- override - The destroy command will usually avoid accidentally
- destroying account objects. This switch overrides this safety.
- force - destroy without confirmation.
- Examples:
- destroy house, roof, door, 44-78
- destroy 5-10, flower, 45
- destroy/force north
-
- Destroys one or many objects. If dbrefs are used, a range to delete can be
- given, e.g. 4-10. Also the end points will be deleted. This command
- displays a confirmation before destroying, to make sure of your choice.
- You can specify the /force switch to bypass this confirmation.
- """
-
- key="@destroy"
- aliases=["@delete","@del"]
- switch_options=("override","force")
- locks="cmd:perm(destroy) or perm(Builder)"
- help_category="Building"
-
- confirm=True# set to False to always bypass confirmation
- default_confirm="yes"# what to assume if just pressing enter (yes/no)
-
-
[docs]deffunc(self):
- """Implements the command."""
-
- caller=self.caller
- delete=True
-
- ifnotself.argsornotself.lhslist:
- caller.msg("Usage: destroy[/switches] [obj, obj2, obj3, [dbref-dbref],...]")
- delete=False
-
- defdelobj(obj):
- # helper function for deleting a single object
- string=""
- ifnotobj.pk:
- string="\nObject %s was already deleted."%obj.db_key
- else:
- objname=obj.name
- ifnot(obj.access(caller,"control")orobj.access(caller,"delete")):
- return"\nYou don't have permission to delete %s."%objname
- ifobj.accountand"override"notinself.switches:
- return(
- "\nObject %s is controlled by an active account. Use /override to delete anyway."
- %objname
- )
- ifobj.dbid==int(settings.DEFAULT_HOME.lstrip("#")):
- return(
- "\nYou are trying to delete |c%s|n, which is set as DEFAULT_HOME. "
- "Re-point settings.DEFAULT_HOME to another "
- "object before continuing."%objname
- )
-
- had_exits=hasattr(obj,"exits")andobj.exits
- had_objs=hasattr(obj,"contents")andany(
- obj
- forobjinobj.contents
- ifnot(hasattr(obj,"exits")andobjnotinobj.exits)
- )
- # do the deletion
- okay=obj.delete()
- ifnotokay:
- string+=(
- "\nERROR: %s not deleted, probably because delete() returned False."
- %objname
- )
- else:
- string+="\n%s was destroyed."%objname
- ifhad_exits:
- string+=" Exits to and from %s were destroyed as well."%objname
- ifhad_objs:
- string+=" Objects inside %s were moved to their homes."%objname
- returnstring
-
- objs=[]
- forobjnameinself.lhslist:
- ifnotdelete:
- continue
-
- if"-"inobjname:
- # might be a range of dbrefs
- dmin,dmax=[utils.dbref(part,reqhash=False)forpartinobjname.split("-",1)]
- ifdminanddmax:
- fordbrefinrange(int(dmin),int(dmax+1)):
- obj=caller.search("#"+str(dbref))
- ifobj:
- objs.append(obj)
- continue
- else:
- obj=caller.search(objname)
- else:
- obj=caller.search(objname)
-
- ifobjisNone:
- self.caller.msg(
- " (Objects to destroy must either be local or specified with a unique #dbref.)"
- )
- elifobjnotinobjs:
- objs.append(obj)
-
- ifobjsand("force"notinself.switchesandtype(self).confirm):
- confirm="Are you sure you want to destroy "
- iflen(objs)==1:
- confirm+=objs[0].get_display_name(caller)
- eliflen(objs)<5:
- confirm+=", ".join([obj.get_display_name(caller)forobjinobjs])
- else:
- confirm+=", ".join(["#{}".format(obj.id)forobjinobjs])
- confirm+=" [yes]/no?"ifself.default_confirm=="yes"else" yes/[no]"
- answer=""
- answer=yield(confirm)
- answer=self.default_confirmifanswer==""elseanswer
-
- ifanswerandanswernotin("yes","y","no","n"):
- caller.msg(
- "Canceled: Either accept the default by pressing return or specify yes/no."
- )
- delete=False
- elifanswer.strip().lower()in("n","no"):
- caller.msg("Canceled: No object was destroyed.")
- delete=False
-
- ifdelete:
- results=[]
- forobjinobjs:
- results.append(delobj(obj))
-
- ifresults:
- caller.msg("".join(results).strip())
-
-
-
[docs]classCmdDig(ObjManipCommand):
- """
- build new rooms and connect them to the current location
-
- Usage:
- dig[/switches] <roomname>[;alias;alias...][:typeclass]
- [= <exit_to_there>[;alias][:typeclass]]
- [, <exit_to_here>[;alias][:typeclass]]
-
- Switches:
- tel or teleport - move yourself to the new room
-
- Examples:
- dig kitchen = north;n, south;s
- dig house:myrooms.MyHouseTypeclass
- dig sheer cliff;cliff;sheer = climb up, climb down
-
- This command is a convenient way to build rooms quickly; it creates the
- new room and you can optionally set up exits back and forth between your
- current room and the new one. You can add as many aliases as you
- like to the name of the room and the exits in question; an example
- would be 'north;no;n'.
- """
-
- key="@dig"
- switch_options=("teleport",)
- locks="cmd:perm(dig) or perm(Builder)"
- help_category="Building"
-
- # lockstring of newly created rooms, for easy overloading.
- # Will be formatted with the {id} of the creating object.
- new_room_lockstring=(
- "control:id({id}) or perm(Admin); "
- "delete:id({id}) or perm(Admin); "
- "edit:id({id}) or perm(Admin)"
- )
-
-
[docs]deffunc(self):
- """Do the digging. Inherits variables from ObjManipCommand.parse()"""
-
- caller=self.caller
-
- ifnotself.lhs:
- string="Usage: dig[/teleport] <roomname>[;alias;alias...]""[:parent] [= <exit_there>"
- string+="[;alias;alias..][:parent]] "
- string+="[, <exit_back_here>[;alias;alias..][:parent]]"
- caller.msg(string)
- return
-
- room=self.lhs_objs[0]
-
- ifnotroom["name"]:
- caller.msg("You must supply a new room name.")
- return
- location=caller.location
-
- # Create the new room
- typeclass=room["option"]
- ifnottypeclass:
- typeclass=settings.BASE_ROOM_TYPECLASS
-
- # create room
- new_room=create.create_object(
- typeclass,room["name"],aliases=room["aliases"],report_to=caller
- )
- lockstring=self.new_room_lockstring.format(id=caller.id)
- new_room.locks.add(lockstring)
- alias_string=""
- ifnew_room.aliases.all():
- alias_string=" (%s)"%", ".join(new_room.aliases.all())
- room_string="Created room %s(%s)%s of type %s."%(
- new_room,
- new_room.dbref,
- alias_string,
- typeclass,
- )
-
- # create exit to room
-
- exit_to_string=""
- exit_back_string=""
-
- ifself.rhs_objs:
- to_exit=self.rhs_objs[0]
- ifnotto_exit["name"]:
- exit_to_string="\nNo exit created to new room."
- elifnotlocation:
- exit_to_string="\nYou cannot create an exit from a None-location."
- else:
- # Build the exit to the new room from the current one
- typeclass=to_exit["option"]
- ifnottypeclass:
- typeclass=settings.BASE_EXIT_TYPECLASS
-
- new_to_exit=create.create_object(
- typeclass,
- to_exit["name"],
- location,
- aliases=to_exit["aliases"],
- locks=lockstring,
- destination=new_room,
- report_to=caller,
- )
- alias_string=""
- ifnew_to_exit.aliases.all():
- alias_string=" (%s)"%", ".join(new_to_exit.aliases.all())
- exit_to_string="\nCreated Exit from %s to %s: %s(%s)%s."
- exit_to_string=exit_to_string%(
- location.name,
- new_room.name,
- new_to_exit,
- new_to_exit.dbref,
- alias_string,
- )
-
- # Create exit back from new room
-
- iflen(self.rhs_objs)>1:
- # Building the exit back to the current room
- back_exit=self.rhs_objs[1]
- ifnotback_exit["name"]:
- exit_back_string="\nNo back exit created."
- elifnotlocation:
- exit_back_string="\nYou cannot create an exit back to a None-location."
- else:
- typeclass=back_exit["option"]
- ifnottypeclass:
- typeclass=settings.BASE_EXIT_TYPECLASS
- new_back_exit=create.create_object(
- typeclass,
- back_exit["name"],
- new_room,
- aliases=back_exit["aliases"],
- locks=lockstring,
- destination=location,
- report_to=caller,
- )
- alias_string=""
- ifnew_back_exit.aliases.all():
- alias_string=" (%s)"%", ".join(new_back_exit.aliases.all())
- exit_back_string="\nCreated Exit back from %s to %s: %s(%s)%s."
- exit_back_string=exit_back_string%(
- new_room.name,
- location.name,
- new_back_exit,
- new_back_exit.dbref,
- alias_string,
- )
- caller.msg("%s%s%s"%(room_string,exit_to_string,exit_back_string))
- ifnew_roomand"teleport"inself.switches:
- caller.move_to(new_room)
-
-
-
[docs]classCmdTunnel(COMMAND_DEFAULT_CLASS):
- """
- create new rooms in cardinal directions only
-
- Usage:
- tunnel[/switch] <direction>[:typeclass] [= <roomname>[;alias;alias;...][:typeclass]]
-
- Switches:
- oneway - do not create an exit back to the current location
- tel - teleport to the newly created room
-
- Example:
- tunnel n
- tunnel n = house;mike's place;green building
-
- This is a simple way to build using pre-defined directions:
- |wn,ne,e,se,s,sw,w,nw|n (north, northeast etc)
- |wu,d|n (up and down)
- |wi,o|n (in and out)
- The full names (north, in, southwest, etc) will always be put as
- main name for the exit, using the abbreviation as an alias (so an
- exit will always be able to be used with both "north" as well as
- "n" for example). Opposite directions will automatically be
- created back from the new room unless the /oneway switch is given.
- For more flexibility and power in creating rooms, use dig.
- """
-
- key="@tunnel"
- aliases=["@tun"]
- switch_options=("oneway","tel")
- locks="cmd: perm(tunnel) or perm(Builder)"
- help_category="Building"
-
- # store the direction, full name and its opposite
- directions={
- "n":("north","s"),
- "ne":("northeast","sw"),
- "e":("east","w"),
- "se":("southeast","nw"),
- "s":("south","n"),
- "sw":("southwest","ne"),
- "w":("west","e"),
- "nw":("northwest","se"),
- "u":("up","d"),
- "d":("down","u"),
- "i":("in","o"),
- "o":("out","i"),
- }
-
-
[docs]deffunc(self):
- """Implements the tunnel command"""
-
- ifnotself.argsornotself.lhs:
- string=(
- "Usage: tunnel[/switch] <direction>[:typeclass] [= <roomname>"
- "[;alias;alias;...][:typeclass]]"
- )
- self.caller.msg(string)
- return
-
- # If we get a typeclass, we need to get just the exitname
- exitshort=self.lhs.split(":")[0]
-
- ifexitshortnotinself.directions:
- string="tunnel can only understand the following directions: %s."%",".join(
- sorted(self.directions.keys())
- )
- string+="\n(use dig for more freedom)"
- self.caller.msg(string)
- return
-
- # retrieve all input and parse it
- exitname,backshort=self.directions[exitshort]
- backname=self.directions[backshort][0]
-
- # if we recieved a typeclass for the exit, add it to the alias(short name)
- if":"inself.lhs:
- # limit to only the first : character
- exit_typeclass=":"+self.lhs.split(":",1)[-1]
- # exitshort and backshort are the last part of the exit strings,
- # so we add our typeclass argument after
- exitshort+=exit_typeclass
- backshort+=exit_typeclass
-
- roomname="Some place"
- ifself.rhs:
- roomname=self.rhs# this may include aliases; that's fine.
-
- telswitch=""
- if"tel"inself.switches:
- telswitch="/teleport"
- backstring=""
- if"oneway"notinself.switches:
- backstring=", %s;%s"%(backname,backshort)
-
- # build the string we will use to call dig
- digstring="dig%s%s = %s;%s%s"%(telswitch,roomname,exitname,exitshort,backstring)
- self.execute_cmd(digstring)
-
-
-
[docs]classCmdLink(COMMAND_DEFAULT_CLASS):
- """
- link existing rooms together with exits
-
- Usage:
- link[/switches] <object> = <target>
- link[/switches] <object> =
- link[/switches] <object>
-
- Switch:
- twoway - connect two exits. For this to work, BOTH <object>
- and <target> must be exit objects.
-
- If <object> is an exit, set its destination to <target>. Two-way operation
- instead sets the destination to the *locations* of the respective given
- arguments.
- The second form (a lone =) sets the destination to None (same as
- the unlink command) and the third form (without =) just shows the
- currently set destination.
- """
-
- key="@link"
- locks="cmd:perm(link) or perm(Builder)"
- help_category="Building"
-
-
[docs]deffunc(self):
- """Perform the link"""
- caller=self.caller
-
- ifnotself.args:
- caller.msg("Usage: link[/twoway] <object> = <target>")
- return
-
- object_name=self.lhs
-
- # try to search locally first
- results=caller.search(object_name,quiet=True)
- iflen(results)>1:# local results was a multimatch. Inform them to be more specific
- _AT_SEARCH_RESULT=variable_from_module(*settings.SEARCH_AT_RESULT.rsplit(".",1))
- return_AT_SEARCH_RESULT(results,caller,query=object_name)
- eliflen(results)==1:# A unique local match
- obj=results[0]
- else:# No matches. Search globally
- obj=caller.search(object_name,global_search=True)
- ifnotobj:
- return
-
- ifself.rhs:
- # this means a target name was given
- target=caller.search(self.rhs,global_search=True)
- ifnottarget:
- return
-
- iftarget==obj:
- self.caller.msg("Cannot link an object to itself.")
- return
-
- string=""
- note="Note: %s(%s) did not have a destination set before. Make sure you linked the right thing."
- ifnotobj.destination:
- string=note%(obj.name,obj.dbref)
- if"twoway"inself.switches:
- ifnot(target.locationandobj.location):
- string="To create a two-way link, %s and %s must both have a location"%(
- obj,
- target,
- )
- string+=" (i.e. they cannot be rooms, but should be exits)."
- self.caller.msg(string)
- return
- ifnottarget.destination:
- string+=note%(target.name,target.dbref)
- obj.destination=target.location
- target.destination=obj.location
- string+="\nLink created %s (in %s) <-> %s (in %s) (two-way)."%(
- obj.name,
- obj.location,
- target.name,
- target.location,
- )
- else:
- obj.destination=target
- string+="\nLink created %s -> %s (one way)."%(obj.name,target)
-
- elifself.rhsisNone:
- # this means that no = was given (otherwise rhs
- # would have been an empty string). So we inspect
- # the home/destination on object
- dest=obj.destination
- ifdest:
- string="%s is an exit to %s."%(obj.name,dest.name)
- else:
- string="%s is not an exit. Its home location is %s."%(obj.name,obj.home)
-
- else:
- # We gave the command link 'obj = ' which means we want to
- # clear destination.
- ifobj.destination:
- obj.destination=None
- string="Former exit %s no longer links anywhere."%obj.name
- else:
- string="%s had no destination to unlink."%obj.name
- # give feedback
- caller.msg(string.strip())
-
-
-
[docs]classCmdUnLink(CmdLink):
- """
- remove exit-connections between rooms
-
- Usage:
- unlink <Object>
-
- Unlinks an object, for example an exit, disconnecting
- it from whatever it was connected to.
- """
-
- # this is just a child of CmdLink
-
- key="unlink"
- locks="cmd:perm(unlink) or perm(Builder)"
- help_key="Building"
-
-
[docs]deffunc(self):
- """
- All we need to do here is to set the right command
- and call func in CmdLink
- """
-
- caller=self.caller
-
- ifnotself.args:
- caller.msg("Usage: unlink <object>")
- return
-
- # This mimics 'link <obj> = ' which is the same as unlink
- self.rhs=""
-
- # call the link functionality
- super().func()
-
-
-
[docs]classCmdSetHome(CmdLink):
- """
- set an object's home location
-
- Usage:
- sethome <obj> [= <home_location>]
- sethom <obj>
-
- The "home" location is a "safety" location for objects; they
- will be moved there if their current location ceases to exist. All
- objects should always have a home location for this reason.
- It is also a convenient target of the "home" command.
-
- If no location is given, just view the object's home location.
- """
-
- key="@sethome"
- locks="cmd:perm(sethome) or perm(Builder)"
- help_category="Building"
-
-
[docs]deffunc(self):
- """implement the command"""
- ifnotself.args:
- string="Usage: sethome <obj> [= <home_location>]"
- self.caller.msg(string)
- return
-
- obj=self.caller.search(self.lhs,global_search=True)
- ifnotobj:
- return
- ifnotself.rhs:
- # just view
- home=obj.home
- ifnothome:
- string="This object has no home location set!"
- else:
- string="%s's current home is %s(%s)."%(obj,home,home.dbref)
- else:
- # set a home location
- new_home=self.caller.search(self.rhs,global_search=True)
- ifnotnew_home:
- return
- old_home=obj.home
- obj.home=new_home
- ifold_home:
- string="Home location of %s was changed from %s(%s) to %s(%s)."%(
- obj,
- old_home,
- old_home.dbref,
- new_home,
- new_home.dbref,
- )
- else:
- string="Home location of %s was set to %s(%s)."%(obj,new_home,new_home.dbref)
- self.caller.msg(string)
-
-
-
[docs]classCmdListCmdSets(COMMAND_DEFAULT_CLASS):
- """
- list command sets defined on an object
-
- Usage:
- cmdsets <obj>
-
- This displays all cmdsets assigned
- to a user. Defaults to yourself.
- """
-
- key="@cmdsets"
- locks="cmd:perm(listcmdsets) or perm(Builder)"
- help_category="Building"
-
-
[docs]classCmdName(ObjManipCommand):
- """
- change the name and/or aliases of an object
-
- Usage:
- name <obj> = <newname>;alias1;alias2
-
- Rename an object to something new. Use *obj to
- rename an account.
-
- """
-
- key="@name"
- aliases=["@rename"]
- locks="cmd:perm(rename) or perm(Builder)"
- help_category="Building"
-
-
[docs]deffunc(self):
- """change the name"""
-
- caller=self.caller
- ifnotself.args:
- caller.msg("Usage: name <obj> = <newname>[;alias;alias;...]")
- return
-
- obj=None
- ifself.lhs_objs:
- objname=self.lhs_objs[0]["name"]
- ifobjname.startswith("*"):
- # account mode
- obj=caller.account.search(objname.lstrip("*"))
- ifobj:
- ifself.rhs_objs[0]["aliases"]:
- caller.msg("Accounts can't have aliases.")
- return
- newname=self.rhs
- ifnotnewname:
- caller.msg("No name defined!")
- return
- ifnot(obj.access(caller,"control")orobj.access(caller,"edit")):
- caller.msg("You don't have right to edit this account %s."%obj)
- return
- obj.username=newname
- obj.save()
- caller.msg("Account's name changed to '%s'."%newname)
- return
- # object search, also with *
- obj=caller.search(objname)
- ifnotobj:
- return
- ifself.rhs_objs:
- newname=self.rhs_objs[0]["name"]
- aliases=self.rhs_objs[0]["aliases"]
- else:
- newname=self.rhs
- aliases=None
- ifnotnewnameandnotaliases:
- caller.msg("No names or aliases defined!")
- return
- ifnot(obj.access(caller,"control")orobj.access(caller,"edit")):
- caller.msg("You don't have the right to edit %s."%obj)
- return
- # change the name and set aliases:
- ifnewname:
- obj.key=newname
- astring=""
- ifaliases:
- [obj.aliases.add(alias)foraliasinaliases]
- astring=" (%s)"%(", ".join(aliases))
- # fix for exits - we need their exit-command to change name too
- ifobj.destination:
- obj.flush_from_cache(force=True)
- caller.msg("Object's name changed to '%s'%s."%(newname,astring))
-
-
-
[docs]classCmdOpen(ObjManipCommand):
- """
- open a new exit from the current room
-
- Usage:
- open <new exit>[;alias;alias..][:typeclass] [,<return exit>[;alias;..][:typeclass]]] = <destination>
-
- Handles the creation of exits. If a destination is given, the exit
- will point there. The <return exit> argument sets up an exit at the
- destination leading back to the current room. Destination name
- can be given both as a #dbref and a name, if that name is globally
- unique.
-
- """
-
- key="@open"
- locks="cmd:perm(open) or perm(Builder)"
- help_category="Building"
-
- new_obj_lockstring="control:id({id}) or perm(Admin);delete:id({id}) or perm(Admin)"
- # a custom member method to chug out exits and do checks
-
[docs]defcreate_exit(self,exit_name,location,destination,exit_aliases=None,typeclass=None):
- """
- Helper function to avoid code duplication.
- At this point we know destination is a valid location
-
- """
- caller=self.caller
- string=""
- # check if this exit object already exists at the location.
- # we need to ignore errors (so no automatic feedback)since we
- # have to know the result of the search to decide what to do.
- exit_obj=caller.search(exit_name,location=location,quiet=True,exact=True)
- iflen(exit_obj)>1:
- # give error message and return
- caller.search(exit_name,location=location,exact=True)
- returnNone
- ifexit_obj:
- exit_obj=exit_obj[0]
- ifnotexit_obj.destination:
- # we are trying to link a non-exit
- string="'%s' already exists and is not an exit!\nIf you want to convert it "
- string+=(
- "to an exit, you must assign an object to the 'destination' property first."
- )
- caller.msg(string%exit_name)
- returnNone
- # we are re-linking an old exit.
- old_destination=exit_obj.destination
- ifold_destination:
- string="Exit %s already exists."%exit_name
- ifold_destination.id!=destination.id:
- # reroute the old exit.
- exit_obj.destination=destination
- ifexit_aliases:
- [exit_obj.aliases.add(alias)foraliasinexit_aliases]
- string+=" Rerouted its old destination '%s' to '%s' and changed aliases."%(
- old_destination.name,
- destination.name,
- )
- else:
- string+=" It already points to the correct place."
-
- else:
- # exit does not exist before. Create a new one.
- lockstring=self.new_obj_lockstring.format(id=caller.id)
- ifnottypeclass:
- typeclass=settings.BASE_EXIT_TYPECLASS
- exit_obj=create.create_object(
- typeclass,
- key=exit_name,
- location=location,
- aliases=exit_aliases,
- locks=lockstring,
- report_to=caller,
- )
- ifexit_obj:
- # storing a destination is what makes it an exit!
- exit_obj.destination=destination
- string=(
- ""
- ifnotexit_aliases
- else" (aliases: %s)"%(", ".join([str(e)foreinexit_aliases]))
- )
- string="Created new Exit '%s' from %s to %s%s."%(
- exit_name,
- location.name,
- destination.name,
- string,
- )
- else:
- string="Error: Exit '%s' not created."%exit_name
- # emit results
- caller.msg(string)
- returnexit_obj
[docs]deffunc(self):
- """
- This is where the processing starts.
- Uses the ObjManipCommand.parser() for pre-processing
- as well as the self.create_exit() method.
- """
- # Create exit
- ok=self.create_exit(self.exit_name,self.location,self.destination,
- self.exit_aliases,self.exit_typeclass)
- ifnotok:
- # an error; the exit was not created, so we quit.
- return
- # Create back exit, if any
- iflen(self.lhs_objs)>1:
- back_exit_name=self.lhs_objs[1]["name"]
- back_exit_aliases=self.lhs_objs[1]["aliases"]
- back_exit_typeclass=self.lhs_objs[1]["option"]
- self.create_exit(back_exit_name,self.destination,self.location,back_exit_aliases,
- back_exit_typeclass)
-
-
-def_convert_from_string(cmd,strobj):
- """
- Converts a single object in *string form* to its equivalent python
- type.
-
- Python earlier than 2.6:
- Handles floats, ints, and limited nested lists and dicts
- (can't handle lists in a dict, for example, this is mainly due to
- the complexity of parsing this rather than any technical difficulty -
- if there is a need for set-ing such complex structures on the
- command line we might consider adding it).
- Python 2.6 and later:
- Supports all Python structures through literal_eval as long as they
- are valid Python syntax. If they are not (such as [test, test2], ie
- without the quotes around the strings), the entire structure will
- be converted to a string and a warning will be given.
-
- We need to convert like this since all data being sent over the
- telnet connection by the Account is text - but we will want to
- store it as the "real" python type so we can do convenient
- comparisons later (e.g. obj.db.value = 2, if value is stored as a
- string this will always fail).
- """
-
- # Use literal_eval to parse python structure exactly.
- try:
- return_LITERAL_EVAL(strobj)
- except(SyntaxError,ValueError):
- # treat as string
- strobj=utils.to_str(strobj)
- string=(
- '|RNote: name "|r%s|R" was converted to a string. '
- "Make sure this is acceptable."%strobj
- )
- cmd.caller.msg(string)
- returnstrobj
- exceptExceptionaserr:
- string="|RUnknown error in evaluating Attribute: {}".format(err)
- returnstring
-
-
-
[docs]classCmdSetAttribute(ObjManipCommand):
- """
- set attribute on an object or account
-
- Usage:
- set[/switch] <obj>/<attr>[:category] = <value>
- set[/switch] <obj>/<attr>[:category] = # delete attribute
- set[/switch] <obj>/<attr>[:category] # view attribute
- set[/switch] *<account>/<attr>[:category] = <value>
-
- Switch:
- edit: Open the line editor (string values only)
- script: If we're trying to set an attribute on a script
- channel: If we're trying to set an attribute on a channel
- account: If we're trying to set an attribute on an account
- room: Setting an attribute on a room (global search)
- exit: Setting an attribute on an exit (global search)
- char: Setting an attribute on a character (global search)
- character: Alias for char, as above.
-
- Example:
- set self/foo = "bar"
- set/delete self/foo
- set self/foo = $dbref(#53)
-
- Sets attributes on objects. The second example form above clears a
- previously set attribute while the third form inspects the current value of
- the attribute (if any). The last one (with the star) is a shortcut for
- operating on a player Account rather than an Object.
-
- If you want <value> to be an object, use $dbef(#dbref) or
- $search(key) to assign it. You need control or edit access to
- the object you are adding.
-
- The most common data to save with this command are strings and
- numbers. You can however also set Python primitives such as lists,
- dictionaries and tuples on objects (this might be important for
- the functionality of certain custom objects). This is indicated
- by you starting your value with one of |c'|n, |c"|n, |c(|n, |c[|n
- or |c{ |n.
-
- Once you have stored a Python primitive as noted above, you can include
- |c[<key>]|n in <attr> to reference nested values in e.g. a list or dict.
-
- Remember that if you use Python primitives like this, you must
- write proper Python syntax too - notably you must include quotes
- around your strings or you will get an error.
-
- """
-
- key="@set"
- locks="cmd:perm(set) or perm(Builder)"
- help_category="Building"
- nested_re=re.compile(r"\[.*?\]")
- not_found=object()
-
-
[docs]defcheck_obj(self,obj):
- """
- This may be overridden by subclasses in case restrictions need to be
- placed on whether certain objects can have attributes set by certain
- accounts.
-
- This function is expected to display its own error message.
-
- Returning False will abort the command.
- """
- returnTrue
-
-
[docs]defcheck_attr(self,obj,attr_name,category):
- """
- This may be overridden by subclasses in case restrictions need to be
- placed on what attributes can be set by who beyond the normal lock.
-
- This functions is expected to display its own error message. It is
- run once for every attribute that is checked, blocking only those
- attributes which are not permitted and letting the others through.
- """
- returnattr_name
-
-
[docs]defsplit_nested_attr(self,attr):
- """
- Yields tuples of (possible attr name, nested keys on that attr).
- For performance, this is biased to the deepest match, but allows compatability
- with older attrs that might have been named with `[]`'s.
-
- > list(split_nested_attr("nested['asdf'][0]"))
- [
- ('nested', ['asdf', 0]),
- ("nested['asdf']", [0]),
- ("nested['asdf'][0]", []),
- ]
- """
- quotes="\"'"
-
- defclean_key(val):
- val=val.strip("[]")
- ifval[0]inquotes:
- returnval.strip(quotes)
- ifval[0]==LIST_APPEND_CHAR:
- # List insert/append syntax
- returnval
- try:
- returnint(val)
- exceptValueError:
- returnval
-
- parts=self.nested_re.findall(attr)
-
- base_attr=""
- ifparts:
- base_attr=attr[:attr.find(parts[0])]
- forindex,partinenumerate(parts):
- yield(base_attr,[clean_key(p)forpinparts[index:]])
- base_attr+=part
- yield(attr,[])
[docs]defview_attr(self,obj,attr,category):
- """
- Look up the value of an attribute and return a string displaying it.
- """
- nested=False
- forkey,nested_keysinself.split_nested_attr(attr):
- nested=True
- ifobj.attributes.has(key):
- val=obj.attributes.get(key)
- val=self.do_nested_lookup(val,*nested_keys)
- ifvalisnotself.not_found:
- returnf"\nAttribute {obj.name}/|w{attr}|n [category:{category}] = {val}"
- error=f"\nAttribute {obj.name}/|w{attr} [category:{category}] does not exist."
- ifnested:
- error+=" (Nested lookups attempted)"
- returnerror
-
-
[docs]defrm_attr(self,obj,attr,category):
- """
- Remove an attribute from the object, or a nested data structure, and report back.
- """
- nested=False
- forkey,nested_keysinself.split_nested_attr(attr):
- nested=True
- ifobj.attributes.has(key,category):
- ifnested_keys:
- del_key=nested_keys[-1]
- val=obj.attributes.get(key,category=category)
- deep=self.do_nested_lookup(val,*nested_keys[:-1])
- ifdeepisnotself.not_found:
- try:
- deldeep[del_key]
- except(IndexError,KeyError,TypeError):
- continue
- returnf"\nDeleted attribute {obj.name}/|w{attr}|n [category:{category}]."
- else:
- exists=obj.attributes.has(key,category)
- ifexists:
- obj.attributes.remove(attr,category=category)
- returnf"\nDeleted attribute {obj.name}/|w{attr}|n [category:{category}]."
- else:
- return(f"\nNo attribute {obj.name}/|w{attr}|n [category: {category}] "
- "was found to delete.")
- error=f"\nNo attribute {obj.name}/|w{attr}|n [category: {category}] was found to delete."
- ifnested:
- error+=" (Nested lookups attempted)"
- returnerror
-
-
[docs]defset_attr(self,obj,attr,value,category):
- done=False
- forkey,nested_keysinself.split_nested_attr(attr):
- ifobj.attributes.has(key,category)andnested_keys:
- acc_key=nested_keys[-1]
- lookup_value=obj.attributes.get(key,category)
- deep=self.do_nested_lookup(lookup_value,*nested_keys[:-1])
- ifdeepisnotself.not_found:
- # To support appending and inserting to lists
- # a key that starts with LIST_APPEND_CHAR will insert a new item at that
- # location, and move the other elements down.
- # Using LIST_APPEND_CHAR alone will append to the list
- ifisinstance(acc_key,str)andacc_key[0]==LIST_APPEND_CHAR:
- try:
- iflen(acc_key)>1:
- where=int(acc_key[1:])
- deep.insert(where,value)
- else:
- deep.append(value)
- except(ValueError,AttributeError):
- pass
- else:
- value=lookup_value
- attr=key
- done=True
- break
-
- # List magic failed, just use like a key/index
- try:
- deep[acc_key]=value
- exceptTypeErroraserr:
- # Tuples can't be modified
- returnf"\n{err} - {deep}"
-
- value=lookup_value
- attr=key
- done=True
- break
-
- verb="Modified"ifobj.attributes.has(attr)else"Created"
- try:
- ifnotdone:
- obj.attributes.add(attr,value,category)
- returnf"\n{verb} attribute {obj.name}/|w{attr}|n [category:{category}] = {value}"
- exceptSyntaxError:
- # this means literal_eval tried to parse a faulty string
- return(
- "\n|RCritical Python syntax error in your value. Only "
- "primitive Python structures are allowed.\nYou also "
- "need to use correct Python syntax. Remember especially "
- "to put quotes around all strings inside lists and "
- "dicts.|n"
- )
-
- @interactive
- defedit_handler(self,obj,attr,caller):
- """Activate the line editor"""
-
- defload(caller):
- """Called for the editor to load the buffer"""
-
- try:
- old_value=obj.attributes.get(attr,raise_exception=True)
- exceptAttributeError:
- # we set empty buffer on nonexisting Attribute because otherwise
- # we'd always have the string "None" in the buffer to start with
- old_value=''
- returnstr(old_value)# we already confirmed we are ok with this
-
- defsave(caller,buf):
- """Called when editor saves its buffer."""
- obj.attributes.add(attr,buf)
- caller.msg("Saved Attribute %s."%attr)
-
- # check non-strings before activating editor
- try:
- old_value=obj.attributes.get(attr,raise_exception=True)
- ifnotisinstance(old_value,str):
- answer=yield(
- f"|rWarning: Attribute |w{attr}|r is of type |w{type(old_value).__name__}|r. "
- "\nTo continue editing, it must be converted to (and saved as) a string. "
- "Continue? [Y]/N?")
- ifanswer.lower()in('n','no'):
- self.caller.msg("Aborted edit.")
- return
- exceptAttributeError:
- pass
-
- # start the editor
- EvEditor(self.caller,load,save,key=f"{obj}/{attr}")
-
-
[docs]defsearch_for_obj(self,objname):
- """
- Searches for an object matching objname. The object may be of different typeclasses.
- Args:
- objname: Name of the object we're looking for
-
- Returns:
- A typeclassed object, or None if nothing is found.
- """
- fromevennia.utils.utilsimportvariable_from_module
-
- _AT_SEARCH_RESULT=variable_from_module(*settings.SEARCH_AT_RESULT.rsplit(".",1))
- caller=self.caller
- ifobjname.startswith("*")or"account"inself.switches:
- found_obj=caller.search_account(objname.lstrip("*"))
- elif"script"inself.switches:
- found_obj=_AT_SEARCH_RESULT(search.search_script(objname),caller)
- elif"channel"inself.switches:
- found_obj=_AT_SEARCH_RESULT(search.search_channel(objname),caller)
- else:
- global_search=True
- if"char"inself.switchesor"character"inself.switches:
- typeclass=settings.BASE_CHARACTER_TYPECLASS
- elif"room"inself.switches:
- typeclass=settings.BASE_ROOM_TYPECLASS
- elif"exit"inself.switches:
- typeclass=settings.BASE_EXIT_TYPECLASS
- else:
- global_search=False
- typeclass=None
- found_obj=caller.search(objname,global_search=global_search,typeclass=typeclass)
- returnfound_obj
-
-
[docs]deffunc(self):
- """Implement the set attribute - a limited form of py."""
-
- caller=self.caller
- ifnotself.args:
- caller.msg("Usage: set obj/attr[:category] = value. Use empty value to clear.")
- return
-
- # get values prepared by the parser
- value=self.rhs
- objname=self.lhs_objattr[0]["name"]
- attrs=self.lhs_objattr[0]["attrs"]
- category=self.lhs_objs[0].get("option")# None if unset
-
- obj=self.search_for_obj(objname)
- ifnotobj:
- return
-
- ifnotself.check_obj(obj):
- return
-
- result=[]
- if"edit"inself.switches:
- # edit in the line editor
- ifnot(obj.access(self.caller,"control")orobj.access(self.caller,"edit")):
- caller.msg("You don't have permission to edit %s."%obj.key)
- return
-
- iflen(attrs)>1:
- caller.msg("The Line editor can only be applied ""to one attribute at a time.")
- return
- ifnotattrs:
- caller.msg("Use `set/edit <objname>/<attr>` to define the Attribute to edit.\nTo "
- "edit the current room description, use `set/edit here/desc` (or "
- "use the `desc` command).")
- return
- self.edit_handler(obj,attrs[0],caller)
- return
- ifnotvalue:
- ifself.rhsisNone:
- # no = means we inspect the attribute(s)
- ifnotattrs:
- attrs=[attr.keyforattrinobj.attributes.get(category=None)]
- forattrinattrs:
- ifnotself.check_attr(obj,attr,category):
- continue
- result.append(self.view_attr(obj,attr,category))
- # we view it without parsing markup.
- self.caller.msg("".join(result).strip(),options={"raw":True})
- return
- else:
- # deleting the attribute(s)
- ifnot(obj.access(self.caller,"control")orobj.access(self.caller,"edit")):
- caller.msg("You don't have permission to edit %s."%obj.key)
- return
- forattrinattrs:
- ifnotself.check_attr(obj,attr,category):
- continue
- result.append(self.rm_attr(obj,attr,category))
- else:
- # setting attribute(s). Make sure to convert to real Python type before saving.
- # add support for $dbref() and $search() in set argument
- global_ATTRFUNCPARSER
- ifnot_ATTRFUNCPARSER:
- _ATTRFUNCPARSER=funcparser.FuncParser(
- {"dbref":funcparser.funcparser_callable_search,
- "search":funcparser.funcparser_callable_search}
- )
-
- ifnot(obj.access(self.caller,"control")orobj.access(self.caller,"edit")):
- caller.msg("You don't have permission to edit %s."%obj.key)
- return
- forattrinattrs:
- ifnotself.check_attr(obj,attr,category):
- continue
- # from evennia import set_trace;set_trace()
- parsed_value=_ATTRFUNCPARSER.parse(value,return_str=False,caller=caller)
- ifhasattr(parsed_value,"access"):
- # if this is an object we must have the right to read it, if so,
- # we will not convert it to a string
- ifnot(parsed_value.access(caller,"control")
- orparsed_value.access(self.caller,"edit")):
- caller.msg("You don't have permission to set "
- f"object with identifier '{value}'.")
- continue
- value=parsed_value
- else:
- value=_convert_from_string(self,value)
- result.append(self.set_attr(obj,attr,value,category))
- # send feedback
- caller.msg("".join(result).strip("\n"))
-
-
-
[docs]classCmdTypeclass(COMMAND_DEFAULT_CLASS):
- """
- set or change an object's typeclass
-
- Usage:
- typeclass[/switch] <object> [= typeclass.path]
- typeclass/prototype <object> = prototype_key
-
- typeclasses or typeclass/list/show [typeclass.path]
- swap - this is a shorthand for using /force/reset flags.
- update - this is a shorthand for using the /force/reload flag.
-
- Switch:
- show, examine - display the current typeclass of object (default) or, if
- given a typeclass path, show the docstring of that typeclass.
- update - *only* re-run at_object_creation on this object
- meaning locks or other properties set later may remain.
- reset - clean out *all* the attributes and properties on the
- object - basically making this a new clean object. This will also
- reset cmdsets!
- force - change to the typeclass also if the object
- already has a typeclass of the same name.
- list - show available typeclasses. Only typeclasses in modules actually
- imported or used from somewhere in the code will show up here
- (those typeclasses are still available if you know the path)
- prototype - clean and overwrite the object with the specified
- prototype key - effectively making a whole new object.
-
- Example:
- type button = examples.red_button.RedButton
- type/prototype button=a red button
-
- If the typeclass_path is not given, the current object's typeclass is
- assumed.
-
- View or set an object's typeclass. If setting, the creation hooks of the
- new typeclass will be run on the object. If you have clashing properties on
- the old class, use /reset. By default you are protected from changing to a
- typeclass of the same name as the one you already have - use /force to
- override this protection.
-
- The given typeclass must be identified by its location using python
- dot-notation pointing to the correct module and class. If no typeclass is
- given (or a wrong typeclass is given). Errors in the path or new typeclass
- will lead to the old typeclass being kept. The location of the typeclass
- module is searched from the default typeclass directory, as defined in the
- server settings.
-
- """
-
- key="@typeclass"
- aliases=["@type","@parent","@swap","@update","@typeclasses"]
- switch_options=("show","examine","update","reset","force","list","prototype")
- locks="cmd:perm(typeclass) or perm(Builder)"
- help_category="Building"
-
- def_generic_search(self,query,typeclass_path):
-
- caller=self.caller
- iftypeclass_path:
- # make sure we search the right database table
- try:
- new_typeclass=class_from_module(typeclass_path)
- exceptImportError:
- # this could be a prototype and not a typeclass at all
- returncaller.search(query)
-
- dbclass=new_typeclass.__dbclass__
-
- ifcaller.__dbclass__==dbclass:
- # object or account match
- obj=caller.search(query)
- ifnotobj:
- return
- elif(self.accountandself.account.__dbclass__==dbclass):
- # applying account while caller is object
- caller.msg(f"Trying to search {new_typeclass} with query '{self.lhs}'.")
- obj=self.account.search(query)
- ifnotobj:
- return
- elifhasattr(caller,"puppet")andcaller.puppet.__dbclass__==dbclass:
- # applying object while caller is account
- caller.msg(f"Trying to search {new_typeclass} with query '{self.lhs}'.")
- obj=caller.puppet.search(query)
- ifnotobj:
- return
- else:
- # other mismatch between caller and specified typeclass
- caller.msg(f"Trying to search {new_typeclass} with query '{self.lhs}'.")
- obj=new_typeclass.search(query)
- ifnotobj:
- ifisinstance(obj,list):
- caller.msg(f"Could not find {new_typeclass} with query '{self.lhs}'.")
- return
- else:
- # no rhs, use caller's typeclass
- obj=caller.search(query)
- ifnotobj:
- return
-
- returnobj
-
-
[docs]deffunc(self):
- """Implements command"""
-
- caller=self.caller
-
- if"list"inself.switchesorself.cmdnamein('typeclasses','@typeclasses'):
- tclasses=get_all_typeclasses()
- contribs=[keyforkeyinsorted(tclasses)ifkey.startswith("evennia.contrib")]or[
- "<None loaded>"
- ]
- core=[
- keyforkeyinsorted(tclasses)ifkey.startswith("evennia")andkeynotincontribs
- ]or["<None loaded>"]
- game=[keyforkeyinsorted(tclasses)ifnotkey.startswith("evennia")]or[
- "<None loaded>"
- ]
- string=(
- "|wCore typeclasses|n\n"
- " {core}\n"
- "|wLoaded Contrib typeclasses|n\n"
- " {contrib}\n"
- "|wGame-dir typeclasses|n\n"
- " {game}"
- ).format(
- core="\n ".join(core),contrib="\n ".join(contribs),game="\n ".join(game)
- )
- EvMore(caller,string,exit_on_lastpage=True)
- return
-
- ifnotself.args:
- caller.msg("Usage: %s <object> [= typeclass]"%self.cmdstring)
- return
-
- if"show"inself.switchesor"examine"inself.switches:
- oquery=self.lhs
- obj=caller.search(oquery,quiet=True)
- ifnotobj:
- # no object found to examine, see if it's a typeclass-path instead
- tclasses=get_all_typeclasses()
- matches=[
- (key,tclass)forkey,tclassintclasses.items()ifkey.endswith(oquery)
- ]
- nmatches=len(matches)
- ifnmatches>1:
- caller.msg(
- "Multiple typeclasses found matching {}:\n{}".format(
- oquery,"\n ".join(tup[0]fortupinmatches)
- )
- )
- elifnotmatches:
- caller.msg("No object or typeclass path found to match '{}'".format(oquery))
- else:
- # one match found
- caller.msg(
- "Docstring for typeclass '{}':\n{}".format(oquery,matches[0][1].__doc__)
- )
- else:
- # do the search again to get the error handling in case of multi-match
- obj=caller.search(oquery)
- ifnotobj:
- return
- caller.msg(
- "{}'s current typeclass is '{}.{}'".format(
- obj.name,obj.__class__.__module__,obj.__class__.__name__
- )
- )
- return
-
- obj=self._generic_search(self.lhs,self.rhs)
- ifnotobj:
- return
-
- ifnothasattr(obj,"__dbclass__"):
- string="%s is not a typed object."%obj.name
- caller.msg(string)
- return
-
- new_typeclass=self.rhsorobj.path
-
- prototype=None
- if"prototype"inself.switches:
- key=self.rhs
- prototype=protlib.search_prototype(key=key)
- iflen(prototype)>1:
- caller.msg(
- "More than one match for {}:\n{}".format(
- key,"\n".join(proto.get("prototype_key","")forprotoinprototype)
- )
- )
- return
- elifprototype:
- # one match
- prototype=prototype[0]
- else:
- # no match
- caller.msg("No prototype '{}' was found.".format(key))
- return
- new_typeclass=prototype["typeclass"]
- self.switches.append("force")
-
- if"show"inself.switchesor"examine"inself.switches:
- string="%s's current typeclass is %s."%(obj.name,obj.__class__)
- caller.msg(string)
- return
-
- ifself.cmdstringin("swap","@swap"):
- self.switches.append("force")
- self.switches.append("reset")
- elifself.cmdstringin("update","@update"):
- self.switches.append("force")
- self.switches.append("update")
-
- ifnot(obj.access(caller,"control")orobj.access(caller,"edit")):
- caller.msg("You are not allowed to do that.")
- return
-
- ifnothasattr(obj,"swap_typeclass"):
- caller.msg("This object cannot have a type at all!")
- return
-
- is_same=obj.is_typeclass(new_typeclass,exact=True)
- ifis_sameand"force"notinself.switches:
- string=(f"{obj.name} already has the typeclass '{new_typeclass}'. "
- "Use /force to override.")
- else:
- update="update"inself.switches
- reset="reset"inself.switches
- hooks="at_object_creation"ifupdateandnotresetelse"all"
- old_typeclass_path=obj.typeclass_path
-
- # special prompt for the user in cases where we want
- # to confirm changes.
- if"prototype"inself.switches:
- diff,_=spawner.prototype_diff_from_object(prototype,obj)
- txt=spawner.format_diff(diff)
- prompt=(
- "Applying prototype '%s' over '%s' will cause the follow changes:\n%s\n"
- %(prototype["key"],obj.name,txt)
- )
- ifnotreset:
- prompt+="\n|yWARNING:|n Use the /reset switch to apply the prototype over a blank state."
- prompt+="\nAre you sure you want to apply these changes [yes]/no?"
- answer=yield(prompt)
- ifanswerandanswerin("no","n"):
- caller.msg("Canceled: No changes were applied.")
- return
-
- # we let this raise exception if needed
- obj.swap_typeclass(
- new_typeclass,clean_attributes=reset,clean_cmdsets=reset,run_start_hooks=hooks
- )
-
- if"prototype"inself.switches:
- modified=spawner.batch_update_objects_with_prototype(
- prototype,objects=[obj],caller=self.caller)
- prototype_success=modified>0
- ifnotprototype_success:
- caller.msg("Prototype %s failed to apply."%prototype["key"])
-
- ifis_same:
- string="%s updated its existing typeclass (%s).\n"%(obj.name,obj.path)
- else:
- string="%s changed typeclass from %s to %s.\n"%(
- obj.name,
- old_typeclass_path,
- obj.typeclass_path,
- )
- ifupdate:
- string+="Only the at_object_creation hook was run (update mode)."
- else:
- string+="All object creation hooks were run."
- ifreset:
- string+=" All old attributes where deleted before the swap."
- else:
- string+=" Attributes set before swap were not removed."
- if"prototype"inself.switchesandprototype_success:
- string+=(
- " Prototype '%s' was successfully applied over the object type."
- %prototype["key"]
- )
-
- caller.msg(string)
-
-
-
[docs]classCmdWipe(ObjManipCommand):
- """
- clear all attributes from an object
-
- Usage:
- wipe <object>[/<attr>[/<attr>...]]
-
- Example:
- wipe box
- wipe box/colour
-
- Wipes all of an object's attributes, or optionally only those
- matching the given attribute-wildcard search string.
- """
-
- key="@wipe"
- locks="cmd:perm(wipe) or perm(Builder)"
- help_category="Building"
-
-
[docs]deffunc(self):
- """
- inp is the dict produced in ObjManipCommand.parse()
- """
-
- caller=self.caller
-
- ifnotself.args:
- caller.msg("Usage: wipe <object>[/<attr>/<attr>...]")
- return
-
- # get the attributes set by our custom parser
- objname=self.lhs_objattr[0]["name"]
- attrs=self.lhs_objattr[0]["attrs"]
-
- obj=caller.search(objname)
- ifnotobj:
- return
- ifnot(obj.access(caller,"control")orobj.access(caller,"edit")):
- caller.msg("You are not allowed to do that.")
- return
- ifnotattrs:
- # wipe everything
- obj.attributes.clear()
- string="Wiped all attributes on %s."%obj.name
- else:
- forattrnameinattrs:
- obj.attributes.remove(attrname)
- string="Wiped attributes %s on %s."
- string=string%(",".join(attrs),obj.name)
- caller.msg(string)
-
-
-
[docs]classCmdLock(ObjManipCommand):
- """
- assign a lock definition to an object
-
- Usage:
- lock <object or *account>[ = <lockstring>]
- or
- lock[/switch] <object or *account>/<access_type>
-
- Switch:
- del - delete given access type
- view - view lock associated with given access type (default)
-
- If no lockstring is given, shows all locks on
- object.
-
- Lockstring is of the form
- access_type:[NOT] func1(args)[ AND|OR][ NOT] func2(args) ...]
- Where func1, func2 ... valid lockfuncs with or without arguments.
- Separator expressions need not be capitalized.
-
- For example:
- 'get: id(25) or perm(Admin)'
- The 'get' lock access_type is checked e.g. by the 'get' command.
- An object locked with this example lock will only be possible to pick up
- by Admins or by an object with id=25.
-
- You can add several access_types after one another by separating
- them by ';', i.e:
- 'get:id(25); delete:perm(Builder)'
- """
-
- key="@lock"
- aliases=["@locks"]
- locks="cmd: perm(locks) or perm(Builder)"
- help_category="Building"
-
-
[docs]deffunc(self):
- """Sets up the command"""
-
- caller=self.caller
- ifnotself.args:
- string=(
- "Usage: lock <object>[ = <lockstring>] or lock[/switch] ""<object>/<access_type>"
- )
- caller.msg(string)
- return
-
- if"/"inself.lhs:
- # call of the form lock obj/access_type
- objname,access_type=[p.strip()forpinself.lhs.split("/",1)]
- obj=None
- ifobjname.startswith("*"):
- obj=caller.search_account(objname.lstrip("*"))
- ifnotobj:
- obj=caller.search(objname)
- ifnotobj:
- return
- has_control_access=obj.access(caller,"control")
- ifaccess_type=="control"andnothas_control_access:
- # only allow to change 'control' access if you have 'control' access already
- caller.msg("You need 'control' access to change this type of lock.")
- return
-
- ifnot(has_control_accessorobj.access(caller,"edit")):
- caller.msg("You are not allowed to do that.")
- return
-
- lockdef=obj.locks.get(access_type)
-
- iflockdef:
- if"del"inself.switches:
- obj.locks.delete(access_type)
- string="deleted lock %s"%lockdef
- else:
- string=lockdef
- else:
- string="%s has no lock of access type '%s'."%(obj,access_type)
- caller.msg(string)
- return
-
- ifself.rhs:
- # we have a = separator, so we are assigning a new lock
- ifself.switches:
- swi=", ".join(self.switches)
- caller.msg(
- "Switch(es) |w%s|n can not be used with a "
- "lock assignment. Use e.g. "
- "|wlock/del objname/locktype|n instead."%swi
- )
- return
-
- objname,lockdef=self.lhs,self.rhs
- obj=None
- ifobjname.startswith("*"):
- obj=caller.search_account(objname.lstrip("*"))
- ifnotobj:
- obj=caller.search(objname)
- ifnotobj:
- return
- ifnot(obj.access(caller,"control")orobj.access(caller,"edit")):
- caller.msg("You are not allowed to do that.")
- return
- ok=False
- lockdef=re.sub(r"\'|\"","",lockdef)
- try:
- ok=obj.locks.add(lockdef)
- exceptLockExceptionase:
- caller.msg(str(e))
- if"cmd"inlockdef.lower()andinherits_from(
- obj,"evennia.objects.objects.DefaultExit"
- ):
- # special fix to update Exits since "cmd"-type locks won't
- # update on them unless their cmdsets are rebuilt.
- obj.at_init()
- ifok:
- caller.msg("Added lock '%s' to %s."%(lockdef,obj))
- return
-
- # if we get here, we are just viewing all locks on obj
- obj=None
- ifself.lhs.startswith("*"):
- obj=caller.search_account(self.lhs.lstrip("*"))
- ifnotobj:
- obj=caller.search(self.lhs)
- ifnotobj:
- return
- ifnot(obj.access(caller,"control")orobj.access(caller,"edit")):
- caller.msg("You are not allowed to do that.")
- return
- caller.msg("\n".join(obj.locks.all()))
-
-
-
[docs]classCmdExamine(ObjManipCommand):
- """
- get detailed information about an object
-
- Usage:
- examine [<object>[/attrname]]
- examine [*<account>[/attrname]]
-
- Switch:
- account - examine an Account (same as adding *)
- object - examine an Object (useful when OOC)
- script - examine a Script
- channel - examine a Channel
-
- The examine command shows detailed game info about an
- object and optionally a specific attribute on it.
- If object is not specified, the current location is examined.
-
- Append a * before the search string to examine an account.
-
- """
-
- key="@examine"
- aliases=["@ex","@exam"]
- locks="cmd:perm(examine) or perm(Builder)"
- help_category="Building"
- arg_regex=r"(/\w+?(\s|$))|\s|$"
- switch_options=["account","object","script","channel"]
-
- object_type="object"
-
- detail_color="|c"
- header_color="|w"
- quell_color="|r"
- separator="-"
-
-
[docs]defmsg(self,text):
- """
- Central point for sending messages to the caller. This tags
- the message as 'examine' for eventual custom markup in the client.
-
- Attributes:
- text (str): The text to send.
-
- """
- self.caller.msg(text=(text,{"type":"examine"}))
[docs]defformat_merged_cmdsets(self,obj,current_cmdset):
- ifnothasattr(obj,"cmdset"):
- returnNone
-
- all_cmdsets=[(cmdset.key,cmdset)forcmdsetincurrent_cmdset.merged_from]
- # we always at least try to add account- and session sets since these are ignored
- # if we merge on the object level.
- ifhasattr(obj,"account")andobj.account:
- # get Attribute-cmdsets if they exist
- all_cmdsets.extend([(cmdset.key,cmdset)forcmdsetinobj.account.cmdset.all()])
- ifobj.sessions.count():
- # if there are more sessions than one on objects it's because of multisession mode
- # we only show the first session's cmdset here (it is -in principle- possible
- # that different sessions have different cmdsets but for admins who want such
- # madness it is better that they overload with their own CmdExamine to handle it).
- all_cmdsets.extend([(cmdset.key,cmdset)
- forcmdsetinobj.account.sessions.all()[0].cmdset.all()])
- else:
- try:
- # we have to protect this since many objects don't have sessions.
- all_cmdsets.extend([(cmdset.key,cmdset)
- forcmdsetinobj.get_session(obj.sessions.get()).cmdset.all()])
- except(TypeError,AttributeError):
- # an error means we are merging an object without a session
- pass
- all_cmdsets=[cmdsetforcmdsetindict(all_cmdsets).values()]
- all_cmdsets.sort(key=lambdax:x.priority,reverse=True)
-
- merged_cmdset_strings=[]
- forcmdsetinall_cmdsets:
- ifcmdset.key!="_EMPTY_CMDSET":
- merged_cmdset_strings.append(self.format_single_cmdset(cmdset))
- return"\n "+"\n ".join(merged_cmdset_strings)
-
- def_search_by_object_type(self,obj_name,objtype):
- """
- Route to different search functions depending on the object type being
- examined. This also handles error reporting for multimatches/no matches.
-
- Args:
- obj_name (str): The search query.
- objtype (str): One of 'object', 'account', 'script' or 'channel'.
- Returns:
- any: `None` if no match or multimatch, otherwise a single result.
-
- """
- obj=None
-
- ifobjtype=="object":
- obj=self.caller.search(obj_name)
- elifobjtype=="account":
- try:
- obj=self.caller.search_account(obj_name.lstrip("*"))
- exceptAttributeError:
- # this means we are calling examine from an account object
- obj=self.caller.search(
- obj_name.lstrip("*"),search_object="object"inself.switches
- )
- else:
- obj=getattr(search,f"search_{objtype}")(obj_name)
- ifnotobj:
- self.caller.msg(f"No {objtype} found with key {obj_name}.")
- obj=None
- eliflen(obj)>1:
- err="Multiple {objtype} found with key {obj_name}:\n{matches}"
- self.caller.msg(err.format(
- obj_name=obj_name,
- matches=", ".join(f"{ob.key}(#{ob.id})"forobinobj)
- ))
- obj=None
- else:
- obj=obj[0]
- returnobj
-
-
[docs]defparse(self):
- super().parse()
-
- self.examine_objs=[]
-
- ifnotself.args:
- # If no arguments are provided, examine the invoker's location.
- ifhasattr(self.caller,"location"):
- self.examine_objs.append((self.caller.location,None))
- else:
- self.msg("You need to supply a target to examine.")
- raiseInterruptCommand
- else:
- forobjdefinself.lhs_objattr:
- # note that we check the objtype for every repeat; this will always
- # be the same result, but it makes for a cleaner code and multi-examine
- # is not so common anyway.
-
- obj=None
- obj_name=objdef["name"]# name
- obj_attrs=objdef["attrs"]# /attrs
-
- # identify object type, in prio account - script - channel
- object_type="object"
- if(utils.inherits_from(self.caller,"evennia.accounts.accounts.DefaultAccount")
- or"account"inself.switchesorobj_name.startswith("*")):
- object_type="account"
- elif"script"inself.switches:
- object_type="script"
- elif"channel"inself.switches:
- object_type="channel"
-
- self.object_type=object_type
- obj=self._search_by_object_type(obj_name,object_type)
-
- ifobj:
- self.examine_objs.append((obj,obj_attrs))
-
-
[docs]deffunc(self):
- """Process command"""
- forobj,obj_attrsinself.examine_objs:
- # these are parsed out in .parse already
-
- ifnotobj.access(self.caller,"examine"):
- # If we don't have special info access, just look
- # at the object instead.
- self.msg(self.caller.at_look(obj))
- continue
-
- ifobj_attrs:
- # we are only interested in specific attributes
- attrs=[attrforattrinobj.db_attributes.all()ifattr.db_keyinobj_attrs]
- ifnotattrs:
- self.msg("No attributes found on {obj.name}.")
- else:
- out_strings=[]
- forattrinattrs:
- out_strings.append(self.format_single_attribute_detail(obj,attr))
- out_str="\n".join(out_strings)
- max_width=max(display_len(line)forlineinout_strings)
- max_width=max(0,min(max_width,self.client_width()))
- sep=self.separator*max_width
- self.msg(f"{sep}\n{out_str}")
- return
-
- # examine the obj itself
-
- ifself.object_typein("object","account"):
- # for objects and accounts we need to set up an asynchronous
- # fetch of the cmdset and not proceed with the examine display
- # until the fetch is complete
- session=None
- ifobj.sessions.count():
- mergemode="session"
- session=obj.sessions.get()[0]
- elifself.object_type=="account":
- mergemode="account"
- else:
- mergemode="object"
-
- account=None
- objct=None
- ifself.object_type=="account":
- account=obj
- else:
- account=obj.account
- objct=obj
-
- # this is usually handled when a command runs, but when we examine
- # we may have leftover inherited cmdsets directly after a move etc.
- obj.cmdset.update()
- # using callback to print results whenever function returns.
-
- def_get_cmdset_callback(current_cmdset):
- self.msg(self.format_output(obj,current_cmdset).strip())
-
- get_and_merge_cmdsets(
- obj,session,account,objct,mergemode,self.raw_string
- ).addCallback(_get_cmdset_callback)
-
- else:
- # for objects without cmdsets we can proceed to examine immediately
- self.msg(self.format_output(obj,None).strip())
-
-
-
[docs]classCmdFind(COMMAND_DEFAULT_CLASS):
- """
- search the database for objects
-
- Usage:
- find[/switches] <name or dbref or *account> [= dbrefmin[-dbrefmax]]
- locate - this is a shorthand for using the /loc switch.
-
- Switches:
- room - only look for rooms (location=None)
- exit - only look for exits (destination!=None)
- char - only look for characters (BASE_CHARACTER_TYPECLASS)
- exact - only exact matches are returned.
- loc - display object location if exists and match has one result
- startswith - search for names starting with the string, rather than containing
-
- Searches the database for an object of a particular name or exact #dbref.
- Use *accountname to search for an account. The switches allows for
- limiting object matches to certain game entities. Dbrefmin and dbrefmax
- limits matches to within the given dbrefs range, or above/below if only
- one is given.
- """
-
- key="@find"
- aliases=["@search","@locate"]
- switch_options=("room","exit","char","exact","loc","startswith")
- locks="cmd:perm(find) or perm(Builder)"
- help_category="Building"
-
-
[docs]deffunc(self):
- """Search functionality"""
- caller=self.caller
- switches=self.switches
-
- ifnotself.argsor(notself.lhsandnotself.rhs):
- caller.msg("Usage: find <string> [= low [-high]]")
- return
-
- if"locate"inself.cmdstring:# Use option /loc as a default for locate command alias
- switches.append("loc")
-
- searchstring=self.lhs
-
- try:
- # Try grabbing the actual min/max id values by database aggregation
- qs=ObjectDB.objects.values("id").aggregate(low=Min("id"),high=Max("id"))
- low,high=sorted(qs.values())
- ifnot(lowandhigh):
- raiseValueError(
- f"{self.__class__.__name__}: Min and max ID not returned by aggregation; falling back to queryset slicing."
- )
- exceptExceptionase:
- logger.log_trace(e)
- # If that doesn't work for some reason (empty DB?), guess the lower
- # bound and do a less-efficient query to find the upper.
- low,high=1,ObjectDB.objects.all().order_by("-id").first().id
-
- ifself.rhs:
- try:
- # Check that rhs is either a valid dbref or dbref range
- bounds=tuple(
- sorted(dbref(x,False)forxinre.split("[-\s]+",self.rhs.strip()))
- )
-
- # dbref() will return either a valid int or None
- assertbounds
- # None should not exist in the bounds list
- assertNonenotinbounds
-
- low=bounds[0]
- iflen(bounds)>1:
- high=bounds[-1]
-
- exceptAssertionError:
- caller.msg("Invalid dbref range provided (not a number).")
- return
- exceptIndexErrorase:
- logger.log_err(
- f"{self.__class__.__name__}: Error parsing upper and lower bounds of query."
- )
- logger.log_trace(e)
-
- low=min(low,high)
- high=max(low,high)
-
- is_dbref=utils.dbref(searchstring)
- is_account=searchstring.startswith("*")
-
- restrictions=""
- ifself.switches:
- restrictions=", %s"%(", ".join(self.switches))
-
- ifis_dbreforis_account:
- ifis_dbref:
- # a dbref search
- result=caller.search(searchstring,global_search=True,quiet=True)
- string="|wExact dbref match|n(#%i-#%i%s):"%(low,high,restrictions)
- else:
- # an account search
- searchstring=searchstring.lstrip("*")
- result=caller.search_account(searchstring,quiet=True)
- string="|wMatch|n(#%i-#%i%s):"%(low,high,restrictions)
-
- if"room"inswitches:
- result=resultifinherits_from(result,ROOM_TYPECLASS)elseNone
- if"exit"inswitches:
- result=resultifinherits_from(result,EXIT_TYPECLASS)elseNone
- if"char"inswitches:
- result=resultifinherits_from(result,CHAR_TYPECLASS)elseNone
-
- ifnotresult:
- string+="\n |RNo match found.|n"
- elifnotlow<=int(result[0].id)<=high:
- string+="\n |RNo match found for '%s' in #dbref interval.|n"%searchstring
- else:
- result=result[0]
- string+="\n|g %s - %s|n"%(result.get_display_name(caller),result.path)
- if"loc"inself.switchesandnotis_accountandresult.location:
- string+=" (|wlocation|n: |g{}|n)".format(
- result.location.get_display_name(caller)
- )
- else:
- # Not an account/dbref search but a wider search; build a queryset.
- # Searches for key and aliases
- if"exact"inswitches:
- keyquery=Q(db_key__iexact=searchstring,id__gte=low,id__lte=high)
- aliasquery=Q(
- db_tags__db_key__iexact=searchstring,
- db_tags__db_tagtype__iexact="alias",
- id__gte=low,
- id__lte=high,
- )
- elif"startswith"inswitches:
- keyquery=Q(db_key__istartswith=searchstring,id__gte=low,id__lte=high)
- aliasquery=Q(
- db_tags__db_key__istartswith=searchstring,
- db_tags__db_tagtype__iexact="alias",
- id__gte=low,
- id__lte=high,
- )
- else:
- keyquery=Q(db_key__icontains=searchstring,id__gte=low,id__lte=high)
- aliasquery=Q(
- db_tags__db_key__icontains=searchstring,
- db_tags__db_tagtype__iexact="alias",
- id__gte=low,
- id__lte=high,
- )
-
- # Keep the initial queryset handy for later reuse
- result_qs=ObjectDB.objects.filter(keyquery|aliasquery).distinct()
- nresults=result_qs.count()
-
- # Use iterator to minimize memory ballooning on large result sets
- results=result_qs.iterator()
-
- # Check and see if type filtering was requested; skip it if not
- ifany(xinswitchesforxin("room","exit","char")):
- obj_ids=set()
- forobjinresults:
- if(
- ("room"inswitchesandinherits_from(obj,ROOM_TYPECLASS))
- or("exit"inswitchesandinherits_from(obj,EXIT_TYPECLASS))
- or("char"inswitchesandinherits_from(obj,CHAR_TYPECLASS))
- ):
- obj_ids.add(obj.id)
-
- # Filter previous queryset instead of requesting another
- filtered_qs=result_qs.filter(id__in=obj_ids).distinct()
- nresults=filtered_qs.count()
-
- # Use iterator again to minimize memory ballooning
- results=filtered_qs.iterator()
-
- # still results after type filtering?
- ifnresults:
- ifnresults>1:
- header=f"{nresults} Matches"
- else:
- header="One Match"
-
- string=f"|w{header}|n(#{low}-#{high}{restrictions}):"
- res=None
- forresinresults:
- string+=f"\n |g{res.get_display_name(caller)} - {res.path}|n"
- if(
- "loc"inself.switches
- andnresults==1
- andres
- andgetattr(res,"location",None)
- ):
- string+=f" (|wlocation|n: |g{res.location.get_display_name(caller)}|n)"
- else:
- string=f"|wNo Matches|n(#{low}-#{high}{restrictions}):"
- string+=f"\n |RNo matches found for '{searchstring}'|n"
-
- # send result
- caller.msg(string.strip())
-
-
-classScriptEvMore(EvMore):
- """
- Listing 1000+ Scripts can be very slow and memory-consuming. So
- we use this custom EvMore child to build en EvTable only for
- each page of the list.
-
- """
-
- definit_pages(self,scripts):
- """Prepare the script list pagination"""
- script_pages=Paginator(scripts,max(1,int(self.height/2)))
- super().init_pages(script_pages)
-
- defpage_formatter(self,scripts):
- """Takes a page of scripts and formats the output
- into an EvTable."""
-
- ifnotscripts:
- return"<No scripts>"
-
- table=EvTable(
- "|wdbref|n",
- "|wobj|n",
- "|wkey|n",
- "|wintval|n",
- "|wnext|n",
- "|wrept|n",
- "|wtypeclass|n",
- "|wdesc|n",
- align="r",
- border="tablecols",
- width=self.width,
- )
-
- forscriptinscripts:
-
- nextrep=script.time_until_next_repeat()
- ifnextrepisNone:
- nextrep=script.db._paused_time
- nextrep=f"PAUSED {int(nextrep)}s"ifnextrepelse"--"
- else:
- nextrep=f"{nextrep}s"
-
- maxrepeat=script.repeats
- remaining=script.remaining_repeats()or0
- ifmaxrepeat:
- rept="%i/%i"%(maxrepeat-remaining,maxrepeat)
- else:
- rept="-/-"
-
- table.add_row(
- f"#{script.id}",
- f"{script.obj.key}({script.obj.dbref})"
- if(hasattr(script,"obj")andscript.obj)
- else"<Global>",
- script.key,
- script.intervalifscript.interval>0else"--",
- nextrep,
- rept,
- script.typeclass_path.rsplit(".",1)[-1],
- crop(script.desc,width=20),
- )
-
- returnstr(table)
-
-
-
[docs]classCmdScripts(COMMAND_DEFAULT_CLASS):
- """
- List and manage all running scripts. Allows for creating new global
- scripts.
-
- Usage:
- script[/switches] [script-#dbref, key, script.path or <obj>]
- script[/start||stop] <obj> = <script.path or script-key>
-
- Switches:
- 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
-
- Examples:
- script - list scripts
- script myobj - list all scripts on object
- script foo.bar.Script - create a new global Script
- script scriptname - examine named existing global script
- script myobj = foo.bar.Script - create and assign script to object
- script/stop myobj = scriptname - stop script on object
- script/pause foo.Bar.Script - pause global script
- script/delete myobj - delete ALL scripts on object
- script/delete #dbref[-#dbref] - delete script or range by dbref
-
- When given with an `<obj>` as left-hand-side, this creates and
- assigns a new script to that object. Without an `<obj>`, this
- manages and inspects global scripts
-
- If no switches are given, this command just views all active
- 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 the `script` build-level command for managing scripts attached to
- objects.
-
- """
-
- key="@scripts"
- aliases=["@script"]
- switch_options=("create","start","stop","pause","delete")
- locks="cmd:perm(scripts) or perm(Builder)"
- 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
- if"-"inargs:
- # may be a dbref-range
- val1,val2=(dbref(part.strip())forpartinargs.split('-',1))
- ifval1andval2:
- scripts=ScriptDB.objects.filter(id__in=(range(val1,val2+1)))
- ifscripts:
- returnscripts
-
-
[docs]deffunc(self):
- """implement method"""
-
- caller=self.caller
-
- ifnotself.args:
- # show all scripts
- scripts=ScriptDB.objects.all()
- ifnotscripts:
- caller.msg("No scripts found.")
- return
- ScriptEvMore(caller,scripts.order_by("id"),session=self.session)
- return
-
- # find script or object to operate on
- scripts,obj=None,None
- ifself.rhs:
- obj_query=self.lhs
- script_query=self.rhs
- else:
- obj_query=script_query=self.args
-
- scripts=self._search_script(script_query)
- objects=ObjectDB.objects.object_search(obj_query)
- obj=objects[0]ifobjectselseNone
-
- ifnotself.switches:
- # creation / view mode
- ifobj:
- # we have an object
- ifself.rhs:
- # creation mode
- ifobj.scripts.add(self.rhs,autostart=True):
- caller.msg(
- f"Script |w{self.rhs}|n successfully added and "
- f"started on {obj.get_display_name(caller)}.")
- else:
- caller.msg(f"Script {self.rhs} could not be added and/or started "
- f"on {obj.get_display_name(caller)} (or it started and "
- "immediately shut down).")
- else:
- # just show all scripts on object
- scripts=ScriptDB.objects.filter(db_obj=obj)
- ifscripts:
- ScriptEvMore(caller,scripts.order_by("id"),session=self.session)
- else:
- caller.msg(f"No scripts defined on {obj}")
-
- elifscripts:
- # show found script(s)
- ScriptEvMore(caller,scripts.order_by("id"),session=self.session)
-
- else:
- # create global script
- try:
- new_script=create.create_script(self.args)
- exceptImportError:
- logger.log_trace()
- new_script=None
-
- ifnew_script:
- caller.msg(f"Global Script Created - "
- f"{new_script.key} ({new_script.typeclass_path})")
- ScriptEvMore(caller,[new_script],session=self.session)
- else:
- caller.msg(f"Global Script |rNOT|n Created |r(see log)|n - "
- f"arguments: {self.args}")
-
- elifscriptsorobj:
- # modification switches - must operate on existing scripts
-
- ifnotscripts:
- scripts=ScriptDB.objects.filter(db_obj=obj)
-
- ifscripts.count()>1:
- ret=yield(f"Multiple scripts found: {scripts}. Are you sure you want to "
- "operate on all of them? [Y]/N? ")
- ifret.lower()in('n','no'):
- caller.msg("Aborted.")
- return
-
- forscriptinscripts:
- script_key=script.key
- script_typeclass_path=script.typeclass_path
- scripttype=f"Script on {obj}"ifobjelse"Global Script"
-
- forswitchinself.switches:
- verb=self.switch_mapping[switch]
- msgs=[]
- try:
- getattr(script,switch)()
- exceptException:
- logger.log_trace()
- msgs.append(f"{scripttype} |rNOT|n {verb} |r(see log)|n - "
- f"{script_key} ({script_typeclass_path})|n")
- else:
- msgs.append(f"{scripttype}{verb} - "
- f"{script_key} ({script_typeclass_path})")
- caller.msg("\n".join(msgs))
- if"delete"notinself.switches:
- ScriptEvMore(caller,[script],session=self.session)
- else:
- caller.msg("No scripts found.")
-
-
-
[docs]classCmdObjects(COMMAND_DEFAULT_CLASS):
- """
- statistics on objects in the database
-
- Usage:
- objects [<nr>]
-
- Gives statictics on objects in database as well as
- a list of <nr> latest objects in database. If not
- given, <nr> defaults to 10.
- """
-
- key="@objects"
- locks="cmd:perm(listobjects) or perm(Builder)"
- help_category="System"
-
-
[docs]classCmdTeleport(COMMAND_DEFAULT_CLASS):
- """
- teleport object to another location
-
- Usage:
- tel/switch [<object> to||=] <target location>
-
- Examples:
- tel Limbo
- tel/quiet box = Limbo
- tel/tonone box
-
- Switches:
- quiet - don't echo leave/arrive messages to the source/target
- locations for the move.
- intoexit - if target is an exit, teleport INTO
- the exit object instead of to its destination
- tonone - if set, teleport the object to a None-location. If this
- switch is set, <target location> is ignored.
- Note that the only way to retrieve
- an object from a None location is by direct #dbref
- reference. A puppeted object cannot be moved to None.
- loc - teleport object to the target's location instead of its contents
-
- Teleports an object somewhere. If no object is given, you yourself are
- teleported to the target location.
-
- To lock an object from being teleported, set its `teleport` lock, it will be
- checked with the caller. To block
- a destination from being teleported to, set the destination's `teleport_here`
- lock - it will be checked with the thing being teleported. Admins and
- higher permissions can always teleport.
-
- """
-
- key="@teleport"
- aliases="@tel"
- switch_options=("quiet","intoexit","tonone","loc")
- rhs_split=("="," to ")# Prefer = delimiter, but allow " to " usage.
- locks="cmd:perm(teleport) or perm(Builder)"
- help_category="Building"
-
-
[docs]defparse(self):
- """
- Breaking out searching here to make this easier to override.
-
- """
- super().parse()
- self.obj_to_teleport=self.caller
- self.destination=None
- ifself.rhs:
- self.obj_to_teleport=self.caller.search(self.lhs,global_search=True)
- ifnotself.obj_to_teleport:
- self.caller.msg("Did not find object to teleport.")
- raiseInterruptCommand
- self.destination=self.caller.search(self.rhs,global_search=True)
- elifself.lhs:
- self.destination=self.caller.search(self.lhs,global_search=True)
-
-
[docs]deffunc(self):
- """Performs the teleport"""
-
- caller=self.caller
- obj_to_teleport=self.obj_to_teleport
- destination=self.destination
-
- if"tonone"inself.switches:
- # teleporting to None
-
- ifdestination:
- # in this case lhs is always the object to teleport
- obj_to_teleport=destination
-
- ifobj_to_teleport.has_account:
- caller.msg(
- "Cannot teleport a puppeted object "
- "(%s, puppeted by %s) to a None-location."
- %(obj_to_teleport.key,obj_to_teleport.account)
- )
- return
- caller.msg("Teleported %s -> None-location."%obj_to_teleport)
- ifobj_to_teleport.locationand"quiet"notinself.switches:
- obj_to_teleport.location.msg_contents(
- "%s teleported %s into nothingness."%(caller,obj_to_teleport),exclude=caller
- )
- obj_to_teleport.location=None
- return
-
- ifnotself.args:
- caller.msg("Usage: teleport[/switches] [<obj> =] <target or (X,Y,Z)>||home")
- return
-
- ifnotdestination:
- caller.msg("Destination not found.")
- return
-
- if"loc"inself.switches:
- destination=destination.location
- ifnotdestination:
- caller.msg("Destination has no location.")
- return
-
- ifobj_to_teleport==destination:
- caller.msg("You can't teleport an object inside of itself!")
- return
-
- ifobj_to_teleport==destination.location:
- caller.msg("You can't teleport an object inside something it holds!")
- return
-
- ifobj_to_teleport.locationandobj_to_teleport.location==destination:
- caller.msg("%s is already at %s."%(obj_to_teleport,destination))
- return
-
- # check any locks
- ifnot(caller.permissions.check("Admin")orobj_to_teleport.access(caller,"teleport")):
- caller.msg(f"{obj_to_teleport} 'teleport'-lock blocks you from teleporting "
- "it anywhere.")
- return
-
- ifnot(caller.permissions.check("Admin")
- ordestination.access(obj_to_teleport,"teleport_here")):
- caller.msg(f"{destination} 'teleport_here'-lock blocks {obj_to_teleport} from "
- "moving there.")
- return
-
- # try the teleport
- ifnotobj_to_teleport.location:
- # teleporting from none-location
- obj_to_teleport.location=destination
- caller.msg(f"Teleported {obj_to_teleport} None -> {destination}")
- elifobj_to_teleport.move_to(
- destination,quiet="quiet"inself.switches,
- emit_to_obj=caller,use_destination="intoexit"notinself.switches):
-
- ifobj_to_teleport==caller:
- caller.msg(f"Teleported to {destination}.")
- else:
- caller.msg(f"Teleported {obj_to_teleport} -> {destination}.")
- else:
- caller.msg("Teleportation failed.")
-
-
-
[docs]classCmdTag(COMMAND_DEFAULT_CLASS):
- """
- handles the tags of an object
-
- Usage:
- tag[/del] <obj> [= <tag>[:<category>]]
- tag/search <tag>[:<category]
-
- Switches:
- search - return all objects with a given Tag
- del - remove the given tag. If no tag is specified,
- clear all tags on object.
-
- Manipulates and lists tags on objects. Tags allow for quick
- grouping of and searching for objects. If only <obj> is given,
- list all tags on the object. If /search is used, list objects
- with the given tag.
- The category can be used for grouping tags themselves, but it
- should be used with restrain - tags on their own are usually
- enough to for most grouping schemes.
- """
-
- key="@tag"
- aliases=["@tags"]
- options=("search","del")
- locks="cmd:perm(tag) or perm(Builder)"
- help_category="Building"
- arg_regex=r"(/\w+?(\s|$))|\s|$"
-
-
[docs]deffunc(self):
- """Implement the tag functionality"""
-
- ifnotself.args:
- self.caller.msg("Usage: tag[/switches] <obj> [= <tag>[:<category>]]")
- return
- if"search"inself.switches:
- # search by tag
- tag=self.args
- category=None
- if":"intag:
- tag,category=[part.strip()forpartintag.split(":",1)]
- objs=search.search_tag(tag,category=category)
- nobjs=len(objs)
- ifnobjs>0:
- catstr=(
- " (category: '|w%s|n')"%category
- ifcategory
- else(""ifnobjs==1else" (may have different tag categories)")
- )
- matchstr=", ".join(o.get_display_name(self.caller)foroinobjs)
-
- string="Found |w%i|n object%s with tag '|w%s|n'%s:\n%s"%(
- nobjs,
- "s"ifnobjs>1else"",
- tag,
- catstr,
- matchstr,
- )
- else:
- string="No objects found with tag '%s%s'."%(
- tag,
- " (category: %s)"%categoryifcategoryelse"",
- )
- self.caller.msg(string)
- return
- if"del"inself.switches:
- # remove one or all tags
- obj=self.caller.search(self.lhs,global_search=True)
- ifnotobj:
- return
- ifself.rhs:
- # remove individual tag
- tag=self.rhs
- category=None
- if":"intag:
- tag,category=[part.strip()forpartintag.split(":",1)]
- ifobj.tags.get(tag,category=category):
- obj.tags.remove(tag,category=category)
- string="Removed tag '%s'%s from %s."%(
- tag,
- " (category: %s)"%categoryifcategoryelse"",
- obj,
- )
- else:
- string="No tag '%s'%s to delete on %s."%(
- tag,
- " (category: %s)"%categoryifcategoryelse"",
- obj,
- )
- else:
- # no tag specified, clear all tags
- old_tags=[
- "%s%s"%(tag," (category: %s)"%categoryifcategoryelse"")
- fortag,categoryinobj.tags.all(return_key_and_category=True)
- ]
- ifold_tags:
- obj.tags.clear()
- string="Cleared all tags from %s: %s"%(obj,", ".join(sorted(old_tags)))
- else:
- string="No Tags to clear on %s."%obj
- self.caller.msg(string)
- return
- # no search/deletion
- ifself.rhs:
- # = is found; command args are of the form obj = tag
- obj=self.caller.search(self.lhs,global_search=True)
- ifnotobj:
- return
- tag=self.rhs
- category=None
- if":"intag:
- tag,category=[part.strip()forpartintag.split(":",1)]
- # create the tag
- obj.tags.add(tag,category=category)
- string="Added tag '%s'%s to %s."%(
- tag,
- " (category: %s)"%categoryifcategoryelse"",
- obj,
- )
- self.caller.msg(string)
- else:
- # no = found - list tags on object
- obj=self.caller.search(self.args,global_search=True)
- ifnotobj:
- return
- tagtuples=obj.tags.all(return_key_and_category=True)
- ntags=len(tagtuples)
- tags=[tup[0]fortupintagtuples]
- categories=[" (category: %s)"%tup[1]iftup[1]else""fortupintagtuples]
- ifntags:
- string="Tag%s on %s: %s"%(
- "s"ifntags>1else"",
- obj,
- ", ".join(sorted("'%s'%s"%(tags[i],categories[i])foriinrange(ntags))),
- )
- else:
- string="No tags attached to %s."%obj
- self.caller.msg(string)
-
-
-# helper functions for spawn
-
-
-
[docs]classCmdSpawn(COMMAND_DEFAULT_CLASS):
- """
- spawn objects from prototype
-
- Usage:
- spawn[/noloc] <prototype_key>
- spawn[/noloc] <prototype_dict>
-
- spawn/search [prototype_keykey][;tag[,tag]]
- spawn/list [tag, tag, ...]
- spawn/list modules - list only module-based prototypes
- spawn/show [<prototype_key>]
- spawn/update <prototype_key>
-
- spawn/save <prototype_dict>
- spawn/edit [<prototype_key>]
- olc - equivalent to spawn/edit
-
- Switches:
- noloc - allow location to be None if not specified explicitly. Otherwise,
- location will default to caller's current location.
- search - search prototype by name or tags.
- list - list available prototypes, optionally limit by tags.
- show, examine - inspect prototype by key. If not given, acts like list.
- raw - show the raw dict of the prototype as a one-line string for manual editing.
- save - save a prototype to the database. It will be listable by /list.
- delete - remove a prototype from database, if allowed to.
- update - find existing objects with the same prototype_key and update
- them with latest version of given prototype. If given with /save,
- will auto-update all objects with the old version of the prototype
- without asking first.
- edit, menu, olc - create/manipulate prototype in a menu interface.
-
- Example:
- spawn GOBLIN
- spawn {"key":"goblin", "typeclass":"monster.Monster", "location":"#2"}
- spawn/save {"key": "grunt", prototype: "goblin"};;mobs;edit:all()
- \f
- Dictionary keys:
- |wprototype_parent |n - name of parent prototype to use. Required if typeclass is
- not set. Can be a path or a list for multiple inheritance (inherits
- left to right). If set one of the parents must have a typeclass.
- |wtypeclass |n - string. Required if prototype_parent is not set.
- |wkey |n - string, the main object identifier
- |wlocation |n - this should be a valid object or #dbref
- |whome |n - valid object or #dbref
- |wdestination|n - only valid for exits (object or dbref)
- |wpermissions|n - string or list of permission strings
- |wlocks |n - a lock-string
- |waliases |n - string or list of strings.
- |wndb_|n<name> - value of a nattribute (ndb_ is stripped)
-
- |wprototype_key|n - name of this prototype. Unique. Used to store/retrieve from db
- and update existing prototyped objects if desired.
- |wprototype_desc|n - desc of this prototype. Used in listings
- |wprototype_locks|n - locks of this prototype. Limits who may use prototype
- |wprototype_tags|n - tags of this prototype. Used to find prototype
-
- any other keywords are interpreted as Attributes and their values.
-
- The available prototypes are defined globally in modules set in
- settings.PROTOTYPE_MODULES. If spawn is used without arguments it
- displays a list of available prototypes.
-
- """
-
- key="@spawn"
- aliases=["@olc"]
- switch_options=(
- "noloc",
- "search",
- "list",
- "show",
- "raw",
- "examine",
- "save",
- "delete",
- "menu",
- "olc",
- "update",
- "edit",
- )
- locks="cmd:perm(spawn) or perm(Builder)"
- help_category="Building"
-
- def_search_prototype(self,prototype_key,quiet=False):
- """
- Search for prototype and handle no/multi-match and access.
-
- Returns a single found prototype or None - in the
- case, the caller has already been informed of the
- search error we need not do any further action.
-
- """
- prototypes=protlib.search_prototype(prototype_key)
- nprots=len(prototypes)
-
- # handle the search result
- err=None
- ifnotprototypes:
- err=f"No prototype named '{prototype_key}' was found."
- elifnprots>1:
- err="Found {} prototypes matching '{}':\n{}".format(
- nprots,
- prototype_key,
- ", ".join(proto.get("prototype_key","")forprotoinprototypes),
- )
- else:
- # we have a single prototype, check access
- prototype=prototypes[0]
- ifnotself.caller.locks.check_lockstring(
- self.caller,prototype.get("prototype_locks",""),access_type="spawn",default=True
- ):
- err="You don't have access to use this prototype."
-
- iferr:
- # return None on any error
- ifnotquiet:
- self.caller.msg(err)
- return
- returnprototype
-
- def_parse_prototype(self,inp,expect=dict):
- """
- Parse a prototype dict or key from the input and convert it safely
- into a dict if appropriate.
-
- Args:
- inp (str): The input from user.
- expect (type, optional):
- Returns:
- prototype (dict, str or None): The parsed prototype. If None, the error
- was already reported.
-
- """
- eval_err=None
- try:
- prototype=_LITERAL_EVAL(inp)
- except(SyntaxError,ValueError)aserr:
- # treat as string
- eval_err=err
- prototype=utils.to_str(inp)
- finally:
- # it's possible that the input was a prototype-key, in which case
- # it's okay for the LITERAL_EVAL to fail. Only if the result does not
- # match the expected type do we have a problem.
- ifnotisinstance(prototype,expect):
- ifeval_err:
- string=(
- f"{inp}\n{eval_err}\n|RCritical Python syntax error in argument. Only primitive "
- "Python structures are allowed. \nMake sure to use correct "
- "Python syntax. Remember especially to put quotes around all "
- "strings inside lists and dicts.|n For more advanced uses, embed "
- "funcparser callables ($funcs) in the strings."
- )
- else:
- string="Expected {}, got {}.".format(expect,type(prototype))
- self.caller.msg(string)
- return
-
- ifexpect==dict:
- # an actual prototype. We need to make sure it's safe,
- # so don't allow exec.
- # TODO: Exec support is deprecated. Remove completely for 1.0.
- if"exec"inprototypeandnotself.caller.check_permstring("Developer"):
- self.caller.msg(
- "Spawn aborted: You are not allowed to ""use the 'exec' prototype key."
- )
- return
- try:
- # we homogenize the protoype first, to be more lenient with free-form
- protlib.validate_prototype(protlib.homogenize_prototype(prototype))
- exceptRuntimeErroraserr:
- self.caller.msg(str(err))
- return
- returnprototype
-
- def_get_prototype_detail(self,query=None,prototypes=None):
- """
- Display the detailed specs of one or more prototypes.
-
- Args:
- query (str, optional): If this is given and `prototypes` is not, search for
- the prototype(s) by this query. This may be a partial query which
- may lead to multiple matches, all being displayed.
- prototypes (list, optional): If given, ignore `query` and only show these
- prototype-details.
- Returns:
- display (str, None): A formatted string of one or more prototype details.
- If None, the caller was already informed of the error.
-
-
- """
- ifnotprototypes:
- # we need to query. Note that if query is None, all prototypes will
- # be returned.
- prototypes=protlib.search_prototype(key=query)
- ifprototypes:
- return"\n".join(protlib.prototype_to_str(prot)forprotinprototypes)
- elifquery:
- self.caller.msg(f"No prototype named '{query}' was found.")
- else:
- self.caller.msg("No prototypes found.")
-
- def_list_prototypes(self,key=None,tags=None):
- """Display prototypes as a list, optionally limited by key/tags. """
- protlib.list_prototypes(self.caller,key=key,tags=tags,session=self.session)
-
- @interactive
- def_update_existing_objects(self,caller,prototype_key,quiet=False):
- """
- Update existing objects (if any) with this prototype-key to the latest
- prototype version.
-
- Args:
- caller (Object): This is necessary for @interactive to work.
- prototype_key (str): The prototype to update.
- quiet (bool, optional): If set, don't report to user if no
- old objects were found to update.
- Returns:
- n_updated (int): Number of updated objects.
-
- """
- prototype=self._search_prototype(prototype_key)
- ifnotprototype:
- return
-
- existing_objects=protlib.search_objects_with_prototype(prototype_key)
- ifnotexisting_objects:
- ifnotquiet:
- caller.msg("No existing objects found with an older version of this prototype.")
- return
-
- ifexisting_objects:
- n_existing=len(existing_objects)
- slow=" (note that this may be slow)"ifn_existing>10else""
- string=(
- f"There are {n_existing} existing object(s) with an older version "
- f"of prototype '{prototype_key}'. Should it be re-applied to them{slow}? [Y]/N"
- )
- answer=yield(string)
- ifanswer.lower()in["n","no"]:
- caller.msg(
- "|rNo update was done of existing objects. "
- "Use spawn/update <key> to apply later as needed.|n"
- )
- return
- try:
- n_updated=spawner.batch_update_objects_with_prototype(
- prototype,objects=existing_objects,caller=caller,
- )
- exceptException:
- logger.log_trace()
- caller.msg(f"{n_updated} objects were updated.")
- return
-
- def_parse_key_desc_tags(self,argstring,desc=True):
- """
- Parse ;-separated input list.
- """
- key,desc,tags="","",[]
- if";"inargstring:
- parts=[part.strip().lower()forpartinargstring.split(";")]
- iflen(parts)>1anddesc:
- key=parts[0]
- desc=parts[1]
- tags=parts[2:]
- else:
- key=parts[0]
- tags=parts[1:]
- else:
- key=argstring.strip().lower()
- returnkey,desc,tags
-
-
[docs]deffunc(self):
- """Implements the spawner"""
-
- caller=self.caller
- noloc="noloc"inself.switches
-
- # run the menu/olc
- if(
- self.cmdstring=="olc"
- or"menu"inself.switches
- or"olc"inself.switches
- or"edit"inself.switches
- ):
- # OLC menu mode
- prototype=None
- ifself.lhs:
- prototype_key=self.lhs
- prototype=self._search_prototype(prototype_key)
- ifnotprototype:
- return
- olc_menus.start_olc(caller,session=self.session,prototype=prototype)
- return
-
- if"search"inself.switches:
- # query for a key match. The arg is a search query or nothing.
-
- ifnotself.args:
- # an empty search returns the full list
- self._list_prototypes()
- return
-
- # search for key;tag combinations
- key,_,tags=self._parse_key_desc_tags(self.args,desc=False)
- self._list_prototypes(key,tags)
- return
-
- if"raw"inself.switches:
- # query for key match and return the prototype as a safe one-liner string.
- ifnotself.args:
- caller.msg("You need to specify a prototype-key to get the raw data for.")
- prototype=self._search_prototype(self.args)
- ifnotprototype:
- return
- caller.msg(str(prototype))
- return
-
- if"show"inself.switchesor"examine"inself.switches:
- # show a specific prot detail. The argument is a search query or empty.
- ifnotself.args:
- # we don't show the list of all details, that's too spammy.
- caller.msg("You need to specify a prototype-key to show.")
- return
-
- detail_string=self._get_prototype_detail(self.args)
- ifnotdetail_string:
- return
- caller.msg(detail_string)
- return
-
- if"list"inself.switches:
- # for list, all optional arguments are tags.
- tags=self.lhslist
- err=self._list_prototypes(tags=tags)
- iferr:
- caller.msg(
- "No prototypes found with prototype-tag(s): {}".format(
- list_to_string(tags,"or")
- )
- )
- return
-
- if"save"inself.switches:
- # store a prototype to the database store
- ifnotself.args:
- caller.msg(
- "Usage: spawn/save [<key>[;desc[;tag,tag[,...][;lockstring]]]] = <prototype_dict>"
- )
- return
- ifself.rhs:
- # input on the form key = prototype
- prototype_key,prototype_desc,prototype_tags=self._parse_key_desc_tags(self.lhs)
- prototype_key=Noneifnotprototype_keyelseprototype_key
- prototype_desc=Noneifnotprototype_descelseprototype_desc
- prototype_tags=Noneifnotprototype_tagselseprototype_tags
- prototype_input=self.rhs.strip()
- else:
- prototype_key=prototype_desc=None
- prototype_tags=None
- prototype_input=self.lhs.strip()
-
- # handle parsing
- prototype=self._parse_prototype(prototype_input)
- ifnotprototype:
- return
-
- prot_prototype_key=prototype.get("prototype_key")
-
- ifnot(prototype_keyorprot_prototype_key):
- caller.msg(
- "A prototype_key must be given, either as `prototype_key = <prototype>` "
- "or as a key 'prototype_key' inside the prototype structure."
- )
- return
-
- ifprototype_keyisNone:
- prototype_key=prot_prototype_key
-
- ifprot_prototype_key!=prototype_key:
- caller.msg("(Replacing `prototype_key` in prototype with given key.)")
- prototype["prototype_key"]=prototype_key
-
- ifprototype_descisnotNoneandprot_prototype_key!=prototype_desc:
- caller.msg("(Replacing `prototype_desc` in prototype with given desc.)")
- prototype["prototype_desc"]=prototype_desc
- ifprototype_tagsisnotNoneandprototype.get("prototype_tags")!=prototype_tags:
- caller.msg("(Replacing `prototype_tags` in prototype with given tag(s))")
- prototype["prototype_tags"]=prototype_tags
-
- string=""
- # check for existing prototype (exact match)
- old_prototype=self._search_prototype(prototype_key,quiet=True)
-
- diff=spawner.prototype_diff(old_prototype,prototype,homogenize=True)
- diffstr=spawner.format_diff(diff)
- new_prototype_detail=self._get_prototype_detail(prototypes=[prototype])
-
- ifold_prototype:
- ifnotdiffstr:
- string=f"|yAlready existing Prototype:|n\n{new_prototype_detail}\n"
- question=(
- "\nThere seems to be no changes. Do you still want to (re)save? [Y]/N"
- )
- else:
- string=(
- f'|yExisting prototype "{prototype_key}" found. Change:|n\n{diffstr}\n'
- f"|yNew changed prototype:|n\n{new_prototype_detail}"
- )
- question=(
- "\n|yDo you want to apply the change to the existing prototype?|n [Y]/N"
- )
- else:
- string=f"|yCreating new prototype:|n\n{new_prototype_detail}"
- question="\nDo you want to continue saving? [Y]/N"
-
- answer=yield(string+question)
- ifanswer.lower()in["n","no"]:
- caller.msg("|rSave cancelled.|n")
- return
-
- # all seems ok. Try to save.
- try:
- prot=protlib.save_prototype(prototype)
- ifnotprot:
- caller.msg("|rError saving:|R {}.|n".format(prototype_key))
- return
- exceptprotlib.PermissionErroraserr:
- caller.msg("|rError saving:|R {}|n".format(err))
- return
- caller.msg("|gSaved prototype:|n {}".format(prototype_key))
-
- # check if we want to update existing objects
-
- self._update_existing_objects(self.caller,prototype_key,quiet=True)
- return
-
- ifnotself.args:
- # all switches beyond this point gets a common non-arg return
- ncount=len(protlib.search_prototype())
- caller.msg(
- "Usage: spawn <prototype-key> or {{key: value, ...}}"
- f"\n ({ncount} existing prototypes. Use /list to inspect)"
- )
- return
-
- if"delete"inself.switches:
- # remove db-based prototype
- prototype_detail=self._get_prototype_detail(self.args)
- ifnotprototype_detail:
- return
-
- string=f"|rDeleting prototype:|n\n{prototype_detail}"
- question="\nDo you want to continue deleting? [Y]/N"
- answer=yield(string+question)
- ifanswer.lower()in["n","no"]:
- caller.msg("|rDeletion cancelled.|n")
- return
-
- try:
- success=protlib.delete_prototype(self.args)
- exceptprotlib.PermissionErroraserr:
- retmsg=f"|rError deleting:|R {err}|n"
- else:
- retmsg=(
- "Deletion successful"
- ifsuccess
- else"Deletion failed (does the prototype exist?)"
- )
- caller.msg(retmsg)
- return
-
- if"update"inself.switches:
- # update existing prototypes
- prototype_key=self.args.strip().lower()
- self._update_existing_objects(self.caller,prototype_key)
- return
-
- # If we get to this point, we use not switches but are trying a
- # direct creation of an object from a given prototype or -key
-
- prototype=self._parse_prototype(
- self.args,expect=dictifself.args.strip().startswith("{")elsestr
- )
- ifnotprototype:
- # this will only let through dicts or strings
- return
-
- key="<unnamed>"
- ifisinstance(prototype,str):
- # A prototype key we are looking to apply
- prototype_key=prototype
- prototype=self._search_prototype(prototype_key)
-
- ifnotprototype:
- return
-
- # proceed to spawning
- try:
- forobjinspawner.spawn(prototype,caller=self.caller):
- self.caller.msg("Spawned %s."%obj.get_display_name(self.caller))
- ifnotprototype.get("location")andnotnoloc:
- # we don't hardcode the location in the prototype (unless the user
- # did so manually) - that would lead to it having to be 'removed' every
- # time we try to update objects with this prototype in the future.
- obj.location=caller.location
- exceptRuntimeErroraserr:
- caller.msg(err)
Source code for evennia.commands.default.cmdset_account
-"""
-
-This is the cmdset for Account (OOC) commands. These are
-stored on the Account object and should thus be able to handle getting
-an Account object as caller rather than a Character.
-
-Note - in order for session-rerouting (in MULTISESSION_MODE=2) to
-function, all commands in this cmdset should use the self.msg()
-command method rather than caller.msg().
-"""
-
-fromevennia.commands.cmdsetimportCmdSet
-fromevennia.commands.defaultimporthelp,comms,admin,system
-fromevennia.commands.defaultimportbuilding,account,general
-
-
-
Source code for evennia.commands.default.cmdset_character
-"""
-This module ties together all the commands default Character objects have
-available (i.e. IC commands). Note that some commands, such as
-communication-commands are instead put on the account level, in the
-Account cmdset. Account commands remain available also to Characters.
-"""
-fromevennia.commands.cmdsetimportCmdSet
-fromevennia.commands.defaultimportgeneral,help,admin,system
-fromevennia.commands.defaultimportbuilding
-fromevennia.commands.defaultimportbatchprocess
-
-
-
Source code for evennia.commands.default.cmdset_unloggedin
-"""
-This module describes the unlogged state of the default game.
-The setting STATE_UNLOGGED should be set to the python path
-of the state instance in this module.
-"""
-fromevennia.commands.cmdsetimportCmdSet
-fromevennia.commands.defaultimportunloggedin
-
-
-
[docs]classUnloggedinCmdSet(CmdSet):
- """
- Sets up the unlogged cmdset.
- """
-
- key="DefaultUnloggedin"
- priority=0
-
-
-"""
-Communication commands:
-
-- channel
-- page
-- irc/rss/grapevine linking
-
-"""
-
-fromdjango.confimportsettings
-fromevennia.comms.modelsimportMsg
-fromevennia.accounts.modelsimportAccountDB
-fromevennia.accountsimportbots
-fromevennia.locks.lockhandlerimportLockException
-fromevennia.comms.commsimportDefaultChannel
-fromevennia.utilsimportcreate,logger,utils
-fromevennia.utils.loggerimporttail_log_file
-fromevennia.utils.utilsimportclass_from_module,strip_unsafe_input
-fromevennia.utils.evmenuimportask_yes_no
-
-COMMAND_DEFAULT_CLASS=class_from_module(settings.COMMAND_DEFAULT_CLASS)
-CHANNEL_DEFAULT_TYPECLASS=class_from_module(
- settings.BASE_CHANNEL_TYPECLASS,fallback=settings.FALLBACK_CHANNEL_TYPECLASS)
-
-
-# limit symbol import for API
-__all__=(
- "CmdChannel",
- "CmdObjectChannel",
-
- "CmdPage",
-
- "CmdIRC2Chan",
- "CmdIRCStatus",
- "CmdRSS2Chan",
- "CmdGrapevine2Chan",
-)
-_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.
-
-
-
[docs]classCmdChannel(COMMAND_DEFAULT_CLASS):
- """
- 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.
-
- """
- 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))
- 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
-
- # avoid unsafe tokens in message
- message=strip_unsafe_input(message,self.session)
-
- 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]classCmdPage(COMMAND_DEFAULT_CLASS):
- """
- send a private message to another account
-
- Usage:
- page <account> <message>
- page[/switches] [<account>,<account>,... = <message>]
- tell ''
- page <number>
-
- 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. The equal sign is needed for
- multiple targets or if sending to target with space in the name.
-
- """
-
- key="page"
- aliases=["tell"]
- switch_options=("last","list")
- locks="cmd:not pperm(page_banned)"
- help_category="Comms"
-
- # this is used by the COMMAND_DEFAULT_CLASS parent
- account_caller=True
-
-
[docs]deffunc(self):
- """Implement function using the Msg methods"""
-
- # Since account_caller is set above, this will be an Account.
- caller=self.caller
-
- # get the messages we've sent (not to channels)
- pages_we_sent=Msg.objects.get_messages_by_sender(caller)
- # get last messages we've got
- pages_we_got=Msg.objects.get_messages_by_receiver(caller)
- targets,message,number=[],None,None
-
- if"last"inself.switches:
- ifpages_we_sent:
- recv=",".join(obj.keyforobjinpages_we_sent[-1].receivers)
- self.msg("You last paged |c%s|n:%s"%(recv,pages_we_sent[-1].message))
- return
- else:
- self.msg("You haven't paged anyone yet.")
- return
-
- 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()
-
- 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
-
- 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
- to_template="|w{date}{clr}{sender}|nto{clr}{receiver}|n:> {message}"
- from_template="|w{date}{clr}{receiver}|nfrom{clr}{sender}|n:< {message}"
- listing=[]
- prev_selfsend=False
- forpageinlastpages:
- multi_send=len(page.senders)>1
- multi_recv=len(page.receivers)>1
- sending=self.callerinpage.senders
- # self-messages all look like sends, so we assume they always
- # come in close pairs and treat the second of the pair as the recv.
- selfsend=sendingandself.callerinpage.receivers
- ifselfsend:
- ifprev_selfsend:
- # this is actually a receive of a self-message
- sending=False
- prev_selfsend=False
- else:
- prev_selfsend=True
-
- clr="|c"ifsendingelse"|g"
-
- sender=f"|n,{clr}".join(obj.keyforobjinpage.senders)
- receiver=f"|n,{clr}".join([obj.nameforobjinpage.receivers])
- ifsending:
- template=to_template
- sender=f"{sender} "ifmulti_sendelse""
- receiver=f" {receiver}"ifmulti_recvelsef" {receiver}"
- else:
- template=from_template
- receiver=f"{receiver} "ifmulti_recvelse""
- sender=f" {sender} "ifmulti_sendelsef" {sender}"
-
- listing.append(
- template.format(
- date=utils.datetime_format(page.date_created),
- clr=clr,
- sender=sender,
- receiver=receiver,
- message=page.message,
- )
-
- )
- lastpages="\n ".join(listing)
-
- iflastpages:
- string="Your latest pages:\n%s"%lastpages
- else:
- string="You haven't paged anyone yet."
- self.msg(string)
- return
-
-
-def_list_bots(cmd):
- """
- Helper function to produce a list of all IRC bots.
-
- Args:
- cmd (Command): Instance of the Bot command.
- Returns:
- bots (str): A table of bots or an error message.
-
- """
- ircbots=[
- botforbotinAccountDB.objects.filter(db_is_bot=True,username__startswith="ircbot-")
- ]
- ifircbots:
- table=cmd.styled_table(
- "|w#dbref|n",
- "|wbotname|n",
- "|wev-channel|n",
- "|wirc-channel|n",
- "|wSSL|n",
- maxwidth=_DEFAULT_WIDTH,
- )
- forircbotinircbots:
- ircinfo="%s (%s:%s)"%(
- ircbot.db.irc_channel,
- ircbot.db.irc_network,
- ircbot.db.irc_port,
- )
- table.add_row(
- "#%i"%ircbot.id,
- ircbot.db.irc_botname,
- ircbot.db.ev_channel,
- ircinfo,
- ircbot.db.irc_ssl,
- )
- returntable
- else:
- return"No irc bots found."
-
-
[docs]classCmdIRC2Chan(COMMAND_DEFAULT_CLASS):
- """
- Link an evennia channel to an external IRC channel
-
- Usage:
- irc2chan[/switches] <evennia_channel> = <ircnetwork> <port> <#irchannel> <botname>[:typeclass]
- irc2chan/delete botname|#dbid
-
- Switches:
- /delete - this will delete the bot and remove the irc connection
- to the channel. Requires the botname or #dbid as input.
- /remove - alias to /delete
- /disconnect - alias to /delete
- /list - show all irc<->evennia mappings
- /ssl - use an SSL-encrypted connection
-
- Example:
- irc2chan myircchan = irc.dalnet.net 6667 #mychannel evennia-bot
- irc2chan public = irc.freenode.net 6667 #evgaming #evbot:accounts.mybot.MyBot
-
- This creates an IRC bot that connects to a given IRC network and
- channel. If a custom typeclass path is given, this will be used
- instead of the default bot class.
- 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.
- Provide an optional bot class path to use a custom bot.
- """
-
- key="irc2chan"
- switch_options=("delete","remove","disconnect","list","ssl")
- locks="cmd:serversetting(IRC_ENABLED) and pperm(Developer)"
- help_category="Comms"
-
-
[docs]deffunc(self):
- """Setup the irc-channel mapping"""
-
- ifnotsettings.IRC_ENABLED:
- string="""IRC is not enabled. You need to activate it in game/settings.py."""
- self.msg(string)
- return
-
- if"list"inself.switches:
- # show all connections
- self.msg(_list_bots(self))
- return
-
- if"disconnect"inself.switchesor"remove"inself.switchesor"delete"inself.switches:
- botname="ircbot-%s"%self.lhs
- matches=AccountDB.objects.filter(db_is_bot=True,username=botname)
- dbref=utils.dbref(self.lhs)
- ifnotmatchesanddbref:
- # try dbref match
- matches=AccountDB.objects.filter(db_is_bot=True,id=dbref)
- ifmatches:
- matches[0].delete()
- self.msg("IRC connection destroyed.")
- else:
- self.msg("IRC connection/bot could not be removed, does it exist?")
- return
-
- ifnotself.argsornotself.rhs:
- string=(
- "Usage: irc2chan[/switches] <evennia_channel> ="
- " <ircnetwork> <port> <#irchannel> <botname>[:typeclass]"
- )
- self.msg(string)
- return
-
- channel=self.lhs
- self.rhs=self.rhs.replace("#"," ")# to avoid Python comment issues
- try:
- irc_network,irc_port,irc_channel,irc_botname=[
- part.strip()forpartinself.rhs.split(None,4)
- ]
- irc_channel="#%s"%irc_channel
- exceptException:
- string="IRC bot definition '%s' is not valid."%self.rhs
- self.msg(string)
- return
-
- botclass=None
- if":"inirc_botname:
- irc_botname,botclass=[part.strip()forpartinirc_botname.split(":",2)]
- botname="ircbot-%s"%irc_botname
- # If path given, use custom bot otherwise use default.
- botclass=botclassifbotclasselsebots.IRCBot
- irc_ssl="ssl"inself.switches
-
- # create a new bot
- bot=AccountDB.objects.filter(username__iexact=botname)
- ifbot:
- # re-use an existing bot
- bot=bot[0]
- ifnotbot.is_bot:
- self.msg("Account '%s' already exists and is not a bot."%botname)
- return
- else:
- try:
- bot=create.create_account(botname,None,None,typeclass=botclass)
- exceptExceptionaserr:
- self.msg("|rError, could not create the bot:|n '%s'."%err)
- return
- bot.start(
- ev_channel=channel,
- irc_botname=irc_botname,
- irc_channel=irc_channel,
- irc_network=irc_network,
- irc_port=irc_port,
- irc_ssl=irc_ssl,
- )
- self.msg("Connection created. Starting IRC bot.")
-
-
-
[docs]classCmdIRCStatus(COMMAND_DEFAULT_CLASS):
- """
- Check and reboot IRC bot.
-
- Usage:
- 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
- see if the connection is still responsive. The 'nicklist' argument
- (aliases are 'who' and 'users') will return a list of users on the
- remote IRC channel. Finally, 'reconnect' will force the client to
- disconnect and reconnect again. This may be a last resort if the
- client has silently lost connection (this may happen if the remote
- network experience network issues). During the reconnection
- messages sent to either channel will be lost.
-
- """
-
- key="ircstatus"
- locks="cmd:serversetting(IRC_ENABLED) and perm(ircstatus) or perm(Builder))"
- help_category="Comms"
-
-
[docs]deffunc(self):
- """Handles the functioning of the command."""
-
- ifnotself.args:
- self.msg(_list_bots(self))
- return
- # should always be on the form botname option
- args=self.args.split()
- iflen(args)!=2:
- self.msg("Usage: ircstatus [#dbref ping||nicklist||reconnect]")
- return
- botname,option=args
- ifoptionnotin("ping","users","reconnect","nicklist","who"):
- self.msg("Not a valid option.")
- return
- matches=None
- ifutils.dbref(botname):
- matches=AccountDB.objects.filter(db_is_bot=True,id=utils.dbref(botname))
- ifnotmatches:
- self.msg(
- "No matching IRC-bot found. Use ircstatus without arguments to list active bots."
- )
- return
- ircbot=matches[0]
- channel=ircbot.db.irc_channel
- network=ircbot.db.irc_network
- port=ircbot.db.irc_port
- chtext="IRC bot '%s' on channel %s (%s:%s)"%(
- ircbot.db.irc_botname,
- channel,
- network,
- port,
- )
- ifoption=="ping":
- # check connection by sending outself a ping through the server.
- self.caller.msg("Pinging through %s."%chtext)
- ircbot.ping(self.caller)
- elifoptionin("users","nicklist","who"):
- # retrieve user list. The bot must handles the echo since it's
- # an asynchronous call.
- self.caller.msg("Requesting nicklist from %s (%s:%s)."%(channel,network,port))
- ircbot.get_nicklist(self.caller)
- elifself.caller.locks.check_lockstring(
- self.caller,"dummy:perm(ircstatus) or perm(Developer)"
- ):
- # reboot the client
- self.caller.msg("Forcing a disconnect + reconnect of %s."%chtext)
- ircbot.reconnect()
- else:
- self.caller.msg("You don't have permission to force-reload the IRC bot.")
-
-
-# RSS connection
-
[docs]classCmdRSS2Chan(COMMAND_DEFAULT_CLASS):
- """
- link an evennia channel to an external RSS feed
-
- Usage:
- rss2chan[/switches] <evennia_channel> = <rss_url>
-
- Switches:
- /disconnect - this will stop the feed and remove the connection to the
- channel.
- /remove - "
- /list - show all rss->evennia mappings
-
- Example:
- rss2chan rsschan = http://code.google.com/feeds/p/evennia/updates/basic
-
- This creates an RSS reader that connects to a given RSS feed url. Updates
- will be echoed as a title and news link to the given channel. The rate of
- updating is set with the RSS_UPDATE_INTERVAL variable in settings (default
- is every 10 minutes).
-
- When disconnecting you need to supply both the channel and url again so as
- to identify the connection uniquely.
- """
-
- key="rss2chan"
- switch_options=("disconnect","remove","list")
- locks="cmd:serversetting(RSS_ENABLED) and pperm(Developer)"
- help_category="Comms"
-
-
[docs]deffunc(self):
- """Setup the rss-channel mapping"""
-
- # checking we have all we need
- ifnotsettings.RSS_ENABLED:
- string="""RSS is not enabled. You need to activate it in game/settings.py."""
- self.msg(string)
- return
- try:
- importfeedparser
-
- assertfeedparser# to avoid checker error of not being used
- exceptImportError:
- string=(
- "RSS requires python-feedparser (https://pypi.python.org/pypi/feedparser)."
- " Install before continuing."
- )
- self.msg(string)
- return
-
- if"list"inself.switches:
- # show all connections
- rssbots=[
- bot
- forbotinAccountDB.objects.filter(db_is_bot=True,username__startswith="rssbot-")
- ]
- ifrssbots:
- table=self.styled_table(
- "|wdbid|n",
- "|wupdate rate|n",
- "|wev-channel",
- "|wRSS feed URL|n",
- border="cells",
- maxwidth=_DEFAULT_WIDTH,
- )
- forrssbotinrssbots:
- table.add_row(
- rssbot.id,rssbot.db.rss_rate,rssbot.db.ev_channel,rssbot.db.rss_url
- )
- self.msg(table)
- else:
- self.msg("No rss bots found.")
- return
-
- if"disconnect"inself.switchesor"remove"inself.switchesor"delete"inself.switches:
- botname="rssbot-%s"%self.lhs
- matches=AccountDB.objects.filter(db_is_bot=True,db_key=botname)
- ifnotmatches:
- # try dbref match
- matches=AccountDB.objects.filter(db_is_bot=True,id=self.args.lstrip("#"))
- ifmatches:
- matches[0].delete()
- self.msg("RSS connection destroyed.")
- else:
- self.msg("RSS connection/bot could not be removed, does it exist?")
- return
-
- ifnotself.argsornotself.rhs:
- string="Usage: rss2chan[/switches] <evennia_channel> = <rss url>"
- self.msg(string)
- return
- channel=self.lhs
- url=self.rhs
-
- botname="rssbot-%s"%url
- bot=AccountDB.objects.filter(username__iexact=botname)
- ifbot:
- # re-use existing bot
- bot=bot[0]
- ifnotbot.is_bot:
- self.msg("Account '%s' already exists and is not a bot."%botname)
- return
- else:
- # create a new bot
- bot=create.create_account(botname,None,None,typeclass=bots.RSSBot)
- bot.start(ev_channel=channel,rss_url=url,rss_rate=10)
- self.msg("RSS reporter created. Fetching RSS.")
-
-
-
[docs]classCmdGrapevine2Chan(COMMAND_DEFAULT_CLASS):
- """
- Link an Evennia channel to an exteral Grapevine channel
-
- Usage:
- grapevine2chan[/switches] <evennia_channel> = <grapevine_channel>
- grapevine2chan/disconnect <connection #id>
-
- Switches:
- /list - (or no switch): show existing grapevine <-> Evennia
- mappings and available grapevine chans
- /remove - alias to disconnect
- /delete - alias to disconnect
-
- Example:
- grapevine2chan mygrapevine = gossip
-
- This creates a link between an in-game Evennia channel and an external
- Grapevine channel. The game must be registered with the Grapevine network
- (register at https://grapevine.haus) and the GRAPEVINE_* auth information
- must be added to game settings.
- """
-
- key="grapevine2chan"
- switch_options=("disconnect","remove","delete","list")
- locks="cmd:serversetting(GRAPEVINE_ENABLED) and pperm(Developer)"
- help_category="Comms"
-
-
-"""
-General Character commands usually available to all characters
-"""
-importre
-fromdjango.confimportsettings
-fromevennia.utilsimportutils
-fromevennia.typeclasses.attributesimportNickTemplateInvalid
-
-COMMAND_DEFAULT_CLASS=utils.class_from_module(settings.COMMAND_DEFAULT_CLASS)
-
-# limit symbol import for API
-__all__=(
- "CmdHome",
- "CmdLook",
- "CmdNick",
- "CmdInventory",
- "CmdSetDesc",
- "CmdGet",
- "CmdDrop",
- "CmdGive",
- "CmdSay",
- "CmdWhisper",
- "CmdPose",
- "CmdAccess",
-)
-
-
-
[docs]classCmdHome(COMMAND_DEFAULT_CLASS):
- """
- move to your character's home location
-
- Usage:
- home
-
- Teleports you to your home location.
- """
-
- key="home"
- locks="cmd:perm(home) or perm(Builder)"
- arg_regex=r"$"
-
-
[docs]deffunc(self):
- """Implement the command"""
- caller=self.caller
- home=caller.home
- ifnothome:
- caller.msg("You have no home!")
- elifhome==caller.location:
- caller.msg("You are already home!")
- else:
- caller.msg("There's no place like home ...")
- caller.move_to(home)
-
-
-
[docs]classCmdLook(COMMAND_DEFAULT_CLASS):
- """
- look at location or object
-
- Usage:
- look
- look <obj>
- look *<account>
-
- Observes your location or objects in your vicinity.
- """
-
- key="look"
- aliases=["l","ls"]
- locks="cmd:all()"
- arg_regex=r"\s|$"
-
-
[docs]deffunc(self):
- """
- Handle the looking.
- """
- caller=self.caller
- ifnotself.args:
- target=caller.location
- ifnottarget:
- caller.msg("You have no location to look at!")
- return
- else:
- target=caller.search(self.args)
- ifnottarget:
- return
- desc=caller.at_look(target)
- # add the type=look to the outputfunc to make it
- # easy to separate this output in client.
- self.msg(text=(desc,{"type":"look"}),options=None)
-
-
-
[docs]classCmdNick(COMMAND_DEFAULT_CLASS):
- """
- define a personal alias/nick by defining a string to
- match and replace it with another on the fly
-
- Usage:
- nick[/switches] <string> [= [replacement_string]]
- nick[/switches] <template> = <replacement_template>
- nick/delete <string> or number
- nicks
-
- Switches:
- inputline - replace on the inputline (default)
- object - replace on object-lookup
- account - replace on account-lookup
- list - show all defined aliases (also "nicks" works)
- delete - remove nick by index in /list
- clearall - clear all nicks
-
- Examples:
- nick hi = say Hello, I'm Sarah!
- nick/object tom = the tall man
- nick build $1 $2 = create/drop $1;$2
- nick tell $1 $2=page $1=$2
- nick tm?$1=page tallman=$1
- nick tm\=$1=page tallman=$1
-
- A 'nick' is a personal string replacement. Use $1, $2, ... to catch arguments.
- Put the last $-marker without an ending space to catch all remaining text. You
- can also use unix-glob matching for the left-hand side <string>:
-
- * - matches everything
- ? - matches 0 or 1 single characters
- [abcd] - matches these chars in any order
- [!abcd] - matches everything not among these chars
- \= - escape literal '=' you want in your <string>
-
- Note that no objects are actually renamed or changed by this command - your nicks
- are only available to you. If you want to permanently add keywords to an object
- for everyone to use, you need build privileges and the alias command.
-
- """
-
- key="nick"
- switch_options=("inputline","object","account","list","delete","clearall")
- aliases=["nickname","nicks"]
- locks="cmd:all()"
-
-
[docs]classCmdGet(COMMAND_DEFAULT_CLASS):
- """
- pick up something
-
- Usage:
- get <obj>
-
- Picks up an object from your location and puts it in
- your inventory.
- """
-
- key="get"
- aliases="grab"
- locks="cmd:all();view:perm(Developer);read:perm(Developer)"
- arg_regex=r"\s|$"
-
-
[docs]classCmdDrop(COMMAND_DEFAULT_CLASS):
- """
- drop something
-
- Usage:
- drop <obj>
-
- Lets you drop an object from your inventory into the
- location you are currently in.
- """
-
- key="drop"
- locks="cmd:all()"
- arg_regex=r"\s|$"
-
-
[docs]deffunc(self):
- """Implement command"""
-
- caller=self.caller
- ifnotself.args:
- caller.msg("Drop what?")
- return
-
- # Because the DROP command by definition looks for items
- # in inventory, call the search function using location = caller
- obj=caller.search(
- self.args,
- location=caller,
- nofound_string="You aren't carrying %s."%self.args,
- multimatch_string="You carry more than one %s:"%self.args,
- )
- ifnotobj:
- return
-
- # Call the object script's at_pre_drop() method.
- ifnotobj.at_pre_drop(caller):
- return
-
- success=obj.move_to(caller.location,quiet=True)
- ifnotsuccess:
- caller.msg("This couldn't be dropped.")
- else:
- caller.msg("You drop %s."%(obj.name,))
- caller.location.msg_contents("%s drops %s."%(caller.name,obj.name),exclude=caller)
- # Call the object script's at_drop() method.
- obj.at_drop(caller)
-
-
-
[docs]classCmdGive(COMMAND_DEFAULT_CLASS):
- """
- give away something to someone
-
- Usage:
- give <inventory obj> <to||=> <target>
-
- Gives an items from your inventory to another character,
- placing it in their inventory.
- """
-
- key="give"
- rhs_split=("="," to ")# Prefer = delimiter, but allow " to " usage.
- locks="cmd:all()"
- arg_regex=r"\s|$"
-
-
[docs]deffunc(self):
- """Implement give"""
-
- caller=self.caller
- ifnotself.argsornotself.rhs:
- caller.msg("Usage: give <inventory object> = <target>")
- return
- to_give=caller.search(
- self.lhs,
- location=caller,
- nofound_string="You aren't carrying %s."%self.lhs,
- multimatch_string="You carry more than one %s:"%self.lhs,
- )
- target=caller.search(self.rhs)
- ifnot(to_giveandtarget):
- return
- iftarget==caller:
- caller.msg("You keep %s to yourself."%to_give.key)
- return
- ifnotto_give.location==caller:
- caller.msg("You are not holding %s."%to_give.key)
- return
-
- # calling at_pre_give hook method
- ifnotto_give.at_pre_give(caller,target):
- return
-
- # give object
- success=to_give.move_to(target,quiet=True)
- ifnotsuccess:
- caller.msg("This could not be given.")
- else:
- caller.msg("You give %s to %s."%(to_give.key,target.key))
- target.msg("%s gives you %s."%(caller.key,to_give.key))
- # Call the object script's at_give() method.
- to_give.at_give(caller,target)
-
-
-
[docs]classCmdSetDesc(COMMAND_DEFAULT_CLASS):
- """
- describe yourself
-
- Usage:
- setdesc <description>
-
- Add a description to yourself. This
- will be visible to people when they
- look at you.
- """
-
- key="setdesc"
- locks="cmd:all()"
- arg_regex=r"\s|$"
-
-
[docs]deffunc(self):
- """add the description"""
-
- ifnotself.args:
- self.caller.msg("You must add a description.")
- return
-
- self.caller.db.desc=self.args.strip()
- self.caller.msg("You set your description.")
-
-
-
[docs]classCmdSay(COMMAND_DEFAULT_CLASS):
- """
- speak as your character
-
- Usage:
- say <message>
-
- Talk to those in your current location.
- """
-
- key="say"
- aliases=['"',"'"]
- locks="cmd:all()"
-
- # don't require a space after `say/'/"`
- arg_regex=None
-
-
[docs]deffunc(self):
- """Run the say command"""
-
- caller=self.caller
-
- ifnotself.args:
- caller.msg("Say what?")
- return
-
- speech=self.args
-
- # Calling the at_pre_say hook on the character
- speech=caller.at_pre_say(speech)
-
- # If speech is empty, stop here
- ifnotspeech:
- return
-
- # Call the at_post_say hook on the character
- caller.at_say(speech,msg_self=True)
-
-
-
[docs]classCmdWhisper(COMMAND_DEFAULT_CLASS):
- """
- Speak privately as your character to another
-
- Usage:
- whisper <character> = <message>
- whisper <char1>, <char2> = <message>
-
- Talk privately to one or more characters in your current location, without
- others in the room being informed.
- """
-
- key="whisper"
- locks="cmd:all()"
-
-
[docs]deffunc(self):
- """Run the whisper command"""
-
- caller=self.caller
-
- ifnotself.lhsornotself.rhs:
- caller.msg("Usage: whisper <character> = <message>")
- return
-
- receivers=[recv.strip()forrecvinself.lhs.split(",")]
-
- receivers=[caller.search(receiver)forreceiverinset(receivers)]
- receivers=[recvforrecvinreceiversifrecv]
-
- speech=self.rhs
- # If the speech is empty, abort the command
- ifnotspeechornotreceivers:
- return
-
- # Call a hook to change the speech before whispering
- speech=caller.at_pre_say(speech,whisper=True,receivers=receivers)
-
- # no need for self-message if we are whispering to ourselves (for some reason)
- msg_self=NoneifcallerinreceiverselseTrue
- caller.at_say(speech,msg_self=msg_self,receivers=receivers,whisper=True)
-
-
-
[docs]classCmdPose(COMMAND_DEFAULT_CLASS):
- """
- strike a pose
-
- Usage:
- pose <pose text>
- pose's <pose text>
-
- Example:
- pose is standing by the wall, smiling.
- -> others will see:
- Tom is standing by the wall, smiling.
-
- Describe an action being taken. The pose text will
- automatically begin with your name.
- """
-
- key="pose"
- aliases=[":","emote"]
- locks="cmd:all()"
- arg_regex=""
-
- # we want to be able to pose without whitespace between
- # the command/alias and the pose (e.g. :pose)
- arg_regex=None
-
-
[docs]defparse(self):
- """
- Custom parse the cases where the emote
- starts with some special letter, such
- as 's, at which we don't want to separate
- the caller's name and the emote with a
- space.
- """
- args=self.args
- ifargsandnotargs[0]in["'",",",":"]:
- args=" %s"%args.strip()
- self.args=args
-
-
[docs]deffunc(self):
- """Hook function"""
- ifnotself.args:
- msg="What do you want to do?"
- self.caller.msg(msg)
- else:
- msg="%s%s"%(self.caller.name,self.args)
- self.caller.location.msg_contents(text=(msg,{"type":"pose"}),from_obj=self.caller)
-
-
-
[docs]classCmdAccess(COMMAND_DEFAULT_CLASS):
- """
- show your current game access
-
- Usage:
- access
-
- This command shows you the permission hierarchy and
- which permission groups you are a member of.
- """
-
- key="access"
- aliases=["groups","hierarchy"]
- locks="cmd:all()"
- arg_regex=r"$"
-
-
-"""
-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``.
-
-"""
-
-importre
-fromitertoolsimportchain
-fromdataclassesimportdataclass
-fromdjango.confimportsettings
-fromcollectionsimportdefaultdict
-fromevennia.utils.utilsimportdedent
-fromevennia.help.modelsimportHelpEntry
-fromevennia.utilsimportcreate,evmore
-fromevennia.utils.ansiimportANSIString
-fromevennia.help.filehelpimportFILE_HELP_ENTRIES
-fromevennia.utils.eveditorimportEvEditor
-fromevennia.utils.utilsimport(
- class_from_module,
- inherits_from,
- format_grid,pad
-)
-fromevennia.help.utilsimporthelp_search_with_index,parse_entry_for_subcategories
-
-CMD_IGNORE_PREFIXES=settings.CMD_IGNORE_PREFIXES
-COMMAND_DEFAULT_CLASS=class_from_module(settings.COMMAND_DEFAULT_CLASS)
-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")
-
-@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,
- "no_prefix":"",
- "tags":"",
- "text":"",
- }
-
- def__hash__(self):
- returnhash(id(self))
-
-
-
[docs]classCmdHelp(COMMAND_DEFAULT_CLASS):
- """
- Get help.
-
- Usage:
- 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.
-
- """
-
- key="help"
- aliases=["?"]
- locks="cmd:all()"
- arg_regex=r"\s|$"
-
- # this is a special cmdhandler flag that makes the cmdhandler also pack
- # the current cmdset with the call to self.func().
- return_cmdset=True
-
- # 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_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
-
- # 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
- that this is a help command result and could be rendered in a separate
- help window
- """
- iftype(self).help_more:
- usemore=True
-
- ifself.sessionandself.session.protocol_keyin("websocket","ajax/comet",):
- try:
- options=self.account.db._saved_webclient_options
- ifoptionsandoptions["helppopup"]:
- usemore=False
- exceptKeyError:
- pass
-
- ifusemore:
- evmore.msg(self.caller,text,session=self.session)
- return
-
- self.msg(text=(text,{"type":"help"}))
-
-
[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, 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:
- help_message (str): Help entry formated for console.
-
- """
- separator="|C"+"-"*self.client_width()+"|n"
- start=f"{separator}\n"
-
- title=f"|CHelp for |w{topic}|n"iftopicelse"|rNo help found|n"
-
- ifaliases:
- 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:
- suggested=sorted(suggested)
- ifclick_topics:
- suggested=[f"|lchelp {sug}|lt|w{sug}|n|le"forsuginsuggested]
- else:
- suggested=[f"|w{sug}|n"forsuginsuggested]
- suggested=(
- "\n|COther topic suggestions:|n\n{}".format(
- "\n ".join(format_grid(suggested,width=self.client_width())))
- )
- else:
- suggested=''
-
- end=start
-
- 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_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.
-
- """
- def_group_by_category(help_dict):
- grid=[]
- verbatim_elements=[]
-
- 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 '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_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:
- The `.auto_help` propery is checked for commands. For all help entries,
- 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.
-
- """
- ifhasattr(cmd_or_topic,"auto_help")andnotcmd_or_topic.auto_help:
- returnFalse
-
- 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 - check the read lock on entries
- 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":"no_prefix","boost":8},
- {"field_name":"category","boost":7},
- {"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.
-
- """
- # parse the query
-
- ifself.args:
- self.subtopics=[part.strip().lower()
- forpartinself.args.split(self.subtopic_separator_char)]
- self.topic=self.subtopics.pop(0)
- else:
- self.topic=""
- self.subtopics=[]
-
-
[docs]defstrip_cmd_prefix(self,key,all_keys):
- """
- Conditional strip of a command prefix, such as @ in @desc. By default
- this will be hidden unless there is a duplicate without the prefix
- in the full command set (such as @open and open).
-
- Args:
- key (str): Command key to analyze.
- all_cmds (list): All command-keys (and potentially aliases).
-
- Returns:
- str: Potentially modified key to use in help display.
-
- """
- ifkeyandkey[0]inCMD_IGNORE_PREFIXESandkey[1:]notinall_keys:
- # filter out e.g. `@` prefixes from display if there is duplicate
- # with the prefix in the set (such as @open/open)
- returnkey[1:]
- returnkey
-
-
-
[docs]deffunc(self):
- """
- Run the dynamic help entry creator.
- """
- caller=self.caller
- query,subtopics,cmdset=self.topic,self.subtopics,self.cmdset
- clickable_topics=self.clickable_topics
-
- ifnotquery:
- # list all available help entries, grouped by category. We want to
- # build dictionaries {category: [topic, topic, ...], ...}
-
- cmd_help_topics,db_help_topics,file_help_topics= \
- self.collect_topics(caller,mode='list')
-
- # 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)
-
- # get a collection of all keys + aliases to be able to strip prefixes like @
- key_and_aliases=set(chain(*(cmd._keyaliasesforcmdincmd_help_topics.values())))
-
- forkey,cmdincmd_help_topics.items():
- key=self.strip_cmd_prefix(key,key_and_aliases)
- 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)
-
- return
-
- # 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')
-
- # get a collection of all keys + aliases to be able to strip prefixes like @
- key_and_aliases=set(
- chain(*(cmd._keyaliasesforcmdincmd_help_topics.values())))
-
- # 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
-
- # lunr search fields/boosts
- match,suggestions=self.do_search(query,entries)
-
- ifnotmatch:
- # no topic matches found. Only give suggestions.
- help_text=f"There is no help topic matching '{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
-
- search_fields=[
- {"field_name":"text","boost":1},
- ]
-
- formatch_queryin[query,f"{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.")
- suggestions=[self.strip_cmd_prefix(sugg,key_and_aliases)
- forsugginsuggestions]
- 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(output)
- return
-
- 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
-
- 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]
-
- topic=self.strip_cmd_prefix(topic,key_and_aliases)
- ifsubtopics:
- aliases=None
- else:
- aliases=[self.strip_cmd_prefix(alias,key_and_aliases)foraliasinaliases]
- suggested=[self.strip_cmd_prefix(sugg,key_and_aliases)forsugginsuggested]
-
- output=self.format_help_entry(
- topic=topic,
- help_text=help_text,
- aliases=aliases,
- subtopics=subtopic_index,
- suggested=suggested,
- click_topics=clickable_topics
- )
-
- self.msg_help(output)
[docs]classCmdSetHelp(CmdHelp):
- """
- Edit the help database.
-
- Usage:
- sethelp[/switches] <topic>[[;alias;alias][,category[,locks]] [= <text>]
-
- Switches:
- edit - open a line editor to edit the topic's help text.
- replace - overwrite existing help topic.
- append - add text to the end of existing topic with a newline between.
- extend - as append, but don't add a newline.
- delete - remove help topic.
-
- Examples:
- sethelp lore = In the beginning was ...
- sethelp/append pickpocketing,Thievery = This steals ...
- sethelp/replace pickpocketing, ,attr(is_thief) = This steals ...
- sethelp/edit thievery
-
- 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"""
-
- switches=self.switches
- lhslist=self.lhslist
-
- ifnotself.args:
- self.msg(
- "Usage: sethelp[/switches] <topic>[;alias;alias][,category[,locks,..] = <text>"
- )
- return
-
- nlist=len(lhslist)
- topicstr=lhslist[0]ifnlist>0else""
- ifnottopicstr:
- self.msg("You have to define a topic!")
- return
- topicstrlist=topicstr.split(";")
- 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
-
- 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 "
- f"file-based help topic '{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=category.lower()
-
- if"edit"inswitches:
- # open the line editor to edit the helptext. No = is needed.
- ifold_entry:
- topicstr=old_entry.key
- ifself.rhs:
- # we assume append here.
- old_entry.entrytext+="\n%s"%self.rhs
- helpentry=old_entry
- else:
- helpentry=create.create_help_entry(
- topicstr,self.rhs,category=category,locks=lockstring,aliases=aliases,
- )
- self.caller.db._editing_help=helpentry
-
- EvEditor(
- self.caller,
- loadfunc=_loadhelp,
- savefunc=_savehelp,
- quitfunc=_quithelp,
- key="topic {}".format(topicstr),
- persistent=True,
- )
- return
-
- if"append"inswitchesor"merge"inswitchesor"extend"inswitches:
- # merge/append operations
- ifnotold_entry:
- self.msg("Could not find topic '%s'. You must give an exact name."%topicstr)
- return
- ifnotself.rhs:
- self.msg("You must supply text to append/merge.")
- return
- if"merge"inswitches:
- old_entry.entrytext+=" "+self.rhs
- else:
- old_entry.entrytext+="\n%s"%self.rhs
- 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 entry
- ifnotold_entry:
- self.msg("Could not find topic '%s'%s."%(topicstr,aliastxt))
- return
- old_entry.delete()
- self.msg("Deleted help entry '%s'%s."%(topicstr,aliastxt))
- return
-
- # at this point it means we want to add a new help entry.
- ifnotself.rhs:
- self.msg("You must supply a help text to add.")
- return
- ifold_entry:
- if"replace"inswitches:
- # overwrite old entry
- old_entry.key=topicstr
- old_entry.entrytext=self.rhs
- old_entry.help_category=category
- old_entry.locks.clear()
- old_entry.locks.add(lockstring)
- old_entry.aliases.add(aliases)
- old_entry.save()
- self.msg("Overwrote the old topic '%s'%s."%(topicstr,aliastxt))
- else:
- self.msg(
- 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.
- new_entry=create.create_help_entry(
- topicstr,self.rhs,category=category,locks=lockstring,aliases=aliases
- )
- ifnew_entry:
- self.msg(f"Topic '{topicstr}'{aliastxt} was successfully created.")
- if"edit"inswitches:
- # open the line editor to edit the helptext
- self.caller.db._editing_help=new_entry
- EvEditor(
- self.caller,
- loadfunc=_loadhelp,
- savefunc=_savehelp,
- quitfunc=_quithelp,
- key="topic {}".format(new_entry.key),
- persistent=True,
- )
- return
- else:
- self.msg(
- f"Error when creating topic '{topicstr}'{aliastxt}! Contact an admin."
- )
Source code for evennia.commands.default.muxcommand
-"""
-The command template for the default MUX-style command set. There
-is also an Account/OOC version that makes sure caller is an Account object.
-"""
-
-fromevennia.utilsimportutils
-fromevennia.commands.commandimportCommand
-
-# limit symbol import for API
-__all__=("MuxCommand","MuxAccountCommand")
-
-
-
[docs]classMuxCommand(Command):
- """
- This sets up the basis for a MUX command. The idea
- is that most other Mux-related commands should just
- inherit from this and don't have to implement much
- parsing of their own unless they do something particularly
- advanced.
-
- Note that the class's __doc__ string (this text) is
- used by Evennia to create the automatic help entry for
- the command, so make sure to document consistently here.
- """
-
-
[docs]defhas_perm(self,srcobj):
- """
- This is called by the cmdhandler to determine
- if srcobj is allowed to execute this command.
- We just show it here for completeness - we
- are satisfied using the default check in Command.
- """
- returnsuper().has_perm(srcobj)
-
-
[docs]defat_pre_cmd(self):
- """
- This hook is called before self.parse() on all commands
- """
- pass
-
-
[docs]defat_post_cmd(self):
- """
- This hook is called after the command has finished executing
- (after self.func()).
- """
- pass
-
-
[docs]defparse(self):
- """
- 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.
- """
- raw=self.args
- args=raw.strip()
- # Without explicitly setting these attributes, they assume default values:
- ifnothasattr(self,"switch_options"):
- self.switch_options=None
- ifnothasattr(self,"rhs_split"):
- self.rhs_split="="
- ifnothasattr(self,"account_caller"):
- self.account_caller=False
-
- # split out switches
- switches,delimiters=[],self.rhs_split
- ifself.switch_options:
- self.switch_options=[opt.lower()foroptinself.switch_options]
- ifargsandlen(args)>1andraw[0]=="/":
- # we have a switch, or a set of switches. These end with a space.
- switches=args[1:].split(None,1)
- iflen(switches)>1:
- switches,args=switches
- switches=switches.split("/")
- else:
- args=""
- switches=switches[0].split("/")
- # If user-provides switches, parse them with parser switch options.
- ifswitchesandself.switch_options:
- valid_switches,unused_switches,extra_switches=[],[],[]
- forelementinswitches:
- option_check=[optforoptinself.switch_optionsifopt==element]
- ifnotoption_check:
- option_check=[
- optforoptinself.switch_optionsifopt.startswith(element)
- ]
- match_count=len(option_check)
- ifmatch_count>1:
- extra_switches.extend(
- option_check
- )# Either the option provided is ambiguous,
- elifmatch_count==1:
- valid_switches.extend(option_check)# or it is a valid option abbreviation,
- elifmatch_count==0:
- unused_switches.append(element)# or an extraneous option to be ignored.
- ifextra_switches:# User provided switches
- self.msg(
- "|g%s|n: |wAmbiguous switch supplied: Did you mean /|C%s|w?"
- %(self.cmdstring," |nor /|C".join(extra_switches))
- )
- ifunused_switches:
- plural=""iflen(unused_switches)==1else"es"
- self.msg(
- '|g%s|n: |wExtra switch%s "/|C%s|w" ignored.'
- %(self.cmdstring,plural,"|n, /|C".join(unused_switches))
- )
- switches=valid_switches# Only include valid_switches in command function call
- arglist=[arg.strip()forarginargs.split()]
-
- # check for arg1, arg2, ... = argA, argB, ... constructs
- lhs,rhs=args.strip(),None
- iflhs:
- ifdelimitersandhasattr(delimiters,"__iter__"):# If delimiter is iterable,
- best_split=delimiters[0]# (default to first delimiter)
- forthis_splitindelimiters:# try each delimiter
- ifthis_splitinlhs:# to find first successful split
- best_split=this_split# to be the best split.
- break
- else:
- best_split=delimiters
- # Parse to separate left into left/right sides using best_split delimiter string
- ifbest_splitinlhs:
- lhs,rhs=lhs.split(best_split,1)
- # Trim user-injected whitespace
- rhs=rhs.strip()ifrhsisnotNoneelseNone
- lhs=lhs.strip()
- # Further split left/right sides by comma delimiter
- lhslist=[arg.strip()forarginlhs.split(",")]iflhsisnotNoneelse[]
- rhslist=[arg.strip()forarginrhs.split(",")]ifrhsisnotNoneelse[]
- # save to object properties:
- self.raw=raw
- self.switches=switches
- self.args=args.strip()
- self.arglist=arglist
- self.lhs=lhs
- self.lhslist=lhslist
- self.rhs=rhs
- self.rhslist=rhslist
-
- # if the class has the account_caller property set on itself, we make
- # sure that self.caller is always the account if possible. We also create
- # a special property "character" for the puppeted object, if any. This
- # is convenient for commands defined on the Account only.
- ifself.account_caller:
- ifutils.inherits_from(self.caller,"evennia.objects.objects.DefaultObject"):
- # caller is an Object/Character
- self.character=self.caller
- self.caller=self.caller.account
- elifutils.inherits_from(self.caller,"evennia.accounts.accounts.DefaultAccount"):
- # caller was already an Account
- self.character=self.caller.get_puppet(self.session)
- else:
- self.character=None
-
-
[docs]defget_command_info(self):
- """
- Update of parent class's get_command_info() for MuxCommand.
- """
- variables="\n".join(
- " |w{}|n ({}): {}".format(key,type(val),val)forkey,valinself.__dict__.items()
- )
- string=f"""
-Command {self} has no defined `func()` - showing on-command variables: No child func() defined for {self} - available variables:
-{variables}
- """
- self.caller.msg(string)
- # a simple test command to show the available properties
- string="-"*50
- string+="\n|w%s|n - Command variables from evennia:\n"%self.key
- string+="-"*50
- string+="\nname of cmd (self.key): |w%s|n\n"%self.key
- string+="cmd aliases (self.aliases): |w%s|n\n"%self.aliases
- string+="cmd locks (self.locks): |w%s|n\n"%self.locks
- string+="help category (self.help_category): |w%s|n\n"%self.help_category
- string+="object calling (self.caller): |w%s|n\n"%self.caller
- string+="object storing cmdset (self.obj): |w%s|n\n"%self.obj
- string+="command string given (self.cmdstring): |w%s|n\n"%self.cmdstring
- # show cmdset.key instead of cmdset to shorten output
- string+=utils.fill("current cmdset (self.cmdset): |w%s|n\n"%self.cmdset)
- string+="\n"+"-"*50
- string+="\nVariables from MuxCommand baseclass\n"
- string+="-"*50
- string+="\nraw argument (self.raw): |w%s|n \n"%self.raw
- string+="cmd args (self.args): |w%s|n\n"%self.args
- string+="cmd switches (self.switches): |w%s|n\n"%self.switches
- string+="cmd options (self.switch_options): |w%s|n\n"%self.switch_options
- string+="cmd parse left/right using (self.rhs_split): |w%s|n\n"%self.rhs_split
- string+="space-separated arg list (self.arglist): |w%s|n\n"%self.arglist
- string+="lhs, left-hand side of '=' (self.lhs): |w%s|n\n"%self.lhs
- string+="lhs, comma separated (self.lhslist): |w%s|n\n"%self.lhslist
- string+="rhs, right-hand side of '=' (self.rhs): |w%s|n\n"%self.rhs
- string+="rhs, comma separated (self.rhslist): |w%s|n\n"%self.rhslist
- string+="-"*50
- self.caller.msg(string)
-
-
[docs]deffunc(self):
- """
- 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.
- """
- self.get_command_info()
-
-
-
[docs]classMuxAccountCommand(MuxCommand):
- """
- This is an on-Account version of the MuxCommand. Since these commands sit
- on Accounts rather than on Characters/Objects, we need to check
- this in the parser.
-
- Account commands are available also when puppeting a Character, it's
- just that they are applied with a lower priority and are always
- available, also when disconnected from a character (i.e. "ooc").
-
- This class makes sure that caller is always an Account object, while
- creating a new property "character" that is set only if a
- character is actually attached to this Account and Session.
- """
-
- account_caller=True# Using MuxAccountCommand explicitly defaults the caller to an account
Source code for evennia.commands.default.syscommands
-"""
-System commands
-
-These are the default commands called by the system commandhandler
-when various exceptions occur. If one of these commands are not
-implemented and part of the current cmdset, the engine falls back
-to a default solution instead.
-
-Some system commands are shown in this module
-as a REFERENCE only (they are not all added to Evennia's
-default cmdset since they don't currently do anything differently from the
-default backup systems hard-wired in the engine).
-
-Overloading these commands in a cmdset can be used to create
-interesting effects. An example is using the NoMatch system command
-to implement a line-editor where you don't have to start each
-line with a command (if there is no match to a known command,
-the line is just added to the editor buffer).
-"""
-
-fromevennia.comms.modelsimportChannelDB
-fromevennia.utilsimportcreate
-fromevennia.utils.utilsimportat_search_result
-
-# The command keys the engine is calling
-# (the actual names all start with __)
-fromevennia.commands.cmdhandlerimportCMD_NOINPUT
-fromevennia.commands.cmdhandlerimportCMD_NOMATCH
-fromevennia.commands.cmdhandlerimportCMD_MULTIMATCH
-fromevennia.utilsimportutils
-
-fromdjango.confimportsettings
-
-COMMAND_DEFAULT_CLASS=utils.class_from_module(settings.COMMAND_DEFAULT_CLASS)
-
-# Command called when there is no input at line
-# (i.e. an lone return key)
-
-
-
[docs]classSystemNoInput(COMMAND_DEFAULT_CLASS):
- """
- This is called when there is no input given
- """
-
- key=CMD_NOINPUT
- locks="cmd:all()"
-
-
-
-
-#
-# Command called when there was no match to the
-# command name
-#
-
[docs]classSystemNoMatch(COMMAND_DEFAULT_CLASS):
- """
- No command was found matching the given input.
- """
-
- key=CMD_NOMATCH
- locks="cmd:all()"
-
-
[docs]deffunc(self):
- """
- This is given the failed raw string as input.
- """
- self.msg("Huh?")
-
-
-#
-# Command called when there were multiple matches to the command.
-#
-
[docs]classSystemMultimatch(COMMAND_DEFAULT_CLASS):
- """
- Multiple command matches.
-
- The cmdhandler adds a special attribute 'matches' to this
- system command.
-
- matches = [(cmdname, args, cmdobj, cmdlen, mratio, raw_cmdname) , (cmdname, ...), ...]
-
- Here, `cmdname` is the command's name and `args` the rest of the incoming string,
- without said command name. `cmdobj` is the Command instance, the cmdlen is
- the same as len(cmdname) and mratio is a measure of how big a part of the
- full input string the cmdname takes up - an exact match would be 1.0. Finally,
- the `raw_cmdname` is the cmdname unmodified by eventual prefix-stripping.
-
- """
-
- key=CMD_MULTIMATCH
- locks="cmd:all()"
-
-
[docs]deffunc(self):
- """
- Handle multiple-matches by using the at_search_result default handler.
-
- """
- # this was set by the cmdparser and is a tuple
- # (cmdname, args, cmdobj, cmdlen, mratio, raw_cmdname). See
- # evennia.commands.cmdparse.create_match for more details.
- 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])
[docs]classCmdReload(COMMAND_DEFAULT_CLASS):
- """
- reload the server
-
- Usage:
- reload [reason]
-
- This restarts the server. The Portal is not
- affected. Non-persistent scripts will survive a reload (use
- reset to purge) and at_reload() hooks will be called.
- """
-
- key="@reload"
- aliases=["@restart"]
- locks="cmd:perm(reload) or perm(Developer)"
- help_category="System"
-
-
[docs]classCmdReset(COMMAND_DEFAULT_CLASS):
- """
- reset and reboot the server
-
- Usage:
- reset
-
- Notes:
- For normal updating you are recommended to use reload rather
- than this command. Use shutdown for a complete stop of
- everything.
-
- This emulates a cold reboot of the Server component of Evennia.
- The difference to shutdown is that the Server will auto-reboot
- and that it does not affect the Portal, so no users will be
- disconnected. Contrary to reload however, all shutdown hooks will
- be called and any non-database saved scripts, ndb-attributes,
- cmdsets etc will be wiped.
-
- """
-
- key="@reset"
- aliases=["@reboot"]
- locks="cmd:perm(reload) or perm(Developer)"
- help_category="System"
-
-
[docs]deffunc(self):
- """
- Reload the system.
- """
- SESSIONS.announce_all(" Server resetting/restarting ...")
- SESSIONS.portal_reset_server()
-
-
-
[docs]classCmdShutdown(COMMAND_DEFAULT_CLASS):
-
- """
- stop the server completely
-
- Usage:
- shutdown [announcement]
-
- Gracefully shut down both Server and Portal.
- """
-
- key="@shutdown"
- locks="cmd:perm(shutdown) or perm(Developer)"
- help_category="System"
-
-
[docs]deffunc(self):
- """Define function"""
- # Only allow shutdown if caller has session
- ifnotself.caller.sessions.get():
- return
- self.msg("Shutting down server ...")
- announcement="\nServer is being SHUT DOWN!\n"
- ifself.args:
- announcement+="%s\n"%self.args
- logger.log_info("Server shutdown by %s."%self.caller.name)
- SESSIONS.announce_all(announcement)
- SESSIONS.portal_shutdown()
-
-
-def_py_load(caller):
- return""
-
-
-def_py_code(caller,buf):
- """
- Execute the buffer.
- """
- measure_time=caller.db._py_measure_time
- client_raw=caller.db._py_clientraw
- string="Executing code%s ..."%(" (measure timing)"ifmeasure_timeelse"")
- caller.msg(string)
- _run_code_snippet(
- caller,buf,mode="exec",measure_time=measure_time,client_raw=client_raw,show_input=False
- )
- returnTrue
-
-
-def_py_quit(caller):
- delcaller.db._py_measure_time
- caller.msg("Exited the code editor.")
-
-
-def_run_code_snippet(
- caller,pycode,mode="eval",measure_time=False,client_raw=False,show_input=True
-):
- """
- Run code and try to display information to the caller.
-
- Args:
- caller (Object): The caller.
- pycode (str): The Python code to run.
- measure_time (bool, optional): Should we measure the time of execution?
- client_raw (bool, optional): Should we turn off all client-specific escaping?
- show_input (bookl, optional): Should we display the input?
-
- """
- # Try to retrieve the session
- session=caller
- ifhasattr(caller,"sessions"):
- sessions=caller.sessions.all()
-
- available_vars=evennia_local_vars(caller)
-
- ifshow_input:
- forsessioninsessions:
- try:
- caller.msg(">>> %s"%pycode,session=session,options={"raw":True})
- exceptTypeError:
- caller.msg(">>> %s"%pycode,options={"raw":True})
-
- try:
- # reroute standard output to game client console
- old_stdout=sys.stdout
- old_stderr=sys.stderr
-
- classFakeStd:
- def__init__(self,caller):
- self.caller=caller
-
- defwrite(self,string):
- self.caller.msg(string.rsplit("\n",1)[0])
-
- fake_std=FakeStd(caller)
- sys.stdout=fake_std
- sys.stderr=fake_std
-
- try:
- pycode_compiled=compile(pycode,"",mode)
- exceptException:
- mode="exec"
- pycode_compiled=compile(pycode,"",mode)
-
- duration=""
- ifmeasure_time:
- t0=time.time()
- ret=eval(pycode_compiled,{},available_vars)
- t1=time.time()
- duration=" (runtime ~ %.4f ms)"%((t1-t0)*1000)
- caller.msg(duration)
- else:
- ret=eval(pycode_compiled,{},available_vars)
-
- exceptException:
- errlist=traceback.format_exc().split("\n")
- iflen(errlist)>4:
- errlist=errlist[4:]
- ret="\n".join("%s"%lineforlineinerrlistifline)
- finally:
- # return to old stdout
- sys.stdout=old_stdout
- sys.stderr=old_stderr
-
- ifretisNone:
- return
- elifisinstance(ret,tuple):
- # we must convert here to allow msg to pass it (a tuple is confused
- # with a outputfunc structure)
- ret=str(ret)
-
- forsessioninsessions:
- try:
- caller.msg(ret,session=session,options={"raw":True,"client_raw":client_raw})
- exceptTypeError:
- caller.msg(ret,options={"raw":True,"client_raw":client_raw})
-
-
-defevennia_local_vars(caller):
- """Return Evennia local variables usable in the py command as a dictionary."""
- importevennia
-
- return{
- "self":caller,
- "me":caller,
- "here":getattr(caller,"location",None),
- "evennia":evennia,
- "ev":evennia,
- "inherits_from":utils.inherits_from,
- }
-
-
-classEvenniaPythonConsole(code.InteractiveConsole):
-
- """Evennia wrapper around a Python interactive console."""
-
- def__init__(self,caller):
- super().__init__(evennia_local_vars(caller))
- self.caller=caller
-
- defwrite(self,string):
- """Don't send to stderr, send to self.caller."""
- self.caller.msg(string)
-
- defpush(self,line):
- """Push some code, whether complete or not."""
- old_stdout=sys.stdout
- old_stderr=sys.stderr
-
- classFakeStd:
- def__init__(self,caller):
- self.caller=caller
-
- defwrite(self,string):
- self.caller.msg(string.split("\n",1)[0])
-
- fake_std=FakeStd(self.caller)
- sys.stdout=fake_std
- sys.stderr=fake_std
- result=None
- try:
- result=super().push(line)
- finally:
- sys.stdout=old_stdout
- sys.stderr=old_stderr
- returnresult
-
-
-
[docs]classCmdPy(COMMAND_DEFAULT_CLASS):
- """
- execute a snippet of python code
-
- Usage:
- py [cmd]
- py/edit
- py/time <cmd>
- py/clientraw <cmd>
- py/noecho
-
- Switches:
- time - output an approximate execution time for <cmd>
- edit - open a code editor for multi-line code experimentation
- clientraw - turn off all client-specific escaping. Note that this may
- lead to different output depending on prototocol (such as angular brackets
- being parsed as HTML in the webclient but not in telnet clients)
- noecho - in Python console mode, turn off the input echo (e.g. if your client
- does this for you already)
-
- Without argument, open a Python console in-game. This is a full console,
- accepting multi-line Python code for testing and debugging. Type `exit()` to
- return to the game. If Evennia is reloaded, the console will be closed.
-
- Enter a line of instruction after the 'py' command to execute it
- immediately. Separate multiple commands by ';' or open the code editor
- using the /edit switch (all lines added in editor will be executed
- immediately when closing or using the execute command in the editor).
-
- A few variables are made available for convenience in order to offer access
- to the system (you can import more at execution time).
-
- Available variables in py environment:
- self, me : caller
- here : caller.location
- evennia : the evennia API
- inherits_from(obj, parent) : check object inheritance
-
- You can explore The evennia API from inside the game by calling
- the `__doc__` property on entities:
- py evennia.__doc__
- py evennia.managers.__doc__
-
- |rNote: In the wrong hands this command is a severe security risk. It
- should only be accessible by trusted server admins/superusers.|n
-
- """
-
- key="@py"
- aliases=["@!"]
- switch_options=("time","edit","clientraw","noecho")
- locks="cmd:perm(py) or perm(Developer)"
- help_category="System"
- arg_regex=""
-
-
[docs]classCmdAccounts(COMMAND_DEFAULT_CLASS):
- """
- Manage registered accounts
-
- Usage:
- accounts [nr]
- accounts/delete <name or #id> [: reason]
-
- Switches:
- delete - delete an account from the server
-
- By default, lists statistics about the Accounts registered with the game.
- It will list the <nr> amount of latest registered accounts
- If not given, <nr> defaults to 10.
- """
-
- key="@accounts"
- aliases=["@account"]
- switch_options=("delete",)
- locks="cmd:perm(listaccounts) or perm(Admin)"
- help_category="System"
-
-
[docs]deffunc(self):
- """List the accounts"""
-
- caller=self.caller
- args=self.args
-
- if"delete"inself.switches:
- account=getattr(caller,"account")
- ifnotaccountornotaccount.check_permstring("Developer"):
- caller.msg("You are not allowed to delete accounts.")
- return
- ifnotargs:
- caller.msg("Usage: accounts/delete <name or #id> [: reason]")
- return
- reason=""
- if":"inargs:
- args,reason=[arg.strip()forarginargs.split(":",1)]
- # We use account_search since we want to be sure to find also accounts
- # that lack characters.
- accounts=search.account_search(args)
- ifnotaccounts:
- self.msg("Could not find an account by that name.")
- return
- iflen(accounts)>1:
- string="There were multiple matches:\n"
- string+="\n".join(" %s%s"%(account.id,account.key)foraccountinaccounts)
- self.msg(string)
- return
- account=accounts.first()
- ifnotaccount.access(caller,"delete"):
- self.msg("You don't have the permissions to delete that account.")
- return
- username=account.username
- # ask for confirmation
- confirm=(
- "It is often better to block access to an account rather than to delete it. "
- "|yAre you sure you want to permanently delete "
- "account '|n{}|y'|n yes/[no]?".format(username)
- )
- answer=yield(confirm)
- ifanswer.lower()notin("y","yes"):
- caller.msg("Canceled deletion.")
- return
-
- # Boot the account then delete it.
- self.msg("Informing and disconnecting account ...")
- string="\nYour account '%s' is being *permanently* deleted.\n"%username
- ifreason:
- string+=" Reason given:\n '%s'"%reason
- account.msg(string)
- logger.log_sec(
- "Account Deleted: %s (Reason: %s, Caller: %s, IP: %s)."
- %(account,reason,caller,self.session.address)
- )
- account.delete()
- self.msg("Account %s was successfully deleted."%username)
- return
-
- # No switches, default to displaying a list of accounts.
- ifself.argsandself.args.isdigit():
- nlim=int(self.args)
- else:
- nlim=10
-
- naccounts=AccountDB.objects.count()
-
- # typeclass table
- dbtotals=AccountDB.objects.object_totals()
- typetable=self.styled_table(
- "|wtypeclass|n","|wcount|n","|w%%|n",border="cells",align="l"
- )
- forpath,countindbtotals.items():
- typetable.add_row(path,count,"%.2f"%((float(count)/naccounts)*100))
- # last N table
- plyrs=AccountDB.objects.all().order_by("db_date_created")[max(0,naccounts-nlim):]
- latesttable=self.styled_table(
- "|wcreated|n","|wdbref|n","|wname|n","|wtypeclass|n",border="cells",align="l"
- )
- forplyinplyrs:
- latesttable.add_row(
- utils.datetime_format(ply.date_created),ply.dbref,ply.key,ply.path
- )
-
- string="\n|wAccount typeclass distribution:|n\n%s"%typetable
- string+="\n|wLast %s Accounts created:|n\n%s"%(min(naccounts,nlim),latesttable)
- caller.msg(string)
-
-
-
[docs]classCmdService(COMMAND_DEFAULT_CLASS):
- """
- manage system services
-
- Usage:
- service[/switch] <service>
-
- Switches:
- list - shows all available services (default)
- start - activates or reactivate a service
- stop - stops/inactivate a service (can often be restarted)
- delete - tries to permanently remove a service
-
- Service management system. Allows for the listing,
- starting, and stopping of services. If no switches
- are given, services will be listed. Note that to operate on the
- service you have to supply the full (green or red) name as given
- in the list.
- """
-
- key="@service"
- aliases=["@services"]
- switch_options=("list","start","stop","delete")
- locks="cmd:perm(service) or perm(Developer)"
- help_category="System"
-
-
[docs]deffunc(self):
- """Implement command"""
-
- caller=self.caller
- switches=self.switches
-
- ifswitchesandswitches[0]notin("list","start","stop","delete"):
- caller.msg("Usage: service/<list|start|stop|delete> [servicename]")
- return
-
- # get all services
- service_collection=SESSIONS.server.services
-
- ifnotswitchesorswitches[0]=="list":
- # Just display the list of installed services and their
- # status, then exit.
- table=self.styled_table(
- "|wService|n (use services/start|stop|delete)","|wstatus",align="l"
- )
- forserviceinservice_collection.services:
- table.add_row(service.name,service.runningand"|gRunning"or"|rNot Running")
- caller.msg(str(table))
- return
-
- # Get the service to start / stop
-
- try:
- service=service_collection.getServiceNamed(self.args)
- exceptException:
- string="Invalid service name. This command is case-sensitive. "
- string+="See service/list for valid service name (enter the full name exactly)."
- caller.msg(string)
- return
-
- ifswitches[0]in("stop","delete"):
- # Stopping/killing a service gracefully closes it and disconnects
- # any connections (if applicable).
-
- delmode=switches[0]=="delete"
- ifnotservice.running:
- caller.msg("That service is not currently running.")
- return
- ifservice.name[:7]=="Evennia":
- ifdelmode:
- caller.msg("You cannot remove a core Evennia service (named 'Evennia*').")
- return
- string=("|RYou seem to be shutting down a core Evennia "
- "service (named 'Evennia*').\nNote that stopping "
- "some TCP port services will *not* disconnect users "
- "*already* connected on those ports, but *may* "
- "instead cause spurious errors for them.\nTo safely "
- "and permanently remove ports, change settings file "
- "and restart the server.|n\n")
- caller.msg(string)
-
- ifdelmode:
- service.stopService()
- service_collection.removeService(service)
- caller.msg("|gStopped and removed service '%s'.|n"%self.args)
- else:
- caller.msg(f"Stopping service '{self.args}'...")
- try:
- service.stopService()
- exceptExceptionaserr:
- caller.msg(f"|rErrors were reported when stopping this service{err}.\n"
- "If there are remaining problems, try reloading "
- "or rebooting the server.")
- caller.msg("|g... Stopped service '%s'.|n"%self.args)
- return
-
- ifswitches[0]=="start":
- # Attempt to start a service.
- ifservice.running:
- caller.msg("That service is already running.")
- return
- caller.msg(f"Starting service '{self.args}' ...")
- try:
- service.startService()
- exceptExceptionaserr:
- caller.msg(f"|rErrors were reported when starting this service{err}.\n"
- "If there are remaining problems, try reloading the server, changing the "
- "settings if it's a non-standard service.|n")
- caller.msg("|gService started.|n")
-
-
-
[docs]classCmdAbout(COMMAND_DEFAULT_CLASS):
- """
- show Evennia info
-
- Usage:
- about
-
- Display info about the game engine.
- """
-
- key="@about"
- aliases="@version"
- locks="cmd:all()"
- help_category="System"
-
-
[docs]classCmdTime(COMMAND_DEFAULT_CLASS):
- """
- show server time statistics
-
- Usage:
- time
-
- List Server time statistics such as uptime
- and the current time stamp.
- """
-
- key="@time"
- aliases="@uptime"
- locks="cmd:perm(time) or perm(Player)"
- help_category="System"
-
-
[docs]deffunc(self):
- """Show server time data in a table."""
- table1=self.styled_table("|wServer time","",align="l",width=78)
- table1.add_row("Current uptime",utils.time_format(gametime.uptime(),3))
- table1.add_row("Portal uptime",utils.time_format(gametime.portal_uptime(),3))
- table1.add_row("Total runtime",utils.time_format(gametime.runtime(),2))
- table1.add_row("First start",datetime.datetime.fromtimestamp(gametime.server_epoch()))
- table1.add_row("Current time",datetime.datetime.now())
- table1.reformat_column(0,width=30)
- table2=self.styled_table(
- "|wIn-Game time",
- "|wReal time x %g"%gametime.TIMEFACTOR,
- align="l",
- width=78,
- border_top=0,
- )
- epochtxt="Epoch (%s)"%("from settings"ifsettings.TIME_GAME_EPOCHelse"server start")
- table2.add_row(epochtxt,datetime.datetime.fromtimestamp(gametime.game_epoch()))
- table2.add_row("Total time passed:",utils.time_format(gametime.gametime(),2))
- table2.add_row(
- "Current time ",datetime.datetime.fromtimestamp(gametime.gametime(absolute=True))
- )
- table2.reformat_column(0,width=30)
- self.caller.msg(str(table1)+"\n"+str(table2))
-
-
-
[docs]classCmdServerLoad(COMMAND_DEFAULT_CLASS):
- """
- show server load and memory statistics
-
- Usage:
- server[/mem]
-
- Switches:
- mem - return only a string of the current memory usage
- flushmem - flush the idmapper cache
-
- This command shows server load statistics and dynamic memory
- usage. It also allows to flush the cache of accessed database
- objects.
-
- Some Important statistics in the table:
-
- |wServer load|n is an average of processor usage. It's usually
- between 0 (no usage) and 1 (100% usage), but may also be
- temporarily higher if your computer has multiple CPU cores.
-
- The |wResident/Virtual memory|n displays the total memory used by
- the server process.
-
- Evennia |wcaches|n all retrieved database entities when they are
- loaded by use of the idmapper functionality. This allows Evennia
- to maintain the same instances of an entity and allowing
- non-persistent storage schemes. The total amount of cached objects
- are displayed plus a breakdown of database object types.
-
- The |wflushmem|n switch allows to flush the object cache. Please
- note that due to how Python's memory management works, releasing
- caches may not show you a lower Residual/Virtual memory footprint,
- the released memory will instead be re-used by the program.
-
- """
-
- key="@server"
- aliases=["@serverload"]
- switch_options=("mem","flushmem")
- locks="cmd:perm(list) or perm(Developer)"
- help_category="System"
-
-
[docs]deffunc(self):
- """Show list."""
-
- global_IDMAPPER
- ifnot_IDMAPPER:
- fromevennia.utils.idmapperimportmodelsas_IDMAPPER
-
- if"flushmem"inself.switches:
- # flush the cache
- prev,_=_IDMAPPER.cache_size()
- nflushed=_IDMAPPER.flush_cache()
- now,_=_IDMAPPER.cache_size()
- string=(
- "The Idmapper cache freed |w{idmapper}|n database objects.\n"
- "The Python garbage collector freed |w{gc}|n Python instances total."
- )
- self.caller.msg(string.format(idmapper=(prev-now),gc=nflushed))
- return
-
- # display active processes
-
- os_windows=os.name=="nt"
- pid=os.getpid()
-
- ifos_windows:
- # Windows requires the psutil module to even get paltry
- # statistics like this (it's pretty much worthless,
- # unfortunately, since it's not specific to the process) /rant
- try:
- importpsutil
-
- has_psutil=True
- exceptImportError:
- has_psutil=False
-
- ifhas_psutil:
- loadavg=psutil.cpu_percent()
- _mem=psutil.virtual_memory()
- rmem=_mem.used/(1000.0*1000)
- pmem=_mem.percent
-
- if"mem"inself.switches:
- string="Total computer memory usage: |w%g|n MB (%g%%)"
- self.caller.msg(string%(rmem,pmem))
- return
- # Display table
- loadtable=self.styled_table("property","statistic",align="l")
- loadtable.add_row("Total CPU load","%g%%"%loadavg)
- loadtable.add_row("Total computer memory usage","%g MB (%g%%)"%(rmem,pmem))
- loadtable.add_row("Process ID","%g"%pid),
- else:
- loadtable=(
- "Not available on Windows without 'psutil' library "
- "(install with |wpip install psutil|n)."
- )
-
- else:
- # Linux / BSD (OSX) - proper pid-based statistics
-
- global_RESOURCE
- ifnot_RESOURCE:
- importresourceas_RESOURCE
-
- loadavg=os.getloadavg()[0]
- rmem=(
- float(os.popen("ps -p %d -o %s | tail -1"%(pid,"rss")).read())/1000.0
- )# resident memory
- vmem=(
- float(os.popen("ps -p %d -o %s | tail -1"%(pid,"vsz")).read())/1000.0
- )# virtual memory
- pmem=float(
- os.popen("ps -p %d -o %s | tail -1"%(pid,"%mem")).read()
- )# % of resident memory to total
- rusage=_RESOURCE.getrusage(_RESOURCE.RUSAGE_SELF)
-
- if"mem"inself.switches:
- string="Memory usage: RMEM: |w%g|n MB (%g%%), VMEM (res+swap+cache): |w%g|n MB."
- self.caller.msg(string%(rmem,pmem,vmem))
- return
-
- loadtable=self.styled_table("property","statistic",align="l")
- loadtable.add_row("Server load (1 min)","%g"%loadavg)
- loadtable.add_row("Process ID","%g"%pid),
- loadtable.add_row("Memory usage","%g MB (%g%%)"%(rmem,pmem))
- loadtable.add_row("Virtual address space","")
- loadtable.add_row("|x(resident+swap+caching)|n","%g MB"%vmem)
- loadtable.add_row(
- "CPU time used (total)",
- "%s (%gs)"%(utils.time_format(rusage.ru_utime),rusage.ru_utime),
- )
- loadtable.add_row(
- "CPU time used (user)",
- "%s (%gs)"%(utils.time_format(rusage.ru_stime),rusage.ru_stime),
- )
- loadtable.add_row(
- "Page faults",
- "%g hard, %g soft, %g swapouts"
- %(rusage.ru_majflt,rusage.ru_minflt,rusage.ru_nswap),
- )
- loadtable.add_row(
- "Disk I/O","%g reads, %g writes"%(rusage.ru_inblock,rusage.ru_oublock)
- )
- loadtable.add_row("Network I/O","%g in, %g out"%(rusage.ru_msgrcv,rusage.ru_msgsnd))
- loadtable.add_row(
- "Context switching",
- "%g vol, %g forced, %g signals"
- %(rusage.ru_nvcsw,rusage.ru_nivcsw,rusage.ru_nsignals),
- )
-
- # os-generic
-
- string="|wServer CPU and Memory load:|n\n%s"%loadtable
-
- # object cache count (note that sys.getsiseof is not called so this works for pypy too.
- total_num,cachedict=_IDMAPPER.cache_size()
- sorted_cache=sorted(
- [(key,num)forkey,numincachedict.items()ifnum>0],
- key=lambdatup:tup[1],
- reverse=True,
- )
- memtable=self.styled_table("entity name","number","idmapper %",align="l")
- fortupinsorted_cache:
- memtable.add_row(tup[0],"%i"%tup[1],"%.2f"%(float(tup[1])/total_num*100))
-
- string+="\n|w Entity idmapper cache:|n %i items\n%s"%(total_num,memtable)
-
- # return to caller
- self.caller.msg(string)
-
-
-
[docs]classCmdTickers(COMMAND_DEFAULT_CLASS):
- """
- View running tickers
-
- Usage:
- tickers
-
- Note: Tickers are created, stopped and manipulated in Python code
- using the TickerHandler. This is merely a convenience function for
- inspecting the current status.
-
- """
-
- key="@tickers"
- help_category="System"
- locks="cmd:perm(tickers) or perm(Builder)"
-
-
[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)
-# -*- coding: utf-8 -*-
-"""
- ** OBS - this is not a normal command module! **
- ** You cannot import anything in this module as a command! **
-
-This is part of the Evennia unittest framework, for testing the
-stability and integrity of the codebase during updates. This module
-test the default command set. It is instantiated by the
-evennia/objects/tests.py module, which in turn is run by as part of the
-main test suite started with
- > python game/manage.py test.
-
-"""
-importdatetime
-fromanythingimportAnything
-
-fromparameterizedimportparameterized
-fromdjango.confimportsettings
-fromtwisted.internetimporttask
-fromunittest.mockimportpatch,Mock,MagicMock
-
-fromevenniaimportDefaultRoom,DefaultExit,ObjectDB
-fromevennia.commands.default.cmdset_characterimportCharacterCmdSet
-fromevennia.utils.test_resourcesimportBaseEvenniaTest,BaseEvenniaCommandTest,EvenniaCommandTest# noqa
-fromevennia.commands.defaultimport(
- helpashelp_module,
- general,
- system,
- admin,
- account,
- building,
- batchprocess,
- comms,
- unloggedin,
- syscommands,
-)
-fromevennia.commands.default.muxcommandimportMuxCommand
-fromevennia.commands.commandimportCommand,InterruptCommand
-fromevennia.commandsimportcmdparser
-fromevennia.commands.cmdsetimportCmdSet
-fromevennia.utilsimportutils,gametime,create
-fromevennia.server.sessionhandlerimportSESSIONS
-fromevenniaimportsearch_object
-fromevenniaimportDefaultObject,DefaultCharacter
-fromevennia.prototypesimportprototypesasprotlib
-
-# ------------------------------------------------------------
-# Command testing
-# ------------------------------------------------------------
-
-
-
[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_module.CmdSetHelp(),
- "testhelp, General = This is a test",
- "Topic 'testhelp' was successfully created.",
- 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_py(self):
- # we are not testing CmdReload, CmdReset and CmdShutdown, CmdService or CmdTime
- # since the server is not running during these tests.
- self.call(system.CmdPy(),"1+2",">>> 1+2|3")
- self.call(system.CmdPy(),"/clientraw 1+2",">>> 1+2|3")
[docs]deftest_call(self):
- args=f'/call {self.task.get_id()}'
- wanted_msg='Call task 1 with completion date'
- cmd_result=self.call(system.CmdTasks(),args,wanted_msg)
- self.assertRegex(cmd_result,' \(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_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_char_create(self):
- self.call(
- account.CmdCharCreate(),
- "Test1=Test char",
- "Created new character Test1. Use ic Test1 to enter the game",
- caller=self.account,
- )
-
-
[docs]deftest_char_delete(self):
- # Chardelete requires user input; this test is mainly to confirm
- # whether permissions are being checked
-
- # Add char to account playable characters
- self.account.db._playable_characters.append(self.char1)
-
- # Try deleting as Developer
- self.call(
- account.CmdCharDelete(),
- "Char",
- "This will permanently destroy 'Char'. This cannot be undone. Continue yes/[no]?",
- caller=self.account,
- )
-
- # Downgrade permissions on account
- self.account.permissions.add("Player")
- self.account.permissions.remove("Developer")
-
- # Set lock on character object to prevent deletion
- self.char1.locks.add("delete:none()")
-
- # Try deleting as Player
- self.call(
- account.CmdCharDelete(),
- "Char",
- "You do not have permission to delete this character.",
- caller=self.account,
- )
-
- # Set lock on character object to allow self-delete
- self.char1.locks.add("delete:pid(%i)"%self.account.id)
-
- # Try deleting as Player again
- self.call(
- account.CmdCharDelete(),
- "Char",
- "This will permanently destroy 'Char'. This cannot be undone. Continue yes/[no]?",
- caller=self.account,
- )
-
-
[docs]deftest_quell(self):
- self.call(
- account.CmdQuell(),
- "",
- "Quelling to current puppet's permissions (developer).",
- caller=self.account,
- )
[docs]deftest_exit_commands(self):
- self.call(
- building.CmdOpen(),"TestExit1=Room2","Created new Exit 'TestExit1' from Room to Room2"
- )
- self.call(building.CmdLink(),"TestExit1=Room","Link created TestExit1 -> Room (one way).")
- self.call(building.CmdUnLink(),"","Usage: ")
- self.call(building.CmdLink(),"NotFound","Could not find 'NotFound'.")
- self.call(building.CmdLink(),"TestExit","TestExit1 is an exit to Room.")
- self.call(building.CmdLink(),"Obj","Obj is not an exit. Its home location is Room.")
- self.call(
- building.CmdUnLink(),"TestExit1","Former exit TestExit1 no longer links anywhere."
- )
-
- self.char1.location=self.room2
- self.call(
- building.CmdOpen(),"TestExit2=Room","Created new Exit 'TestExit2' from Room2 to Room."
- )
- self.call(
- building.CmdOpen(),
- "TestExit2=Room",
- "Exit TestExit2 already exists. It already points to the correct place.",
- )
-
- # ensure it matches locally first
- self.call(
- building.CmdLink(),"TestExit=Room2","Link created TestExit2 -> Room2 (one way)."
- )
- self.call(
- building.CmdLink(),
- "/twoway TestExit={}".format(self.exit.dbref),
- "Link created TestExit2 (in Room2) <-> out (in Room) (two-way).",
- )
- self.call(
- building.CmdLink(),
- "/twoway TestExit={}".format(self.room1.dbref),
- "To create a two-way link, TestExit2 and Room must both have a location ",
- )
- self.call(
- building.CmdLink(),
- "/twoway {}={}".format(self.exit.dbref,self.exit.dbref),
- "Cannot link an object to itself.",
- )
- self.call(building.CmdLink(),"","Usage: ")
- # ensure can still match globally when not a local name
- self.call(building.CmdLink(),"TestExit1=Room2","Note: TestExit1")
- self.call(
- building.CmdLink(),"TestExit1=","Former exit TestExit1 no longer links anywhere."
- )
-
-
[docs]deftest_set_home(self):
- self.call(
- building.CmdSetHome(),"Obj = Room2","Home location of Obj was changed from Room"
- )
- self.call(building.CmdSetHome(),"","Usage: ")
- self.call(building.CmdSetHome(),"self","Char's current home is Room")
- self.call(building.CmdSetHome(),"Obj","Obj's current home is Room2")
- self.obj1.home=None
- self.call(building.CmdSetHome(),"Obj = Room2","Home location of Obj was set to Room")
[docs]@patch("evennia.commands.default.comms.CHANNEL_DEFAULT_TYPECLASS",DefaultChannel)
-classTestCommsChannel(BaseEvenniaCommandTest):
- """
- Test the central `channel` command.
-
- """
-
[docs]deftest_page(self):
- self.call(
- comms.CmdPage(),
- "TestAccount2 = Test",
- "TestAccount2 is offline. They will see your message if they list their pages later."
- "|You paged TestAccount2 with: 'Test'.",
- receiver=self.account,
- )
-
-
-
[docs]classTestBatchProcess(BaseEvenniaCommandTest):
- """
- Test the batch processor.
-
- """
- # there is some sort of issue with the mock; it needs to loaded once to work
- fromevennia.contrib.tutorials.red_buttonimportred_button# noqa
-
-
[docs]@patch("evennia.contrib.tutorials.red_button.red_button.repeat")
- @patch("evennia.contrib.tutorials.red_button.red_button.delay")
- deftest_batch_commands(self,mock_tutorials,mock_repeat):
- # cannot test batchcode here, it must run inside the server process
- self.call(
- batchprocess.CmdBatchCommands(),
- "batchprocessor.example_batch_cmds",
- "Running Batch-command processor - Automatic mode for batchprocessor.example_batch_cmds",
- )
- # we make sure to delete the button again here to stop the running reactor
- confirm=building.CmdDestroy.confirm
- building.CmdDestroy.confirm=False
- self.call(building.CmdDestroy(),"button","button was destroyed.")
- building.CmdDestroy.confirm=confirm
- mock_repeat.assert_called()
[docs]deftest_info_command(self):
- # instead of using SERVER_START_TIME (0), we use 86400 because Windows won't let us use anything lower
- gametime.SERVER_START_TIME=86400
- expected=(
- "## BEGIN INFO 1.1\nName: %s\nUptime: %s\nConnected: %d\nVersion: Evennia %s\n## END INFO"
- %(
- settings.SERVERNAME,
- datetime.datetime.fromtimestamp(gametime.SERVER_START_TIME).ctime(),
- SESSIONS.account_count(),
- utils.get_evennia_version(),
- )
- )
- self.call(unloggedin.CmdUnconnectedInfo(),"",expected)
- delgametime.SERVER_START_TIME
Source code for evennia.commands.default.unloggedin
-"""
-Commands that are available from the connect screen.
-"""
-importre
-importdatetime
-fromcodecsimportlookupascodecs_lookup
-fromdjango.confimportsettings
-fromevennia.comms.modelsimportChannelDB
-fromevennia.server.sessionhandlerimportSESSIONS
-
-fromevennia.utilsimportclass_from_module,create,logger,utils,gametime
-fromevennia.commands.cmdhandlerimportCMD_LOGINSTART
-
-COMMAND_DEFAULT_CLASS=utils.class_from_module(settings.COMMAND_DEFAULT_CLASS)
-
-# limit symbol import for API
-__all__=(
- "CmdUnconnectedConnect",
- "CmdUnconnectedCreate",
- "CmdUnconnectedQuit",
- "CmdUnconnectedLook",
- "CmdUnconnectedHelp",
- "CmdUnconnectedEncoding",
- "CmdUnconnectedInfo",
- "CmdUnconnectedScreenreader",
-)
-
-MULTISESSION_MODE=settings.MULTISESSION_MODE
-CONNECTION_SCREEN_MODULE=settings.CONNECTION_SCREEN_MODULE
-
-
-defcreate_guest_account(session):
- """
- Creates a guest account/character for this session, if one is available.
-
- Args:
- session (Session): the session which will use the guest account/character.
-
- Returns:
- GUEST_ENABLED (boolean), account (Account):
- the boolean is whether guest accounts are enabled at all.
- the Account which was created from an available guest name.
- """
- enabled=settings.GUEST_ENABLED
- address=session.address
-
- # Get account class
- Guest=class_from_module(settings.BASE_GUEST_TYPECLASS)
-
- # Get an available guest account
- # authenticate() handles its own throttling
- account,errors=Guest.authenticate(ip=address)
- ifaccount:
- returnenabled,account
- else:
- session.msg("|R%s|n"%"\n".join(errors))
- returnenabled,None
-
-
-defcreate_normal_account(session,name,password):
- """
- Creates an account with the given name and password.
-
- Args:
- session (Session): the session which is requesting to create an account.
- name (str): the name that the account wants to use for login.
- password (str): the password desired by this account, for login.
-
- Returns:
- account (Account): the account which was created from the name and password.
- """
- # Get account class
- Account=class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
-
- address=session.address
-
- # Match account name and check password
- # authenticate() handles all its own throttling
- account,errors=Account.authenticate(
- username=name,password=password,ip=address,session=session
- )
- ifnotaccount:
- # No accountname or password match
- session.msg("|R%s|n"%"\n".join(errors))
- returnNone
-
- returnaccount
-
-
-
[docs]classCmdUnconnectedConnect(COMMAND_DEFAULT_CLASS):
- """
- connect to the game
-
- Usage (at login screen):
- connect accountname password
- connect "account name" "pass word"
-
- Use the create command to first create an account before logging in.
-
- If you have spaces in your name, enclose it in double quotes.
- """
-
- key="connect"
- aliases=["conn","con","co"]
- locks="cmd:all()"# not really needed
- arg_regex=r"\s.*?|$"
-
-
[docs]deffunc(self):
- """
- Uses the Django admin api. Note that unlogged-in commands
- have a unique position in that their func() receives
- a session object instead of a source_object like all
- other types of logged-in commands (this is because
- there is no object yet before the account has logged in)
- """
- session=self.caller
- address=session.address
-
- args=self.args
- # extract double quote parts
- parts=[part.strip()forpartinre.split(r"\"",args)ifpart.strip()]
- iflen(parts)==1:
- # this was (hopefully) due to no double quotes being found, or a guest login
- parts=parts[0].split(None,1)
-
- # Guest login
- iflen(parts)==1andparts[0].lower()=="guest":
- # Get Guest typeclass
- Guest=class_from_module(settings.BASE_GUEST_TYPECLASS)
-
- account,errors=Guest.authenticate(ip=address)
- ifaccount:
- session.sessionhandler.login(session,account)
- return
- else:
- session.msg("|R%s|n"%"\n".join(errors))
- return
-
- iflen(parts)!=2:
- session.msg("\n\r Usage (without <>): connect <name> <password>")
- return
-
- # Get account class
- Account=class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
-
- name,password=parts
- account,errors=Account.authenticate(
- username=name,password=password,ip=address,session=session
- )
- ifaccount:
- session.sessionhandler.login(session,account)
- else:
- session.msg("|R%s|n"%"\n".join(errors))
-
-
-
[docs]classCmdUnconnectedCreate(COMMAND_DEFAULT_CLASS):
- """
- create a new account account
-
- Usage (at login screen):
- create <accountname> <password>
- create "account name" "pass word"
-
- This creates a new account account.
-
- If you have spaces in your name, enclose it in double quotes.
- """
-
- key="create"
- aliases=["cre","cr"]
- locks="cmd:all()"
- arg_regex=r"\s.*?|$"
-
-
[docs]deffunc(self):
- """Do checks and create account"""
-
- session=self.caller
- args=self.args.strip()
-
- address=session.address
-
- # Get account class
- Account=class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
-
- # extract double quoted parts
- parts=[part.strip()forpartinre.split(r"\"",args)ifpart.strip()]
- iflen(parts)==1:
- # this was (hopefully) due to no quotes being found
- parts=parts[0].split(None,1)
- iflen(parts)!=2:
- string=(
- "\n Usage (without <>): create <name> <password>"
- "\nIf <name> or <password> contains spaces, enclose it in double quotes."
- )
- session.msg(string)
- return
-
- username,password=parts
-
- # pre-normalize username so the user know what they get
- non_normalized_username=username
- username=Account.normalize_username(username)
- ifnon_normalized_username!=username:
- session.msg("Note: your username was normalized to strip spaces and remove characters "
- "that could be visually confusing.")
-
- # have the user verify their new account was what they intended
- answer=yield(f"You want to create an account '{username}' with password '{password}'."
- "\nIs this what you intended? [Y]/N?")
- ifanswer.lower()in('n','no'):
- session.msg("Aborted. If your user name contains spaces, surround it by quotes.")
- return
-
- # everything's ok. Create the new account account.
- account,errors=Account.create(
- username=username,password=password,ip=address,session=session
- )
- ifaccount:
- # tell the caller everything went well.
- string="A new account '%s' was created. Welcome!"
- if" "inusername:
- string+=(
- "\n\nYou can now log in with the command 'connect \"%s\" <your password>'."
- )
- else:
- string+="\n\nYou can now log with the command 'connect %s <your password>'."
- session.msg(string%(username,username))
- else:
- session.msg("|R%s|n"%"\n".join(errors))
-
-
-
[docs]classCmdUnconnectedQuit(COMMAND_DEFAULT_CLASS):
- """
- quit when in unlogged-in state
-
- Usage:
- quit
-
- We maintain a different version of the quit command
- here for unconnected accounts for the sake of simplicity. The logged in
- version is a bit more complicated.
- """
-
- key="quit"
- aliases=["q","qu"]
- locks="cmd:all()"
-
-
[docs]deffunc(self):
- """Simply close the connection."""
- session=self.caller
- session.sessionhandler.disconnect(session,"Good bye! Disconnecting.")
-
-
-
[docs]classCmdUnconnectedLook(COMMAND_DEFAULT_CLASS):
- """
- look when in unlogged-in state
-
- Usage:
- look
-
- This is an unconnected version of the look command for simplicity.
-
- This is called by the server and kicks everything in gear.
- All it does is display the connect screen.
- """
-
- key=CMD_LOGINSTART
- aliases=["look","l"]
- locks="cmd:all()"
-
-
[docs]classCmdUnconnectedHelp(COMMAND_DEFAULT_CLASS):
- """
- get help when in unconnected-in state
-
- Usage:
- help
-
- This is an unconnected version of the help command,
- for simplicity. It shows a pane of info.
- """
-
- key="help"
- aliases=["h","?"]
- locks="cmd:all()"
-
-
[docs]deffunc(self):
- """Shows help"""
-
- string="""
-You are not yet logged into the game. Commands available at this point:
-
- |wcreate|n - create a new account
- |wconnect|n - connect with an existing account
- |wlook|n - re-show the connection screen
- |whelp|n - show this help
- |wencoding|n - change the text encoding to match your client
- |wscreenreader|n - make the server more suitable for use with screen readers
- |wquit|n - abort the connection
-
-First 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|n
-Next you can connect to the game: |wconnect Anna c67jHL8p|n
-
-You can use the |wlook|n command if you want to see the connect screen again.
-
-"""
-
- ifsettings.STAFF_CONTACT_EMAIL:
- string+="For support, please contact: %s"%settings.STAFF_CONTACT_EMAIL
- self.caller.msg(string)
-
-
-
[docs]classCmdUnconnectedEncoding(COMMAND_DEFAULT_CLASS):
- """
- set which text encoding to use in unconnected-in state
-
- Usage:
- encoding/switches [<encoding>]
-
- Switches:
- clear - clear your custom encoding
-
-
- This sets the text encoding for communicating with Evennia. This is mostly
- an issue only if you want to use non-ASCII characters (i.e. letters/symbols
- not found in English). If you see that your characters look strange (or you
- get encoding errors), you should use this command to set the server
- encoding to be the same used in your client program.
-
- Common encodings are utf-8 (default), latin-1, ISO-8859-1 etc.
-
- If you don't submit an encoding, the current encoding will be displayed
- instead.
- """
-
- key="encoding"
- aliases="encode"
- locks="cmd:all()"
-
-
[docs]deffunc(self):
- """
- Sets the encoding.
- """
-
- ifself.sessionisNone:
- return
-
- sync=False
- if"clear"inself.switches:
- # remove customization
- old_encoding=self.session.protocol_flags.get("ENCODING",None)
- ifold_encoding:
- string="Your custom text encoding ('%s') was cleared."%old_encoding
- else:
- string="No custom encoding was set."
- self.session.protocol_flags["ENCODING"]="utf-8"
- sync=True
- elifnotself.args:
- # just list the encodings supported
- pencoding=self.session.protocol_flags.get("ENCODING",None)
- string=""
- ifpencoding:
- string+=(
- "Default encoding: |g%s|n (change with |wencoding <encoding>|n)"%pencoding
- )
- encodings=settings.ENCODINGS
- ifencodings:
- string+=(
- "\nServer's alternative encodings (tested in this order):\n |g%s|n"
- %", ".join(encodings)
- )
- ifnotstring:
- string="No encodings found."
- else:
- # change encoding
- old_encoding=self.session.protocol_flags.get("ENCODING",None)
- encoding=self.args
- try:
- codecs_lookup(encoding)
- exceptLookupError:
- string=(
- "|rThe encoding '|w%s|r' is invalid. Keeping the previous encoding '|w%s|r'.|n"
- %(encoding,old_encoding)
- )
- else:
- self.session.protocol_flags["ENCODING"]=encoding
- string="Your custom text encoding was changed from '|w%s|n' to '|w%s|n'."%(
- old_encoding,
- encoding,
- )
- sync=True
- ifsync:
- self.session.sessionhandler.session_portal_sync(self.session)
- self.caller.msg(string.strip())
-
-
-
[docs]classCmdUnconnectedScreenreader(COMMAND_DEFAULT_CLASS):
- """
- Activate screenreader mode.
-
- Usage:
- screenreader
-
- Used to flip screenreader mode on and off before logging in (when
- logged in, use option screenreader on).
- """
-
- key="screenreader"
-
-
[docs]classCmdUnconnectedInfo(COMMAND_DEFAULT_CLASS):
- """
- Provides MUDINFO output, so that Evennia games can be added to Mudconnector
- and Mudstats. Sadly, the MUDINFO specification seems to have dropped off the
- face of the net, but it is still used by some crawlers. This implementation
- was created by looking at the MUDINFO implementation in MUX2, TinyMUSH, Rhost,
- and PennMUSH.
- """
-
- key="info"
- locks="cmd:all()"
-
-
[docs]deffunc(self):
- self.caller.msg(
- "## BEGIN INFO 1.1\nName: %s\nUptime: %s\nConnected: %d\nVersion: Evennia %s\n## END INFO"
- %(
- settings.SERVERNAME,
- datetime.datetime.fromtimestamp(gametime.SERVER_START_TIME).ctime(),
- SESSIONS.account_count(),
- utils.get_evennia_version(),
- )
- )
-
-
-def_create_account(session,accountname,password,permissions,typeclass=None,email=None):
- """
- Helper function, creates an account of the specified typeclass.
- """
- try:
- new_account=create.create_account(
- accountname,email,password,permissions=permissions,typeclass=typeclass
- )
-
- exceptExceptionase:
- session.msg(
- "There was an error creating the Account:\n%s\n If this problem persists, contact an admin."
- %e
- )
- logger.log_trace()
- returnFalse
-
- # This needs to be set so the engine knows this account is
- # logging in for the first time. (so it knows to call the right
- # hooks during login later)
- new_account.db.FIRST_LOGIN=True
-
- # join the new account to the public channel
- pchannel=ChannelDB.objects.get_channel(settings.DEFAULT_CHANNELS[0]["key"])
- ifnotpchannelornotpchannel.connect(new_account):
- string="New account '%s' could not connect to public channel!"%new_account.key
- logger.log_err(string)
- returnnew_account
-
-
-def_create_character(session,new_account,typeclass,home,permissions):
- """
- Helper function, creates a character based on an account's name.
- This is meant for Guest and MULTISESSION_MODE < 2 situations.
- """
- try:
- new_character=create.create_object(
- typeclass,key=new_account.key,home=home,permissions=permissions
- )
- # set playable character list
- new_account.db._playable_characters.append(new_character)
-
- # allow only the character itself and the account to puppet this character (and Developers).
- new_character.locks.add(
- "puppet:id(%i) or pid(%i) or perm(Developer) or pperm(Developer)"
- %(new_character.id,new_account.id)
- )
-
- # If no description is set, set a default description
- ifnotnew_character.db.desc:
- new_character.db.desc="This is a character."
- # We need to set this to have ic auto-connect to this character
- new_account.db._last_puppet=new_character
- exceptExceptionase:
- session.msg(
- "There was an error creating the Character:\n%s\n If this problem persists, contact an admin."
- %e
- )
- logger.log_trace()
-
[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
- is saved to the database. Generally, don't overload this but
- the hooks called by this method.
-
- """
- self.basetype_setup()
- self.at_channel_creation()
- ifhasattr(self,"_createdict"):
- # this is only set if the channel was created
- # with the utils.create.create_channel function.
- cdict=self._createdict
- ifnotcdict.get("key"):
- ifnotself.db_key:
- self.db_key="#i"%self.dbid
- elifcdict["key"]andself.key!=cdict["key"]:
- self.key=cdict["key"]
- ifcdict.get("aliases"):
- self.aliases.add(cdict["aliases"])
- ifcdict.get("locks"):
- self.locks.add(cdict["locks"])
- ifcdict.get("keep_log"):
- self.attributes.add("keep_log",cdict["keep_log"])
- ifcdict.get("desc"):
- self.attributes.add("desc",cdict["desc"])
- ifcdict.get("tags"):
- self.tags.batch_add(*cdict["tags"])
-
-
[docs]defbasetype_setup(self):
- 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]defat_channel_creation(self):
- """
- Called once, when the channel is first created.
-
- """
- pass
[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
- to this channel.
-
- Args:
- subscriber (Account or Object): Entity to check.
-
- Returns:
- has_sub (bool): Whether the subscriber is subscribing to
- this channel or not.
-
- Notes:
- This will first try Account subscribers and only try Object
- if the Account fails.
-
- """
- has_sub=self.subscriptions.has(subscriber)
- ifnothas_subandhasattr(subscriber,"account"):
- # it's common to send an Object when we
- # by default only allow Accounts to subscribe.
- has_sub=self.subscriptions.has(subscriber.account)
- returnhas_sub
[docs]defmute(self,subscriber,**kwargs):
- """
- Adds an entity to the list of muted subscribers.
- A muted subscriber will no longer see channel messages,
- but may use channel commands.
-
- Args:
- subscriber (Object or Account): Subscriber to mute.
- **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.mutelist
- ifsubscribernotinmutelist:
- mutelist.append(subscriber)
- self.db.mute_list=mutelist
- returnTrue
- returnFalse
-
-
[docs]defunmute(self,subscriber,**kwargs):
- """
- 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.mutelist
- ifsubscriberinmutelist:
- mutelist.remove(subscriber)
- 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=banlist
- returnTrue
- returnFalse
-
-
[docs]defconnect(self,subscriber,**kwargs):
- """
- Connect the user to this channel. This checks access.
-
- Args:
- subscriber (Account or Object): the entity to subscribe
- to this channel.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- Returns:
- success (bool): Whether or not the addition was
- successful.
-
- """
- # check access
- ifsubscriberinself.banlistornotself.access(subscriber,"listen"):
- returnFalse
- # pre-join hook
- connect=self.pre_join_channel(subscriber)
- ifnotconnect:
- returnFalse
- # subscribe
- self.subscriptions.add(subscriber)
- # unmute
- self.unmute(subscriber)
- # post-join hook
- self.post_join_channel(subscriber)
- returnTrue
-
-
[docs]defdisconnect(self,subscriber,**kwargs):
- """
- Disconnect entity from this channel.
-
- Args:
- subscriber (Account of Object): the
- entity to disconnect.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- Returns:
- success (bool): Whether or not the removal was
- successful.
-
- """
- # pre-disconnect hook
- disconnect=self.pre_leave_channel(subscriber)
- ifnotdisconnect:
- returnFalse
- # disconnect
- self.subscriptions.remove(subscriber)
- # unmute
- self.unmute(subscriber)
- # post-disconnect hook
- self.post_leave_channel(subscriber)
- returnTrue
-
-
[docs]defaccess(
- self,
- accessing_obj,
- access_type="listen",
- default=False,
- no_superuser_bypass=False,
- **kwargs,
- ):
- """
- Determines if another object has permission to access.
-
- Args:
- accessing_obj (Object): Object trying to access this one.
- access_type (str, optional): Type of access sought.
- default (bool, optional): What to return if no lock of access_type was found
- no_superuser_bypass (bool, optional): Turns off superuser
- lock bypass. Be careful with this one.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- Returns:
- return (bool): Result of lock check.
-
- """
- returnself.locks.check(
- accessing_obj,
- access_type=access_type,
- default=default,
- no_superuser_bypass=no_superuser_bypass,
- )
-
-
[docs]@classmethod
- defcreate(cls,key,creator=None,*args,**kwargs):
- """
- Creates a basic Channel with default parameters, unless otherwise
- specified or extended.
-
- Provides a friendlier interface to the utils.create_channel() function.
-
- Args:
- key (str): This must be unique.
- 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.
- description (str): A description of the channel, for use in listings.
- locks (str): Lockstring.
- keep_log (bool): Log channel throughput.
- typeclass (str or class): The typeclass of the Channel (not
- often used).
- ip (str): IP address of creator (for object auditing).
-
- Returns:
- channel (Channel): A newly created Channel.
- errors (list): A list of errors in string form, if any.
-
- """
- errors=[]
- obj=None
- ip=kwargs.pop("ip","")
-
- try:
- kwargs["desc"]=kwargs.pop("description","")
- kwargs["typeclass"]=kwargs.get("typeclass",cls)
- obj=create.create_channel(key,*args,**kwargs)
-
- # Record creator id and creation IP
- ifip:
- obj.db.creator_ip=ip
- ifcreator:
- obj.db.creator_id=creator.id
-
- exceptExceptionasexc:
- errors.append("An error occurred while creating this '%s' object."%key)
- logger.log_err(exc)
-
- returnobj,errors
-
-
[docs]defdelete(self):
- """
- Deletes channel.
-
- Returns:
- bool: If deletion was successful. Only time it can fail would be
- if channel was already deleted. Even if it were to fail, all subscribers
- will be disconnected.
-
- """
- self.attributes.clear()
- self.aliases.clear()
- forsubscriberinself.subscriptions.all():
- self.disconnect(subscriber)
- ifnotself.pk:
- returnFalse
- super().delete()
- returnTrue
-
-
[docs]defchannel_prefix(self):
- """
- Hook method. How the channel should prefix itself for users.
-
- Returns:
- str: The channel prefix.
-
- """
- returnself.channel_prefix_string.format(channelname=self.key)
-
-
[docs]defadd_user_channel_alias(self,user,alias,**kwargs):
- """
- Add a personal user-alias for this channel to a given subscriber.
-
- Args:
- user (Object or Account): The one to alias this channel.
- alias (str): The desired alias.
-
- 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:
- 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).
-
- """
- user.nicks.remove(alias,category="channel",**kwargs)
- msg_nick_pattern=cls.channel_msg_nick_pattern.format(alias=alias)
- user.nicks.remove(msg_nick_pattern,category="inputline",**kwargs)
-
-
[docs]defat_pre_msg(self,message,**kwargs):
- """
- 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:
- message (str): The message to send.
- **kwargs (any): Keywords passed on from `.msg`. This includes
- `senders`.
-
- Returns:
- str, False or None: Any custom changes made to the message. If
- falsy, no message will be sent.
-
- """
- 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:
- receivers=self.subscriptions.all()
- ifnotbypass_mute:
- receivers=[receiverforreceiverinreceiversifreceivernotinself.mutelist]
-
- send_kwargs={'senders':senders,'bypass_mute':bypass_mute,**kwargs}
-
- # pre-send hook
- message=self.at_pre_msg(message,**send_kwargs)
- ifmessagein(None,False):
- return
-
- forreceiverinreceivers:
- # send to each individual subscriber
-
- try:
- recv_message=receiver.at_pre_channel_msg(message,self,**send_kwargs)
- ifrecv_messagein(None,False):
- return
-
- receiver.channel_msg(recv_message,self,**send_kwargs)
-
- receiver.at_post_channel_msg(recv_message,self,**send_kwargs)
-
- exceptException:
- logger.log_trace(f"Error sending channel message to {receiver}.")
-
- # post-send hook
- self.at_post_msg(message,**send_kwargs)
-
-
[docs]defat_post_msg(self,message,**kwargs):
- """
- This is called after sending to *all* valid recipients. It is normally
- used for logging/channel history.
-
- Args:
- message (str): The message sent.
- **kwargs (any): Keywords passed on from `msg`, including `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):
- """
- Hook method. Runs right before a channel is joined. If this
- returns a false value, channel joining is aborted.
-
- Args:
- joiner (object): The joining object.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- Returns:
- should_join (bool): If `False`, channel joining is aborted.
-
- """
- returnTrue
-
-
[docs]defpost_join_channel(self,joiner,**kwargs):
- """
- Hook method. Runs right after an object or account joins a channel.
-
- Args:
- joiner (object): The joining object.
- **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.
-
- """
- key_and_aliases=[self.key.lower()]+[alias.lower()foraliasinself.aliases.all()]
- forkey_or_aliasinkey_and_aliases:
- self.add_user_channel_alias(joiner,key_or_alias,**kwargs)
-
-
[docs]defpre_leave_channel(self,leaver,**kwargs):
- """
- Hook method. Runs right before a user leaves a channel. If this returns a false
- value, leaving the channel will be aborted.
-
- Args:
- leaver (object): The leaving object.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- Returns:
- should_leave (bool): If `False`, channel parting is aborted.
-
- """
- returnTrue
-
-
[docs]defpost_leave_channel(self,leaver,**kwargs):
- """
- Hook method. Runs right after an object or account leaves a channel.
-
- Args:
- leaver (object): The leaving object.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- """
- chan_key=self.key.lower()
- key_or_aliases=[self.key.lower()]+[alias.lower()foraliasinself.aliases.all()]
- nicktuples=leaver.nicks.get(category="channel",return_tuple=True,return_list=True)
- key_or_aliases+=[tup[2]fortupinnicktuplesiftup[3].lower()==chan_key]
- forkey_or_aliasinkey_or_aliases:
- self.remove_user_channel_alias(leaver,key_or_alias,**kwargs)
-
-
[docs]defat_init(self):
- """
- Hook method. This is always called whenever this channel is
- initiated -- that is, whenever it its typeclass is cached from
- memory. This happens on-demand first time the channel is used
- or activated in some way after being created but also after
- each server restart or reload.
-
- """
- pass
-
- #
- # Web/Django methods
- #
-
-
[docs]defweb_get_admin_url(self):
- """
- Returns the URI path for the Django Admin page for this object.
-
- ex. Account#1 = '/admin/accounts/accountdb/1/change/'
-
- Returns:
- path (str): URI path to Django Admin page for object.
-
- """
- content_type=ContentType.objects.get_for_model(self.__class__)
- returnreverse(
- "admin:%s_%s_change"%(content_type.app_label,content_type.model),args=(self.id,)
- )
-
-
[docs]@classmethod
- defweb_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 'channel-create' would be referenced by this method.
-
- ex.
- url(r'channels/create/', ChannelCreateView.as_view(), name='channel-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.
-
- """
- try:
- returnreverse("%s-create"%slugify(cls._meta.verbose_name))
- exceptException:
- return"#"
-
-
[docs]defweb_get_detail_url(self):
- """
- Returns the URI path for a View that allows users to view details for
- this object.
-
- ex. 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 'channel-detail' would be referenced by this method.
-
- ex.
- ::
-
- 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.
-
- 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:
- path (str): URI path to object detail page, if defined.
-
- """
- try:
- returnreverse(
- "%s-detail"%slugify(self._meta.verbose_name),
- kwargs={"slug":slugify(self.db_key)},
- )
- exceptException:
- return"#"
-
-
[docs]defweb_get_update_url(self):
- """
- Returns the URI path for a View that allows users to update this
- object.
-
- ex. 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 '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')
-
- 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:
- path (str): URI path to object update page, if defined.
-
- """
- try:
- returnreverse(
- "%s-update"%slugify(self._meta.verbose_name),
- kwargs={"slug":slugify(self.db_key)},
- )
- exceptException:
- return"#"
-
-
[docs]defweb_get_delete_url(self):
- """
- Returns the URI path for a View that allows users to delete this object.
-
- ex. 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 'channel-delete' would be referenced by this method.
-
- ex.
- url(r'channels/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/delete/$',
- ChannelDeleteView.as_view(), name='channel-delete')
-
- 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:
- path (str): URI path to object deletion page, if defined.
-
- """
- try:
- returnreverse(
- "%s-delete"%slugify(self._meta.verbose_name),
- kwargs={"slug":slugify(self.db_key)},
- )
- exceptException:
- return"#"
-
- # Used by Django Sites/Admin
- 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.")
-"""
-These managers define helper methods for accessing the database from
-Comm system components.
-
-"""
-
-
-fromdjango.confimportsettings
-fromdjango.db.modelsimportQ
-fromevennia.typeclasses.managersimportTypedObjectManager,TypeclassManager
-fromevennia.serverimportsignals
-fromevennia.utilsimportlogger
-fromevennia.utils.utilsimportdbref,make_iter,class_from_module
-
-_GA=object.__getattribute__
-_AccountDB=None
-_ObjectDB=None
-_ChannelDB=None
-_ScriptDB=None
-_SESSIONS=None
-
-# error class
-
-
-
[docs]classCommError(Exception):
- """
- Raised by comm system, to allow feedback to player when caught.
- """
-
- pass
-
-
-#
-# helper functions
-#
-
-
-
[docs]defidentify_object(inp):
- """
- Helper function. Identifies if an object is an account or an object;
- return its database model
-
- Args:
- inp (any): Entity to be idtified.
-
- Returns:
- identified (tuple): This is a tuple with (`inp`, identifier)
- where `identifier` is one of "account", "object", "channel",
- "string", "dbref" or None.
-
- """
- ifhasattr(inp,"__dbclass__"):
- clsname=inp.__dbclass__.__name__
- ifclsname=="AccountDB":
- returninp,"account"
- elifclsname=="ObjectDB":
- returninp,"object"
- elifclsname=="ChannelDB":
- returninp,"channel"
- elifclsname=="ScriptDB":
- returninp,"script"
- ifisinstance(inp,str):
- returninp,"string"
- elifdbref(inp):
- returndbref(inp),"dbref"
- else:
- returninp,None
-
-
-
[docs]defto_object(inp,objtype="account"):
- """
- Locates the object related to the given accountname or channel key.
- If input was already the correct object, return it.
-
- Args:
- inp (any): The input object/string
- objtype (str): Either 'account' or 'channel'.
-
- Returns:
- obj (object): The correct object related to `inp`.
-
- """
- obj,typ=identify_object(inp)
- iftyp==objtype:
- returnobj
- ifobjtype=="account":
- iftyp=="object":
- returnobj.account
- iftyp=="string":
- return_AccountDB.objects.get(user_username__iexact=obj)
- iftyp=="dbref":
- return_AccountDB.objects.get(id=obj)
- logger.log_err("%s%s%s%s%s"%(objtype,inp,obj,typ,type(inp)))
- raiseCommError()
- elifobjtype=="object":
- iftyp=="account":
- returnobj.obj
- iftyp=="string":
- return_ObjectDB.objects.get(db_key__iexact=obj)
- iftyp=="dbref":
- return_ObjectDB.objects.get(id=obj)
- logger.log_err("%s%s%s%s%s"%(objtype,inp,obj,typ,type(inp)))
- raiseCommError()
- elifobjtype=="channel":
- iftyp=="string":
- return_ChannelDB.objects.get(db_key__iexact=obj)
- iftyp=="dbref":
- return_ChannelDB.objects.get(id=obj)
- logger.log_err("%s%s%s%s%s"%(objtype,inp,obj,typ,type(inp)))
- raiseCommError()
- elifobjtype=="script":
- iftyp=="string":
- return_ScriptDB.objects.get(db_key__iexact=obj)
- iftyp=="dbref":
- return_ScriptDB.objects.get(id=obj)
- logger.log_err("%s%s%s%s%s"%(objtype,inp,obj,typ,type(inp)))
- raiseCommError()
-
- # an unknown
- returnNone
-
-
-#
-# Msg manager
-#
-
-
-
[docs]classMsgManager(TypedObjectManager):
- """
- This MsgManager implements methods for searching and manipulating
- Messages directly from the database.
-
- These methods will all return database objects (or QuerySets)
- directly.
-
- A Message represents one unit of communication, be it over a
- Channel or via some form of in-game mail system. Like an e-mail,
- it always has a sender and can have any number of receivers (some
- of which may be Channels).
-
- """
-
-
[docs]defidentify_object(self,inp):
- """
- Wrapper to identify_object if accessing via the manager directly.
-
- Args:
- inp (any): Entity to be idtified.
-
- Returns:
- identified (tuple): This is a tuple with (`inp`, identifier)
- where `identifier` is one of "account", "object", "channel",
- "string", "dbref" or None.
-
- """
- returnidentify_object(inp)
-
-
[docs]defget_message_by_id(self,idnum):
- """
- Retrieve message by its id.
-
- Args:
- idnum (int or str): The dbref to retrieve.
-
- Returns:
- message (Msg): The message.
-
- """
- try:
- returnself.get(id=self.dbref(idnum,reqhash=False))
- exceptException:
- returnNone
-
-
[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.
-
- Returns:
- QuerySet: Matching messages.
-
- Raises:
- CommError: For incorrect sender types.
-
- """
- obj,typ=identify_object(sender)
- 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:
- raiseCommError
-
-
[docs]defget_messages_by_receiver(self,recipient):
- """
- Get all messages sent to one given recipient.
-
- Args:
- recipient (Object, Account or Channel): The recipient of the messages to search for.
-
- Returns:
- Queryset: Matching messages.
-
- Raises:
- CommError: If the `recipient` is not of a valid type.
-
- """
- obj,typ=identify_object(recipient)
- iftyp=="account":
- returnself.filter(db_receivers_accounts=obj).exclude(db_hide_from_accounts=obj)
- eliftyp=="object":
- returnself.filter(db_receivers_objects=obj).exclude(db_hide_from_objects=obj)
- eliftyp=='script':
- returnself.filter(db_receivers_scripts=obj)
- else:
- raiseCommError
-
-
[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, 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:
- This can potentially be slow, so make sure to supply one of
- the other arguments to limit the search.
- dbref (int): The exact database id of the message. This will override
- all other search criteria since it's unique and
- always gives only one match.
-
- Returns:
- Queryset: Iterable with 0, 1 or more matches.
-
- """
- # unique msg id
- ifdbref:
- 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 (we need __pk to avoid an error with empty Q() objects)
- sender,styp=identify_object(sender)
- ifsender:
- spk=sender.pk
- ifstyp=="account":
- sender_restrict=Q(db_sender_accounts__pk=spk)&~Q(db_hide_from_accounts__pk=spk)
- elifstyp=="object":
- 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 receiver
- receiver,rtyp=identify_object(receiver)
- ifreceiver:
- rpk=receiver.pk
- ifrtyp=="account":
- receiver_restrict=(
- Q(db_receivers_accounts__pk=rpk)&~Q(db_hide_from_accounts__pk=rpk))
- elifrtyp=="object":
- 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":
- raiseDeprecationWarning(
- "Msg.objects.search don't accept channel recipients since "
- "Channels no longer accepts Msg objects.")
- else:
- receiver_restrict=Q()
- # filter by full text
- iffreetext:
- fulltext_restrict=Q(db_header__icontains=freetext)|Q(db_message__icontains=freetext)
- else:
- fulltext_restrict=Q()
- # execute the query
- returnself.filter(sender_restrict&receiver_restrict&fulltext_restrict)
-
- # back-compatibility alias
- message_search=search_message
-
-
[docs]defcreate_message(self,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, 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.
- 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 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."
- )
-
- ifnotmessage:
- # we don't allow empty messages.
- returnNone
- new_message=self.model(db_message=message)
- new_message.save()
- forsenderinmake_iter(senderobj):
- new_message.senders=sender
- new_message.header=header
- forreceiverinmake_iter(receivers):
- new_message.receivers=receiver
- iflocks:
- new_message.locks.add(locks)
- iftags:
- new_message.tags.batch_add(*tags)
-
- new_message.save()
- returnnew_message
-
-#
-# Channel manager
-#
-
-
-
[docs]classChannelDBManager(TypedObjectManager):
- """
- This ChannelManager implements methods for searching and
- manipulating Channels directly from the database.
-
- These methods will all return database objects (or QuerySets)
- directly.
-
- A Channel is an in-game venue for communication. It's essentially
- representation of a re-sender: Users sends Messages to the
- Channel, and the Channel re-sends those messages to all users
- subscribed to the Channel.
-
- """
-
-
[docs]defget_all_channels(self):
- """
- Get all channels.
-
- Returns:
- channels (list): All channels in game.
-
- """
- returnself.all()
-
-
[docs]defget_channel(self,channelkey):
- """
- Return the channel object if given its key.
- Also searches its aliases.
-
- Args:
- channelkey (str): Channel key to search for.
-
- Returns:
- channel (Channel or None): A channel match.
-
- """
- dbref=self.dbref(channelkey)
- ifdbref:
- try:
- returnself.get(id=dbref)
- exceptself.model.DoesNotExist:
- pass
- results=self.filter(
- Q(db_key__iexact=channelkey)
- |Q(db_tags__db_tagtype__iexact="alias",db_tags__db_key__iexact=channelkey)
- ).distinct()
- returnresults[0]ifresultselseNone
-
-
[docs]defget_subscriptions(self,subscriber):
- """
- Return all channels a given entity is subscribed to.
-
- Args:
- subscriber (Object or Account): The one subscribing.
-
- Returns:
- subscriptions (list): Channel subscribed to.
-
- """
- clsname=subscriber.__dbclass__.__name__
- ifclsname=="AccountDB":
- returnsubscriber.account_subscription_set.all()
- ifclsname=="ObjectDB":
- returnsubscriber.object_subscription_set.all()
- return[]
-
-
[docs]defsearch_channel(self,ostring,exact=True):
- """
- Search the channel database for a particular channel.
-
- Args:
- ostring (str): The key or database id of the channel.
- exact (bool, optional): Require an exact (but not
- case sensitive) match.
-
- Returns:
- Queryset: Iterable with 0, 1 or more matches.
-
- """
- dbref=self.dbref(ostring)
- ifdbref:
- dbref_match=self.search_dbref(dbref)
- ifdbref_match:
- returndbref_match
-
- ifexact:
- channels=self.filter(
- Q(db_key__iexact=ostring)
- |Q(db_tags__db_tagtype__iexact="alias",db_tags__db_key__iexact=ostring)
- ).distinct()
- else:
- channels=self.filter(
- Q(db_key__icontains=ostring)
- |Q(db_tags__db_tagtype__iexact="alias",db_tags__db_key__icontains=ostring)
- ).distinct()
- returnchannels
-
-
[docs]defcreate_channel(
- self,key,aliases=None,desc=None,locks=None,keep_log=True,typeclass=None,tags=None
- ):
- """
- Create A communication Channel. A Channel serves as a central hub
- for distributing Msgs to groups of people without specifying the
- receivers explicitly. Instead accounts may 'connect' to the channel
- and follow the flow of messages. By default the channel allows
- access to all old messages, but this can be turned off with the
- keep_log switch.
-
- Args:
- key (str): This must be unique.
-
- Keyword Args:
- aliases (list of str): List of alternative (likely shorter) keynames.
- desc (str): A description of the channel, for use in listings.
- locks (str): Lockstring.
- keep_log (bool): Log channel throughput.
- typeclass (str or class): The typeclass of the Channel (not
- often used).
- tags (list): A list of tags or tuples `(tag, category)`.
-
- Returns:
- channel (Channel): A newly created channel.
-
- """
- typeclass=typeclassiftypeclasselsesettings.BASE_CHANNEL_TYPECLASS
-
- ifisinstance(typeclass,str):
- # a path is given. Load the actual typeclass
- typeclass=class_from_module(typeclass,settings.TYPECLASS_PATHS)
-
- # create new instance
- new_channel=typeclass(db_key=key)
-
- # store call signature for the signal
- new_channel._createdict=dict(
- key=key,aliases=aliases,desc=desc,locks=locks,keep_log=keep_log,tags=tags
- )
-
- # this will trigger the save signal which in turn calls the
- # at_first_save hook on the typeclass, where the _createdict can be
- # used.
- new_channel.save()
-
- signals.SIGNAL_CHANNEL_POST_CREATE.send(sender=new_channel)
-
- returnnew_channel
-
- # back-compatibility alias
- channel_search=search_channel
-
-
-
[docs]classChannelManager(ChannelDBManager,TypeclassManager):
- """
- Wrapper to group the typeclass manager to a consistent name.
- """
-
- pass
-"""
-Models for the in-game communication system.
-
-The comm system could take the form of channels, but can also be
-adopted for storing tells or in-game mail.
-
-The comsystem's main component is the Message (Msg), which carries the
-actual information between two parties. Msgs are stored in the
-database and usually not deleted. A Msg always have one sender (a
-user), but can have any number targets, both users and channels.
-
-For non-persistent (and slightly faster) use one can also use the
-TempMsg, which mimics the Msg API but without actually saving to the
-database.
-
-Channels are central objects that act as targets for Msgs. Accounts can
-connect to channels by use of a ChannelConnect object (this object is
-necessary to easily be able to delete connections on the fly).
-
-"""
-fromdjango.confimportsettings
-fromdjango.utilsimporttimezone
-fromdjango.dbimportmodels
-fromevennia.typeclasses.modelsimportTypedObject
-fromevennia.typeclasses.tagsimportTag,TagHandler
-fromevennia.utils.idmapper.modelsimportSharedMemoryModel
-fromevennia.commsimportmanagers
-fromevennia.locks.lockhandlerimportLockHandler
-fromevennia.utils.utilsimportcrop,make_iter,lazy_property
-
-__all__=("Msg","TempMsg","ChannelDB","SubscriptionHandler")
-
-
-_GA=object.__getattribute__
-_SA=object.__setattr__
-_DA=object.__delattr__
-
-
-# ------------------------------------------------------------
-#
-# Msg
-#
-# ------------------------------------------------------------
-
-
-
[docs]classMsg(SharedMemoryModel):
- """
- A single message. This model describes all ooc messages
- sent in-game, both to channels and between accounts.
-
- The Msg class defines the following database fields (all
- accessed via specific handler methods):
-
- - db_sender_accounts: Account senders
- - db_sender_objects: Object senders
- - db_sender_scripts: Script senders
- - 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_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_lock_storage: Internal storage of lock strings.
-
- """
-
- #
- # Msg database model setup
- #
- #
- # These databse fields are all set using their corresponding properties,
- # named same as the field, but withtout the db_* prefix.
-
- db_sender_accounts=models.ManyToManyField(
- "accounts.AccountDB",
- related_name="sender_account_set",
- blank=True,
- verbose_name="Senders (Accounts)",
- db_index=True,
- )
-
- db_sender_objects=models.ManyToManyField(
- "objects.ObjectDB",
- related_name="sender_object_set",
- blank=True,
- verbose_name="Senders (Objects)",
- db_index=True,
- )
- db_sender_scripts=models.ManyToManyField(
- "scripts.ScriptDB",
- related_name="sender_script_set",
- blank=True,
- verbose_name="Senders (Scripts)",
- db_index=True,
- )
- db_sender_external=models.CharField(
- "external sender",
- max_length=255,
- null=True,
- blank=True,
- db_index=True,
- help_text="Identifier for single external sender, for use with senders "
- "not represented by a regular database model."
- )
-
- db_receivers_accounts=models.ManyToManyField(
- "accounts.AccountDB",
- related_name="receiver_account_set",
- blank=True,
- verbose_name="Receivers (Accounts)",
- help_text="account receivers",
- )
-
- db_receivers_objects=models.ManyToManyField(
- "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_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
- # it, or as a separate store for the mail subject line maybe.
- db_header=models.TextField("header",null=True,blank=True)
- # the message body itself
- db_message=models.TextField("message")
- # send date
- db_date_created=models.DateTimeField(
- "date sent",editable=False,auto_now_add=True,db_index=True
- )
- # lock storage
- db_lock_storage=models.TextField(
- "locks",blank=True,help_text="access locks on this message."
- )
-
- # these can be used to filter/hide a given message from supplied objects/accounts
- db_hide_from_accounts=models.ManyToManyField(
- "accounts.AccountDB",related_name="hide_from_accounts_set",blank=True
- )
-
- db_hide_from_objects=models.ManyToManyField(
- "objects.ObjectDB",related_name="hide_from_objects_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.",
- )
-
- # Database manager
- objects=managers.MsgManager()
- _is_deleted=False
-
- classMeta(object):
- "Define Django meta options"
- verbose_name="Msg"
-
-
-
- # Wrapper properties to easily set database fields. These are
- # @property decorators that allows to access these fields using
- # normal python operations (without having to remember to save()
- # etc). So e.g. a property 'attr' has a get/set/del decorator
- # defined that allows the user to do self.attr = value,
- # value = self.attr and del self.attr respectively (where self
- # is the object in question).
-
- @property
- defsenders(self):
- "Getter. Allows for value = self.senders"
- return(
- list(self.db_sender_accounts.all())
- +list(self.db_sender_objects.all())
- +list(self.db_sender_scripts.all())
- +([self.db_sender_external]ifself.db_sender_externalelse[])
- )
-
- @senders.setter
- defsenders(self,senders):
- "Setter. Allows for self.sender = value"
-
- ifisinstance(senders,str):
- self.db_sender_external=senders
- self.save(update_fields=["db_sender_external"])
- return
-
- forsenderinmake_iter(senders):
- ifnotsender:
- continue
- ifnothasattr(sender,"__dbclass__"):
- raiseValueError("This is a not a typeclassed object!")
- clsname=sender.__dbclass__.__name__
- ifclsname=="ObjectDB":
- self.db_sender_objects.add(sender)
- elifclsname=="AccountDB":
- self.db_sender_accounts.add(sender)
- elifclsname=="ScriptDB":
- self.db_sender_scripts.add(sender)
-
- @senders.deleter
- defsenders(self):
- "Deleter. Clears all senders"
- self.db_sender_accounts.clear()
- self.db_sender_objects.clear()
- self.db_sender_scripts.clear()
- self.db_sender_external=""
- self.save()
-
-
[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
- ifnothasattr(sender,"__dbclass__"):
- raiseValueError("This is a not a typeclassed object!")
- clsname=sender.__dbclass__.__name__
- ifclsname=="ObjectDB":
- self.db_sender_objects.remove(sender)
- elifclsname=="AccountDB":
- self.db_sender_accounts.remove(sender)
- elifclsname=="ScriptDB":
- self.db_sender_accounts.remove(sender)
-
- @property
- defreceivers(self):
- """
- Getter. Allows for value = self.receivers.
- 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())
- +([self.db_receiver_external]ifself.db_receiver_externalelse[])
- )
-
- @receivers.setter
- defreceivers(self,receivers):
- """
- 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
- ifnothasattr(receiver,"__dbclass__"):
- raiseValueError("This is a not a typeclassed object!")
- clsname=receiver.__dbclass__.__name__
- ifclsname=="ObjectDB":
- self.db_receivers_objects.add(receiver)
- elifclsname=="AccountDB":
- self.db_receivers_accounts.add(receiver)
- elifclsname=="ScriptDB":
- self.db_receivers_scripts.add(receiver)
-
- @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_receiver_external=""
- self.save()
-
-
[docs]defremove_receiver(self,receivers):
- """
- Remove a single receiver, a list of receivers, or a single extral receiver.
-
- Args:
- 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
- elifnothasattr(receiver,"__dbclass__"):
- raiseValueError("This is a not a typeclassed object!")
- clsname=receiver.__dbclass__.__name__
- ifclsname=="ObjectDB":
- self.db_receivers_objects.remove(receiver)
- elifclsname=="AccountDB":
- self.db_receivers_accounts.remove(receiver)
- elifclsname=="ScriptDB":
- self.db_receivers_scripts.remove(receiver)
-
- @property
- defhide_from(self):
- """
- Getter. Allows for value = self.hide_from.
- Returns two lists of accounts and objects.
-
- """
- return(
- self.db_hide_from_accounts.all(),
- self.db_hide_from_objects.all(),
- )
-
- @hide_from.setter
- defhide_from(self,hiders):
- """
- Setter. Allows for self.hide_from = value. Will append to hiders.
-
- """
- forhiderinmake_iter(hiders):
- ifnothider:
- continue
- ifnothasattr(hider,"__dbclass__"):
- raiseValueError("This is a not a typeclassed object!")
- clsname=hider.__dbclass__.__name__
- ifclsname=="AccountDB":
- self.db_hide_from_accounts.add(hider.__dbclass__)
- elifclsname=="ObjectDB":
- self.db_hide_from_objects.add(hider.__dbclass__)
-
- @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.save()
-
- #
- # 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)
- receivers=",".join(getattr(obj,"key",str(obj))forobjinself.receivers)
- return"%s->%s: %s"%(senders,receivers,crop(self.message,width=40))
-
-
[docs]defaccess(self,accessing_obj,access_type="read",default=False):
- """
- Checks lock access.
-
- Args:
- accessing_obj (Object or Account): The object trying to gain access.
- access_type (str, optional): The type of lock access to check.
- default (bool): Fallback to use if `access_type` lock is not defined.
-
- Returns:
- result (bool): If access was granted or not.
-
- """
- returnself.locks.check(accessing_obj,access_type=access_type,default=default)
[docs]classTempMsg:
- """
- 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,
- message="",
- header="",
- type="",
- lockstring="",
- hide_from=None,
- ):
- """
- Creates the temp message.
-
- Args:
- senders (any or list, optional): Senders of the message.
- 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, or list, optional): Entities to hide this message from.
-
- """
- self.senders=sendersandmake_iter(senders)or[]
- self.receivers=receiversandmake_iter(receivers)or[]
- self.type=type
- self.header=header
- self.message=message
- self.lock_storage=lockstring
- 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(obj.keyforobjinself.receivers)
- return"%s->%s: %s"%(senders,receivers,crop(self.message,width=40))
-
-
[docs]defremove_sender(self,sender):
- """
- Remove a sender or a list of senders.
-
- Args:
- sender (Object, Account, str or list): Senders to remove.
-
- """
- foroinmake_iter(sender):
- try:
- self.senders.remove(o)
- exceptValueError:
- pass# nothing to remove
-
-
[docs]defremove_receiver(self,receiver):
- """
- Remove a receiver or a list of receivers
-
- Args:
- receiver (Object, Account, Script, str or list): Receivers to remove.
-
- """
-
- foroinmake_iter(receiver):
- try:
- self.senders.remove(o)
- exceptValueError:
- pass# nothing to remove
-
-
[docs]defaccess(self,accessing_obj,access_type="read",default=False):
- """
- Checks lock access.
-
- Args:
- accessing_obj (Object or Account): The object trying to gain access.
- access_type (str, optional): The type of lock access to check.
- default (bool): Fallback to use if `access_type` lock is not defined.
-
- Returns:
- result (bool): If access was granted or not.
-
- """
- returnself.locks.check(accessing_obj,access_type=access_type,default=default)
[docs]classSubscriptionHandler(object):
- """
- This handler manages subscriptions to the
- channel and hides away which type of entity is
- subscribing (Account or Object)
-
- """
-
-
[docs]def__init__(self,obj):
- """
- Initialize the handler
-
- Attr:
- obj (ChannelDB): The channel the handler sits on.
-
- """
- self.obj=obj
- self._cache=None
[docs]defhas(self,entity):
- """
- Check if the given entity subscribe to this channel
-
- Args:
- entity (str, Account or Object): The entity to return. If
- a string, it assumed to be the key or the #dbref
- of the entity.
-
- Returns:
- subscriber (Account, Object or None): The given
- subscriber.
-
- """
- ifself._cacheisNone:
- self._recache()
- returnentityinself._cache
-
-
[docs]defadd(self,entity):
- """
- Subscribe an entity to this channel.
-
- Args:
- entity (Account, Object or list): The entity or
- list of entities to subscribe to this channel.
-
- Note:
- No access-checking is done here, this must have
- been done before calling this method. Also
- no hooks will be called.
-
- """
- forsubscriberinmake_iter(entity):
- ifsubscriber:
- clsname=subscriber.__dbclass__.__name__
- # chooses the right type
- ifclsname=="ObjectDB":
- self.obj.db_object_subscriptions.add(subscriber)
- elifclsname=="AccountDB":
- self.obj.db_account_subscriptions.add(subscriber)
- self._recache()
-
-
[docs]defremove(self,entity):
- """
- Remove a subscriber from the channel.
-
- Args:
- entity (Account, Object or list): The entity or
- entities to un-subscribe from the channel.
-
- """
- forsubscriberinmake_iter(entity):
- ifsubscriber:
- clsname=subscriber.__dbclass__.__name__
- # chooses the right type
- ifclsname=="AccountDB":
- self.obj.db_account_subscriptions.remove(entity)
- elifclsname=="ObjectDB":
- self.obj.db_object_subscriptions.remove(entity)
- self._recache()
-
-
[docs]defall(self):
- """
- Get all subscriptions to this channel.
-
- Returns:
- subscribers (list): The subscribers. This
- may be a mix of Accounts and Objects!
-
- """
- ifself._cacheisNone:
- self._recache()
- returnself._cache
-
- get=all# alias
-
-
[docs]defonline(self):
- """
- Get all online accounts from our cache
- Returns:
- subscribers (list): Subscribers who are online or
- are puppeted by an online account.
- """
- subs=[]
- recache_needed=False
- forobjinself.all():
- fromdjango.core.exceptionsimportObjectDoesNotExist
-
- try:
- ifhasattr(obj,"account")andobj.account:
- obj=obj.account
- ifnotobj.is_connected:
- continue
- exceptObjectDoesNotExist:
- # a subscribed object has already been deleted. Mark that we need a recache and
- # ignore it
- recache_needed=True
- continue
- subs.append(obj)
- ifrecache_needed:
- self._recache()
- returnsubs
-
-
[docs]defclear(self):
- """
- Remove all subscribers from channel.
-
- """
- self.obj.db_account_subscriptions.clear()
- self.obj.db_object_subscriptions.clear()
- self._cache=None
-
-
-
[docs]classChannelDB(TypedObject):
- """
- This is the basis of a comm channel, only implementing
- the very basics of distributing messages.
-
- The Channel class defines the following database fields
- beyond the ones inherited from TypedObject:
-
- - db_account_subscriptions: The Account subscriptions.
- - db_object_subscriptions: The Object subscriptions.
-
- """
-
- db_account_subscriptions=models.ManyToManyField(
- "accounts.AccountDB",
- related_name="account_subscription_set",
- blank=True,
- verbose_name="account subscriptions",
- db_index=True,
- )
-
- db_object_subscriptions=models.ManyToManyField(
- "objects.ObjectDB",
- related_name="object_subscription_set",
- blank=True,
- verbose_name="object subscriptions",
- db_index=True,
- )
-
- # Database manager
- objects=managers.ChannelDBManager()
-
- __settingclasspath__=settings.BASE_CHANNEL_TYPECLASS
- __defaultclasspath__="evennia.comms.comms.DefaultChannel"
- __applabel__="comms"
-
- classMeta:
- "Define Django meta options"
- verbose_name="Channel"
- verbose_name_plural="Channels"
-
- def__str__(self):
- "Echoes the text representation of the channel."
- return"Channel '%s' (%s)"%(self.key,self.db.desc)
-
-
[docs]defget_all_topics(self):
- """
- Get all topics.
-
- Returns:
- all (HelpEntries): All topics.
-
- """
- returnself.all()
-
-
[docs]defget_all_categories(self):
- """
- Return all defined category names with at least one topic in
- them.
-
- Returns:
- matches (list): Unique list of category names across all
- topics.
-
- """
- returnlist(set(topic.help_categoryfortopicinself.all()))
-
-
[docs]defall_to_category(self,default_category):
- """
- Shifts all help entries in database to default_category. This
- action cannot be reverted. It is used primarily by the engine
- when importing a default help database, making sure this ends
- up in one easily separated category.
-
- Args:
- default_category (str): Category to move entries to.
-
- """
- topics=self.all()
- fortopicintopics:
- topic.help_category=default_category
- topic.save()
- string="Help database moved to category {default_category}".format(
- default_category=default_category
- )
- logger.log_info(string)
-
-
[docs]defsearch_help(self,ostring,help_category=None):
- """
- Retrieve a search entry object.
-
- Args:
- ostring (str): The help topic to look for.
- category (str): Limit the search to a particular help topic
-
- Returns:
- Queryset: An iterable with 0, 1 or more matches.
-
- """
- ostring=ostring.strip().lower()
- ifhelp_category:
- returnself.filter(db_key__iexact=ostring,db_help_category__iexact=help_category)
- else:
- returnself.filter(db_key__iexact=ostring)
-
-
[docs]defcreate_help(self,key,entrytext,category="General",locks=None,aliases=None,tags=None):
- """
- Create a static help entry in the help database. Note that Command
- help entries are dynamic and directly taken from the __doc__
- entries of the command. The database-stored help entries are
- intended for more general help on the game, more extensive info,
- in-game setting information and so on.
-
- Args:
- key (str): The name of the help entry.
- entrytext (str): The body of te help entry
- category (str, optional): The help category of the entry.
- locks (str, optional): A lockstring to restrict access.
- aliases (list of str, optional): List of alternative (likely shorter) keynames.
- tags (lst, optional): List of tags or tuples `(tag, category)`.
-
- Returns:
- help (HelpEntry): A newly created help entry.
-
- """
- try:
- new_help=self.model()
- new_help.key=key
- new_help.entrytext=entrytext
- new_help.help_category=category
- iflocks:
- new_help.locks.add(locks)
- ifaliases:
- new_help.aliases.add(make_iter(aliases))
- iftags:
- new_help.tags.batch_add(*tags)
- new_help.save()
- returnnew_help
- exceptIntegrityError:
- string="Could not add help entry: key '%s' already exists."%key
- logger.log_err(string)
- returnNone
- exceptException:
- logger.log_trace()
- returnNone
-
- signals.SIGNAL_HELPENTRY_POST_CREATE.send(sender=new_help)
-"""
-Models for the help system.
-
-The database-tied help system is only half of Evennia's help
-functionality, the other one being the auto-generated command help
-that is created on the fly from each command's `__doc__` string. The
-persistent database system defined here is intended for all other
-forms of help that do not concern commands, like information about the
-game world, policy info, rules and similar.
-
-"""
-fromdjango.contrib.contenttypes.modelsimportContentType
-fromdjango.dbimportmodels
-fromdjango.urlsimportreverse
-fromdjango.utils.textimportslugify
-
-fromevennia.utils.idmapper.modelsimportSharedMemoryModel
-fromevennia.help.managerimportHelpEntryManager
-fromevennia.typeclasses.modelsimportTag,TagHandler,AliasHandler
-fromevennia.locks.lockhandlerimportLockHandler
-fromevennia.utils.utilsimportlazy_property
-
-__all__=("HelpEntry",)
-
-
-# ------------------------------------------------------------
-#
-# HelpEntry
-#
-# ------------------------------------------------------------
-
-
-
[docs]classHelpEntry(SharedMemoryModel):
- """
- A generic help entry.
-
- An HelpEntry object has the following properties defined:
- key - main name of entry
- help_category - which category entry belongs to (defaults to General)
- entrytext - the actual help text
- permissions - perm strings
-
- Method:
- access
-
- """
-
- #
- # HelpEntry Database Model setup
- #
- #
- # These database fields are all set using their corresponding properties,
- # named same as the field, but withtout the db_* prefix.
-
- # title of the help entry
- db_key=models.CharField(
- "help key",max_length=255,unique=True,help_text="key to search for"
- )
-
- # help category
- db_help_category=models.CharField(
- "help category",
- max_length=255,
- default="General",
- help_text="organizes help entries in lists",
- )
-
- # the actual help entry text, in any formatting.
- db_entrytext=models.TextField(
- "help entry",blank=True,help_text="the main body of help text"
- )
- # lock string storage
- db_lock_storage=models.TextField("locks",blank=True,help_text="normally view:all().")
- # tags are primarily used for permissions
- db_tags=models.ManyToManyField(
- Tag,
- blank=True,
- help_text="tags on this object. Tags are simple string markers to "
- "identify, group and alias objects.",
- )
- # Creation date. This is not changed once the object is created.
- db_date_created=models.DateTimeField("creation date",editable=False,auto_now=True)
-
- # Database manager
- objects=HelpEntryManager()
- _is_deleted=False
-
- # lazy-loaded handlers
-
-
[docs]defaccess(self,accessing_obj,access_type="read",default=True):
- """
- 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()),
- "no_prefix":"",
- "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.
-
- ex. Account#1 = '/admin/accounts/accountdb/1/change/'
-
- Returns:
- path (str): URI path to Django Admin page for object.
-
- """
- content_type=ContentType.objects.get_for_model(self.__class__)
- returnreverse(
- "admin:%s_%s_change"%(content_type.app_label,content_type.model),args=(self.id,)
- )
-
-
[docs]@classmethod
- defweb_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.
-
- """
- try:
- returnreverse("%s-create"%slugify(cls._meta.verbose_name))
- exceptException:
- return"#"
-
-
[docs]defweb_get_detail_url(self):
- """
- Returns the URI path for a View that allows users to view details for
- this object.
-
- ex. 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.
-
- ex.
- ::
- 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.
-
- 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:
- path (str): URI path to object detail page, if defined.
-
- """
-
- try:
- returnreverse(
- "%s-detail"%slugify(self._meta.verbose_name),
- kwargs={"category":slugify(self.db_help_category),"topic":slugify(self.db_key)},
- )
- exceptException:
- return"#"
-
-
[docs]defweb_get_update_url(self):
- """
- Returns the URI path for a View that allows users to update this
- object.
-
- ex. 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.
-
- ex.
- ::
-
- 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.
-
- 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:
- path (str): URI path to object update page, if defined.
-
- """
- try:
- returnreverse(
- "%s-update"%slugify(self._meta.verbose_name),
- kwargs={"category":slugify(self.db_help_category),"topic":slugify(self.db_key)},
- )
- exceptException:
- return"#"
-
-
[docs]defweb_get_delete_url(self):
- """
- Returns the URI path for a View that allows users to delete this object.
-
- ex. 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.
-
- ex.
- ::
-
- 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.
-
- 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:
- path (str): URI path to object deletion page, if defined.
-
- """
- try:
- returnreverse(
- "%s-delete"%slugify(self._meta.verbose_name),
- kwargs={"category":slugify(self.db_help_category),"topic":slugify(self.db_key)},
- )
- exceptException:
- return"#"
-
- # Used by Django Sites/Admin
- get_absolute_url=web_get_detail_url
-"""
-This module provides a set of permission lock functions for use
-with Evennia's permissions system.
-
-To call these locks, make sure this module is included in the
-settings tuple `PERMISSION_FUNC_MODULES` then define a lock on the form
-'<access_type>:func(args)' and add it to the object's lockhandler.
-Run the `access()` method of the handler to execute the lock check.
-
-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.
-
-"""
-
-
-fromastimportliteral_eval
-fromdjango.confimportsettings
-fromevennia.utilsimportutils
-
-_PERMISSION_HIERARCHY=[pe.lower()forpeinsettings.PERMISSION_HIERARCHY]
-# also accept different plural forms
-_PERMISSION_HIERARCHY_PLURAL=[
- pe+"s"ifnotpe.endswith("s")elsepeforpein_PERMISSION_HIERARCHY
-]
-
-
-def_to_account(accessing_obj):
- "Helper function. Makes sure an accessing object is an account object"
- ifutils.inherits_from(accessing_obj,"evennia.objects.objects.DefaultObject"):
- # an object. Convert to account.
- accessing_obj=accessing_obj.account
- returnaccessing_obj
-
-
-# lock functions
-
-
-
[docs]defself(accessing_obj,accessed_obj,*args,**kwargs):
- """
- Check if accessing_obj is the same as accessed_obj
-
- Usage:
- self()
-
- This can be used to lock specifically only to
- the same object that the lock is defined on.
- """
- returnaccessing_obj==accessed_obj
-
-
-
[docs]defperm(accessing_obj,accessed_obj,*args,**kwargs):
- """
- The basic permission-checker. Ignores case.
-
- Usage:
- perm(<permission>)
-
- where <permission> is the permission accessing_obj must
- have in order to pass the lock.
-
- If the given permission is part of settings.PERMISSION_HIERARCHY,
- permission is also granted to all ranks higher up in the hierarchy.
-
- If accessing_object is an Object controlled by an Account, the
- permissions of the Account is used unless the Attribute _quell
- is set to True on the Object. In this case however, the
- LOWEST hieararcy-permission of the Account/Object-pair will be used
- (this is order to avoid Accounts potentially escalating their own permissions
- by use of a higher-level Object)
-
- For non-hierarchical permissions, a puppeted object's account is checked first,
- followed by the puppet (unless quelled, when only puppet's access is checked).
-
- """
- # this allows the perm_above lockfunc to make use of this function too
- try:
- permission=args[0].lower()
- perms_object=accessing_obj.permissions.all()
- except(AttributeError,IndexError):
- returnFalse
-
- gtmode=kwargs.pop("_greater_than",False)
- is_quell=False
-
- account=(
- utils.inherits_from(accessing_obj,"evennia.objects.objects.DefaultObject")
- andaccessing_obj.account
- )
- # check object perms (note that accessing_obj could be an Account too)
- perms_account=[]
- ifaccount:
- perms_account=account.permissions.all()
- is_quell=account.attributes.get("_quell")
-
- # Check hirarchy matches; handle both singular/plural forms in hierarchy
- hpos_target=None
- ifpermissionin_PERMISSION_HIERARCHY:
- hpos_target=_PERMISSION_HIERARCHY.index(permission)
- ifpermission.endswith("s")andpermission[:-1]in_PERMISSION_HIERARCHY:
- hpos_target=_PERMISSION_HIERARCHY.index(permission[:-1])
- ifhpos_targetisnotNone:
- # hieratchy match
- hpos_account=-1
- hpos_object=-1
-
- ifaccount:
- # we have an account puppeting this object. We must check what perms it has
- perms_account_single=[p[:-1]ifp.endswith("s")elsepforpinperms_account]
- hpos_account=[
- hpos
- forhpos,hperminenumerate(_PERMISSION_HIERARCHY)
- ifhperminperms_account_single
- ]
- hpos_account=hpos_accountandhpos_account[-1]or-1
-
- ifnotaccountoris_quell:
- # only get the object-level perms if there is no account or quelling
- perms_object_single=[p[:-1]ifp.endswith("s")elsepforpinperms_object]
- hpos_object=[
- hpos
- forhpos,hperminenumerate(_PERMISSION_HIERARCHY)
- ifhperminperms_object_single
- ]
- hpos_object=hpos_objectandhpos_object[-1]or-1
-
- ifaccountandis_quell:
- # quell mode: use smallest perm from account and object
- ifgtmode:
- returnhpos_target<min(hpos_account,hpos_object)
- else:
- returnhpos_target<=min(hpos_account,hpos_object)
- elifaccount:
- # use account perm
- ifgtmode:
- returnhpos_target<hpos_account
- else:
- returnhpos_target<=hpos_account
- else:
- # use object perm
- ifgtmode:
- returnhpos_target<hpos_object
- else:
- returnhpos_target<=hpos_object
- else:
- # no hierarchy match - check direct matches
- ifaccount:
- # account exists
- ifis_quellandpermissioninperms_object:
- # if quelled, first check object
- returnTrue
- elifpermissioninperms_account:
- # unquelled - check account
- returnTrue
- else:
- # no account-pass, check object pass
- returnpermissioninperms_object
-
- elifpermissioninperms_object:
- returnTrue
-
- returnFalse
-
-
-
[docs]defperm_above(accessing_obj,accessed_obj,*args,**kwargs):
- """
- Only allow objects with a permission *higher* in the permission
- hierarchy than the one given. If there is no such higher rank,
- it's assumed we refer to superuser. If no hierarchy is defined,
- this function has no meaning and returns False.
- """
- kwargs["_greater_than"]=True
- returnperm(accessing_obj,accessed_obj,*args,**kwargs)
-
-
-
[docs]defpperm(accessing_obj,accessed_obj,*args,**kwargs):
- """
- The basic permission-checker only for Account objects. Ignores case.
-
- Usage:
- pperm(<permission>)
-
- where <permission> is the permission accessing_obj must
- have in order to pass the lock. If the given permission
- is part of _PERMISSION_HIERARCHY, permission is also granted
- to all ranks higher up in the hierarchy.
- """
- returnperm(_to_account(accessing_obj),accessed_obj,*args,**kwargs)
-
-
-
[docs]defpperm_above(accessing_obj,accessed_obj,*args,**kwargs):
- """
- Only allow Account objects with a permission *higher* in the permission
- hierarchy than the one given. If there is no such higher rank,
- it's assumed we refer to superuser. If no hierarchy is defined,
- this function has no meaning and returns False.
- """
- returnperm_above(_to_account(accessing_obj),accessed_obj,*args,**kwargs)
-
-
-
[docs]defdbref(accessing_obj,accessed_obj,*args,**kwargs):
- """
- Usage:
- dbref(3)
-
- This lock type checks if the checking object
- has a particular dbref. Note that this only
- works for checking objects that are stored
- in the database (e.g. not for commands)
- """
- ifnotargs:
- returnFalse
- try:
- dbr=int(args[0].strip().strip("#"))
- exceptValueError:
- returnFalse
- ifhasattr(accessing_obj,"dbid"):
- returndbr==accessing_obj.dbid
- returnFalse
-
-
-
[docs]defpdbref(accessing_obj,accessed_obj,*args,**kwargs):
- """
- Same as dbref, but making sure accessing_obj is an account.
- """
- returndbref(_to_account(accessing_obj),accessed_obj,*args,**kwargs)
-
-
-
[docs]defid(accessing_obj,accessed_obj,*args,**kwargs):
- "Alias to dbref"
- returndbref(accessing_obj,accessed_obj,*args,**kwargs)
-
-
-
[docs]defpid(accessing_obj,accessed_obj,*args,**kwargs):
- "Alias to dbref, for Accounts"
- returndbref(_to_account(accessing_obj),accessed_obj,*args,**kwargs)
-
-
-# this is more efficient than multiple if ... elif statments
-CF_MAPPING={
- "eq":lambdaval1,val2:val1==val2orstr(val1)==str(val2)orfloat(val1)==float(val2),
- "gt":lambdaval1,val2:float(val1)>float(val2),
- "lt":lambdaval1,val2:float(val1)<float(val2),
- "ge":lambdaval1,val2:float(val1)>=float(val2),
- "le":lambdaval1,val2:float(val1)<=float(val2),
- "ne":lambdaval1,val2:float(val1)!=float(val2),
- "default":lambdaval1,val2:False,
-}
-
-
-
[docs]defattr(accessing_obj,accessed_obj,*args,**kwargs):
- """
- Usage:
- attr(attrname)
- attr(attrname, value)
- attr(attrname, value, compare=type)
-
- where compare's type is one of (eq,gt,lt,ge,le,ne) and signifies
- how the value should be compared with one on accessing_obj (so
- compare=gt means the accessing_obj must have a value greater than
- the one given).
-
- Searches attributes *and* properties stored on the accessing_obj.
- if accessing_obj has a property "obj", then this is used as
- accessing_obj (this makes this usable for Commands too)
-
- The first form works like a flag - if the attribute/property
- exists on the object, the value is checked for True/False. The
- second form also requires that the value of the attribute/property
- matches. Note that all retrieved values will be converted to
- strings before doing the comparison.
- """
- # deal with arguments
- ifnotargs:
- returnFalse
- attrname=args[0].strip()
- value=None
- iflen(args)>1:
- value=args[1].strip()
- compare="eq"
- ifkwargs:
- compare=kwargs.get("compare","eq")
-
- defvalcompare(val1,val2,typ="eq"):
- "compare based on type"
- try:
- returnCF_MAPPING.get(typ,CF_MAPPING["default"])(val1,val2)
- exceptException:
- # this might happen if we try to compare two things that
- # cannot be compared
- returnFalse
-
- ifhasattr(accessing_obj,"obj"):
- # NOTE: this is relevant for Commands. It may clash with scripts
- # (they have Attributes and .obj) , but are scripts really
- # used so that one ever wants to check the property on the
- # Script rather than on its owner?
- accessing_obj=accessing_obj.obj
-
- # first, look for normal properties on the object trying to gain access
- ifhasattr(accessing_obj,attrname):
- ifvalue:
- returnvalcompare(str(getattr(accessing_obj,attrname)),value,compare)
- # will return Fail on False value etc
- returnbool(getattr(accessing_obj,attrname))
- # check attributes, if they exist
- ifhasattr(accessing_obj,"attributes")andaccessing_obj.attributes.has(attrname):
- ifvalue:
- returnhasattr(accessing_obj,"attributes")andvalcompare(
- accessing_obj.attributes.get(attrname),value,compare
- )
- # fails on False/None values
- returnbool(accessing_obj.attributes.get(attrname))
- returnFalse
-
-
-
[docs]defobjattr(accessing_obj,accessed_obj,*args,**kwargs):
- """
- Usage:
- objattr(attrname)
- objattr(attrname, value)
- objattr(attrname, value, compare=type)
-
- Works like attr, except it looks for an attribute on
- accessed_obj instead.
-
- """
- returnattr(accessed_obj,accessed_obj,*args,**kwargs)
-
-
-
[docs]deflocattr(accessing_obj,accessed_obj,*args,**kwargs):
- """
- Usage:
- locattr(attrname)
- locattr(attrname, value)
- locattr(attrname, value, compare=type)
-
- Works like attr, except it looks for an attribute on
- accessing_obj.location, if such an entity exists.
-
- if accessing_obj has a property ".obj" (such as is the case for a
- Command), then accessing_obj.obj.location is used instead.
-
- """
- ifhasattr(accessing_obj,"obj"):
- accessing_obj=accessing_obj.obj
- ifhasattr(accessing_obj,"location"):
- returnattr(accessing_obj.location,accessed_obj,*args,**kwargs)
- returnFalse
-
-
-
[docs]defobjlocattr(accessing_obj,accessed_obj,*args,**kwargs):
- """
- Usage:
- locattr(attrname)
- locattr(attrname, value)
- locattr(attrname, value, compare=type)
-
- Works like attr, except it looks for an attribute on
- accessed_obj.location, if such an entity exists.
-
- if accessed_obj has a property ".obj" (such as is the case for a
- Command), then accessing_obj.obj.location is used instead.
-
- """
- ifhasattr(accessed_obj,"obj"):
- accessed_obj=accessed_obj.obj
- ifhasattr(accessed_obj,"location"):
- returnattr(accessed_obj.location,accessed_obj,*args,**kwargs)
- returnFalse
[docs]defattr_gt(accessing_obj,accessed_obj,*args,**kwargs):
- """
- Usage:
- attr_gt(attrname, 54)
-
- Only true if access_obj's attribute > the value given.
- """
- returnattr(accessing_obj,accessed_obj,*args,**{"compare":"gt"})
-
-
-
[docs]defattr_ge(accessing_obj,accessed_obj,*args,**kwargs):
- """
- Usage:
- attr_gt(attrname, 54)
-
- Only true if access_obj's attribute >= the value given.
- """
- returnattr(accessing_obj,accessed_obj,*args,**{"compare":"ge"})
-
-
-
[docs]defattr_lt(accessing_obj,accessed_obj,*args,**kwargs):
- """
- Usage:
- attr_gt(attrname, 54)
-
- Only true if access_obj's attribute < the value given.
- """
- returnattr(accessing_obj,accessed_obj,*args,**{"compare":"lt"})
-
-
-
[docs]defattr_le(accessing_obj,accessed_obj,*args,**kwargs):
- """
- Usage:
- attr_gt(attrname, 54)
-
- Only true if access_obj's attribute <= the value given.
- """
- returnattr(accessing_obj,accessed_obj,*args,**{"compare":"le"})
-
-
-
[docs]defattr_ne(accessing_obj,accessed_obj,*args,**kwargs):
- """
- Usage:
- attr_gt(attrname, 54)
-
- Only true if access_obj's attribute != the value given.
- """
- returnattr(accessing_obj,accessed_obj,*args,**{"compare":"ne"})
-
-
-
[docs]deftag(accessing_obj,accessed_obj,*args,**kwargs):
- """
- Usage:
- tag(tagkey)
- tag(tagkey, category)
-
- Only true if accessing_obj has the specified tag and optional
- category.
- If accessing_obj has the ".obj" property (such as is the case for
- a command), then accessing_obj.obj is used instead.
- """
- ifhasattr(accessing_obj,"obj"):
- accessing_obj=accessing_obj.obj
- tagkey=args[0]ifargselseNone
- category=args[1]iflen(args)>1elseNone
- returnbool(accessing_obj.tags.get(tagkey,category=category))
-
-
-
[docs]defobjtag(accessing_obj,accessed_obj,*args,**kwargs):
- """
- Usage:
- objtag(tagkey)
- objtag(tagkey, category)
-
- Only true if accessed_obj has the specified tag and optional
- category.
- """
- tagkey=args[0]ifargselseNone
- category=args[1]iflen(args)>1elseNone
- returnbool(accessed_obj.tags.get(tagkey,category=category))
-
-
-
[docs]definside(accessing_obj,accessed_obj,*args,**kwargs):
- """
- Usage:
- inside()
-
- True if accessing_obj is 'inside' accessing_obj. Note that this only checks
- one level down. So if if the lock is on a room, you will pass but not your
- inventory (since their location is you, not the locked object). If you
- want also nested objects to pass the lock, use the `insiderecursive`
- lockfunc.
- """
- ifhasattr(accessed_obj,"obj"):
- accessed_obj=accessed_obj.obj
- returnaccessing_obj.location==accessed_obj
-
-
-
[docs]definside_rec(accessing_obj,accessed_obj,*args,**kwargs):
- """
- Usage:
- inside_rec()
-
- True if accessing_obj is inside the accessed obj, at up to 10 levels
- of recursion (so if this lock is on a room, then an object inside a box
- in your inventory will also pass the lock).
- """
-
- def_recursive_inside(obj,accessed_obj,lvl=1):
- ifobj.location:
- ifobj.location==accessed_obj:
- returnTrue
- eliflvl>=10:
- # avoid infinite recursions
- returnFalse
- else:
- return_recursive_inside(obj.location,accessed_obj,lvl+1)
- returnFalse
-
- return_recursive_inside(accessing_obj,accessed_obj)
-
-
-
[docs]defholds(accessing_obj,accessed_obj,*args,**kwargs):
- """
- Usage:
- holds() checks if accessed_obj or accessed_obj.obj
- is held by accessing_obj
- holds(key/dbref) checks if accessing_obj holds an object
- with given key/dbref
- holds(attrname, value) checks if accessing_obj holds an
- object with the given attrname and value
-
- This is passed if accessed_obj is carried by accessing_obj (that is,
- accessed_obj.location == accessing_obj), or if accessing_obj itself holds
- an object matching the given key.
- """
- try:
- # commands and scripts don't have contents, so we are usually looking
- # for the contents of their .obj property instead (i.e. the object the
- # command/script is attached to).
- contents=accessing_obj.contents
- exceptAttributeError:
- try:
- contents=accessing_obj.obj.contents
- exceptAttributeError:
- returnFalse
-
- defcheck_holds(objid):
- # helper function. Compares both dbrefs and keys/aliases.
- objid=str(objid)
- dbref=utils.dbref(objid,reqhash=False)
- ifdbrefandany((Trueforobjincontentsifobj.dbid==dbref)):
- returnTrue
- objid=objid.lower()
- returnany(
- (
- True
- forobjincontents
- ifobj.key.lower()==objidorobjidin[al.lower()foralinobj.aliases.all()]
- )
- )
-
- ifnotargs:
- # holds() - check if accessed_obj or accessed_obj.obj is held by accessing_obj
- try:
- ifcheck_holds(accessed_obj.dbid):
- returnTrue
- exceptException:
- # we need to catch any trouble here
- pass
- returnhasattr(accessed_obj,"obj")andcheck_holds(accessed_obj.obj.dbid)
- iflen(args)==1:
- # command is holds(dbref/key) - check if given objname/dbref is held by accessing_ob
- returncheck_holds(args[0])
- eliflen(args)>1:
- # command is holds(attrname, value) check if any held object has the given attribute and value
- forobjincontents:
- ifobj.attributes.get(args[0])==args[1]:
- returnTrue
- returnFalse
-
-
-
[docs]defhas_account(accessing_obj,accessed_obj,*args,**kwargs):
- """
- Only returns true if accessing_obj has_account is true, that is,
- this is an account-controlled object. It fails on actual accounts!
-
- This is a useful lock for traverse-locking Exits to restrain NPC
- mobiles from moving outside their areas.
- """
- returnhasattr(accessing_obj,"has_account")andaccessing_obj.has_account
-
-
-
[docs]defserversetting(accessing_obj,accessed_obj,*args,**kwargs):
- """
- Only returns true if the Evennia settings exists, alternatively has
- a certain value.
-
- Usage:
- serversetting(IRC_ENABLED)
- serversetting(BASE_SCRIPT_PATH, ['types'])
-
- A given True/False or integers will be converted properly. Note that
- everything will enter this function as strings, so they have to be
- unpacked to their real value. We only support basic properties.
- """
- ifnotargsornotargs[0]:
- returnFalse
- iflen(args)<2:
- setting=args[0]
- val="True"
- else:
- setting,val=args[0],args[1]
- # convert
- try:
- val=literal_eval(val)
- exceptException:
- # we swallow errors here, lockfuncs has noone to report to
- returnFalse
-
- ifsettinginsettings._wrapped.__dict__:
- returnsettings._wrapped.__dict__[setting]==val
- returnFalse
-"""
-A *lock* defines access to a particular subsystem or property of
-Evennia. For example, the "owner" property can be impmemented as a
-lock. Or the disability to lift an object or to ban users.
-
-
-A lock consists of three parts:
-
- - access_type - this defines what kind of access this lock regulates. This
- just a string.
- - function call - this is one or many calls to functions that will determine
- if the lock is passed or not.
- - lock function(s). These are regular python functions with a special
- set of allowed arguments. They should always return a boolean depending
- on if they allow access or not.
-
-A lock function is defined by existing in one of the modules
-listed by settings.LOCK_FUNC_MODULES. It should also always
-take four arguments looking like this:
-
- funcname(accessing_obj, accessed_obj, *args, **kwargs):
- [...]
-
-The accessing object is the object wanting to gain access.
-The accessed object is the object this lock resides on
-args and kwargs will hold optional arguments and/or keyword arguments
-to the function as a list and a dictionary respectively.
-
-Example:
-
- perm(accessing_obj, accessed_obj, *args, **kwargs):
- "Checking if the object has a particular, desired permission"
- if args:
- desired_perm = args[0]
- return desired_perm in accessing_obj.permissions.all()
- return False
-
-Lock functions should most often be pretty general and ideally possible to
-re-use and combine in various ways to build clever locks.
-
-
-
-Lock definition ("Lock string")
-
-A lock definition is a string with a special syntax. It is added to
-each object's lockhandler, making that lock available from then on.
-
-The lock definition looks like this:
-
- 'access_type:[NOT] func1(args)[ AND|OR][NOT] func2() ...'
-
-That is, the access_type, a colon followed by calls to lock functions
-combined with AND or OR. NOT negates the result of the following call.
-
-Example:
-
- We want to limit who may edit a particular object (let's call this access_type
-for 'edit', it depends on what the command is looking for). We want this to
-only work for those with the Permission 'Builder'. So we use our lock
-function above and define it like this:
-
- 'edit:perm(Builder)'
-
-Here, the lock-function perm() will be called with the string
-'Builder' (accessing_obj and accessed_obj are added automatically,
-you only need to add the args/kwargs, if any).
-
-If we wanted to make sure the accessing object was BOTH a Builder and a
-GoodGuy, we could use AND:
-
- 'edit:perm(Builder) AND perm(GoodGuy)'
-
-To allow EITHER Builder and GoodGuys, we replace AND with OR. perm() is just
-one example, the lock function can do anything and compare any properties of
-the calling object to decide if the lock is passed or not.
-
- 'lift:attrib(very_strong) AND NOT attrib(bad_back)'
-
-To make these work, add the string to the lockhandler of the object you want
-to apply the lock to:
-
- obj.lockhandler.add('edit:perm(Builder)')
-
-From then on, a command that wants to check for 'edit' access on this
-object would do something like this:
-
- if not target_obj.lockhandler.has_perm(caller, 'edit'):
- caller.msg("Sorry, you cannot edit that.")
-
-All objects also has a shortcut called 'access' that is recommended to
-use instead:
-
- if not target_obj.access(caller, 'edit'):
- caller.msg("Sorry, you cannot edit that.")
-
-
-Permissions
-
-Permissions are just text strings stored in a comma-separated list on
-typeclassed objects. The default perm() lock function uses them,
-taking into account settings.PERMISSION_HIERARCHY. Also, the
-restricted @perm command sets them, but otherwise they are identical
-to any other identifier you can use.
-
-"""
-
-importre
-fromdjango.confimportsettings
-fromevennia.utilsimportlogger,utils
-fromdjango.utils.translationimportgettextas_
-
-__all__=("LockHandler","LockException")
-
-WARNING_LOG=settings.LOCKWARNING_LOG_FILE
-_LOCK_HANDLER=None
-
-
-#
-# Exception class. This will be raised
-# by errors in lock definitions.
-#
-
-
-
[docs]classLockException(Exception):
- """
- Raised during an error in a lock.
-
- """
-
- pass
[docs]classLockHandler:
- """
- This handler should be attached to all objects implementing
- permission checks, under the property 'lockhandler'.
-
- """
-
-
[docs]def__init__(self,obj):
- """
- Loads and pre-caches all relevant locks and their functions.
-
- Args:
- obj (object): The object on which the lockhandler is
- defined.
-
- """
- ifnot_LOCKFUNCS:
- _cache_lockfuncs()
- self.obj=obj
- self.locks={}
- try:
- self.reset()
- exceptLockExceptionaserr:
- logger.log_trace(err)
-
- def__str__(self):
- return";".join(self.locks[key][2]forkeyinsorted(self.locks))
-
- def_log_error(self,message):
- "Try to log errors back to object"
- raiseLockException(message)
-
- def_parse_lockstring(self,storage_lockstring):
- """
- Helper function. This is normally only called when the
- lockstring is cached and does preliminary checking. locks are
- stored as a string
-
- atype:[NOT] lock()[[ AND|OR [NOT] lock()[...]];atype...
-
- Args:
- storage_locksring (str): The lockstring to parse.
-
- """
- locks={}
- ifnotstorage_lockstring:
- returnlocks
- duplicates=0
- elist=[]# errors
- wlist=[]# warnings
- forraw_lockstringinstorage_lockstring.split(";"):
- ifnotraw_lockstring:
- continue
- lock_funcs=[]
- try:
- access_type,rhs=(part.strip()forpartinraw_lockstring.split(":",1))
- exceptValueError:
- logger.log_trace()
- returnlocks
-
- # parse the lock functions and separators
- funclist=_RE_FUNCS.findall(rhs)
- evalstring=rhs
- forpatternin("AND","OR","NOT"):
- evalstring=re.sub(r"\b%s\b"%pattern,pattern.lower(),evalstring)
- nfuncs=len(funclist)
- forfuncstringinfunclist:
- funcname,rest=(part.strip().strip(")")forpartinfuncstring.split("(",1))
- func=_LOCKFUNCS.get(funcname,None)
- ifnotcallable(func):
- elist.append(_("Lock: lock-function '{lockfunc}' is not available.").format(
- lockfunc=funcstring))
- continue
- args=list(arg.strip()forarginrest.split(",")ifargand"="notinarg)
- kwargs=dict(
- [
- (part.strip()forpartinarg.split("=",1))
- forarginrest.split(",")
- ifargand"="inarg
- ]
- )
- lock_funcs.append((func,args,kwargs))
- evalstring=evalstring.replace(funcstring,"%s")
- iflen(lock_funcs)<nfuncs:
- continue
- try:
- # purge the eval string of any superfluous items, then test it
- evalstring=" ".join(_RE_OK.findall(evalstring))
- eval(evalstring%tuple(Trueforfuncinfunclist),{},{})
- exceptException:
- elist.append(
- _("Lock: definition '{lock_string}' has syntax errors.").format(
- lock_string=raw_lockstring
- )
- )
- continue
- ifaccess_typeinlocks:
- duplicates+=1
- 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:
- # a warning text was set, it's not an error, so only report
- logger.log_file("\n".join(wlist),WARNING_LOG)
- ifelist:
- # an error text was set, raise exception.
- raiseLockException("\n".join(elist))
- # return the gathered locks in an easily executable form
- returnlocks
-
- 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()])
-
-
[docs]defcache_lock_bypass(self,obj):
- """
- We cache superuser bypass checks here for efficiency. This
- needs to be re-run when an account is assigned to a character.
- We need to grant access to superusers. We need to check both
- directly on the object (accounts), through obj.account and using
- the get_account() method (this sits on serversessions, in some
- rare cases where a check is done before the login process has
- yet been fully finalized)
-
- Args:
- obj (object): This is checked for the `is_superuser` property.
-
- """
- self.lock_bypass=hasattr(obj,"is_superuser")andobj.is_superuser
-
-
[docs]defadd(self,lockstring,validate_only=False):
- """
- Add a new lockstring to handler.
-
- Args:
- lockstring (str or list): A string on the form
- `"<access_type>:<functions>"`. Multiple access types
- should be separated by semicolon (`;`). Alternatively,
- a list with lockstrings.
- validate_only (bool, optional): If True, validate the lockstring but
- don't actually store it.
- Returns:
- success (bool): The outcome of the addition, `False` on
- error. If `validate_only` is True, this will be a tuple
- (bool, error), for pass/fail and a string error.
-
- """
- ifisinstance(lockstring,str):
- lockdefs=lockstring.split(";")
- else:
- lockdefs=[lockdefforlocksinlockstringforlockdefinlocks.split(";")]
- lockstring=";".join(lockdefs)
-
- err=""
- # sanity checks
- forlockdefinlockdefs:
- if":"notinlockdef:
- err=_("Lock: '{lockdef}' contains no colon (:).").format(lockdef=lockdef)
- ifvalidate_only:
- returnFalse,err
- else:
- self._log_error(err)
- returnFalse
- access_type,rhs=[part.strip()forpartinlockdef.split(":",1)]
- ifnotaccess_type:
- err=_(
- "Lock: '{lockdef}' has no access_type ""(left-side of colon is empty)."
- ).format(lockdef=lockdef)
- ifvalidate_only:
- returnFalse,err
- else:
- self._log_error(err)
- returnFalse
- ifrhs.count("(")!=rhs.count(")"):
- err=_("Lock: '{lockdef}' has mismatched parentheses.").format(lockdef=lockdef)
- ifvalidate_only:
- returnFalse,err
- else:
- self._log_error(err)
- returnFalse
- ifnot_RE_FUNCS.findall(rhs):
- err=_("Lock: '{lockdef}' has no valid lock functions.").format(lockdef=lockdef)
- ifvalidate_only:
- returnFalse,err
- else:
- self._log_error(err)
- returnFalse
- ifvalidate_only:
- returnTrue,None
- # get the lock string
- storage_lockstring=self.obj.lock_storage
- ifstorage_lockstring:
- storage_lockstring=storage_lockstring+";"+lockstring
- else:
- storage_lockstring=lockstring
- # cache the locks will get rid of eventual doublets
- self._cache_locks(storage_lockstring)
- self._save_locks()
- returnTrue
-
-
[docs]defvalidate(self,lockstring):
- """
- Validate lockstring syntactically, without saving it.
-
- Args:
- lockstring (str): Lockstring to validate.
- Returns:
- valid (bool): If validation passed or not.
-
- """
- returnself.add(lockstring,validate_only=True)
-
-
[docs]defreplace(self,lockstring):
- """
- Replaces the lockstring entirely.
-
- Args:
- lockstring (str): The new lock definition.
-
- Return:
- success (bool): False if an error occurred.
-
- Raises:
- LockException: If a critical error occurred.
- If so, the old string is recovered.
-
- """
- old_lockstring=str(self)
- self.clear()
- try:
- returnself.add(lockstring)
- exceptLockException:
- self.add(old_lockstring)
- raise
-
-
[docs]defget(self,access_type=None):
- """
- Get the full lockstring or the lockstring of a particular
- access type.
-
- Args:
- access_type (str, optional):
-
- Returns:
- lockstring (str): The matched lockstring, or the full
- lockstring if no access_type was given.
- """
-
- ifaccess_type:
- returnself.locks.get(access_type,["","",""])[2]
- returnstr(self)
-
-
[docs]defall(self):
- """
- Return all lockstrings
-
- Returns:
- lockstrings (list): All separate lockstrings
-
- """
- returnstr(self).split(";")
-
-
[docs]defremove(self,access_type):
- """
- Remove a particular lock from the handler
-
- Args:
- access_type (str): The type of lock to remove.
-
- Returns:
- success (bool): If the access_type was not found
- in the lock, this returns `False`.
-
- """
- ifaccess_typeinself.locks:
- delself.locks[access_type]
- self._save_locks()
- returnTrue
- returnFalse
-
- delete=remove# alias for historical reasons
-
-
[docs]defclear(self):
- """
- Remove all locks in the handler.
-
- """
- self.locks={}
- self.lock_storage=""
- self._save_locks()
-
-
[docs]defreset(self):
- """
- Set the reset flag, so the the lock will be re-cached at next
- checking. This is usually called by @reload.
-
- """
- self._cache_locks(self.obj.lock_storage)
- self.cache_lock_bypass(self.obj)
-
-
[docs]defappend(self,access_type,lockstring,op="or"):
- """
- Append a lock definition to access_type if it doesn't already exist.
-
- Args:
- access_type (str): Access type.
- lockstring (str): A valid lockstring, without the operator to
- link it to an eventual existing lockstring.
- op (str): An operator 'and', 'or', 'and not', 'or not' used
- for appending the lockstring to an existing access-type.
- Note:
- The most common use of this method is for use in commands where
- the user can specify their own lockstrings. This method allows
- the system to auto-add things like Admin-override access.
-
- """
- old_lockstring=self.get(access_type)
- ifnotlockstring.strip().lower()inold_lockstring.lower():
- lockstring="{old}{op}{new}".format(
- old=old_lockstring,op=op,new=lockstring.strip()
- )
- self.add(lockstring)
-
-
[docs]defcheck(self,accessing_obj,access_type,default=False,no_superuser_bypass=False):
- """
- Checks a lock of the correct type by passing execution off to
- the lock function(s).
-
- Args:
- accessing_obj (object): The object seeking access.
- access_type (str): The type of access wanted.
- default (bool, optional): If no suitable lock type is
- found, default to this result.
- no_superuser_bypass (bool): Don't use this unless you
- really, really need to, it makes supersusers susceptible
- to the lock check.
-
- Notes:
- A lock is executed in the follwoing way:
-
- Parsing the lockstring, we (during cache) extract the valid
- lock functions and store their function objects in the right
- order along with their args/kwargs. These are now executed in
- sequence, creating a list of True/False values. This is put
- into the evalstring, which is a string of AND/OR/NOT entries
- separated by placeholders where each function result should
- go. We just put those results in and evaluate the string to
- get a final, combined True/False value for the lockstring.
-
- The important bit with this solution is that the full
- lockstring is never blindly evaluated, and thus there (should
- be) no way to sneak in malign code in it. Only "safe" lock
- functions (as defined by your settings) are executed.
-
- """
- try:
- # check if the lock should be bypassed (e.g. superuser status)
- ifaccessing_obj.locks.lock_bypassandnotno_superuser_bypass:
- returnTrue
- exceptAttributeError:
- # happens before session is initiated.
- ifnotno_superuser_bypassand(
- (hasattr(accessing_obj,"is_superuser")andaccessing_obj.is_superuser)
- or(
- hasattr(accessing_obj,"account")
- andhasattr(accessing_obj.account,"is_superuser")
- andaccessing_obj.account.is_superuser
- )
- or(
- hasattr(accessing_obj,"get_account")
- and(
- notaccessing_obj.get_account()oraccessing_obj.get_account().is_superuser
- )
- )
- ):
- returnTrue
-
- # no superuser or bypass -> normal lock operation
- ifaccess_typeinself.locks:
- # we have a lock, test it.
- evalstring,func_tup,raw_string=self.locks[access_type]
- # execute all lock funcs in the correct order, producing a tuple of True/False results.
- true_false=tuple(
- bool(tup[0](accessing_obj,self.obj,*tup[1],**tup[2]))fortupinfunc_tup
- )
- # the True/False tuple goes into evalstring, which combines them
- # with AND/OR/NOT in order to get the final result.
- returneval(evalstring%true_false)
- else:
- returndefault
-
- def_eval_access_type(self,accessing_obj,locks,access_type):
- """
- Helper method for evaluating the access type using eval().
-
- Args:
- accessing_obj (object): Object seeking access.
- locks (dict): The pre-parsed representation of all access-types.
- access_type (str): An access-type key to evaluate.
-
- """
- evalstring,func_tup,raw_string=locks[access_type]
- true_false=tuple(tup[0](accessing_obj,self.obj,*tup[1],**tup[2])fortupinfunc_tup)
- returneval(evalstring%true_false)
-
-
[docs]defcheck_lockstring(
- self,accessing_obj,lockstring,no_superuser_bypass=False,default=False,access_type=None
- ):
- """
- Do a direct check against a lockstring ('atype:func()..'),
- without any intermediary storage on the accessed object.
-
- Args:
- accessing_obj (object or None): The object seeking access.
- Importantly, this can be left unset if the lock functions
- don't access it, no updating or storage of locks are made
- against this object in this method.
- lockstring (str): Lock string to check, on the form
- `"access_type:lock_definition"` where the `access_type`
- part can potentially be set to a dummy value to just check
- a lock condition.
- no_superuser_bypass (bool, optional): Force superusers to heed lock.
- default (bool, optional): Fallback result to use if `access_type` is set
- but no such `access_type` is found in the given `lockstring`.
- access_type (str, bool): If set, only this access_type will be looked up
- among the locks defined by `lockstring`.
-
- Return:
- access (bool): If check is passed or not.
-
- """
- try:
- ifaccessing_obj.locks.lock_bypassandnotno_superuser_bypass:
- returnTrue
- exceptAttributeError:
- ifno_superuser_bypassand(
- (hasattr(accessing_obj,"is_superuser")andaccessing_obj.is_superuser)
- or(
- hasattr(accessing_obj,"account")
- andhasattr(accessing_obj.account,"is_superuser")
- andaccessing_obj.account.is_superuser
- )
- or(
- hasattr(accessing_obj,"get_account")
- and(
- notaccessing_obj.get_account()oraccessing_obj.get_account().is_superuser
- )
- )
- ):
- returnTrue
- if":"notinlockstring:
- lockstring="%s:%s"%("_dummy",lockstring)
-
- locks=self._parse_lockstring(lockstring)
-
- ifaccess_type:
- ifaccess_typenotinlocks:
- returndefault
- else:
- returnself._eval_access_type(accessing_obj,locks,access_type)
- else:
- # if no access types was given and multiple locks were
- # embedded in the lockstring we assume all must be true
- returnall(
- self._eval_access_type(accessing_obj,locks,access_type)foraccess_typeinlocks
- )
-
-
-# convenience access function
-
-# dummy to be able to call check_lockstring from the outside
-
-
-class_ObjDummy:
- lock_storage=""
-
-
-defcheck_lockstring(
- accessing_obj,lockstring,no_superuser_bypass=False,default=False,access_type=None
-):
- """
- Do a direct check against a lockstring ('atype:func()..'),
- without any intermediary storage on the accessed object.
-
- Args:
- accessing_obj (object or None): The object seeking access.
- Importantly, this can be left unset if the lock functions
- don't access it, no updating or storage of locks are made
- against this object in this method.
- lockstring (str): Lock string to check, on the form
- `"access_type:lock_definition"` where the `access_type`
- part can potentially be set to a dummy value to just check
- a lock condition.
- no_superuser_bypass (bool, optional): Force superusers to heed lock.
- default (bool, optional): Fallback result to use if `access_type` is set
- but no such `access_type` is found in the given `lockstring`.
- access_type (str, bool): If set, only this access_type will be looked up
- among the locks defined by `lockstring`.
-
- Return:
- access (bool): If check is passed or not.
-
- """
- global_LOCK_HANDLER
- ifnot_LOCK_HANDLER:
- _LOCK_HANDLER=LockHandler(_ObjDummy())
- return_LOCK_HANDLER.check_lockstring(
- accessing_obj,
- lockstring,
- no_superuser_bypass=no_superuser_bypass,
- default=default,
- 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):
- """
- Validate so lockstring is on a valid form.
-
- Args:
- lockstring (str): Lockstring to validate.
-
- Returns:
- is_valid (bool): If the lockstring is valid or not.
- error (str or None): A string describing the error, or None
- if no error was found.
-
- """
- global_LOCK_HANDLER
- ifnot_LOCK_HANDLER:
- _LOCK_HANDLER=LockHandler(_ObjDummy())
- return_LOCK_HANDLER.validate(lockstring)
-
-
-defget_all_lockfuncs():
- """
- Get a dict of available lock funcs.
-
- Returns:
- lockfuncs (dict): Mapping {lockfuncname:func}.
-
- """
- ifnot_LOCKFUNCS:
- _cache_lockfuncs()
- return_LOCKFUNCS
-
-
-def_test():
- # testing
-
- classTestObj(object):
- pass
-
- importpdb
-
- obj1=TestObj()
- obj2=TestObj()
-
- # obj1.lock_storage = "owner:dbref(#4);edit:dbref(#5) or perm(Admin);examine:perm(Builder);delete:perm(Admin);get:all()"
- # obj1.lock_storage = "cmd:all();admin:id(1);listen:all();send:all()"
- obj1.lock_storage="listen:perm(Developer)"
-
- pdb.set_trace()
- obj1.locks=LockHandler(obj1)
- obj2.permissions.add("Developer")
- obj2.id=4
-
- # obj1.locks.add("edit:attr(test)")
-
- print("comparing obj2.permissions (%s) vs obj1.locks (%s)"%(obj2.permissions,obj1.locks))
- print(obj1.locks.check(obj2,"owner"))
- print(obj1.locks.check(obj2,"edit"))
- print(obj1.locks.check(obj2,"examine"))
- print(obj1.locks.check(obj2,"delete"))
- print(obj1.locks.check(obj2,"get"))
- print(obj1.locks.check(obj2,"listen"))
-
-"""
-Custom manager for Objects.
-"""
-importre
-fromdjango.db.modelsimportQ
-fromdjango.confimportsettings
-fromdjango.db.models.fieldsimportexceptions
-fromevennia.typeclasses.managersimportTypedObjectManager,TypeclassManager
-fromevennia.utils.utilsimportis_iter,make_iter,string_partial_matching
-fromevennia.utils.utilsimportclass_from_module,dbid_to_obj
-fromevennia.serverimportsignals
-
-
-__all__=("ObjectManager","ObjectDBManager")
-_GA=object.__getattribute__
-
-# delayed import
-_ATTR=None
-
-_MULTIMATCH_REGEX=re.compile(settings.SEARCH_MULTIMATCH_REGEX,re.I+re.U)
-
-# Try to use a custom way to parse id-tagged multimatches.
-
-
-
[docs]classObjectDBManager(TypedObjectManager):
- """
- This ObjectManager implements methods for searching
- and manipulating Objects directly from the database.
-
- Evennia-specific search methods (will return Typeclasses or
- lists of Typeclasses, whereas Django-general methods will return
- Querysets or database objects).
-
- dbref (converter)
- dbref_search
- get_dbref_range
- object_totals
- typeclass_search
- get_object_with_account
- get_objs_with_key_and_typeclass
- get_objs_with_attr
- get_objs_with_attr_match
- get_objs_with_db_property
- get_objs_with_db_property_match
- get_objs_with_key_or_alias
- get_contents
- object_search (interface to many of the above methods,
- equivalent to evennia.search_object)
- copy_object
-
- """
-
- #
- # ObjectManager Get methods
- #
-
- # account related
-
-
[docs]defget_object_with_account(self,ostring,exact=True,candidates=None):
- """
- Search for an object based on its account's name or dbref.
-
- Args:
- ostring (str or int): Search criterion or dbref. Searching
- for an account is sometimes initiated by appending an `*` to
- the beginning of the search criterion (e.g. in
- local_and_global_search). This is stripped here.
- exact (bool, optional): Require an exact account match.
- candidates (list, optional): Only search among this list of possible
- object candidates.
-
- Return:
- match (query): Matching query.
-
- """
- ostring=str(ostring).lstrip("*")
- # simplest case - search by dbref
- dbref=self.dbref(ostring)
- ifdbref:
- try:
- returnself.get(db_account__id=dbref)
- exceptself.model.DoesNotExist:
- pass
-
- # not a dbref. Search by name.
- cand_restriction=(
- candidatesisnotNone
- andQ(pk__in=[_GA(obj,"id")forobjinmake_iter(candidates)ifobj])
- orQ()
- )
- ifexact:
- returnself.filter(cand_restriction&Q(db_account__username__iexact=ostring)).order_by(
- "id"
- )
- else:# fuzzy matching
- obj_cands=self.select_related().filter(
- cand_restriction&Q(db_account__username__istartswith=ostring)
- )
- acct_cands=[obj.accountforobjinobj_cands]
-
- ifobj_cands:
- index_matches=string_partial_matching(
- [acct.keyforacctinacct_cands],ostring,ret_index=True
- )
- acct_cands=[acct_cands[i].idforiinindex_matches]
- returnobj_cands.filter(db_account__id__in=acct_cands).order_by("id")
-
-
[docs]defget_objs_with_key_and_typeclass(self,oname,otypeclass_path,candidates=None):
- """
- Returns objects based on simultaneous key and typeclass match.
-
- Args:
- oname (str): Object key to search for
- otypeclass_path (str): Full Python path to tyepclass to search for
- candidates (list, optional): Only match among the given list of candidates.
-
- Returns:
- matches (query): The matching objects.
- """
- cand_restriction=(
- candidatesisnotNone
- andQ(pk__in=[_GA(obj,"id")forobjinmake_iter(candidates)ifobj])
- orQ()
- )
- returnself.filter(
- cand_restriction&Q(db_key__iexact=oname,db_typeclass_path__exact=otypeclass_path)
- ).order_by("id")
-
- # attr/property related
-
-
[docs]defget_objs_with_attr(self,attribute_name,candidates=None):
- """
- Get objects based on having a certain Attribute defined.
-
- Args:
- attribute_name (str): Attribute name to search for.
- candidates (list, optional): Only match among the given list of object
- candidates.
-
- Returns:
- matches (query): All objects having the given attribute_name defined at all.
-
- """
- cand_restriction=(
- candidatesisnotNoneandQ(id__in=[obj.idforobjincandidates])orQ()
- )
- returnself.filter(cand_restriction&Q(db_attributes__db_key=attribute_name)).order_by(
- "id"
- )
-
-
[docs]defget_objs_with_attr_value(
- self,attribute_name,attribute_value,candidates=None,typeclasses=None
- ):
- """
- Get all objects having the given attrname set to the given value.
-
- Args:
- attribute_name (str): Attribute key to search for.
- 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.
-
- Returns:
- Queryset: Iterable with 0, 1 or more matches fullfilling both the `attribute_name` and
- `attribute_value` criterions.
-
- Notes:
- This uses the Attribute's PickledField to transparently search the database by matching
- the internal representation. This is reasonably effective but since Attribute values
- cannot be indexed, searching by Attribute key is to be preferred whenever possible.
-
- """
- cand_restriction=(
- candidatesisnotNone
- andQ(pk__in=[_GA(obj,"id")forobjinmake_iter(candidates)ifobj])
- orQ()
- )
- type_restriction=typeclassesandQ(db_typeclass_path__in=make_iter(typeclasses))orQ()
-
- results=self.filter(
- cand_restriction
- &type_restriction
- &Q(db_attributes__db_key=attribute_name)
- &Q(db_attributes__db_value=attribute_value)
- ).order_by("id")
- returnresults
-
-
[docs]defget_objs_with_db_property(self,property_name,candidates=None):
- """
- Get all objects having a given db field property.
-
- Args:
- property_name (str): The name of the field to match for.
- candidates (list, optional): Only search among th egiven candidates.
-
- Returns:
- matches (list): The found matches.
-
- """
- property_name="db_%s"%property_name.lstrip("db_")
- cand_restriction=(
- candidatesisnotNone
- andQ(pk__in=[_GA(obj,"id")forobjinmake_iter(candidates)ifobj])
- orQ()
- )
- querykwargs={property_name:None}
- try:
- returnlist(self.filter(cand_restriction).exclude(Q(**querykwargs)).order_by("id"))
- exceptexceptions.FieldError:
- return[]
-
-
[docs]defget_objs_with_db_property_value(
- self,property_name,property_value,candidates=None,typeclasses=None
- ):
- """
- Get objects with a specific field name and value.
-
- Args:
- property_name (str): Field name to search for.
- property_value (any): Value required for field with `property_name` to have.
- candidates (list, optional): List of objects to limit search to.
- typeclasses (list, optional): List of typeclass-path strings to restrict matches with
-
- Returns:
- Queryset: Iterable with 0, 1 or more matches.
-
- """
- ifisinstance(property_name,str):
- ifnotproperty_name.startswith("db_"):
- property_name="db_%s"%property_name
- querykwargs={property_name:property_value}
- cand_restriction=(
- candidatesisnotNone
- andQ(pk__in=[_GA(obj,"id")forobjinmake_iter(candidates)ifobj])
- orQ()
- )
- type_restriction=typeclassesandQ(db_typeclass_path__in=make_iter(typeclasses))orQ()
- try:
- returnself.filter(
- cand_restriction&type_restriction&Q(**querykwargs)).order_by("id")
- exceptexceptions.FieldError:
- returnself.none()
- exceptValueError:
- fromevennia.utilsimportlogger
-
- logger.log_err(
- "The property '%s' does not support search criteria of the type %s."
- %(property_name,type(property_value))
- )
- returnself.none()
-
-
[docs]defget_contents(self,location,excludeobj=None):
- """
- Get all objects that has a location set to this one.
-
- Args:
- location (Object): Where to get contents from.
- excludeobj (Object or list, optional): One or more objects
- to exclude from the match.
-
- Returns:
- Queryset: Iterable with 0, 1 or more matches.
-
- """
- exclude_restriction=(
- Q(pk__in=[_GA(obj,"id")forobjinmake_iter(excludeobj)])ifexcludeobjelseQ()
- )
- returnself.filter(db_location=location).exclude(exclude_restriction).order_by("id")
-
-
[docs]defget_objs_with_key_or_alias(self,ostring,exact=True,candidates=None,typeclasses=None):
- """
- Args:
- ostring (str): A search criterion.
- exact (bool, optional): Require exact match of ostring
- (still case-insensitive). If `False`, will do fuzzy matching
- using `evennia.utils.utils.string_partial_matching` algorithm.
- candidates (list): Only match among these candidates.
- typeclasses (list): Only match objects with typeclasses having thess path strings.
-
- Returns:
- Queryset: An iterable with 0, 1 or more matches.
-
- """
- ifnotisinstance(ostring,str):
- ifhasattr(ostring,"key"):
- ostring=ostring.key
- else:
- returnself.none()
- ifis_iter(candidates)andnotlen(candidates):
- # if candidates is an empty iterable there can be no matches
- # Exit early.
- returnself.none()
-
- # build query objects
- candidates_id=[_GA(obj,"id")forobjinmake_iter(candidates)ifobj]
- cand_restriction=candidatesisnotNoneandQ(pk__in=candidates_id)orQ()
- type_restriction=typeclassesandQ(db_typeclass_path__in=make_iter(typeclasses))orQ()
- ifexact:
- # exact match - do direct search
- return(
- (
- self.filter(
- cand_restriction
- &type_restriction
- &(
- Q(db_key__iexact=ostring)
- |Q(db_tags__db_key__iexact=ostring)
- &Q(db_tags__db_tagtype__iexact="alias")
- )
- )
- )
- .distinct()
- .order_by("id")
- )
- elifcandidates:
- # fuzzy with candidates
- search_candidates=(
- self.filter(cand_restriction&type_restriction).distinct().order_by("id")
- )
- else:
- # fuzzy without supplied candidates - we select our own candidates
- search_candidates=(
- self.filter(
- type_restriction
- &(Q(db_key__istartswith=ostring)|Q(db_tags__db_key__istartswith=ostring))
- )
- .distinct()
- .order_by("id")
- )
- # fuzzy matching
- key_strings=search_candidates.values_list("db_key",flat=True).order_by("id")
-
- match_ids=[]
- index_matches=string_partial_matching(key_strings,ostring,ret_index=True)
- ifindex_matches:
- # a match by key
- match_ids=[obj.idforind,objinenumerate(search_candidates)
- ifindinindex_matches]
- else:
- # match by alias rather than by key
- search_candidates=search_candidates.filter(
- db_tags__db_tagtype__iexact="alias",db_tags__db_key__icontains=ostring
- ).distinct()
- alias_strings=[]
- alias_candidates=[]
- # TODO create the alias_strings and alias_candidates lists more efficiently?
- forcandidateinsearch_candidates:
- foraliasincandidate.aliases.all():
- alias_strings.append(alias)
- alias_candidates.append(candidate)
- index_matches=string_partial_matching(alias_strings,ostring,ret_index=True)
- ifindex_matches:
- # it's possible to have multiple matches to the same Object, we must weed those out
- match_ids=[alias_candidates[ind].idforindinindex_matches]
- # TODO - not ideal to have to do a second lookup here, but we want to return a queryset
- # rather than a list ... maybe the above queries can be improved.
- returnself.filter(id__in=match_ids)
-
- # main search methods and helper functions
-
-
[docs]defsearch_object(
- self,
- searchdata,
- attribute_name=None,
- typeclass=None,
- candidates=None,
- exact=True,
- use_dbref=True,
- ):
- """
- Search as an object globally or in a list of candidates and
- return results. The result is always an Object. Always returns
- a list.
-
- Args:
- searchdata (str or Object): The entity to match for. This is
- usually a key string but may also be an object itself.
- By default (if no `attribute_name` is set), this will
- search `object.key` and `object.aliases` in order.
- Can also be on the form #dbref, which will (if
- `exact=True`) be matched against primary key.
- attribute_name (str): Use this named Attribute to
- match searchdata against, instead of the defaults. If
- this is the name of a database field (with or without
- the `db_` prefix), that will be matched too.
- typeclass (str or TypeClass): restrict matches to objects
- having this typeclass. This will help speed up global
- searches.
- candidates (list): If supplied, search will
- only be performed among the candidates in this list. A
- common list of candidates is the contents of the
- current location searched.
- exact (bool): Match names/aliases exactly or partially.
- Partial matching matches the beginning of words in the
- names/aliases, using a matching routine to separate
- multiple matches in names with multiple components (so
- "bi sw" will match "Big sword"). Since this is more
- expensive than exact matching, it is recommended to be
- used together with the `candidates` keyword to limit the
- number of possibilities. This value has no meaning if
- searching for attributes/properties.
- use_dbref (bool): If False, bypass direct lookup of a string
- on the form #dbref and treat it like any string.
-
- Returns:
- matches (list): Matching objects
-
- """
-
- def_searcher(searchdata,candidates,typeclass,exact=False):
- """
- Helper method for searching objects. `typeclass` is only used
- for global searching (no candidates)
- """
- ifattribute_name:
- # attribute/property search (always exact).
- matches=self.get_objs_with_db_property_value(
- attribute_name,searchdata,candidates=candidates,typeclasses=typeclass
- )
- ifmatches:
- returnmatches
- returnself.get_objs_with_attr_value(
- attribute_name,searchdata,candidates=candidates,typeclasses=typeclass
- )
- else:
- # normal key/alias search
- returnself.get_objs_with_key_or_alias(
- searchdata,exact=exact,candidates=candidates,typeclasses=typeclass
- )
-
- ifnotsearchdataandsearchdata!=0:
- returnself.none()
-
- iftypeclass:
- # typeclass may also be a list
- typeclasses=make_iter(typeclass)
- fori,typeclassinenumerate(make_iter(typeclasses)):
- ifcallable(typeclass):
- typeclasses[i]="%s.%s"%(typeclass.__module__,typeclass.__name__)
- else:
- typeclasses[i]="%s"%typeclass
- typeclass=typeclasses
-
- ifcandidatesisnotNone:
- ifnotcandidates:
- # candidates is the empty list. This should mean no matches can ever be acquired.
- return[]
- # Convenience check to make sure candidates are really dbobjs
- candidates=[candforcandinmake_iter(candidates)ifcand]
- iftypeclass:
- candidates=[
- candforcandincandidatesif_GA(cand,"db_typeclass_path")intypeclass
- ]
-
- dbref=notattribute_nameandexactanduse_dbrefandself.dbref(searchdata)
- ifdbref:
- # Easiest case - dbref matching (always exact)
- dbref_match=self.dbref_search(dbref)
- ifdbref_match:
- dmatch=dbref_match[0]
- ifnotcandidatesordmatchincandidates:
- returndbref_match
- else:
- returnself.none()
-
- # Search through all possibilities.
- match_number=None
- # always run first check exact - we don't want partial matches
- # if on the form of 1-keyword etc.
- matches=_searcher(searchdata,candidates,typeclass,exact=True)
- ifnotmatches:
- # no matches found - check if we are dealing with N-keyword
- # query - if so, strip it.
- match=_MULTIMATCH_REGEX.match(str(searchdata))
- match_number=None
- ifmatch:
- # strips the number
- match_number,searchdata=match.group("number"),match.group("name")
- match_number=int(match_number)-1
- ifmatch_numberisnotNoneornotexact:
- # run search again, with the exactness set by call
- matches=_searcher(searchdata,candidates,typeclass,exact=exact)
-
- # deal with result
- iflen(matches)==1andmatch_numberisnotNoneandmatch_number!=0:
- # this indicates trying to get a single match with a match-number
- # targeting some higher-number match (like 2-box when there is only
- # one box in the room). This leads to a no-match.
- matches=self.none()
- eliflen(matches)>1andmatch_numberisnotNone:
- # multiple matches, but a number was given to separate them
- if0<=match_number<len(matches):
- # limit to one match (we still want a queryset back)
- # TODO: Can we do this some other way and avoid a second lookup?
- matches=self.filter(id=matches[match_number].id)
- else:
- # a number was given outside of range. This means a no-match.
- matches=self.none()
-
- # return a list (possibly empty)
- returnmatches
[docs]defcopy_object(
- self,
- original_object,
- new_key=None,
- new_location=None,
- new_home=None,
- new_permissions=None,
- new_locks=None,
- new_aliases=None,
- new_destination=None,
- ):
- """
- Create and return a new object as a copy of the original object. All
- will be identical to the original except for the arguments given
- specifically to this method. Object contents will not be copied.
-
- Args:
- original_object (Object): The object to make a copy from.
- new_key (str, optional): Name of the copy, if different
- from the original.
- new_location (Object, optional): Alternate location.
- new_home (Object, optional): Change the home location
- new_aliases (list, optional): Give alternate object
- aliases as a list of strings.
- new_destination (Object, optional): Used only by exits.
-
- Returns:
- copy (Object or None): The copy of `original_object`,
- optionally modified as per the ingoing keyword
- arguments. `None` if an error was encountered.
-
- """
-
- # get all the object's stats
- typeclass_path=original_object.typeclass_path
- ifnotnew_key:
- new_key=original_object.key
- ifnotnew_location:
- new_location=original_object.location
- ifnotnew_home:
- new_home=original_object.home
- ifnotnew_aliases:
- new_aliases=original_object.aliases.all()
- ifnotnew_locks:
- new_locks=original_object.db_lock_storage
- ifnotnew_permissions:
- new_permissions=original_object.permissions.all()
- ifnotnew_destination:
- new_destination=original_object.destination
-
- # create new object
- fromevennia.utilsimportcreate
- fromevennia.scripts.modelsimportScriptDB
-
- new_object=create.create_object(
- typeclass_path,
- key=new_key,
- location=new_location,
- home=new_home,
- permissions=new_permissions,
- locks=new_locks,
- aliases=new_aliases,
- destination=new_destination,
- )
- ifnotnew_object:
- returnNone
-
- # copy over all attributes from old to new.
- attrs=(
- (a.key,a.value,a.category,a.lock_storage)forainoriginal_object.attributes.all()
- )
- new_object.attributes.batch_add(*attrs)
-
- # copy over all cmdsets, if any
- foricmdset,cmdsetinenumerate(original_object.cmdset.all()):
- ificmdset==0:
- new_object.cmdset.add_default(cmdset)
- else:
- new_object.cmdset.add(cmdset)
-
- # copy over all scripts, if any
- forscriptinoriginal_object.scripts.all():
- ScriptDB.objects.copy_script(script,new_obj=new_object)
-
- # copy over all tags, if any
- tags=(
- (t.db_key,t.db_category,t.db_data)fortinoriginal_object.tags.all(return_objs=True)
- )
- new_object.tags.batch_add(*tags)
-
- returnnew_object
-
-
[docs]defclear_all_sessids(self):
- """
- 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)
-
-
[docs]defcreate_object(
- self,
- typeclass=None,
- key=None,
- location=None,
- home=None,
- permissions=None,
- locks=None,
- aliases=None,
- tags=None,
- destination=None,
- report_to=None,
- nohome=False,
- attributes=None,
- nattributes=None,
- ):
- """
-
- Create a new in-game object.
-
- 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.
- location (Object or str): Obj or #dbref to use as the location of the new object.
- 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.
- 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.
- 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.
-
- 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.
-
- """
- typeclass=typeclassiftypeclasselsesettings.BASE_OBJECT_TYPECLASS
-
- # convenience converters to avoid common usage mistake
- permissions=make_iter(permissions)ifpermissionsisnotNoneelseNone
- locks=make_iter(locks)iflocksisnotNoneelseNone
- aliases=make_iter(aliases)ifaliasesisnotNoneelseNone
- tags=make_iter(tags)iftagsisnotNoneelseNone
- attributes=make_iter(attributes)ifattributesisnotNoneelseNone
-
- ifisinstance(typeclass,str):
- # a path is given. Load the actual typeclass
- typeclass=class_from_module(typeclass,settings.TYPECLASS_PATHS)
-
- # Setup input for the create command. We use ObjectDB as baseclass here
- # to give us maximum freedom (the typeclasses will load
- # correctly when each object is recovered).
-
- location=dbid_to_obj(location,self.model)
- destination=dbid_to_obj(destination,self.model)
-
- ifhome:
- home_obj_or_dbref=home
- elifnohome:
- home_obj_or_dbref=None
- else:
- home_obj_or_dbref=settings.DEFAULT_HOME
-
- try:
- home=dbid_to_obj(home_obj_or_dbref,self.model)
- exceptself.model.DoesNotExist:
- ifsettings._TEST_ENVIRONMENT:
- # this happens for databases where the #1 location is flushed during tests
- home=None
- else:
- raiseself.model.DoesNotExist(
- f"settings.DEFAULT_HOME (= '{settings.DEFAULT_HOME}') does not exist, "
- "or the setting is malformed."
- )
-
- # create new instance
- new_object=typeclass(
- db_key=key,
- db_location=location,
- db_destination=destination,
- db_home=home,
- db_typeclass_path=typeclass.path,
- )
- # store the call signature for the signal
- new_object._createdict=dict(
- key=key,
- location=location,
- destination=destination,
- home=home,
- typeclass=typeclass.path,
- permissions=permissions,
- locks=locks,
- aliases=aliases,
- tags=tags,
- report_to=report_to,
- nohome=nohome,
- attributes=attributes,
- nattributes=nattributes,
- )
- # this will trigger the save signal which in turn calls the
- # at_first_save hook on the typeclass, where the _createdict can be
- # used.
- new_object.save()
-
- signals.SIGNAL_OBJECT_POST_CREATE.send(sender=new_object)
-
- returnnew_object
-"""
-This module defines the database models for all in-game objects, that
-is, all objects that has an actual existence in-game.
-
-Each database object is 'decorated' with a 'typeclass', a normal
-python class that implements all the various logics needed by the game
-in question. Objects created of this class transparently communicate
-with its related database object for storing all attributes. The
-admin should usually not have to deal directly with this database
-object layer.
-
-Attributes are separate objects that store values persistently onto
-the database object. Like everything else, they can be accessed
-transparently through the decorating TypeClass.
-"""
-fromcollectionsimportdefaultdict
-fromdjango.confimportsettings
-fromdjango.dbimportmodels
-fromdjango.core.exceptionsimportObjectDoesNotExist
-fromdjango.core.validatorsimportvalidate_comma_separated_integer_list
-
-fromevennia.typeclasses.modelsimportTypedObject
-fromevennia.objects.managerimportObjectDBManager
-fromevennia.utilsimportlogger
-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
- for object-cmdsets). It is stored on the 'contents_cache' property
- of the ObjectDB.
- """
-
-
[docs]def__init__(self,obj):
- """
- Sets up the contents handler.
-
- Args:
- obj (Object): The object on which the
- handler is defined
-
- """
- self.obj=obj
- self._pkcache=set()
- self._idcache=obj.__class__.__instance_cache__
- self._typecache=defaultdict(set)
- self.init()
-
-
[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
-
- """
- 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:
- # this can happen if the idmapper cache was cleared for an object
- # in the contents cache. If so we need to re-initialize and try again.
- self.init()
- try:
- return[self._idcache[pk]forpkinpks]
- exceptKeyError:
- # 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]defadd(self,obj):
- """
- Add a new object to this location
-
- Args:
- obj (Object): object to add
-
- """
- self._pkcache.add(obj.pk)
- forctypeinobj._content_types:
- self._typecache[ctype].add(obj.pk)
-
-
[docs]defremove(self,obj):
- """
- Remove object from this location
-
- Args:
- obj (Object): object to remove
-
- """
- try:
- self._pkcache.remove(obj.pk)
- exceptKeyError:
- # not in pk cache, but can happen deletions happens
- # remotely from out-of-thread.
- pass
- forctypeinobj._content_types:
- ifobj.pkinself._typecache[ctype]:
- self._typecache[ctype].remove(obj.pk)
-
-
[docs]defclear(self):
- """
- Clear the contents cache and re-initialize
-
- """
- self._pkcache={}
- self._typecache=defaultdict(set)
- self.init()
[docs]classObjectDB(TypedObject):
- """
- All objects in the game use the ObjectDB model to store
- data in the database. This is handled transparently through
- the typeclass system.
-
- Note that the base objectdb is very simple, with
- few defined fields. Use attributes to extend your
- type class with new database-stored variables.
-
- The TypedObject supplies the following (inherited) properties:
-
- - key - main name
- - name - alias for key
- - db_typeclass_path - the path to the decorating typeclass
- - db_date_created - time stamp of object creation
- - permissions - perm strings
- - locks - lock definitions (handler)
- - dbref - #id of object
- - db - persistent attribute storage
- - ndb - non-persistent attribute storage
-
- The ObjectDB adds the following properties:
-
- - account - optional connected account (always together with sessid)
- - sessid - optional connection session id (always together with account)
- - location - in-game location of object
- - home - safety location for object (handler)
- - scripts - scripts assigned to object (handler from typeclass)
- - cmdset - active cmdset on object (handler from typeclass)
- - aliases - aliases for this object (property)
- - nicks - nicknames for *other* things in Evennia (handler)
- - sessions - sessions connected to this object (see also account)
- - has_account - bool if an active account is currently connected
- - contents - other objects having this object as location
- - exits - exits from this object
-
- """
-
- #
- # ObjectDB Database model setup
- #
- #
- # inherited fields (from TypedObject):
- # db_key (also 'name' works), db_typeclass_path, db_date_created,
- # db_permissions
- #
- # These databse fields (including the inherited ones) should normally be
- # managed by their corresponding wrapper properties, named same as the
- # field, but without the db_* prefix (e.g. the db_key field is set with
- # self.key instead). The wrappers are created at the metaclass level and
- # will automatically save and cache the data more efficiently.
-
- # If this is a character object, the account is connected here.
- db_account=models.ForeignKey(
- "accounts.AccountDB",
- null=True,
- verbose_name="account",
- on_delete=models.SET_NULL,
- help_text="an Account connected to this object, if any.",
- )
-
- # the session id associated with this account, if any
- db_sessid=models.CharField(
- null=True,
- max_length=32,
- validators=[validate_comma_separated_integer_list],
- verbose_name="session id",
- help_text="csv list of session ids of connected Account, if any.",
- )
- # The location in the game world. Since this one is likely
- # to change often, we set this with the 'location' property
- # to transparently handle Typeclassing.
- db_location=models.ForeignKey(
- "self",
- related_name="locations_set",
- db_index=True,
- on_delete=models.SET_NULL,
- blank=True,
- null=True,
- verbose_name="game location",
- )
- # a safety location, this usually don't change much.
- db_home=models.ForeignKey(
- "self",
- related_name="homes_set",
- on_delete=models.SET_NULL,
- blank=True,
- null=True,
- verbose_name="home location",
- )
- # destination of this object - primarily used by exits.
- db_destination=models.ForeignKey(
- "self",
- related_name="destinations_set",
- db_index=True,
- on_delete=models.SET_NULL,
- blank=True,
- null=True,
- verbose_name="destination",
- help_text="a destination, used only by exit objects.",
- )
- # database storage of persistant cmdsets.
- db_cmdset_storage=models.CharField(
- "cmdset",
- max_length=255,
- null=True,
- blank=True,
- help_text="optional python path to a cmdset class.",
- )
-
- # Database manager
- objects=ObjectDBManager()
-
- # defaults
- __settingsclasspath__=settings.BASE_OBJECT_TYPECLASS
- __defaultclasspath__="evennia.objects.objects.DefaultObject"
- __applabel__="objects"
-
-
-
- # cmdset_storage property handling
- def__cmdset_storage_get(self):
- """getter"""
- storage=self.db_cmdset_storage
- return[path.strip()forpathinstorage.split(",")]ifstorageelse[]
-
- def__cmdset_storage_set(self,value):
- """setter"""
- self.db_cmdset_storage=",".join(str(val).strip()forvalinmake_iter(value))
- self.save(update_fields=["db_cmdset_storage"])
-
- def__cmdset_storage_del(self):
- """deleter"""
- self.db_cmdset_storage=None
- self.save(update_fields=["db_cmdset_storage"])
-
- cmdset_storage=property(__cmdset_storage_get,__cmdset_storage_set,__cmdset_storage_del)
-
- # location getsetter
- def__location_get(self):
- """Get location"""
- returnself.db_location
-
- def__location_set(self,location):
- """Set location, checking for loops and allowing dbref"""
- ifisinstance(location,(str,int)):
- # allow setting of #dbref
- dbid=dbref(location,reqhash=False)
- ifdbid:
- try:
- location=ObjectDB.objects.get(id=dbid)
- exceptObjectDoesNotExist:
- # maybe it is just a name that happens to look like a dbid
- pass
- try:
-
- defis_loc_loop(loc,depth=0):
- """Recursively traverse target location, trying to catch a loop."""
- ifdepth>10:
- returnNone
- elifloc==self:
- raiseRuntimeError
- eliflocisNone:
- raiseRuntimeWarning
- returnis_loc_loop(loc.db_location,depth+1)
-
- try:
- is_loc_loop(location)
- exceptRuntimeWarning:
- # we caught an infinite location loop!
- # (location1 is in location2 which is in location1 ...)
- pass
-
- # if we get to this point we are ready to change location
-
- old_location=self.db_location
-
- # this is checked in _db_db_location_post_save below
- self._safe_contents_update=True
-
- # actually set the field (this will error if location is invalid)
- self.db_location=location
- self.save(update_fields=["db_location"])
-
- # remove the safe flag
- delself._safe_contents_update
-
- # update the contents cache
- ifold_location:
- old_location.contents_cache.remove(self)
- ifself.db_location:
- self.db_location.contents_cache.add(self)
-
- exceptRuntimeError:
- errmsg="Error: %s.location = %s creates a location loop."%(self.key,location)
- raiseRuntimeError(errmsg)
- exceptException:
- # raising here gives more info for now
- raise
- # errmsg = "Error (%s): %s is not a valid location." % (str(e), location)
- # raise RuntimeError(errmsg)
- return
-
- def__location_del(self):
- """Cleanly delete the location reference"""
- self.db_location=None
- self.save(update_fields=["db_location"])
-
- location=property(__location_get,__location_set,__location_del)
-
-
[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
- _safe_contents_update to know if the save was triggered via
- the location handler (which updates the contents cache) or
- not.
-
- Args:
- new (bool): Set if this location has not yet been saved before.
-
- """
- ifnothasattr(self,"_safe_contents_update"):
- # changed/set outside of the location handler
- ifnew:
- # if new, there is no previous location to worry about
- ifself.db_location:
- self.db_location.contents_cache.add(self)
- else:
- # Since we cannot know at this point was old_location was, we
- # trigger a full-on contents_cache update here.
- logger.log_warn(
- "db_location direct save triggered contents_cache.init() for all objects!"
- )
- [o.contents_cache.init()foroinself.__dbclass__.get_all_cached_instances()]
-"""
-This module defines the basic `DefaultObject` and its children
-`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).
-
-"""
-importtime
-fromcollectionsimportdefaultdict
-
-importinflect
-fromdjango.confimportsettings
-fromdjango.utils.translationimportgettextas_
-
-fromevennia.commandsimportcmdset
-fromevennia.commands.cmdsethandlerimportCmdSetHandler
-fromevennia.objects.managerimportObjectManager
-fromevennia.objects.modelsimportObjectDB
-fromevennia.scripts.scripthandlerimportScriptHandler
-fromevennia.typeclasses.attributesimportModelAttributeBackend,NickHandler
-fromevennia.typeclasses.modelsimportTypeclassBase
-fromevennia.utilsimportansi,create,funcparser,logger,search
-fromevennia.utils.utilsimport(class_from_module,is_iter,lazy_property,
- list_to_string,make_iter,to_str,
- variable_from_module)
-
-_INFLECT=inflect.engine()
-_MULTISESSION_MODE=settings.MULTISESSION_MODE
-
-_ScriptDB=None
-_SESSIONS=None
-_CMDHANDLER=None
-
-_AT_SEARCH_RESULT=variable_from_module(*settings.SEARCH_AT_RESULT.rsplit(".",1))
-_COMMAND_DEFAULT_CLASS=class_from_module(settings.COMMAND_DEFAULT_CLASS)
-# the sessid_max is based on the length of the db_sessid csv field (excluding commas)
-_SESSID_MAX=16if_MULTISESSION_MODEin(1,3)else1
-
-# init the actor-stance funcparser for msg_contents
-_MSG_CONTENTS_PARSER=funcparser.FuncParser(funcparser.ACTOR_STANCE_CALLABLES)
-
-
-
[docs]classObjectSessionHandler:
- """
- Handles the get/setting of the sessid comma-separated integer field
-
- """
-
-
[docs]def__init__(self,obj):
- """
- Initializes the handler.
-
- Args:
- obj (Object): The object on which the handler is defined.
-
- """
- self.obj=obj
- self._sessid_cache=[]
- self._recache()
-
- def_recache(self):
- global_SESSIONS
- ifnot_SESSIONS:
- fromevennia.server.sessionhandlerimportSESSIONSas_SESSIONS
- self._sessid_cache=list(
- set(int(val)forvalin(self.obj.db_sessidor"").split(",")ifval)
- )
- ifany(sessidforsessidinself._sessid_cacheifsessidnotin_SESSIONS):
- # cache is out of sync with sessionhandler! Only retain the ones in the handler.
- self._sessid_cache=[sessidforsessidinself._sessid_cacheifsessidin_SESSIONS]
- self.obj.db_sessid=",".join(str(val)forvalinself._sessid_cache)
- self.obj.save(update_fields=["db_sessid"])
-
-
[docs]defget(self,sessid=None):
- """
- Get the sessions linked to this Object.
-
- Args:
- sessid (int, optional): A specific session id.
-
- Returns:
- sessions (list): The sessions connected to this object. If `sessid` is given,
- this is a list of one (or zero) elements.
-
- Notes:
- Aliased to `self.all()`.
-
- """
- global_SESSIONS
- ifnot_SESSIONS:
- fromevennia.server.sessionhandlerimportSESSIONSas_SESSIONS
- ifsessid:
- sessions=(
- [_SESSIONS[sessid]ifsessidin_SESSIONSelseNone]
- ifsessidinself._sessid_cache
- else[]
- )
- else:
- sessions=[
- _SESSIONS[ssid]ifssidin_SESSIONSelseNoneforssidinself._sessid_cache
- ]
- ifNoneinsessions:
- # this happens only if our cache has gone out of sync with the SessionHandler.
-
- returnself.get(sessid=sessid)
- returnsessions
-
-
[docs]defall(self):
- """
- Alias to get(), returning all sessions.
-
- Returns:
- sessions (list): All sessions.
-
- """
- returnself.get()
-
-
[docs]defadd(self,session):
- """
- Add session to handler.
-
- Args:
- session (Session or int): Session or session id to add.
-
- Notes:
- We will only add a session/sessid if this actually also exists
- in the the core sessionhandler.
-
- """
- global_SESSIONS
- ifnot_SESSIONS:
- fromevennia.server.sessionhandlerimportSESSIONSas_SESSIONS
- try:
- sessid=session.sessid
- exceptAttributeError:
- sessid=session
-
- sessid_cache=self._sessid_cache
- ifsessidin_SESSIONSandsessidnotinsessid_cache:
- iflen(sessid_cache)>=_SESSID_MAX:
- return
- sessid_cache.append(sessid)
- self.obj.db_sessid=",".join(str(val)forvalinsessid_cache)
- self.obj.save(update_fields=["db_sessid"])
-
-
[docs]defremove(self,session):
- """
- Remove session from handler.
-
- Args:
- session (Session or int): Session or session id to remove.
-
- """
- try:
- sessid=session.sessid
- exceptAttributeError:
- sessid=session
-
- sessid_cache=self._sessid_cache
- ifsessidinsessid_cache:
- sessid_cache.remove(sessid)
- self.obj.db_sessid=",".join(str(val)forvalinsessid_cache)
- self.obj.save(update_fields=["db_sessid"])
[docs]defcount(self):
- """
- Get amount of sessions connected.
-
- Returns:
- sesslen (int): Number of sessions handled.
-
- """
- returnlen(self._sessid_cache)
-
-
-#
-# Base class to inherit from.
-
-
-
[docs]classDefaultObject(ObjectDB,metaclass=TypeclassBase):
- """
- This is the root typeclass object, representing all entities that
- have an actual presence in-game. DefaultObjects generally have a
- location. They can also be manipulated and looked at. Game
- entities you define should inherit from DefaultObject at some distance.
-
- It is recommended to create children of this class using the
- `evennia.create_object()` function rather than to initialize the class
- directly - this will both set things up and efficiently save the object
- without `obj.save()` having to be called explicitly.
-
- """
-
- # 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)"
-
- objects=ObjectManager()
-
- # populated by `return_apperance`
- appearance_template='''
-{header}
-|c{name}|n
-{desc}
-{exits}{characters}{things}
-{footer}
- '''
-
- # on-object properties
-
-
-
- @property
- defis_connected(self):
- # we get an error for objects subscribed to channels without this
- ifself.account:# seems sane to pass on the account
- returnself.account.is_connected
- else:
- returnFalse
-
- @property
- defhas_account(self):
- """
- Convenience property for checking if an active account is
- currently connected to this object.
-
- """
- returnself.sessions.count()
-
- @property
- defis_superuser(self):
- """
- Check if user has an account, and if so, if it is a superuser.
-
- """
- return(
- self.db_account
- andself.db_account.is_superuser
- andnotself.db_account.attributes.get("_quell")
- )
-
-
[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.
- This should be publically available.
-
- 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, minus exclusion
- and filtering.
-
- """
- returnself.contents_cache.get(exclude=exclude,content_type=content_type)
-
-
[docs]defcontents_set(self,*args):
- "You cannot replace this property"
- raiseAttributeError(
- "{}.contents is read-only. Use obj.move_to or "
- "obj.location to move an object here.".format(self.__class__)
- )
-
- contents=property(contents_get,contents_set,contents_set)
-
- @property
- defexits(self):
- """
- Returns all exits from this object, i.e. all objects at this
- location having the property destination != `None`.
-
- """
- return[exiforexiinself.contentsifexi.destination]
-
- # main methods
-
-
[docs]defget_display_name(self,looker=None,**kwargs):
- """
- Displays the name of the object in a viewer-aware manner.
-
- Args:
- looker (TypedObject): The object or account that is looking
- at/getting inforamtion for this object.
-
- Returns:
- name (str): A string containing the name of the object,
- including the DBREF if this user is privileged to control
- said object.
-
- Notes:
- This function could be extended to change how object names
- appear to users in character, but be wary. This function
- does not change an object's keys or aliases when
- searching, and is expected to produce something useful for
- builders.
-
- """
- iflookerandself.locks.check_lockstring(looker,"perm(Builder)"):
- return"{}(#{})".format(self.name,self.id)
- returnself.name
-
-
[docs]defget_numbered_name(self,count,looker,**kwargs):
- """
- Return the numbered (singular, plural) forms of this object's key. This is by default called
- by return_appearance and is used for grouping multiple same-named of this object. Note that
- this will be called on *every* member of a group even though the plural name will be only
- shown once. Also the singular display version, such as 'an apple', 'a tree' is determined
- from this method.
-
- Args:
- count (int): Number of objects of this type
- looker (Object): Onlooker. Not used by default.
- Keyword Args:
- key (str): Optional key to pluralize, if given, use this instead of the object's key.
- 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)
- key=ansi.ANSIString(key)# this is needed to allow inflection of colored names
- try:
- plural=_INFLECT.plural(key,count)
- plural="{}{}".format(_INFLECT.number_to_words(count,threshold=12),plural)
- exceptIndexError:
- # this is raised by inflect if the input is not a proper noun
- plural=key
- singular=_INFLECT.an(key)
- ifnotself.aliases.get(plural,category=plural_category):
- # we need to wipe any old plurals/an/a in case key changed in the interrim
- self.aliases.clear(category=plural_category)
- self.aliases.add(plural,category=plural_category)
- # save the singular form as an alias here too so we can display "an egg" and also
- # look at 'an egg'.
- self.aliases.add(singular,category=plural_category)
- returnsingular,plural
-
-
[docs]defsearch(
- self,
- searchdata,
- global_search=False,
- use_nicks=True,
- typeclass=None,
- location=None,
- attribute_name=None,
- quiet=False,
- exact=False,
- candidates=None,
- nofound_string=None,
- multimatch_string=None,
- use_dbref=None,
- stacked=0,
- ):
- """
- 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
- objects in the current `location` of `self` or its inventory are searched for.
-
- Args:
- searchdata (str or obj): Primary search criterion. Will be matched
- against `object.key` (with `object.aliases` second) unless
- the keyword attribute_name specifies otherwise.
-
- Special keywords:
-
- - `#<num>`: search by unique dbref. This is always a global search.
- - `me,self`: self-reference to this object
- - `<num>-<string>` - can be used to differentiate
- between multiple same-named matches. The exact form of this input
- is given by `settings.SEARCH_MULTIMATCH_REGEX`.
-
- global_search (bool): Search all objects globally. This overrules 'location' data.
- use_nicks (bool): Use nickname-replace (nicktype "object") on `searchdata`.
- typeclass (str or Typeclass, or list of either): Limit search only
- to `Objects` with this typeclass. May be a list of typeclasses
- for a broader search.
- location (Object or list): Specify a location or multiple locations
- 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. 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
- prepended), and if that fails, it will try to return
- objects having Attributes with this name and value
- equal to searchdata. A special use is to search for
- "key" here if you want to do a key-search without
- including aliases.
- quiet (bool): don't display default error messages - this tells the
- search method that the user wants to handle all errors
- themselves. It also changes the return value type, see
- below.
- exact (bool): if unset (default) - prefers to match to beginning of
- string rather than not matching at all. If set, requires
- exact matching of entire string.
- candidates (list of objects): this is an optional custom list of objects
- to search (filter) between. It is ignored if `global_search`
- is given. If not set, this list will automatically be defined
- to include the location, the contents of location and the
- caller's contents (inventory).
- nofound_string (str): optional custom string for not-found error message.
- multimatch_string (str): optional custom string for multimatch error header.
- use_dbref (bool or None, optional): If `True`, allow to enter e.g. a query "#123"
- to find an object (globally) by its database-id 123. If `False`, the string "#123"
- 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:
- Object, None or list: Will return an `Object` or `None` if `quiet=False`. Will return
- a `list` with 0, 1 or more matches if `quiet=True`. If `stacked` is a positive integer,
- this list may contain all stacked identical matches.
-
- Notes:
- To find Accounts, use eg. `evennia.account_search`. If
- `quiet=False`, error messages will be handled by
- `settings.SEARCH_AT_RESULT` and echoed automatically (on
- error, return will be `None`). If `quiet=True`, the error
- messaging is assumed to be handled by the caller.
-
- """
- is_string=isinstance(searchdata,str)
-
- ifis_string:
- # searchdata is a string; wrap some common self-references
- ifsearchdata.lower()in("here",):
- return[self.location]ifquietelseself.location
- ifsearchdata.lower()in("me","self"):
- return[self]ifquietelseself
-
- ifuse_dbrefisNone:
- use_dbref=self.locks.check_lockstring(self,"_dummy:perm(Builder)")
-
- ifuse_nicks:
- # do nick-replacement on search
- searchdata=self.nicks.nickreplace(
- searchdata,categories=("object","account"),include_account=True
- )
-
- ifglobal_searchor(
- is_string
- andsearchdata.startswith("#")
- andlen(searchdata)>1
- andsearchdata[1:].isdigit()
- ):
- # only allow exact matching if searching the entire database
- # or unique #dbrefs
- exact=True
- candidates=None
-
- elifcandidatesisNone:
- # no custom candidates given - get them automatically
- iflocation:
- # location(s) were given
- candidates=[]
- forobjinmake_iter(location):
- candidates.extend(obj.contents)
- else:
- # local search. Candidates are taken from
- # self.contents, self.location and
- # self.location.contents
- location=self.location
- candidates=self.contents
- iflocation:
- candidates=candidates+[location]+location.contents
- else:
- # normally we don't need this since we are
- # included in location.contents
- candidates.append(self)
-
- results=ObjectDB.objects.object_search(
- searchdata,
- attribute_name=attribute_name,
- typeclass=typeclass,
- candidates=candidates,
- exact=exact,
- 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 messaging
- returnlist(results)
-
- # handle error messages
- return_AT_SEARCH_RESULT(
- results,
- self,
- query=searchdata,
- nofound_string=nofound_string,
- multimatch_string=multimatch_string,
- )
-
-
[docs]defsearch_account(self,searchdata,quiet=False):
- """
- Simple shortcut wrapper to search for accounts, not characters.
-
- Args:
- searchdata (str): Search criterion - the key or dbref of the account
- to search for. If this is "here" or "me", search
- for the account connected to this object.
- quiet (bool): Returns the results as a list rather than
- echo eventual standard error messages. Default `False`.
-
- Returns:
- result (Account, None or list): Just what is returned depends on
- the `quiet` setting:
- - `quiet=True`: No match or multumatch auto-echoes errors
- to self.msg, then returns `None`. The esults are passed
- through `settings.SEARCH_AT_RESULT` and
- `settings.SEARCH_AT_MULTIMATCH_INPUT`. If there is a
- unique match, this will be returned.
- - `quiet=True`: No automatic error messaging is done, and
- what is returned is always a list with 0, 1 or more
- matching Accounts.
-
- """
- ifisinstance(searchdata,str):
- # searchdata is a string; wrap some common self-references
- ifsearchdata.lower()in("me","self"):
- return[self.account]ifquietelseself.account
-
- results=search.search_account(searchdata)
-
- ifquiet:
- returnresults
- return_AT_SEARCH_RESULT(results,self,query=searchdata)
-
-
[docs]defexecute_cmd(self,raw_string,session=None,**kwargs):
- """
- Do something as this object. This is never called normally,
- it's only used when wanting specifically to let an object be
- the caller of a command. It makes use of nicks of eventual
- connected accounts as well.
-
- Args:
- raw_string (string): Raw command input
- session (Session, optional): Session to
- return results to
-
- Keyword Args:
- Other keyword arguments will be added to the found command
- object instace as variables before it executes. This is
- unused by default Evennia but may be used to set flags and
- change operating paramaters for commands at run-time.
-
- Returns:
- defer (Deferred): This is an asynchronous Twisted object that
- will not fire until the command has actually finished
- executing. To overload this one needs to attach
- callback functions to it, with addCallback(function).
- This function will be called with an eventual return
- value from the command execution. This return is not
- used at all by Evennia by default, but might be useful
- for coders intending to implement some sort of nested
- command structure.
-
- """
- # break circular import issues
- global_CMDHANDLER
- ifnot_CMDHANDLER:
- fromevennia.commands.cmdhandlerimportcmdhandleras_CMDHANDLER
-
- # nick replacement - we require full-word matching.
- # do text encoding conversion
- raw_string=self.nicks.nickreplace(
- raw_string,categories=("inputline","channel"),include_account=True
- )
- return_CMDHANDLER(
- self,raw_string,callertype="object",session=session,**kwargs
- )
-
-
[docs]defmsg(self,text=None,from_obj=None,session=None,options=None,**kwargs):
- """
- Emits something to a session attached to the object.
-
- Args:
- text (str or tuple, optional): The message to send. This
- is treated internally like any send-command, so its
- value can be a tuple if sending multiple arguments to
- the `text` oob command.
- from_obj (obj or list, optional): object that is sending. If
- given, at_msg_send will be called. This value will be
- passed on to the protocol. If iterable, will execute hook
- on all entities in it.
- session (Session or list, optional): Session or list of
- Sessions to relay data to, if any. If set, will force send
- to these sessions. If unset, who receives the message
- depends on the MULTISESSION_MODE.
- options (dict, optional): Message-specific option-value
- pairs. These will be applied at the protocol level.
- Keyword Args:
- any (string or tuples): All kwarg keys not listed above
- will be treated as send-command names and their arguments
- (which can be a string or a tuple).
-
- Notes:
- `at_msg_receive` will be called on this Object.
- All extra kwargs will be passed on to the protocol.
-
- """
- # try send hooks
- iffrom_obj:
- forobjinmake_iter(from_obj):
- try:
- obj.at_msg_send(text=text,to_obj=self,**kwargs)
- exceptException:
- logger.log_trace()
- kwargs["options"]=options
- try:
- ifnotself.at_msg_receive(text=text,from_obj=from_obj,**kwargs):
- # if at_msg_receive returns false, we abort message to this object
- return
- exceptException:
- logger.log_trace()
-
- iftextisnotNone:
- ifnot(isinstance(text,str)orisinstance(text,tuple)):
- # sanitize text before sending across the wire
- try:
- text=to_str(text)
- exceptException:
- text=repr(text)
- kwargs["text"]=text
-
- # relay to session(s)
- sessions=make_iter(session)ifsessionelseself.sessions.all()
- forsessioninsessions:
- session.data_out(**kwargs)
-
-
[docs]deffor_contents(self,func,exclude=None,**kwargs):
- """
- Runs a function on every object contained within this one.
-
- Args:
- func (callable): Function to call. This must have the
- formal call sign func(obj, **kwargs), where obj is the
- object currently being processed and `**kwargs` are
- passed on from the call to `for_contents`.
- exclude (list, optional): A list of object not to call the
- function on.
-
- Keyword Args:
- Keyword arguments will be passed to the function for all objects.
-
- """
- contents=self.contents
- ifexclude:
- exclude=make_iter(exclude)
- contents=[objforobjincontentsifobjnotinexclude]
- forobjincontents:
- func(obj,**kwargs)
-
-
[docs]defmsg_contents(self,text=None,exclude=None,from_obj=None,mapping=None,**kwargs):
- """
- Emits a message to all objects inside this object.
-
- Args:
- 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. The message will be parsed for `{key}` formatting and
- `$You/$you()/$You()`, `$obj(name)`, `$conj(verb)` and `$pron(pronoun, option)`
- inline function callables.
- The `name` is taken from the `mapping` kwarg {"name": 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 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:
- 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"`
-
- 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_outcmdelsetext
- outkwargs=text[1]ifis_outcmdandlen(text)>1else{}
- mapping=mappingor{}
- you=from_objorself
-
- if'you'notinmapping:
- mapping[you]=you
-
- contents=self.contents
- ifexclude:
- exclude=make_iter(exclude)
- contents=[objforobjincontentsifobjnotinexclude]
-
- forreceiverincontents:
-
- # actor-stance replacements
- inmessage=_MSG_CONTENTS_PARSER.parse(
- inmessage,raise_errors=True,return_string=True,
- caller=you,receiver=receiver,mapping=mapping)
-
- # director-stance replacements
- outmessage=inmessage.format(
- **{key:obj.get_display_name(looker=receiver)
- ifhasattr(obj,"get_display_name")elsestr(obj)
- forkey,objinmapping.items()})
-
- receiver.msg(text=(outmessage,outkwargs),from_obj=from_obj,**kwargs)
-
-
[docs]defmove_to(
- self,
- destination,
- quiet=False,
- emit_to_obj=None,
- use_destination=True,
- to_none=False,
- move_hooks=True,
- **kwargs,
- ):
- """
- Moves this object to a new location.
-
- Args:
- destination (Object): Reference to the object to move to. This
- can also be an exit object, in which case the
- destination property is used as destination.
- quiet (bool): If true, turn off the calling of the emit hooks
- (announce_move_to/from etc)
- emit_to_obj (Object): object to receive error messages
- use_destination (bool): Default is for objects to use the "destination"
- property of destinations as the target to move to. Turning off this
- keyword allows objects to move "inside" exit objects.
- to_none (bool): Allow destination to be None. Note that no hooks are run when
- moving to a None location. If you want to run hooks, run them manually
- (and make sure they can manage None locations).
- move_hooks (bool): If False, turn off the calling of move-related hooks
- (at_pre/post_move etc) with quiet=True, this is as quiet a move
- as can be done.
-
- Keyword Args:
- Passed on to announce_move_to and announce_move_from hooks.
-
- Returns:
- result (bool): True/False depending on if there were problems with the move.
- This method may also return various error messages to the
- `emit_to_obj`.
-
- Notes:
- No access checks are done in this method, these should be handled before
- calling `move_to`.
-
- The `DefaultObject` hooks called (if `move_hooks=True`) are, in order:
-
- 1. `self.at_pre_move(destination)` (if this returns False, move is aborted)
- 2. `source_location.at_object_leave(self, destination)`
- 3. `self.announce_move_from(destination)`
- 4. (move happens here)
- 5. `self.announce_move_to(source_location)`
- 6. `destination.at_object_receive(self, source_location)`
- 7. `self.at_post_move(source_location)`
-
- """
- deflogerr(string="",err=None):
- """Simple log helper method"""
- logger.log_trace()
- self.msg("%s%s"%(string,""iferrisNoneelse" (%s)"%err))
- return
-
- errtxt=_("Couldn't perform move ({err}). Contact an admin.")
- ifnotemit_to_obj:
- emit_to_obj=self
-
- ifnotdestination:
- ifto_none:
- # immediately move to None. There can be no hooks called since
- # there is no destination to call them with.
- self.location=None
- returnTrue
- emit_to_obj.msg(_("The destination doesn't exist."))
- returnFalse
- ifdestination.destinationanduse_destination:
- # traverse exits
- destination=destination.destination
- # Before the move, call eventual pre-commands.
- ifmove_hooks:
- try:
- ifnotself.at_pre_move(destination,**kwargs):
- returnFalse
- exceptExceptionaserr:
- logerr(errtxt.format(err="at_pre_move()"),err)
- returnFalse
-
- # Save the old location
- source_location=self.location
-
- # Call hook on source location
- ifmove_hooksandsource_location:
- try:
- source_location.at_object_leave(self,destination,**kwargs)
- exceptExceptionaserr:
- logerr(errtxt.format(err="at_object_leave()"),err)
- returnFalse
-
- ifnotquiet:
- # tell the old room we are leaving
- try:
- self.announce_move_from(destination,**kwargs)
- exceptExceptionaserr:
- logerr(errtxt.format(err="at_announce_move()"),err)
- returnFalse
-
- # Perform move
- try:
- self.location=destination
- exceptExceptionaserr:
- logerr(errtxt.format(err="location change"),err)
- returnFalse
-
- ifnotquiet:
- # Tell the new room we are there.
- try:
- self.announce_move_to(source_location,**kwargs)
- exceptExceptionaserr:
- logerr(errtxt.format(err="announce_move_to()"),err)
- returnFalse
-
- ifmove_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,**kwargs)
- exceptExceptionaserr:
- 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_post_move(source_location,**kwargs)
- exceptExceptionaserr:
- logerr(errtxt.format(err="at_post_move"),err)
- returnFalse
- returnTrue
-
-
[docs]defclear_exits(self):
- """
- 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()
- forin_exitinObjectDB.objects.filter(db_destination=self):
- in_exit.delete()
-
-
[docs]defclear_contents(self):
- """
- 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("#"))
- try:
- default_home=ObjectDB.objects.get(id=default_home_id)
- ifdefault_home.dbid==self.dbid:
- # we are deleting default home!
- default_home=None
- exceptException:
- string=_("Could not find default home '(#{dbid})'.")
- logger.log_err(string.format(dbid=default_home_id))
- default_home=None
-
- forobjinself.contents:
- home=obj.home
- # Obviously, we can't send it back to here.
- ifnothomeor(homeandhome.dbid==self.dbid):
- obj.home=default_home
- home=default_home
-
- # If for some reason it's still None...
- ifnothome:
- obj.location=None
- obj.msg(_("Something went wrong! You are dumped into nowhere. Contact an admin."))
- logger.log_err("Missing default home - '{name}(#{dbid})' now "
- "has a null location.".format(name=obj.name,dbid=obj.dbid))
- return
-
- ifobj.has_account:
- ifhome:
- string="Your current location has ceased to exist,"
- 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."
- obj.msg(_(string))
- obj.move_to(home)
-
-
[docs]@classmethod
- defcreate(cls,key,account=None,**kwargs):
- """
- Creates a basic object with default parameters, unless otherwise
- specified or extended.
-
- Provides a friendlier interface to the utils.create_object() function.
-
- Args:
- key (str): Name of the new object.
- account (Account): Account to attribute this object to.
-
- Keyword Args:
- description (str): Brief description for this object.
- ip (str): IP address of creator (for object auditing).
-
- Returns:
- object (Object): A newly created object of the given typeclass.
- errors (list): A list of errors in string form, if any.
-
- """
- errors=[]
- obj=None
-
- # Get IP address of creator, if available
- ip=kwargs.pop("ip","")
-
- # If no typeclass supplied, use this class
- kwargs["typeclass"]=kwargs.pop("typeclass",cls)
-
- # Set the supplied key as the name of the intended object
- kwargs["key"]=key
-
- # Get a supplied description, if any
- description=kwargs.pop("description","")
-
- # Create a sane lockstring if one wasn't supplied
- lockstring=kwargs.get("locks")
- ifaccountandnotlockstring:
- lockstring=cls.lockstring.format(account_id=account.id)
- kwargs["locks"]=lockstring
-
- # Create object
- try:
- obj=create.create_object(**kwargs)
-
- # Record creator id and creation IP
- ifip:
- obj.db.creator_ip=ip
- ifaccount:
- obj.db.creator_id=account.id
-
- # Set description if there is none, or update it if provided
- ifdescriptionornotobj.db.desc:
- desc=descriptionifdescriptionelse"You see nothing special."
- obj.db.desc=desc
-
- exceptExceptionase:
- errors.append("An error occurred while creating this '%s' object."%key)
- logger.log_err(e)
-
- returnobj,errors
-
-
[docs]defcopy(self,new_key=None,**kwargs):
- """
- Makes an identical copy of this object, identical except for a
- new dbref in the database. If you want to customize the copy
- by changing some settings, use ObjectDB.object.copy_object()
- directly.
-
- Args:
- new_key (string): New key/name of copied object. If new_key is not
- specified, the copy will be named <old_key>_copy by default.
- Returns:
- copy (Object): A copy of this object.
-
- """
-
- deffind_clone_key():
- """
- Append 01, 02 etc to obj.key. Checks next higher number in the
- same location, then adds the next number available
-
- returns the new clone name on the form keyXX
- """
- key=self.key
- num=sum(
- 1
- forobjinself.location.contents
- ifobj.key.startswith(key)andobj.key.lstrip(key).isdigit()
- )
- return"%s%03i"%(key,num)
-
- new_key=new_keyorfind_clone_key()
- new_obj=ObjectDB.objects.copy_object(self,new_key=new_key,**kwargs)
- self.at_object_post_copy(new_obj,**kwargs)
- returnnew_obj
-
-
[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.
-
- Args:
- new_obj (Object): The new Copy of this object.
-
- Returns:
- None
- """
- pass
-
-
[docs]defdelete(self):
- """
- Deletes this object. Before deletion, this method makes sure
- to move all contained objects to their respective home
- locations, as well as clean up all exits to/from the object.
-
- Returns:
- noerror (bool): Returns whether or not the delete completed
- successfully or not.
-
- """
- global_ScriptDB
- ifnot_ScriptDB:
- fromevennia.scripts.modelsimportScriptDBas_ScriptDB
-
- ifnotself.pkornotself.at_object_delete():
- # This object has already been deleted,
- # or the pre-delete check return False
- returnFalse
-
- # See if we need to kick the account off.
-
- forsessioninself.sessions.all():
- session.msg(_("Your character {key} has been destroyed.").format(key=self.key))
- # no need to disconnect, Account just jumps to OOC mode.
- # sever the connection (important!)
- ifself.account:
- # Remove the object from playable characters list
- ifselfinself.account.db._playable_characters:
- self.account.db._playable_characters=[
- xforxinself.account.db._playable_charactersifx!=self
- ]
- forsessioninself.sessions.all():
- self.account.unpuppet_object(session)
-
- self.account=None
-
- forscriptin_ScriptDB.objects.get_all_scripts_on_obj(self):
- script.delete()
-
- # Destroy any exits to and from this room, if any
- self.clear_exits()
- # Clear out any non-exit objects located within the object
- self.clear_contents()
- self.attributes.clear()
- self.nicks.clear()
- self.aliases.clear()
- self.location=None# this updates contents_cache for our location
-
- # Perform the deletion of the object
- super().delete()
- returnTrue
-
-
[docs]defaccess(
- self,accessing_obj,access_type="read",default=False,no_superuser_bypass=False,**kwargs
- ):
- """
- Determines if another object has permission to access this object
- in whatever way.
-
- Args:
- accessing_obj (Object): Object trying to access this one.
- access_type (str, optional): Type of access sought.
- default (bool, optional): What to return if no lock of access_type was found.
- no_superuser_bypass (bool, optional): If `True`, don't skip
- lock check for superuser (be careful with this one).
-
- Keyword Args:
- Passed on to the at_access hook along with the result of the access check.
-
- """
- result=super().access(
- accessing_obj,
- access_type=access_type,
- default=default,
- no_superuser_bypass=no_superuser_bypass,
- )
- self.at_access(result,accessing_obj,access_type,**kwargs)
- returnresult
-
- #
- # Hook methods
- #
-
-
[docs]defat_first_save(self):
- """
- This is called by the typeclass system whenever an instance of
- this class is saved for the first time. It is a generic hook
- for calling the startup hooks for the various game entities.
- When overloading you generally don't overload this but
- overload the hooks called by this method.
-
- """
- self.basetype_setup()
- self.at_object_creation()
-
- ifhasattr(self,"_createdict"):
- # this will only be set if the utils.create function
- # was used to create the object. We want the create
- # call's kwargs to override the values set by hooks.
- cdict=self._createdict
- updates=[]
- ifnotcdict.get("key"):
- ifnotself.db_key:
- self.db_key="#%i"%self.dbid
- updates.append("db_key")
- elifself.key!=cdict.get("key"):
- updates.append("db_key")
- self.db_key=cdict["key"]
- ifcdict.get("location")andself.location!=cdict["location"]:
- self.db_location=cdict["location"]
- updates.append("db_location")
- ifcdict.get("home")andself.home!=cdict["home"]:
- self.home=cdict["home"]
- updates.append("db_home")
- ifcdict.get("destination")andself.destination!=cdict["destination"]:
- self.destination=cdict["destination"]
- updates.append("db_destination")
- ifupdates:
- self.save(update_fields=updates)
-
- ifcdict.get("permissions"):
- self.permissions.batch_add(*cdict["permissions"])
- ifcdict.get("locks"):
- self.locks.add(cdict["locks"])
- ifcdict.get("aliases"):
- self.aliases.batch_add(*cdict["aliases"])
- ifcdict.get("location"):
- cdict["location"].at_object_receive(self,None)
- self.at_post_move(None)
- ifcdict.get("tags"):
- # this should be a list of tags, tuples (key, category) or (key, category, data)
- self.tags.batch_add(*cdict["tags"])
- ifcdict.get("attributes"):
- # this should be tuples (key, val, ...)
- self.attributes.batch_add(*cdict["attributes"])
- ifcdict.get("nattributes"):
- # this should be a dict of nattrname:value
- forkey,valueincdict["nattributes"]:
- self.nattributes.add(key,value)
-
- delself._createdict
-
- self.basetype_posthook_setup()
-
- # hooks called by the game engine #
-
-
[docs]defbasetype_setup(self):
- """
- This sets up the default properties of an Object, just before
- the more general at_object_creation.
-
- You normally don't need to change this unless you change some
- fundamental things like names of permission groups.
-
- """
- # the default security setup fallback for a generic
- # object. Overload in child for a custom setup. Also creation
- # commands may set this (create an item and you should be its
- # controller, for example)
-
- self.locks.add(
- ";".join(
- [
- "control:perm(Developer)",# edit locks/permissions, delete
- "examine:perm(Builder)",# examine properties
- "view:all()",# look at object (visibility)
- "edit:perm(Admin)",# edit properties/attributes
- "delete:perm(Admin)",# delete object
- "get:all()",# pick up object
- "drop:holds()",# drop only that which you hold
- "call:true()",# allow to call commands on this object
- "tell:perm(Admin)",# allow emits to this object
- "puppet:pperm(Developer)",
- ]
- )
- )# lock down puppeting only to staff by default
-
-
[docs]defbasetype_posthook_setup(self):
- """
- Called once, after basetype_setup and at_object_creation. This
- should generally not be overloaded unless you are redefining
- how a room/exit/object works. It allows for basetype-like
- setup after the object is created. An example of this is
- EXITs, who need to know keys, aliases, locks etc to set up
- their exit-cmdsets.
-
- """
- pass
-
-
[docs]defat_object_creation(self):
- """
- Called once, when this object is first created. This is the
- normal hook to overload for most object types.
-
- """
- pass
-
-
[docs]defat_object_delete(self):
- """
- Called just before the database object is persistently
- delete()d from the database. If this method returns False,
- deletion is aborted.
-
- """
- returnTrue
-
-
[docs]defat_init(self):
- """
- This is always called whenever this object is initiated --
- that is, whenever it its typeclass is cached from memory. This
- happens on-demand first time the object is used or activated
- in some way after being created but also after each server
- restart or reload.
-
- """
- pass
-
-
[docs]defat_cmdset_get(self,**kwargs):
- """
- Called just before cmdsets on this object are requested by the
- command handler. If changes need to be done on the fly to the
- cmdset before passing them on to the cmdhandler, this is the
- place to do it. This is called also if the object currently
- have no cmdsets.
-
- Keyword Args:
- caller (Session, Object or Account): The caller requesting
- this cmdset.
-
- """
- pass
-
-
[docs]defat_pre_puppet(self,account,session=None,**kwargs):
- """
- Called just before an Account connects to this object to puppet
- it.
-
- Args:
- account (Account): This is the connecting account.
- session (Session): Session controlling the connection.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- """
- pass
-
-
[docs]defat_post_puppet(self,**kwargs):
- """
- Called just after puppeting has been completed and all
- Account<->Object links have been established.
-
- Args:
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
- Note:
- You can use `self.account` and `self.sessions.get()` to get
- account and sessions at this point; the last entry in the
- list from `self.sessions.get()` is the latest Session
- puppeting this Object.
-
- """
- self.msg(f"You become |w{self.key}|n.")
- self.account.db._last_puppet=self
-
-
[docs]defat_pre_unpuppet(self,**kwargs):
- """
- Called just before beginning to un-connect a puppeting from
- this Account.
-
- Args:
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
- Note:
- You can use `self.account` and `self.sessions.get()` to get
- account and sessions at this point; the last entry in the
- list from `self.sessions.get()` is the latest Session
- puppeting this Object.
-
- """
- pass
-
-
[docs]defat_post_unpuppet(self,account,session=None,**kwargs):
- """
- Called just after the Account successfully disconnected from
- this object, severing all connections.
-
- Args:
- account (Account): The account object that just disconnected
- from this object.
- session (Session): Session id controlling the connection that
- just disconnected.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- """
- pass
-
-
[docs]defat_server_reload(self):
- """
- This hook is called whenever the server is shutting down for
- restart/reboot. If you want to, for example, save non-persistent
- properties across a restart, this is the place to do it.
-
- """
- pass
-
-
[docs]defat_server_shutdown(self):
- """
- This hook is called whenever the server is shutting down fully
- (i.e. not for a restart).
-
- """
- pass
-
-
[docs]defat_access(self,result,accessing_obj,access_type,**kwargs):
- """
- This is called with the result of an access call, along with
- any kwargs used for that call. The return of this method does
- not affect the result of the lock check. It can be used e.g. to
- customize error messages in a central location or other effects
- based on the access result.
-
- Args:
- result (bool): The outcome of the access call.
- accessing_obj (Object or Account): The entity trying to gain access.
- access_type (str): The type of access that was requested.
-
- Keyword Args:
- Not used by default, added for possible expandability in a
- game.
-
- """
- pass
-
- # hooks called when moving the object
-
-
[docs]defat_pre_move(self,destination,**kwargs):
- """
- Called just before starting to move this object to
- destination.
-
- Args:
- destination (Object): The object we are moving to
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- Returns:
- shouldmove (bool): If we should move or not.
-
- Notes:
- If this method returns False/None, the move is cancelled
- before it is even started.
-
- """
- # return has_perm(self, destination, "can_move")
- returnTrue
-
- # deprecated alias
- at_before_move=at_pre_move
-
-
[docs]defannounce_move_from(self,destination,msg=None,mapping=None,**kwargs):
- """
- Called if the move is to be announced. This is
- called while we are still standing in the old
- location.
-
- Args:
- destination (Object): The place we are going to.
- msg (str, optional): a replacement message.
- mapping (dict, optional): additional mapping objects.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- You can override this method and call its parent with a
- message to simply change the default message. In the string,
- you can use the following as mappings (between braces):
- object: the object which is moving.
- exit: the exit from which the object is moving (if found).
- origin: the location of the object before the move.
- destination: the location of the object after moving.
-
- """
- ifnotself.location:
- return
- ifmsg:
- string=msg
- else:
- string="{object} is leaving {origin}, heading for {destination}."
-
- location=self.location
- exits=[
- oforoinlocation.contentsifo.locationislocationando.destinationisdestination
- ]
- ifnotmapping:
- mapping={}
-
- mapping.update(
- {
- "object":self,
- "exit":exits[0]ifexitselse"somewhere",
- "origin":locationor"nowhere",
- "destination":destinationor"nowhere",
- }
- )
-
- location.msg_contents(string,exclude=(self,),from_obj=self,mapping=mapping)
-
-
[docs]defannounce_move_to(self,source_location,msg=None,mapping=None,**kwargs):
- """
- Called after the move if the move was not quiet. At this point
- we are standing in the new location.
-
- Args:
- source_location (Object): The place we came from
- msg (str, optional): the replacement message if location.
- mapping (dict, optional): additional mapping objects.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- Notes:
- You can override this method and call its parent with a
- message to simply change the default message. In the string,
- you can use the following as mappings (between braces):
- object: the object which is moving.
- exit: the exit from which the object is moving (if found).
- origin: the location of the object before the move.
- destination: the location of the object after moving.
-
- """
-
- 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 {name} in your possession.").format(
- name=self.get_display_name(self.location))
- self.location.msg(string)
- return
-
- ifsource_location:
- ifmsg:
- string=msg
- else:
- string=_("{object} arrives to {destination} from {origin}.")
- else:
- string=_("{object} arrives to {destination}.")
-
- origin=source_location
- destination=self.location
- exits=[]
- iforigin:
- exits=[
- o
- foroindestination.contents
- ifo.locationisdestinationando.destinationisorigin
- ]
-
- ifnotmapping:
- mapping={}
-
- mapping.update(
- {
- "object":self,
- "exit":exits[0]ifexitselse"somewhere",
- "origin":originor"nowhere",
- "destination":destinationor"nowhere",
- }
- )
-
- destination.msg_contents(string,exclude=(self,),from_obj=self,mapping=mapping)
-
-
[docs]defat_post_move(self,source_location,**kwargs):
- """
- Called after move has completed, regardless of quiet mode or
- not. Allows changes to the object due to the location it is
- now in.
-
- Args:
- source_location (Object): Wwhere we came from. This may be `None`.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- """
- pass
-
- # deprecated
- at_after_move=at_post_move
-
-
[docs]defat_object_leave(self,moved_obj,target_location,**kwargs):
- """
- Called just before an object leaves from inside this object
-
- Args:
- moved_obj (Object): The object leaving
- target_location (Object): Where `moved_obj` is going.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- """
- pass
-
-
[docs]defat_object_receive(self,moved_obj,source_location,**kwargs):
- """
- Called after an object has been moved into this object.
-
- Args:
- moved_obj (Object): The object moved into this one
- source_location (Object): Where `moved_object` came from.
- Note that this could be `None`.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- """
- pass
-
-
[docs]defat_traverse(self,traversing_object,target_location,**kwargs):
- """
- This hook is responsible for handling the actual traversal,
- normally by calling
- `traversing_object.move_to(target_location)`. It is normally
- only implemented by Exit objects. If it returns False (usually
- because `move_to` returned False), `at_post_traverse` below
- should not be called and instead `at_failed_traverse` should be
- called.
-
- Args:
- traversing_object (Object): Object traversing us.
- target_location (Object): Where target is going.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- """
- pass
-
-
[docs]defat_post_traverse(self,traversing_object,source_location,**kwargs):
- """
- Called just after an object successfully used this object to
- traverse to another object (i.e. this object is a type of
- Exit)
-
- Args:
- traversing_object (Object): The object traversing us.
- source_location (Object): Where `traversing_object` came from.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- Notes:
- The target location should normally be available as `self.destination`.
- """
- pass
[docs]defat_failed_traverse(self,traversing_object,**kwargs):
- """
- This is called if an object fails to traverse this object for
- some reason.
-
- Args:
- traversing_object (Object): The object that failed traversing us.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- Notes:
- Using the default exits, this hook will not be called if an
- Attribute `err_traverse` is defined - this will in that case be
- read for an error string instead.
-
- """
- pass
-
-
[docs]defat_msg_receive(self,text=None,from_obj=None,**kwargs):
- """
- This hook is called whenever someone sends a message to this
- object using the `msg` method.
-
- Note that from_obj may be None if the sender did not include
- itself as an argument to the obj.msg() call - so you have to
- check for this. .
-
- Consider this a pre-processing method before msg is passed on
- to the user session. If this method returns False, the msg
- will not be passed on.
-
- Args:
- text (str, optional): The message received.
- from_obj (any, optional): The object sending the message.
-
- Keyword Args:
- This includes any keywords sent to the `msg` method.
-
- Returns:
- receive (bool): If this message should be received.
-
- Notes:
- If this method returns False, the `msg` operation
- will abort without sending the message.
-
- """
- returnTrue
-
-
[docs]defat_msg_send(self,text=None,to_obj=None,**kwargs):
- """
- This is a hook that is called when *this* object sends a
- message to another object with `obj.msg(text, to_obj=obj)`.
-
- Args:
- text (str, optional): Text to send.
- to_obj (any, optional): The object to send to.
-
- Keyword Args:
- Keywords passed from msg()
-
- Notes:
- Since this method is executed by `from_obj`, if no `from_obj`
- was passed to `DefaultCharacter.msg` this hook will never
- get called.
-
- """
- pass
-
- # hooks called by the default cmdset.
-
-
[docs]defget_visible_contents(self,looker,**kwargs):
- """
- Get all contents of this object that a looker can see (whatever that means, by default it
- checks the 'view' lock), grouped by type. Helper method to return_appearance.
-
- Args:
- looker (Object): The entity looking.
- **kwargs (any): Passed from `return_appearance`. Unused by default.
-
- Returns:
- dict: A dict of lists categorized by type. Byt default this
- contains 'exits', 'characters' and 'things'. The elements of these
- lists are the actual objects.
-
- """
- deffilter_visible(obj_list):
- return[objforobjinobj_listifobj!=lookerandobj.access(looker,"view")]
-
- return{
- "exits":filter_visible(self.contents_get(content_type="exit")),
- "characters":filter_visible(self.contents_get(content_type="character")),
- "things":filter_visible(self.contents_get(content_type="object"))
- }
-
-
[docs]defget_content_names(self,looker,**kwargs):
- """
- Get the proper names for all contents of this object. Helper method
- for return_appearance.
-
- Args:
- looker (Object): The entity looking.
- **kwargs (any): Passed from `return_appearance`. Passed into
- `get_display_name` for each found entity.
-
- Returns:
- dict: A dict of lists categorized by type. Byt default this
- contains 'exits', 'characters' and 'things'. The elements
- of these lists are strings - names of the objects that
- can depend on the looker and also be grouped in the case
- of multiple same-named things etc.
-
- Notes:
- This method shouldn't add extra coloring to the names beyond what is
- already given by the .get_display_name() (and the .name field) already.
- Per-type coloring can be applied in `return_apperance`.
-
- """
- # a mapping {'exits': [...], 'characters': [...], 'things': [...]}
- contents_map=self.get_visible_contents(looker,**kwargs)
-
- character_names=[char.get_display_name(looker,**kwargs)
- forcharincontents_map['characters']]
- exit_names=[exi.get_display_name(looker,**kwargs)forexiincontents_map['exits']]
-
- # group all same-named things under one name
- things=defaultdict(list)
- forthingincontents_map['things']:
- things[thing.get_display_name(looker,**kwargs)].append(thing)
-
- # pluralize same-named things
- thing_names=[]
- forthingname,thinglistinsorted(things.items()):
- nthings=len(thinglist)
- thing=thinglist[0]
- singular,plural=thing.get_numbered_name(nthings,looker,key=thingname)
- thing_names.append(singularifnthings==1elseplural)
-
- return{
- "exits":exit_names,
- "characters":character_names,
- "things":thing_names
- }
-
-
[docs]defreturn_appearance(self,looker,**kwargs):
- """
- Main callback used by 'look' for the object to describe itself.
- This formats a description. By default, this looks for the `appearance_template`
- string set on this class and populates it with formatting keys
- 'name', 'desc', 'exits', 'characters', 'things' as well as
- (currently empty) 'header'/'footer'.
-
- Args:
- looker (Object): Object doing the looking.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call. This is passed into the helper
- methods and into `get_display_name` calls.
-
- Returns:
- str: The description of this entity. By default this includes
- the entity's name, description and any contents inside it.
-
- Notes:
- To simply change the layout of how the object displays itself (like
- adding some line decorations or change colors of different sections),
- you can simply edit `.appearance_template`. You only need to override
- this method (and/or its helpers) if you want to change what is passed
- into the template or want the most control over output.
-
- """
-
- ifnotlooker:
- return''
-
- # ourselves
- name=self.get_display_name(looker,**kwargs)
- desc=self.db.descor"You see nothing special."
-
- # contents
- content_names_map=self.get_content_names(looker,**kwargs)
- exits=list_to_string(content_names_map['exits'])
- characters=list_to_string(content_names_map['characters'])
- things=list_to_string(content_names_map['things'])
-
- # populate the appearance_template string. It's a good idea to strip it and
- # let the client add any extra spaces instead.
- returnself.appearance_template.format(
- header='',
- name=name,
- desc=desc,
- exits=f"|wExits:|n {exits}"ifexitselse'',
- characters=f"\n|wCharacters:|n {characters}"ifcharacterselse'',
- things=f"\n|wYou see:|n {things}"ifthingselse'',
- footer=''
- ).strip()
-
-
[docs]defat_look(self,target,**kwargs):
- """
- Called when this object performs a look. It allows to
- customize just what this means. It will not itself
- send any data.
-
- Args:
- target (Object): The target being looked at. This is
- commonly an object or the current location. It will
- be checked for the "view" type access.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call. This will be passed into
- return_appearance, get_display_name and at_desc but is not used
- by default.
-
- Returns:
- lookstring (str): A ready-processed look string
- potentially ready to return to the looker.
-
- """
- ifnottarget.access(self,"view"):
- try:
- return"Could not view '%s'."%target.get_display_name(self,**kwargs)
- exceptAttributeError:
- return"Could not view '%s'."%target.key
-
- description=target.return_appearance(self,**kwargs)
-
- # the target's at_desc() method.
- # this must be the last reference to target so it may delete itself when acted on.
- target.at_desc(looker=self,**kwargs)
-
- returndescription
-
-
[docs]defat_desc(self,looker=None,**kwargs):
- """
- This is called whenever someone looks at this object.
-
- Args:
- looker (Object, optional): The object requesting the description.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- """
- pass
-
-
[docs]defat_pre_get(self,getter,**kwargs):
- """
- Called by the default `get` command before this object has been
- picked up.
-
- Args:
- getter (Object): The object about to get this object.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- Returns:
- shouldget (bool): If the object should be gotten or not.
-
- Notes:
- If this method returns False/None, the getting is cancelled
- before it is even started.
- """
- returnTrue
-
- # deprecated
- at_before_get=at_pre_get
-
-
[docs]defat_get(self,getter,**kwargs):
- """
- Called by the default `get` command when this object has been
- picked up.
-
- Args:
- getter (Object): The object getting this object.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- Notes:
- This hook cannot stop the pickup from happening. Use
- permissions or the at_pre_get() hook for that.
-
- """
- pass
-
-
[docs]defat_pre_give(self,giver,getter,**kwargs):
- """
- Called by the default `give` command before this object has been
- given.
-
- Args:
- giver (Object): The object about to give this object.
- getter (Object): The object about to get this object.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- Returns:
- shouldgive (bool): If the object should be given or not.
-
- Notes:
- If this method returns False/None, the giving is cancelled
- before it is even started.
-
- """
- returnTrue
-
- # deprecated
- at_before_give=at_pre_give
-
-
[docs]defat_give(self,giver,getter,**kwargs):
- """
- Called by the default `give` command when this object has been
- given.
-
- Args:
- giver (Object): The object giving this object.
- getter (Object): The object getting this object.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- Notes:
- This hook cannot stop the give from happening. Use
- permissions or the at_pre_give() hook for that.
-
- """
- pass
-
-
[docs]defat_pre_drop(self,dropper,**kwargs):
- """
- Called by the default `drop` command before this object has been
- dropped.
-
- Args:
- dropper (Object): The object which will drop this object.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- Returns:
- shoulddrop (bool): If the object should be dropped or not.
-
- Notes:
- If this method returns False/None, the dropping is cancelled
- before it is even started.
-
- """
- ifnotself.locks.get("drop"):
- # TODO: This if-statment will be removed in Evennia 1.0
- returnTrue
- ifnotself.access(dropper,"drop",default=False):
- dropper.msg(f"You cannot drop {self.get_display_name(dropper)}")
- returnFalse
- returnTrue
-
- # deprecated
- at_before_drop=at_pre_drop
-
-
[docs]defat_drop(self,dropper,**kwargs):
- """
- Called by the default `drop` command when this object has been
- dropped.
-
- Args:
- dropper (Object): The object which just dropped this object.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- Notes:
- This hook cannot stop the drop from happening. Use
- permissions or the at_pre_drop() hook for that.
-
- """
- pass
-
-
[docs]defat_pre_say(self,message,**kwargs):
- """
- Before the object says something.
-
- This hook is by default used by the 'say' and 'whisper'
- commands as used by this command it is called before the text
- is said/whispered and can be used to customize the outgoing
- text from the object. Returning `None` aborts the command.
-
- Args:
- message (str): The suggested say/whisper text spoken by self.
- Keyword Args:
- whisper (bool): If True, this is a whisper rather than
- 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.
-
- Returns:
- message (str): The (possibly modified) text to be spoken.
-
- """
- returnmessage
-
- # deprecated
- at_before_say=at_pre_say
-
-
[docs]defat_say(
- self,
- message,
- msg_self=None,
- msg_location=None,
- receivers=None,
- msg_receivers=None,
- **kwargs,
- ):
- """
- Display the actual say (or whisper) of self.
-
- This hook should display the actual say/whisper of the object in its
- location. It should both alert the object (self) and its
- location that some text is spoken. The overriding of messages or
- `mapping` allows for simple customization of the hook without
- re-writing it completely.
-
- Args:
- message (str): The message to convey.
- msg_self (bool or str, optional): If boolean True, echo `message` to self. If a string,
- return that message. If False or unset, don't echo to self.
- msg_location (str, optional): The message to echo to self's location.
- 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:
- whisper (bool): If this is a whisper rather than a say. Kwargs
- can be used by other verbal commands in a similar way.
- mapping (dict): Pass an additional mapping to the message.
-
- Notes:
-
-
- Messages can contain {} markers. These are substituted against the values
- passed in the `mapping` argument.
-
- msg_self = 'You say: "{speech}"'
- msg_location = '{object} says: "{speech}"'
- msg_receivers = '{object} whispers: "{speech}"'
-
- Supported markers by default:
- {self}: text to self-reference with (default 'You')
- {speech}: the text spoken/whispered by self.
- {object}: the object speaking.
- {receiver}: replaced with a single receiver only for strings meant for a specific
- receiver (otherwise 'None').
- {all_receivers}: comma-separated list of all receivers,
- if more than one, otherwise same as receiver
- {location}: the location where object is.
-
- """
- msg_type="say"
- ifkwargs.get("whisper",False):
- # whisper mode
- msg_type="whisper"
- msg_self=(
- '{self} whisper to {all_receivers}, "|n{speech}|n"'
- ifmsg_selfisTrueelsemsg_self
- )
- msg_receivers=msg_receiversor'{object} whispers: "|n{speech}|n"'
- msg_location=None
- else:
- msg_self='{self} say, "|n{speech}|n"'ifmsg_selfisTrueelsemsg_self
- msg_location=msg_locationor'{object} says, "{speech}"'
- msg_receivers=msg_receiversormessage
-
- custom_mapping=kwargs.get("mapping",{})
- receivers=make_iter(receivers)ifreceiverselseNone
- location=self.location
-
- ifmsg_self:
- self_mapping={
- "self":"You",
- "object":self.get_display_name(self),
- "location":location.get_display_name(self)iflocationelseNone,
- "receiver":None,
- "all_receivers":", ".join(recv.get_display_name(self)forrecvinreceivers)
- ifreceivers
- elseNone,
- "speech":message,
- }
- self_mapping.update(custom_mapping)
- self.msg(text=(msg_self.format(**self_mapping),{"type":msg_type}),from_obj=self)
-
- ifreceiversandmsg_receivers:
- receiver_mapping={
- "self":"You",
- "object":None,
- "location":None,
- "receiver":None,
- "all_receivers":None,
- "speech":message,
- }
- forreceiverinmake_iter(receivers):
- individual_mapping={
- "object":self.get_display_name(receiver),
- "location":location.get_display_name(receiver),
- "receiver":receiver.get_display_name(receiver),
- "all_receivers":", ".join(recv.get_display_name(recv)forrecvinreceivers)
- ifreceivers
- elseNone,
- }
- receiver_mapping.update(individual_mapping)
- receiver_mapping.update(custom_mapping)
- receiver.msg(
- text=(msg_receivers.format(**receiver_mapping),{"type":msg_type}),
- from_obj=self,
- )
-
- ifself.locationandmsg_location:
- location_mapping={
- "self":"You",
- "object":self,
- "location":location,
- "all_receivers":", ".join(str(recv)forrecvinreceivers)ifreceiverselseNone,
- "receiver":None,
- "speech":message,
- }
- location_mapping.update(custom_mapping)
- exclude=[]
- ifmsg_self:
- exclude.append(self)
- ifreceivers:
- exclude.extend(receivers)
- self.location.msg_contents(
- text=(msg_location,{"type":msg_type}),
- from_obj=self,
- exclude=exclude,
- mapping=location_mapping,
- )
-
-
-#
-# Base Character object
-#
-
-
-
[docs]classDefaultCharacter(DefaultObject):
- """
- This implements an Object puppeted by a Session - that is,
- a character avatar controlled by an account.
-
- """
-
- # 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=(
- "puppet:id({character_id}) or pid({account_id}) or perm(Developer) or pperm(Developer);"
- "delete:id({account_id}) or perm(Admin)"
- )
-
-
[docs]@classmethod
- defcreate(cls,key,account=None,**kwargs):
- """
- Creates a basic Character with default parameters, unless otherwise
- specified or extended.
-
- Provides a friendlier interface to the utils.create_character() function.
-
- Args:
- key (str): Name of the new Character.
- account (obj, optional): Account to associate this Character with.
- If unset supplying None-- it will
- change the default lockset and skip creator attribution.
-
- Keyword Args:
- description (str): Brief description for this object.
- ip (str): IP address of creator (for object auditing).
- All other kwargs will be passed into the create_object call.
-
- Returns:
- character (Object): A newly created Character of the given typeclass.
- errors (list): A list of errors in string form, if any.
-
- """
- errors=[]
- obj=None
- # Get IP address of creator, if available
- ip=kwargs.pop("ip","")
-
- # If no typeclass supplied, use this class
- kwargs["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 object
- kwargs["key"]=key
-
- # Get permissions
- kwargs["permissions"]=kwargs.get("permissions",settings.PERMISSION_ACCOUNT_DEFAULT)
-
- # Get description if provided
- description=kwargs.pop("description","")
-
- # Get locks if provided
- locks=kwargs.pop("locks","")
-
- try:
- # Check to make sure account does not have too many chars
- ifaccount:
- iflen(account.characters)>=settings.MAX_NR_CHARACTERS:
- errors.append(_("There are too many characters associated with this account."))
- returnobj,errors
-
- # Create the Character
- obj=create.create_object(**kwargs)
-
- # Record creator id and creation IP
- ifip:
- obj.db.creator_ip=ip
- ifaccount:
- obj.db.creator_id=account.id
- ifobjnotinaccount.characters:
- account.db._playable_characters.append(obj)
-
- # Add locks
- ifnotlocksandaccount:
- # 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})
-
- obj.locks.add(locks)
-
- # If no description is set, set a default description
- ifdescriptionornotobj.db.desc:
- obj.db.desc=descriptionifdescriptionelse_("This is a character.")
-
- exceptExceptionase:
- 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.
-
- You should normally not need to overload this, but if you do,
- make sure to reproduce at least the two last commands in this
- method (unless you want to fundamentally change how a
- Character object works).
-
- """
- super().basetype_setup()
- self.locks.add(
- ";".join(["get:false()","call:false()"])# noone can pick up the character
- )# no commands can be called on character from outside
- # add the default cmdset
- self.cmdset.add_default(settings.CMDSET_CHARACTER,persistent=True)
-
-
[docs]defat_post_move(self,source_location,**kwargs):
- """
- We make sure to look around after a move.
-
- """
- ifself.location.access(self,"view"):
- self.msg(text=(self.at_look(self.location),{"type":"look"}))
-
- # deprecated
- at_after_move=at_post_move
-
-
[docs]defat_pre_puppet(self,account,session=None,**kwargs):
- """
- Return the character from storage in None location in `at_post_unpuppet`.
- Args:
- account (Account): This is the connecting account.
- session (Session): Session controlling the connection.
-
- """
- if(
- self.locationisNone
- ):# Make sure character's location is never None before being puppeted.
- # Return to last location (or home, which should always exist),
- self.location=self.db.prelogout_locationifself.db.prelogout_locationelseself.home
- self.location.at_object_receive(
- self,None
- )# and trigger the location's reception hook.
- ifself.location:# If the character is verified to be somewhere,
- self.db.prelogout_location=self.location# save location again to be sure.
- else:
- account.msg(
- _("|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):
- """
- Called just after puppeting has been completed and all
- Account<->Object links have been established.
-
- Args:
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
- Note:
- You can use `self.account` and `self.sessions.get()` to get
- account and sessions at this point; the last entry in the
- list from `self.sessions.get()` is the latest Session
- puppeting this Object.
-
- """
- 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(_("{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)
-
-
[docs]defat_post_unpuppet(self,account,session=None,**kwargs):
- """
- We stove away the character when the account goes ooc/logs off,
- otherwise the character object will remain in the room also
- after the account logged off ("headless", so to say).
-
- Args:
- account (Account): The account object that just disconnected
- from this object.
- session (Session): Session controlling the connection that
- just disconnected.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
- """
- ifnotself.sessions.count():
- # only remove this char from grid if no sessions control it anymore.
- ifself.location:
-
- defmessage(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
- self.location=None
-
- @property
- defidle_time(self):
- """
- 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:
- returntime.time()-float(max(idle))
- returnNone
-
- @property
- defconnection_time(self):
- """
- 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:
- returntime.time()-float(min(conn))
- returnNone
-
-
-#
-# Base Room object
-
-
-
[docs]classDefaultRoom(DefaultObject):
- """
- This is the base room object. It's just like any Object except its
- 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=(
- "control:id({id}) or perm(Admin); "
- "delete:id({id}) or perm(Admin); "
- "edit:id({id}) or perm(Admin)"
- )
-
-
[docs]@classmethod
- defcreate(cls,key,account=None,**kwargs):
- """
- Creates a basic Room with default parameters, unless otherwise
- specified or extended.
-
- Provides a friendlier interface to the utils.create_object() function.
-
- Args:
- key (str): Name of the new Room.
- account (obj, optional): Account to associate this Room with. If
- given, it will be given specific control/edit permissions to this
- object (along with normal Admin perms). If not given, default
-
- Keyword Args:
- description (str): Brief description for this object.
- ip (str): IP address of creator (for object auditing).
-
- Returns:
- room (Object): A newly created Room of the given typeclass.
- errors (list): A list of errors in string form, if any.
-
- """
- errors=[]
- obj=None
-
- # Get IP address of creator, if available
- ip=kwargs.pop("ip","")
-
- # If no typeclass supplied, use this class
- kwargs["typeclass"]=kwargs.pop("typeclass",cls)
-
- # Set the supplied key as the name of the intended object
- kwargs["key"]=key
-
- # Get who to send errors to
- kwargs["report_to"]=kwargs.pop("report_to",account)
-
- # Get description, if provided
- description=kwargs.pop("description","")
-
- # get locks if provided
- locks=kwargs.pop("locks","")
-
- try:
- # Create the Room
- obj=create.create_object(**kwargs)
-
- # Add locks
- ifnotlocksandaccount:
- locks=cls.lockstring.format(**{"id":account.id})
- elifnotlocksandnotaccount:
- locks=cls.lockstring.format(**{"id":obj.id})
-
- obj.locks.add(locks)
-
- # Record creator id and creation IP
- ifip:
- obj.db.creator_ip=ip
- ifaccount:
- obj.db.creator_id=account.id
-
- # If no description is set, set a default description
- ifdescriptionornotobj.db.desc:
- obj.db.desc=descriptionifdescriptionelse_("This is a room.")
-
- exceptExceptionase:
- errors.append("An error occurred while creating this '%s' object."%key)
- logger.log_err(e)
-
- returnobj,errors
-
-
[docs]defbasetype_setup(self):
- """
- Simple room setup setting locks to make sure the room
- cannot be picked up.
-
- """
-
- super().basetype_setup()
- self.locks.add(
- ";".join(["get:false()","puppet:false()"])
- )# would be weird to puppet a room ...
- self.location=None
-
-
-#
-# Default Exit command, used by the base exit object
-#
-
-
[docs]classExitCommand(_COMMAND_DEFAULT_CLASS):
- """
- This is a command that simply cause the caller to traverse
- the object it is attached to.
-
- """
-
- obj=None
-
-
[docs]deffunc(self):
- """
- Default exit traverse if no syscommand is defined.
- """
-
- ifself.obj.access(self.caller,"traverse"):
- # we may traverse the exit.
- self.obj.at_traverse(self.caller,self.obj.destination)
- else:
- # exit is locked
- ifself.obj.db.err_traverse:
- # if exit has a better error message, let's use it.
- self.caller.msg(self.obj.db.err_traverse)
- else:
- # No shorthand error message. Call hook.
- self.obj.at_failed_traverse(self.caller)
-
-
[docs]defget_extra_info(self,caller,**kwargs):
- """
- Shows a bit of information on where the exit leads.
-
- Args:
- caller (Object): The object (usually a character) that entered an ambiguous command.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- Returns:
- 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)
- else:
- return" (%s)"%self.obj.get_display_name(caller)
-
-
-#
-# Base Exit object
-
-
-
[docs]classDefaultExit(DefaultObject):
- """
- This is the base exit object - it connects a location to another.
- This is done by the exit assigning a "command" on itself with the
- same name as the exit object (to do this we need to remember to
- re-create the command when the object is cached since it must be
- created dynamically depending on what the exit is called). This
- command (which has a high priority) will thus allow us to traverse
- exits simply by giving the exit-object's name on its own.
-
- """
-
- _content_types=("exit",)
- exit_command=ExitCommand
- priority=101
-
- # lockstring of newly created exits, for easy overloading.
- # Will be formatted with the {id} of the creating object.
- lockstring=(
- "control:id({id}) or perm(Admin); "
- "delete:id({id}) or perm(Admin); "
- "edit:id({id}) or perm(Admin)"
- )
-
- # Helper classes and methods to implement the Exit. These need not
- # be overloaded unless one want to change the foundation for how
- # Exits work. See the end of the class for hook methods to overload.
-
-
[docs]defcreate_exit_cmdset(self,exidbobj):
- """
- Helper function for creating an exit command set + command.
-
- The command of this cmdset has the same name as the Exit
- object and allows the exit to react when the account enter the
- exit's name, triggering the movement between rooms.
-
- Args:
- exidbobj (Object): The DefaultExit object to base the command on.
-
- """
-
- # create an exit command. We give the properties here,
- # to always trigger metaclass preparations
- cmd=self.exit_command(
- key=exidbobj.db_key.strip().lower(),
- aliases=exidbobj.aliases.all(),
- locks=str(exidbobj.locks),
- auto_help=False,
- destination=exidbobj.db_destination,
- arg_regex=r"^$",
- is_exit=True,
- obj=exidbobj,
- )
- # create a cmdset
- exit_cmdset=cmdset.CmdSet(None)
- exit_cmdset.key="ExitCmdSet"
- exit_cmdset.priority=self.priority
- exit_cmdset.duplicates=True
- # add command to cmdset
- exit_cmdset.add(cmd)
- returnexit_cmdset
-
- # Command hooks
-
-
[docs]@classmethod
- defcreate(cls,key,source,dest,account=None,**kwargs):
- """
- Creates a basic Exit with default parameters, unless otherwise
- specified or extended.
-
- Provides a friendlier interface to the utils.create_object() function.
-
- Args:
- key (str): Name of the new Exit, as it should appear from the
- source room.
- account (obj): Account to associate this Exit with.
- source (Room): The room to create this exit in.
- dest (Room): The room to which this exit should go.
-
- Keyword Args:
- description (str): Brief description for this object.
- ip (str): IP address of creator (for object auditing).
-
- Returns:
- exit (Object): A newly created Room of the given typeclass.
- errors (list): A list of errors in string form, if any.
-
- """
- errors=[]
- obj=None
-
- # Get IP address of creator, if available
- ip=kwargs.pop("ip","")
-
- # If no typeclass supplied, use this class
- kwargs["typeclass"]=kwargs.pop("typeclass",cls)
-
- # Set the supplied key as the name of the intended object
- kwargs["key"]=key
-
- # Get who to send errors to
- kwargs["report_to"]=kwargs.pop("report_to",account)
-
- # Set to/from rooms
- kwargs["location"]=source
- kwargs["destination"]=dest
-
- description=kwargs.pop("description","")
-
- locks=kwargs.get("locks","")
-
- try:
- # Create the Exit
- obj=create.create_object(**kwargs)
-
- # Set appropriate locks
- ifnotlocksandaccount:
- locks=cls.lockstring.format(**{"id":account.id})
- elifnotlocksandnotaccount:
- locks=cls.lockstring.format(**{"id":obj.id})
- obj.locks.add(locks)
-
- # Record creator id and creation IP
- ifip:
- obj.db.creator_ip=ip
- ifaccount:
- obj.db.creator_id=account.id
-
- # If no description is set, set a default description
- ifdescriptionornotobj.db.desc:
- obj.db.desc=descriptionifdescriptionelse_("This is an exit.")
-
- exceptExceptionase:
- errors.append("An error occurred while creating this '%s' object."%key)
- logger.log_err(e)
-
- returnobj,errors
-
-
[docs]defbasetype_setup(self):
- """
- Setup exit-security
-
- You should normally not need to overload this - if you do make
- sure you include all the functionality in this method.
-
- """
- super().basetype_setup()
-
- # setting default locks (overload these in at_object_creation()
- self.locks.add(
- ";".join(
- [
- "puppet:false()",# would be weird to puppet an exit ...
- "traverse:all()",# who can pass through exit by default
- "get:false()",# noone can pick up the exit
- ]
- )
- )
-
- # an exit should have a destination (this is replaced at creation time)
- ifself.location:
- self.destination=self.location
-
-
[docs]defat_cmdset_get(self,**kwargs):
- """
- Called just before cmdsets on this object are requested by the
- command handler. If changes need to be done on the fly to the
- cmdset before passing them on to the cmdhandler, this is the
- place to do it. This is called also if the object currently
- has no cmdsets.
-
- Keyword Args:
- force_init (bool): If `True`, force a re-build of the cmdset
- (for example to update aliases).
-
- """
-
- if"force_init"inkwargsornotself.cmdset.has_cmdset("ExitCmdSet",must_be_default=True):
- # we are resetting, or no exit-cmdset was set. Create one dynamically.
- self.cmdset.add_default(self.create_exit_cmdset(self),persistent=False)
-
-
[docs]defat_init(self):
- """
- This is called when this objects is re-loaded from cache. When
- that happens, we make sure to remove any old ExitCmdSet cmdset
- (this most commonly occurs when renaming an existing exit)
- """
- self.cmdset.remove_default()
-
-
[docs]defat_traverse(self,traversing_object,target_location,**kwargs):
- """
- This implements the actual traversal. The traverse lock has
- already been checked (in the Exit command) at this point.
-
- Args:
- traversing_object (Object): Object traversing us.
- target_location (Object): Where target is going.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- """
- source_location=traversing_object.location
- iftraversing_object.move_to(target_location):
- self.at_post_traverse(traversing_object,source_location)
- else:
- ifself.db.err_traverse:
- # if exit has a better error message, let's use it.
- traversing_object.msg(self.db.err_traverse)
- else:
- # No shorthand error message. Call hook.
- self.at_failed_traverse(traversing_object)
-
-
[docs]defat_failed_traverse(self,traversing_object,**kwargs):
- """
- Overloads the default hook to implement a simple default error message.
-
- Args:
- traversing_object (Object): The object that failed traversing us.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- Notes:
- Using the default exits, this hook will not be called if an
- Attribute `err_traverse` is defined - this will in that case be
- read for an error string instead.
-
- """
- traversing_object.msg(_("You cannot go there."))
-"""
-
-OLC Prototype menu nodes
-
-"""
-
-importjson
-importre
-fromrandomimportchoice
-fromdjango.db.modelsimportQ
-fromdjango.confimportsettings
-fromevennia.objects.modelsimportObjectDB
-fromevennia.utils.evmenuimportEvMenu,list_node
-fromevennia.utilsimportevmore
-fromevennia.utils.ansiimportstrip_ansi
-fromevennia.utilsimportutils
-fromevennia.locks.lockhandlerimportget_all_lockfuncs
-fromevennia.prototypesimportprototypesasprotlib
-fromevennia.prototypesimportspawner
-
-# ------------------------------------------------------------
-#
-# OLC Prototype design menu
-#
-# ------------------------------------------------------------
-
-_MENU_CROP_WIDTH=15
-_MENU_ATTR_LITERAL_EVAL_ERROR=(
- "|rCritical Python syntax error in your value. Only primitive Python structures are allowed.\n"
- "You also need to use correct Python syntax. Remember especially to put quotes around all "
- "strings inside lists and dicts.|n"
-)
-
-
-# Helper functions
-
-
-def_get_menu_prototype(caller):
- """Return currently active menu prototype."""
- prototype=None
- ifhasattr(caller.ndb._menutree,"olc_prototype"):
- prototype=caller.ndb._menutree.olc_prototype
- ifnotprototype:
- caller.ndb._menutree.olc_prototype=prototype={}
- caller.ndb._menutree.olc_new=True
- returnprototype
-
-
-def_get_flat_menu_prototype(caller,refresh=False,validate=False):
- """Return prototype where parent values are included"""
- flat_prototype=None
- ifnotrefreshandhasattr(caller.ndb._menutree,"olc_flat_prototype"):
- flat_prototype=caller.ndb._menutree.olc_flat_prototype
- ifnotflat_prototype:
- prot=_get_menu_prototype(caller)
- caller.ndb._menutree.olc_flat_prototype=flat_prototype=spawner.flatten_prototype(
- prot,validate=validate
- )
- returnflat_prototype
-
-
-def_get_unchanged_inherited(caller,protname):
- """Return prototype values inherited from parent(s), which are not replaced in child"""
- prototype=_get_menu_prototype(caller)
- ifprotnameinprototype:
- returnprotname[protname],False
- else:
- flattened=_get_flat_menu_prototype(caller)
- ifprotnameinflattened:
- returnprotname[protname],True
- returnNone,False
-
-
-def_set_menu_prototype(caller,prototype):
- """Set the prototype with existing one"""
- caller.ndb._menutree.olc_prototype=prototype
- caller.ndb._menutree.olc_new=False
- returnprototype
-
-
-def_is_new_prototype(caller):
- """Check if prototype is marked as new or was loaded from a saved one."""
- returnhasattr(caller.ndb._menutree,"olc_new")
-
-
-def_format_option_value(prop,required=False,prototype=None,cropper=None):
- """
- Format wizard option values.
-
- Args:
- prop (str): Name or value to format.
- required (bool, optional): The option is required.
- prototype (dict, optional): If given, `prop` will be considered a key in this prototype.
- cropper (callable, optional): A function to crop the value to a certain width.
-
- Returns:
- value (str): The formatted value.
- """
- ifprototypeisnotNone:
- prop=prototype.get(prop,"")
-
- out=prop
- ifcallable(prop):
- ifhasattr(prop,"__name__"):
- out="<{}>".format(prop.__name__)
- else:
- out=repr(prop)
- ifutils.is_iter(prop):
- out=", ".join(str(pr)forprinprop)
- ifnotoutandrequired:
- out="|runset"
- ifout:
- return" ({}|n)".format(cropper(out)ifcropperelseutils.crop(out,_MENU_CROP_WIDTH))
- return""
-
-
-def_set_prototype_value(caller,field,value,parse=True):
- """Set prototype's field in a safe way."""
- prototype=_get_menu_prototype(caller)
- prototype[field]=value
- caller.ndb._menutree.olc_prototype=prototype
- returnprototype
-
-
-def_set_property(caller,raw_string,**kwargs):
- """
- Add or update a property. To be called by the 'goto' option variable.
-
- Args:
- caller (Object, Account): The user of the wizard.
- raw_string (str): Input from user on given node - the new value to set.
-
- Keyword Args:
- test_parse (bool): If set (default True), parse raw_string for protfuncs and obj-refs and
- try to run result through literal_eval. The parser will be run in 'testing' mode and any
- parsing errors will shown to the user. Note that this is just for testing, the original
- given string will be what is inserted.
- prop (str): Property name to edit with `raw_string`.
- processor (callable): Converts `raw_string` to a form suitable for saving.
- next_node (str): Where to redirect to after this has run.
-
- Returns:
- next_node (str): Next node to go to.
-
- """
- prop=kwargs.get("prop","prototype_key")
- processor=kwargs.get("processor",None)
- next_node=kwargs.get("next_node",None)
-
- ifcallable(processor):
- try:
- value=processor(raw_string)
- exceptExceptionaserr:
- caller.msg(
- "Could not set {prop} to {value} ({err})".format(
- prop=prop.replace("_","-").capitalize(),value=raw_string,err=str(err)
- )
- )
- # this means we'll re-run the current node.
- returnNone
- else:
- value=raw_string
-
- ifnotvalue:
- returnnext_node
-
- prototype=_set_prototype_value(caller,prop,value)
- caller.ndb._menutree.olc_prototype=prototype
-
- try:
- # TODO simple way to get rid of the u'' markers in list reprs, remove this when on py3.
- repr_value=json.dumps(value)
- exceptException:
- repr_value=value
-
- out=[" Set {prop} to {value} ({typ}).".format(prop=prop,value=repr_value,typ=type(value))]
-
- ifkwargs.get("test_parse",True):
- out.append(" Simulating prototype-func parsing ...")
- parsed_value=protlib.protfunc_parser(value,testing=True,prototype=prototype)
- ifparsed_value!=value:
- out.append(
- " |g(Example-)value when parsed ({}):|n {}".format(type(parsed_value),parsed_value)
- )
- else:
- out.append(" |gNo change when parsed.")
-
- caller.msg("\n".join(out))
-
- returnnext_node
-
-
-def_wizard_options(curr_node,prev_node,next_node,color="|W",search=False):
- """Creates default navigation options available in the wizard."""
- options=[]
- ifprev_node:
- options.append(
- {
- "key":("|wB|Wack","b"),
- "desc":"{color}({node})|n".format(color=color,node=prev_node.replace("_","-")),
- "goto":"node_{}".format(prev_node),
- }
- )
- ifnext_node:
- options.append(
- {
- "key":("|wF|Worward","f"),
- "desc":"{color}({node})|n".format(color=color,node=next_node.replace("_","-")),
- "goto":"node_{}".format(next_node),
- }
- )
-
- options.append({"key":("|wI|Wndex","i"),"goto":"node_index"})
-
- ifcurr_node:
- options.append(
- {
- "key":("|wV|Walidate prototype","validate","v"),
- "goto":("node_validate_prototype",{"back":curr_node}),
- }
- )
- ifsearch:
- options.append(
- {
- "key":("|wSE|Warch objects","search object","search","se"),
- "goto":("node_search_object",{"back":curr_node}),
- }
- )
-
- returnoptions
-
-
-def_set_actioninfo(caller,string):
- caller.ndb._menutree.actioninfo=string
-
-
-def_path_cropper(pythonpath):
- "Crop path to only the last component"
- returnpythonpath.split(".")[-1]
-
-
-def_validate_prototype(prototype):
- """Run validation on prototype"""
-
- txt=protlib.prototype_to_str(prototype)
- errors="\n\n|g No validation errors found.|n (but errors could still happen at spawn-time)"
- err=False
- try:
- # validate, don't spawn
- spawner.spawn(prototype,only_validate=True)
- exceptRuntimeErrorasexc:
- errors="\n\n|r{}|n".format(exc)
- err=True
- exceptRuntimeWarningasexc:
- errors="\n\n|y{}|n".format(exc)
- err=True
-
- text=txt+errors
- returnerr,text
-
-
-def_format_protfuncs():
- out=[]
- sorted_funcs=[
- (key,func)forkey,funcinsorted(protlib.FUNC_PARSER.callables.items(),key=lambdatup:tup[0])
- ]
- forprotfunc_name,protfuncinsorted_funcs:
- out.append(
- "- |c${name}|n - |W{docs}".format(
- name=protfunc_name,
- docs=utils.justify(protfunc.__doc__.strip(),align="l",indent=10).strip(),
- )
- )
- return"\n ".join(out)
-
-
-def_format_lockfuncs():
- out=[]
- sorted_funcs=[
- (key,func)forkey,funcinsorted(get_all_lockfuncs().items(),key=lambdatup:tup[0])
- ]
- forlockfunc_name,lockfuncinsorted_funcs:
- doc=(lockfunc.__doc__or"").strip()
- out.append(
- "- |c${name}|n - |W{docs}".format(
- name=lockfunc_name,docs=utils.justify(doc,align="l",indent=10).strip()
- )
- )
- return"\n".join(out)
-
-
-def_format_list_actions(*args,**kwargs):
- """Create footer text for nodes with extra list actions
-
- Args:
- actions (str): Available actions. The first letter of the action name will be assumed
- to be a shortcut.
- Keyword Args:
- prefix (str): Default prefix to use.
- Returns:
- string (str): Formatted footer for adding to the node text.
-
- """
- actions=[]
- prefix=kwargs.get("prefix","|WSelect with |w<num>|W. Other actions:|n ")
- foractioninargs:
- actions.append("|w{}|n|W{} |w<num>|n".format(action[0],action[1:]))
- returnprefix+" |W|||n ".join(actions)
-
-
-def_get_current_value(caller,keyname,comparer=None,formatter=str,only_inherit=False):
- """
- Return current value, marking if value comes from parent or set in this prototype.
-
- Args:
- keyname (str): Name of prototoype key to get current value of.
- comparer (callable, optional): This will be called as comparer(prototype_value,
- flattened_value) and is expected to return the value to show as the current
- or inherited one. If not given, a straight comparison is used and what is returned
- depends on the only_inherit setting.
- formatter (callable, optional)): This will be called with the result of comparer.
- only_inherit (bool, optional): If a current value should only be shown if all
- the values are inherited from the prototype parent (otherwise, show an empty string).
- Returns:
- current (str): The current value.
-
- """
-
- def_default_comparer(protval,flatval):
- ifonly_inherit:
- return""ifprotvalelseflatval
- else:
- returnprotvalifprotvalelseflatval
-
- ifnotcallable(comparer):
- comparer=_default_comparer
-
- prot=_get_menu_prototype(caller)
- flat_prot=_get_flat_menu_prototype(caller)
-
- out=""
- ifkeynameinprot:
- ifkeynameinflat_prot:
- out=formatter(comparer(prot[keyname],flat_prot[keyname]))
- ifonly_inherit:
- ifstr(out).strip():
- return"|WCurrent|n {} |W(|binherited|W):|n {}".format(keyname,out)
- return""
- else:
- ifout:
- return"|WCurrent|n {}|W:|n {}".format(keyname,out)
- return"|W[No {} set]|n".format(keyname)
- elifonly_inherit:
- return""
- else:
- out=formatter(prot[keyname])
- return"|WCurrent|n {}|W:|n {}".format(keyname,out)
- elifkeynameinflat_prot:
- out=formatter(flat_prot[keyname])
- ifout:
- return"|WCurrent|n {} |W(|n|binherited|W):|n {}".format(keyname,out)
- else:
- return""
- elifonly_inherit:
- return""
- else:
- return"|W[No {} set]|n".format(keyname)
-
-
-def_default_parse(raw_inp,choices,*args):
- """
- Helper to parse default input to a node decorated with the node_list decorator on
- the form l1, l 2, look 1, etc. Spaces are ignored, as is case.
-
- Args:
- raw_inp (str): Input from the user.
- choices (list): List of available options on the node listing (list of strings).
- args (tuples): The available actions, each specifed as a tuple (name, alias, ...)
- Returns:
- choice (str): A choice among the choices, or None if no match was found.
- action (str): The action operating on the choice, or None.
-
- """
- raw_inp=raw_inp.lower().strip()
- mapping={t.lower():tup[0]fortupinargsfortintup}
- match=re.match(r"(%s)\s*?(\d+)$"%"|".join(mapping.keys()),raw_inp)
- ifmatch:
- action=mapping.get(match.group(1),None)
- num=int(match.group(2))-1
- num=numif0<=num<len(choices)elseNone
- ifactionisnotNoneandnumisnotNone:
- returnchoices[num],action
- returnNone,None
-
-
-# Menu nodes ------------------------------
-
-# helper nodes
-
-# validate prototype (available as option from all nodes)
-
-
-
[docs]defnode_validate_prototype(caller,raw_string,**kwargs):
- """General node to view and validate a protototype"""
- prototype=_get_flat_menu_prototype(caller,refresh=True,validate=False)
- prev_node=kwargs.get("back","index")
-
- _,text=_validate_prototype(prototype)
-
- helptext="""
- The validator checks if the prototype's various values are on the expected form. It also tests
- any $protfuncs.
-
- """
-
- text=(text,helptext)
-
- options=_wizard_options(None,prev_node,None)
- options.append({"key":"_default","goto":"node_"+prev_node})
-
- returntext,options
-
-
-# node examine_entity
-
-
-
[docs]defnode_examine_entity(caller,raw_string,**kwargs):
- """
- General node to view a text and then return to previous node. Kwargs should contain "text" for
- the text to show and 'back" pointing to the node to return to.
- """
- text=kwargs.get("text","Nothing was found here.")
- helptext="Use |wback|n to return to the previous node."
- prev_node=kwargs.get("back","index")
-
- text=(text,helptext)
-
- options=_wizard_options(None,prev_node,None)
- options.append({"key":"_default","goto":"node_"+prev_node})
-
- returntext,options
-
-
-# node object_search
-
-
-def_search_object(caller):
- "update search term based on query stored on menu; store match too"
- try:
- searchstring=caller.ndb._menutree.olc_search_object_term.strip()
- caller.ndb._menutree.olc_search_object_matches=[]
- exceptAttributeError:
- return[]
-
- ifnotsearchstring:
- caller.msg("Must specify a search criterion.")
- return[]
-
- is_dbref=utils.dbref(searchstring)
- is_account=searchstring.startswith("*")
-
- ifis_dbreforis_account:
-
- ifis_dbref:
- # a dbref search
- results=caller.search(searchstring,global_search=True,quiet=True)
- else:
- # an account search
- searchstring=searchstring.lstrip("*")
- results=caller.search_account(searchstring,quiet=True)
- else:
- keyquery=Q(db_key__istartswith=searchstring)
- aliasquery=Q(
- db_tags__db_key__istartswith=searchstring,db_tags__db_tagtype__iexact="alias"
- )
- results=ObjectDB.objects.filter(keyquery|aliasquery).distinct()
-
- caller.msg("Searching for '{}' ...".format(searchstring))
- caller.ndb._menutree.olc_search_object_matches=results
- return["{}(#{})".format(obj.key,obj.id)forobjinresults]
-
-
-def_object_search_select(caller,obj_entry,**kwargs):
- choices=kwargs["available_choices"]
- num=choices.index(obj_entry)
- matches=caller.ndb._menutree.olc_search_object_matches
- obj=matches[num]
-
- ifnotobj.access(caller,"examine"):
- caller.msg("|rYou don't have 'examine' access on this object.|n")
- delcaller.ndb._menutree.olc_search_object_term
- return"node_search_object"
-
- prot=spawner.prototype_from_object(obj)
- txt=protlib.prototype_to_str(prot)
- return"node_examine_entity",{"text":txt,"back":"search_object"}
-
-
-def_object_search_actions(caller,raw_inp,**kwargs):
- "All this does is to queue a search query"
- choices=kwargs["available_choices"]
- obj_entry,action=_default_parse(
- raw_inp,choices,("examine","e"),("create prototype from object","create","c")
- )
-
- raw_inp=raw_inp.strip()
-
- ifobj_entry:
-
- num=choices.index(obj_entry)
- matches=caller.ndb._menutree.olc_search_object_matches
- obj=matches[num]
- prot=spawner.prototype_from_object(obj)
-
- ifaction=="examine":
-
- ifnotobj.access(caller,"examine"):
- caller.msg("\n|rYou don't have 'examine' access on this object.|n")
- delcaller.ndb._menutree.olc_search_object_term
- return"node_search_object"
-
- txt=protlib.prototype_to_str(prot)
- return"node_examine_entity",{"text":txt,"back":"search_object"}
- else:
- # load prototype
-
- ifnotobj.access(caller,"edit"):
- caller.msg("|rYou don't have access to do this with this object.|n")
- delcaller.ndb._menutree.olc_search_object_term
- return"node_search_object"
-
- _set_menu_prototype(caller,prot)
- caller.msg("Created prototype from object.")
- return"node_index"
- elifraw_inp:
- caller.ndb._menutree.olc_search_object_term=raw_inp
- return"node_search_object",kwargs
- else:
- # empty input - exit back to previous node
- prev_node="node_"+kwargs.get("back","index")
- returnprev_node
-
-
-@list_node(_search_object,_object_search_select)
-defnode_search_object(caller,raw_inp,**kwargs):
- """
- Node for searching for an existing object.
- """
- try:
- matches=caller.ndb._menutree.olc_search_object_matches
- exceptAttributeError:
- matches=[]
- nmatches=len(matches)
- prev_node=kwargs.get("back","index")
-
- ifmatches:
- text="""
- Found {num} match{post}.
-
- (|RWarning: creating a prototype will |roverwrite|r |Rthe current prototype!)|n""".format(
- num=nmatches,post="es"ifnmatches>1else""
- )
- _set_actioninfo(
- caller,
- _format_list_actions("examine","create prototype from object",prefix="Actions: "),
- )
- else:
- text="Enter search criterion."
-
- helptext="""
- You can search objects by specifying partial key, alias or its exact #dbref. Use *query to
- search for an Account instead.
-
- Once having found any matches you can choose to examine it or use |ccreate prototype from
- object|n. If doing the latter, a prototype will be calculated from the selected object and
- loaded as the new 'current' prototype. This is useful for having a base to build from but be
- careful you are not throwing away any existing, unsaved, prototype work!
- """
-
- text=(text,helptext)
-
- options=_wizard_options(None,prev_node,None)
- options.append({"key":"_default","goto":(_object_search_actions,{"back":prev_node})})
-
- returntext,options
-
-
-# main index (start page) node
-
-
-
[docs]defnode_index(caller):
- prototype=_get_menu_prototype(caller)
-
- text="""
- |c --- Prototype wizard --- |n
-%s
-
- A |cprototype|n is a 'template' for |wspawning|n an in-game entity. A field of the prototype
- can either be hard-coded, left empty or scripted using |w$protfuncs|n - for example to
- randomize the value every time a new entity is spawned. The fields whose names start with
- 'Prototype-' are not fields on the object itself but are used for prototype-inheritance, or
- when saving and loading.
-
- Select prototype field to edit. If you are unsure, start from [|w1|n]. Enter [|wh|n]elp at
- any menu node for more info.
-
- """
- helptxt="""
- |c- prototypes |n
-
- A prototype is really just a Python dictionary. When spawning, this dictionary is essentially
- passed into `|wevennia.utils.create.create_object(**prototype)|n` to create a new object. By
- using different prototypes you can customize instances of objects without having to do code
- changes to their typeclass (something which requires code access). The classical example is
- to spawn goblins with different names, looks, equipment and skill, each based on the same
- `Goblin` typeclass.
-
- At any time you can [|wV|n]alidate that the prototype works correctly and use it to
- [|wSP|n]awn a new entity. You can also [|wSA|n]ve|n your work, [|wLO|n]oad an existing
- prototype to [|wSE|n]arch for existing objects to use as a base. Use [|wL|n]ook to re-show a
- menu node. [|wQ|n]uit will always exit the menu and [|wH|n]elp will show context-sensitive
- help.
-
-
- |c- $protfuncs |n
-
- Prototype-functions (protfuncs) allow for limited scripting within a prototype. These are
- entered as a string $funcname(arg, arg, ...) and are evaluated |wat the time of spawning|n
- only. They can also be nested for combined effects.
-
-{pfuncs}
- """.format(
- pfuncs=_format_protfuncs()
- )
-
- # If a prototype is being edited, show its key and
- # prototype_key under the title
- loaded_prototype=""
- if"prototype_key"inprototypeor"key"inprototype:
- loaded_prototype=" --- Editing: |y{}({})|n --- ".format(
- prototype.get("key",""),prototype.get("prototype_key","")
- )
- text=text%(loaded_prototype)
-
- text=(text,helptxt)
-
- options=[]
- options.append(
- {
- "desc":"|WPrototype-Key|n|n{}".format(
- _format_option_value("Key","prototype_key"notinprototype,prototype,None)
- ),
- "goto":"node_prototype_key",
- }
- )
- forkeyin(
- "Prototype_Parent",
- "Typeclass",
- "Key",
- "Aliases",
- "Attrs",
- "Tags",
- "Locks",
- "Permissions",
- "Location",
- "Home",
- "Destination",
- ):
- required=False
- cropper=None
- ifkeyin("Prototype_Parent","Typeclass"):
- required=("prototype_parent"notinprototype)and("typeclass"notinprototype)
- ifkey=="Typeclass":
- cropper=_path_cropper
- options.append(
- {
- "desc":"{}{}|n{}".format(
- "|W"ifkey=="Prototype_Parent"else"|w",
- key.replace("_","-"),
- _format_option_value(key,required,prototype,cropper=cropper),
- ),
- "goto":"node_{}".format(key.lower()),
- }
- )
- required=False
- forkeyin("Desc","Tags","Locks"):
- options.append(
- {
- "desc":"|WPrototype-{}|n|n{}".format(
- key,_format_option_value(key,required,prototype,None)
- ),
- "goto":"node_prototype_{}".format(key.lower()),
- }
- )
-
- options.extend(
- (
- {"key":("|wV|Walidate prototype","validate","v"),"goto":"node_validate_prototype"},
- {"key":("|wSA|Wve prototype","save","sa"),"goto":"node_prototype_save"},
- {"key":("|wSP|Wawn prototype","spawn","sp"),"goto":"node_prototype_spawn"},
- {"key":("|wLO|Wad prototype","load","lo"),"goto":"node_prototype_load"},
- {"key":("|wSE|Warch objects|n","search","se"),"goto":"node_search_object"},
- )
- )
-
- returntext,options
-
-
-# prototype_key node
-
-
-def_check_prototype_key(caller,key):
- old_prototype=protlib.search_prototype(key)
- olc_new=_is_new_prototype(caller)
- key=key.strip().lower()
- ifold_prototype:
- old_prototype=old_prototype[0]
- # we are starting a new prototype that matches an existing
- ifnotcaller.locks.check_lockstring(
- caller,old_prototype["prototype_locks"],access_type="edit"
- ):
- # return to the node_prototype_key to try another key
- caller.msg(
- "Prototype '{key}' already exists and you don't "
- "have permission to edit it.".format(key=key)
- )
- return"node_prototype_key"
- elifolc_new:
- # we are selecting an existing prototype to edit. Reset to index.
- delcaller.ndb._menutree.olc_new
- caller.ndb._menutree.olc_prototype=old_prototype
- caller.msg("Prototype already exists. Reloading.")
- return"node_index"
-
- return_set_property(caller,key,prop="prototype_key")
-
-
-
[docs]defnode_prototype_key(caller):
-
- text="""
- The |cPrototype-Key|n uniquely identifies the prototype and is |wmandatory|n. It is used to
- find and use the prototype to spawn new entities. It is not case sensitive.
-
- (To set a new value, just write it and press enter)
-
-{current}""".format(
- current=_get_current_value(caller,"prototype_key")
- )
-
- helptext="""
- The prototype-key is not itself used when spawnng the new object, but is only used for
- managing, storing and loading the prototype. It must be globally unique, so existing keys
- will be checked before a new key is accepted. If an existing key is picked, the existing
- prototype will be loaded.
- """
-
- options=_wizard_options("prototype_key","index","prototype_parent")
- options.append({"key":"_default","goto":_check_prototype_key})
-
- text=(text,helptext)
- returntext,options
-
-
-# prototype_parents node
-
-
-def_all_prototype_parents(caller):
- """Return prototype_key of all available prototypes for listing in menu"""
- return[
- prototype["prototype_key"]
- forprototypeinprotlib.search_prototype()
- if"prototype_key"inprototype
- ]
-
-
-def_prototype_parent_actions(caller,raw_inp,**kwargs):
- """Parse the default Convert prototype to a string representation for closer inspection"""
- choices=kwargs.get("available_choices",[])
- prototype_parent,action=_default_parse(
- raw_inp,choices,("examine","e","l"),("add","a"),("remove","r","delete","d")
- )
-
- ifprototype_parent:
- # a selection of parent was made
- prototype_parent=protlib.search_prototype(key=prototype_parent)[0]
- prototype_parent_key=prototype_parent["prototype_key"]
-
- # which action to apply on the selection
- ifaction=="examine":
- # examine the prototype
- txt=protlib.prototype_to_str(prototype_parent)
- kwargs["text"]=txt
- kwargs["back"]="prototype_parent"
- return"node_examine_entity",kwargs
- elifaction=="add":
- # add/append parent
- prot=_get_menu_prototype(caller)
- current_prot_parent=prot.get("prototype_parent",None)
- ifcurrent_prot_parent:
- current_prot_parent=utils.make_iter(current_prot_parent)
- ifprototype_parent_keyincurrent_prot_parent:
- caller.msg("Prototype_parent {} is already used.".format(prototype_parent_key))
- return"node_prototype_parent"
- else:
- current_prot_parent.append(prototype_parent_key)
- caller.msg("Add prototype parent for multi-inheritance.")
- else:
- current_prot_parent=prototype_parent_key
- try:
- ifprototype_parent:
- spawner.flatten_prototype(prototype_parent,validate=True)
- else:
- raiseRuntimeError("Not found.")
- exceptRuntimeErroraserr:
- caller.msg(
- "Selected prototype-parent {} "
- "caused Error(s):\n|r{}|n".format(prototype_parent,err)
- )
- return"node_prototype_parent"
- _set_prototype_value(caller,"prototype_parent",current_prot_parent)
- _get_flat_menu_prototype(caller,refresh=True)
- elifaction=="remove":
- # remove prototype parent
- prot=_get_menu_prototype(caller)
- current_prot_parent=prot.get("prototype_parent",None)
- ifcurrent_prot_parent:
- current_prot_parent=utils.make_iter(current_prot_parent)
- try:
- current_prot_parent.remove(prototype_parent_key)
- _set_prototype_value(caller,"prototype_parent",current_prot_parent)
- _get_flat_menu_prototype(caller,refresh=True)
- caller.msg("Removed prototype parent {}.".format(prototype_parent_key))
- exceptValueError:
- caller.msg(
- "|rPrototype-parent {} could not be removed.".format(prototype_parent_key)
- )
- return"node_prototype_parent"
-
-
-def_prototype_parent_select(caller,new_parent):
-
- ret=None
- prototype_parent=protlib.search_prototype(new_parent)
- try:
- ifprototype_parent:
- spawner.flatten_prototype(prototype_parent[0],validate=True)
- else:
- raiseRuntimeError("Not found.")
- exceptRuntimeErroraserr:
- caller.msg(
- "Selected prototype-parent {} ""caused Error(s):\n|r{}|n".format(new_parent,err)
- )
- else:
- ret=_set_property(
- caller,
- new_parent,
- prop="prototype_parent",
- processor=str,
- next_node="node_prototype_parent",
- )
- _get_flat_menu_prototype(caller,refresh=True)
- caller.msg("Selected prototype parent |c{}|n.".format(new_parent))
- returnret
-
-
-@list_node(_all_prototype_parents,_prototype_parent_select)
-defnode_prototype_parent(caller):
- prototype=_get_menu_prototype(caller)
-
- prot_parent_keys=prototype.get("prototype_parent")
-
- text="""
- The |cPrototype Parent|n allows you to |winherit|n prototype values from another named
- prototype (given as that prototype's |wprototype_key|n). If not changing these values in
- the current prototype, the parent's value will be used. Pick the available prototypes below.
-
- Note that somewhere in the prototype's parentage, a |ctypeclass|n must be specified. If no
- parent is given, this prototype must define the typeclass (next menu node).
-
-{current}
- """
- helptext="""
- Prototypes can inherit from one another. Changes in the child replace any values set in a
- parent. The |wtypeclass|n key must exist |wsomewhere|n in the parent chain for the
- prototype to be valid.
- """
-
- _set_actioninfo(caller,_format_list_actions("examine","add","remove"))
-
- ptexts=[]
- ifprot_parent_keys:
- forpkeyinutils.make_iter(prot_parent_keys):
- prot_parent=protlib.search_prototype(pkey)
- ifprot_parent:
- prot_parent=prot_parent[0]
- ptexts.append(
- "|c -- {pkey} -- |n\n{prot}".format(
- pkey=pkey,prot=protlib.prototype_to_str(prot_parent)
- )
- )
- else:
- ptexts.append("Prototype parent |r{pkey} was not found.".format(pkey=pkey))
-
- ifnotptexts:
- ptexts.append("[No prototype_parent set]")
-
- text=text.format(current="\n\n".join(ptexts))
-
- text=(text,helptext)
-
- options=_wizard_options("prototype_parent","prototype_key","typeclass",color="|W")
- options.append({"key":"_default","goto":_prototype_parent_actions})
-
- returntext,options
-
-
-# typeclasses node
-
-
-def_all_typeclasses(caller):
- """Get name of available typeclasses."""
- returnlist(
- name
- fornameinsorted(utils.get_all_typeclasses("evennia.objects.models.ObjectDB").keys())
- ifname!="evennia.objects.models.ObjectDB"
- )
-
-
-def_typeclass_actions(caller,raw_inp,**kwargs):
- """Parse actions for typeclass listing"""
-
- choices=kwargs.get("available_choices",[])
- typeclass_path,action=_default_parse(
- raw_inp,choices,("examine","e","l"),("remove","r","delete","d")
- )
-
- iftypeclass_path:
- ifaction=="examine":
- typeclass=utils.get_all_typeclasses().get(typeclass_path)
- iftypeclass:
- docstr=[]
- forlineintypeclass.__doc__.split("\n"):
- ifline.strip():
- docstr.append(line)
- elifdocstr:
- break
- docstr="\n".join(docstr)ifdocstrelse"<empty>"
- txt=(
- "Typeclass |c{typeclass_path}|n; "
- "First paragraph of docstring:\n\n{docstring}".format(
- typeclass_path=typeclass_path,docstring=docstr
- )
- )
- else:
- txt="This is typeclass |y{}|n.".format(typeclass)
- return"node_examine_entity",{"text":txt,"back":"typeclass"}
- elifaction=="remove":
- prototype=_get_menu_prototype(caller)
- old_typeclass=prototype.pop("typeclass",None)
- ifold_typeclass:
- _set_menu_prototype(caller,prototype)
- caller.msg("Cleared typeclass {}.".format(old_typeclass))
- else:
- caller.msg("No typeclass to remove.")
- return"node_typeclass"
-
-
-def_typeclass_select(caller,typeclass,**kwargs):
- """Select typeclass from list and add it to prototype. Return next node to go to."""
- ret=_set_property(caller,typeclass,prop="typeclass",processor=str)
- caller.msg("Selected typeclass |c{}|n.".format(typeclass))
- returnret
-
-
-@list_node(_all_typeclasses,_typeclass_select)
-defnode_typeclass(caller):
- text="""
- The |cTypeclass|n defines what 'type' of object this is - the actual working code to use.
-
- All spawned objects must have a typeclass. If not given here, the typeclass must be set in
- one of the prototype's |cparents|n.
-
-{current}
- """.format(
- current=_get_current_value(caller,"typeclass"),
- actions="|WSelect with |w<num>|W. Other actions: "
- "|we|Wxamine |w<num>|W, |wr|Wemove selection",
- )
-
- helptext="""
- A |nTypeclass|n is specified by the actual python-path to the class definition in the
- Evennia code structure.
-
- Which |cAttributes|n, |cLocks|n and other properties have special
- effects or expects certain values depend greatly on the code in play.
- """
-
- text=(text,helptext)
-
- options=_wizard_options("typeclass","prototype_parent","key",color="|W")
- options.append({"key":"_default","goto":_typeclass_actions})
- returntext,options
-
-
-# key node
-
-
-
[docs]defnode_key(caller):
- text="""
- The |cKey|n is the given name of the object to spawn. This will retain the given case.
-
-{current}
- """.format(
- current=_get_current_value(caller,"key")
- )
-
- helptext="""
- The key should often not be identical for every spawned object. Using a randomising
- $protfunc can be used, for example |c$choice(Alan, Tom, John)|n will give one of the three
- names every time an object of this prototype is spawned.
-
- |c$protfuncs|n
-{pfuncs}
- """.format(
- pfuncs=_format_protfuncs()
- )
-
- text=(text,helptext)
-
- options=_wizard_options("key","typeclass","aliases")
- options.append(
- {
- "key":"_default",
- "goto":(_set_property,dict(prop="key",processor=lambdas:s.strip())),
- }
- )
- returntext,options
-
-
-# aliases node
-
-
-def_all_aliases(caller):
- "Get aliases in prototype"
- prototype=_get_menu_prototype(caller)
- returnprototype.get("aliases",[])
-
-
-def_aliases_select(caller,alias):
- "Add numbers as aliases"
- aliases=_all_aliases(caller)
- try:
- ind=str(aliases.index(alias)+1)
- ifindnotinaliases:
- aliases.append(ind)
- _set_prototype_value(caller,"aliases",aliases)
- caller.msg("Added alias '{}'.".format(ind))
- except(IndexError,ValueError)aserr:
- caller.msg("Error: {}".format(err))
-
- return"node_aliases"
-
-
-def_aliases_actions(caller,raw_inp,**kwargs):
- """Parse actions for aliases listing"""
- choices=kwargs.get("available_choices",[])
- alias,action=_default_parse(raw_inp,choices,("remove","r","delete","d"))
-
- aliases=_all_aliases(caller)
- ifaliasandaction=="remove":
- try:
- aliases.remove(alias)
- _set_prototype_value(caller,"aliases",aliases)
- caller.msg("Removed alias '{}'.".format(alias))
- exceptValueError:
- caller.msg("No matching alias found to remove.")
- else:
- # if not a valid remove, add as a new alias
- alias=raw_inp.lower().strip()
- ifaliasandaliasnotinaliases:
- aliases.append(alias)
- _set_prototype_value(caller,"aliases",aliases)
- caller.msg("Added alias '{}'.".format(alias))
- else:
- caller.msg("Alias '{}' was already set.".format(alias))
- return"node_aliases"
-
-
-@list_node(_all_aliases,_aliases_select)
-defnode_aliases(caller):
-
- text="""
- |cAliases|n are alternative ways to address an object, next to its |cKey|n. Aliases are not
- case sensitive.
-
-{current}
- """.format(
- current=_get_current_value(
- caller,
- "aliases",
- comparer=lambdapropval,flatval:[alforalinflatvalifalnotinpropval],
- formatter=lambdalst:"\n"+", ".join(lst),
- only_inherit=True,
- )
- )
- _set_actioninfo(
- caller,_format_list_actions("remove",prefix="|w<text>|W to add new alias. Other action: ")
- )
-
- helptext="""
- Aliases are fixed alternative identifiers and are stored with the new object.
-
- |c$protfuncs|n
-
-{pfuncs}
- """.format(
- pfuncs=_format_protfuncs()
- )
-
- text=(text,helptext)
-
- options=_wizard_options("aliases","key","attrs")
- options.append({"key":"_default","goto":_aliases_actions})
- returntext,options
-
-
-# attributes node
-
-
-def_caller_attrs(caller):
- prototype=_get_menu_prototype(caller)
- attrs=[
- "{}={}".format(tup[0],utils.crop(utils.to_str(tup[1]),width=10))
- fortupinprototype.get("attrs",[])
- ]
- returnattrs
-
-
-def_get_tup_by_attrname(caller,attrname):
- prototype=_get_menu_prototype(caller)
- attrs=prototype.get("attrs",[])
- try:
- inp=[tup[0]fortupinattrs].index(attrname)
- returnattrs[inp]
- exceptValueError:
- returnNone
-
-
-def_display_attribute(attr_tuple):
- """Pretty-print attribute tuple"""
- attrkey,value,category,locks=attr_tuple
- value=protlib.protfunc_parser(value)
- typ=type(value)
- out="{attrkey} |c=|n {value} |W({typ}{category}{locks})|n".format(
- attrkey=attrkey,
- value=value,
- typ=typ,
- category=", category={}".format(category)ifcategoryelse"",
- locks=", locks={}".format(";".join(locks))ifany(locks)else"",
- )
-
- returnout
-
-
-def_add_attr(caller,attr_string,**kwargs):
- """
- Add new attribute, parsing input.
-
- Args:
- caller (Object): Caller of menu.
- attr_string (str): Input from user
- attr is entered on these forms
- attr = value
- attr;category = value
- attr;category;lockstring = value
- Keyword Args:
- delete (str): If this is set, attr_string is
- considered the name of the attribute to delete and
- no further parsing happens.
- Returns:
- result (str): Result string of action.
- """
- attrname=""
- value=""
- category=None
- locks=""
-
- if"delete"inkwargs:
- attrname=attr_string.lower().strip()
- elif"="inattr_string:
- attrname,value=(part.strip()forpartinattr_string.split("=",1))
- attrname=attrname.lower()
- nameparts=attrname.split(";",2)
- nparts=len(nameparts)
- ifnparts==2:
- attrname,category=nameparts
- elifnparts>2:
- attrname,category,locks=nameparts
- attr_tuple=(attrname,value,category,str(locks))
-
- ifattrname:
- prot=_get_menu_prototype(caller)
- attrs=prot.get("attrs",[])
-
- if"delete"inkwargs:
- try:
- ind=[tup[0]fortupinattrs].index(attrname)
- delattrs[ind]
- _set_prototype_value(caller,"attrs",attrs)
- return"Removed Attribute '{}'".format(attrname)
- exceptIndexError:
- return"Attribute to delete not found."
-
- try:
- # replace existing attribute with the same name in the prototype
- ind=[tup[0]fortupinattrs].index(attrname)
- attrs[ind]=attr_tuple
- text="Edited Attribute '{}'.".format(attrname)
- exceptValueError:
- attrs.append(attr_tuple)
- text="Added Attribute "+_display_attribute(attr_tuple)
-
- _set_prototype_value(caller,"attrs",attrs)
- else:
- text="Attribute must be given as 'attrname[;category;locks] = <value>'."
-
- returntext
-
-
-def_attr_select(caller,attrstr):
- attrname,_=attrstr.split("=",1)
- attrname=attrname.strip()
-
- attr_tup=_get_tup_by_attrname(caller,attrname)
- ifattr_tup:
- return("node_examine_entity",{"text":_display_attribute(attr_tup),"back":"attrs"})
- else:
- caller.msg("Attribute not found.")
- return"node_attrs"
-
-
-def_attrs_actions(caller,raw_inp,**kwargs):
- """Parse actions for attribute listing"""
- choices=kwargs.get("available_choices",[])
- attrstr,action=_default_parse(
- raw_inp,choices,("examine","e"),("remove","r","delete","d")
- )
- ifattrstrisNone:
- attrstr=raw_inp
- try:
- attrname,_=attrstr.split("=",1)
- exceptValueError:
- caller.msg("|rNeed to enter the attribute on the form attrname=value.|n")
- return"node_attrs"
-
- attrname=attrname.strip()
- attr_tup=_get_tup_by_attrname(caller,attrname)
-
- ifactionandattr_tup:
- ifaction=="examine":
- return("node_examine_entity",{"text":_display_attribute(attr_tup),"back":"attrs"})
- elifaction=="remove":
- res=_add_attr(caller,attrname,delete=True)
- caller.msg(res)
- else:
- res=_add_attr(caller,raw_inp)
- caller.msg(res)
- return"node_attrs"
-
-
-@list_node(_caller_attrs,_attr_select)
-defnode_attrs(caller):
- def_currentcmp(propval,flatval):
- "match by key + category"
- cmp1=[(tup[0].lower(),tup[2].lower()iftup[2]elseNone)fortupinpropval]
- return[
- tup
- fortupinflatval
- if(tup[0].lower(),tup[2].lower()iftup[2]elseNone)notincmp1
- ]
-
- text="""
- |cAttributes|n are custom properties of the object. Enter attributes on one of these forms:
-
- attrname=value
- attrname;category=value
- attrname;category;lockstring=value
-
- To give an attribute without a category but with a lockstring, leave that spot empty
- (attrname;;lockstring=value). Attribute values can have embedded $protfuncs.
-
-{current}
- """.format(
- current=_get_current_value(
- caller,
- "attrs",
- comparer=_currentcmp,
- formatter=lambdalst:"\n"+"\n".join(_display_attribute(tup)fortupinlst),
- only_inherit=True,
- )
- )
- _set_actioninfo(caller,_format_list_actions("examine","remove",prefix="Actions: "))
-
- helptext="""
- Most commonly, Attributes don't need any categories or locks. If using locks, the lock-types
- 'attredit' and 'attrread' are used to limit editing and viewing of the Attribute. Putting
- the lock-type `attrcreate` in the |clocks|n prototype key can be used to restrict builders
- from adding new Attributes.
-
- |c$protfuncs
-
-{pfuncs}
- """.format(
- pfuncs=_format_protfuncs()
- )
-
- text=(text,helptext)
-
- options=_wizard_options("attrs","aliases","tags")
- options.append({"key":"_default","goto":_attrs_actions})
- returntext,options
-
-
-# tags node
-
-
-def_caller_tags(caller):
- prototype=_get_menu_prototype(caller)
- tags=[tup[0]fortupinprototype.get("tags",[])]
- returntags
-
-
-def_get_tup_by_tagname(caller,tagname):
- prototype=_get_menu_prototype(caller)
- tags=prototype.get("tags",[])
- try:
- inp=[tup[0]fortupintags].index(tagname)
- returntags[inp]
- exceptValueError:
- returnNone
-
-
-def_display_tag(tag_tuple):
- """Pretty-print tag tuple"""
- tagkey,category,data=tag_tuple
- out="Tag: '{tagkey}' (category: {category}{dat})".format(
- tagkey=tagkey,category=category,dat=", data: {}".format(data)ifdataelse""
- )
- returnout
-
-
-def_add_tag(caller,tag_string,**kwargs):
- """
- Add tags to the system, parsing input
-
- Args:
- caller (Object): Caller of menu.
- tag_string (str): Input from user on one of these forms
- tagname
- tagname;category
- tagname;category;data
-
- Keyword Args:
- delete (str): If this is set, tag_string is considered
- the name of the tag to delete.
-
- Returns:
- result (str): Result string of action.
-
- """
- tag=tag_string.strip().lower()
- category=None
- data=""
-
- if"delete"inkwargs:
- tag=tag_string.lower().strip()
- else:
- nameparts=tag.split(";",2)
- ntuple=len(nameparts)
- ifntuple==2:
- tag,category=nameparts
- elifntuple>2:
- tag,category,data=nameparts[:3]
-
- tag_tuple=(tag.lower(),category.lower()ifcategoryelseNone,data)
-
- iftag:
- prot=_get_menu_prototype(caller)
- tags=prot.get("tags",[])
-
- old_tag=_get_tup_by_tagname(caller,tag)
-
- if"delete"inkwargs:
-
- ifold_tag:
- tags.pop(tags.index(old_tag))
- text="Removed Tag '{}'.".format(tag)
- else:
- text="Found no Tag to remove."
- elifnotold_tag:
- # a fresh, new tag
- tags.append(tag_tuple)
- text="Added Tag '{}'".format(tag)
- else:
- # old tag exists; editing a tag means replacing old with new
- ind=tags.index(old_tag)
- tags[ind]=tag_tuple
- text="Edited Tag '{}'".format(tag)
-
- _set_prototype_value(caller,"tags",tags)
- else:
- text="Tag must be given as 'tag[;category;data]'."
-
- returntext
-
-
-def_tag_select(caller,tagname):
- tag_tup=_get_tup_by_tagname(caller,tagname)
- iftag_tup:
- return"node_examine_entity",{"text":_display_tag(tag_tup),"back":"attrs"}
- else:
- caller.msg("Tag not found.")
- return"node_attrs"
-
-
-def_tags_actions(caller,raw_inp,**kwargs):
- """Parse actions for tags listing"""
- choices=kwargs.get("available_choices",[])
- tagname,action=_default_parse(
- raw_inp,choices,("examine","e"),("remove","r","delete","d")
- )
-
- iftagnameisNone:
- tagname=raw_inp.lower().strip()
-
- tag_tup=_get_tup_by_tagname(caller,tagname)
-
- iftag_tup:
- ifaction=="examine":
- return("node_examine_entity",{"text":_display_tag(tag_tup),"back":"tags"})
- elifaction=="remove":
- res=_add_tag(caller,tagname,delete=True)
- caller.msg(res)
- else:
- res=_add_tag(caller,raw_inp)
- caller.msg(res)
- return"node_tags"
-
-
-@list_node(_caller_tags,_tag_select)
-defnode_tags(caller):
- def_currentcmp(propval,flatval):
- "match by key + category"
- cmp1=[(tup[0].lower(),tup[1].lower()iftup[2]elseNone)fortupinpropval]
- return[
- tup
- fortupinflatval
- if(tup[0].lower(),tup[1].lower()iftup[1]elseNone)notincmp1
- ]
-
- text="""
- |cTags|n are used to group objects so they can quickly be found later. Enter tags on one of
- the following forms:
- tagname
- tagname;category
- tagname;category;data
-
-{current}
- """.format(
- current=_get_current_value(
- caller,
- "tags",
- comparer=_currentcmp,
- formatter=lambdalst:"\n"+"\n".join(_display_tag(tup)fortupinlst),
- only_inherit=True,
- )
- )
- _set_actioninfo(caller,_format_list_actions("examine","remove",prefix="Actions: "))
-
- helptext="""
- Tags are shared between all objects with that tag. So the 'data' field (which is not
- commonly used) can only hold eventual info about the Tag itself, not about the individual
- object on which it sits.
-
- All objects created with this prototype will automatically get assigned a tag named the same
- as the |cprototype_key|n and with a category "{tag_category}". This allows the spawner to
- optionally update previously spawned objects when their prototype changes.
- """.format(
- tag_category=protlib.PROTOTYPE_TAG_CATEGORY
- )
-
- text=(text,helptext)
- options=_wizard_options("tags","attrs","locks")
- options.append({"key":"_default","goto":_tags_actions})
- returntext,options
-
-
-# locks node
-
-
-def_caller_locks(caller):
- locks=_get_menu_prototype(caller).get("locks","")
- return[lckforlckinlocks.split(";")iflck]
-
-
-def_locks_display(caller,lock):
- returnlock
-
-
-def_lock_select(caller,lockstr):
- return("node_examine_entity",{"text":_locks_display(caller,lockstr),"back":"locks"})
-
-
-def_lock_add(caller,lock,**kwargs):
- locks=_caller_locks(caller)
-
- try:
- locktype,lockdef=lock.split(":",1)
- exceptValueError:
- return"Lockstring lacks ':'."
-
- locktype=locktype.strip().lower()
-
- if"delete"inkwargs:
- try:
- ind=locks.index(lock)
- locks.pop(ind)
- _set_prototype_value(caller,"locks",";".join(locks),parse=False)
- ret="Lock {} deleted.".format(lock)
- exceptValueError:
- ret="No lock found to delete."
- returnret
- try:
- locktypes=[lck.split(":",1)[0].strip().lower()forlckinlocks]
- ind=locktypes.index(locktype)
- locks[ind]=lock
- ret="Lock with locktype '{}' updated.".format(locktype)
- exceptValueError:
- locks.append(lock)
- ret="Added lock '{}'.".format(lock)
- _set_prototype_value(caller,"locks",";".join(locks))
- returnret
-
-
-def_locks_actions(caller,raw_inp,**kwargs):
- choices=kwargs.get("available_choices",[])
- lock,action=_default_parse(
- raw_inp,choices,("examine","e"),("remove","r","delete","d")
- )
-
- iflock:
- ifaction=="examine":
- return("node_examine_entity",{"text":_locks_display(caller,lock),"back":"locks"})
- elifaction=="remove":
- ret=_lock_add(caller,lock,delete=True)
- caller.msg(ret)
- else:
- ret=_lock_add(caller,raw_inp)
- caller.msg(ret)
-
- return"node_locks"
-
-
-@list_node(_caller_locks,_lock_select)
-defnode_locks(caller):
- def_currentcmp(propval,flatval):
- "match by locktype"
- cmp1=[lck.split(":",1)[0]forlckinpropval.split(";")]
- return";".join(lstrforlstrinflatval.split(";")iflstr.split(":",1)[0]notincmp1)
-
- text="""
- The |cLock string|n defines limitations for accessing various properties of the object once
- it's spawned. The string should be on one of the following forms:
-
- locktype:[NOT] lockfunc(args)
- locktype: [NOT] lockfunc(args) [AND|OR|NOT] lockfunc(args) [AND|OR|NOT] ...
-
-{current}{action}
- """.format(
- current=_get_current_value(
- caller,
- "locks",
- comparer=_currentcmp,
- formatter=lambdalockstr:"\n".join(
- _locks_display(caller,lstr)forlstrinlockstr.split(";")
- ),
- only_inherit=True,
- ),
- action=_format_list_actions("examine","remove",prefix="Actions: "),
- )
-
- helptext="""
- Here is an example of two lock strings:
-
- edit:false()
- call:tag(Foo) OR perm(Builder)
-
- Above locks limit two things, 'edit' and 'call'. Which lock types are actually checked
- depend on the typeclass of the object being spawned. Here 'edit' is never allowed by anyone
- while 'call' is allowed to all accessors with a |ctag|n 'Foo' OR which has the
- |cPermission|n 'Builder'.
-
- |cAvailable lockfuncs:|n
-
-{lfuncs}
- """.format(
- lfuncs=_format_lockfuncs()
- )
-
- text=(text,helptext)
-
- options=_wizard_options("locks","tags","permissions")
- options.append({"key":"_default","goto":_locks_actions})
-
- returntext,options
-
-
-# permissions node
-
-
-def_caller_permissions(caller):
- prototype=_get_menu_prototype(caller)
- perms=prototype.get("permissions",[])
- returnperms
-
-
-def_display_perm(caller,permission,only_hierarchy=False):
- hierarchy=settings.PERMISSION_HIERARCHY
- perm_low=permission.lower()
- txt=""
- ifperm_lowin[prm.lower()forprminhierarchy]:
- txt="Permission (in hieararchy): {}".format(
- ", ".join(
- [
- "|w[{}]|n".format(prm)ifprm.lower()==perm_lowelse"|W{}|n".format(prm)
- forprminhierarchy
- ]
- )
- )
- elifnotonly_hierarchy:
- txt="Permission: '{}'".format(permission)
- returntxt
-
-
-def_permission_select(caller,permission,**kwargs):
- return(
- "node_examine_entity",
- {"text":_display_perm(caller,permission),"back":"permissions"},
- )
-
-
-def_add_perm(caller,perm,**kwargs):
- ifperm:
- perm_low=perm.lower()
- perms=_caller_permissions(caller)
- perms_low=[prm.lower()forprminperms]
- if"delete"inkwargs:
- try:
- ind=perms_low.index(perm_low)
- delperms[ind]
- text="Removed Permission '{}'.".format(perm)
- exceptValueError:
- text="Found no Permission to remove."
- else:
- ifperm_lowinperms_low:
- text="Permission already set."
- else:
- perms.append(perm)
- _set_prototype_value(caller,"permissions",perms)
- text="Added Permission '{}'".format(perm)
- returntext
-
-
-def_permissions_actions(caller,raw_inp,**kwargs):
- """Parse actions for permission listing"""
- choices=kwargs.get("available_choices",[])
- perm,action=_default_parse(
- raw_inp,choices,("examine","e"),("remove","r","delete","d")
- )
-
- ifperm:
- ifaction=="examine":
- return(
- "node_examine_entity",
- {"text":_display_perm(caller,perm),"back":"permissions"},
- )
- elifaction=="remove":
- res=_add_perm(caller,perm,delete=True)
- caller.msg(res)
- else:
- res=_add_perm(caller,raw_inp.strip())
- caller.msg(res)
- return"node_permissions"
-
-
-@list_node(_caller_permissions,_permission_select)
-defnode_permissions(caller):
- def_currentcmp(pval,fval):
- cmp1=[perm.lower()forperminpval]
- return[permforperminfvalifperm.lower()notincmp1]
-
- text="""
- |cPermissions|n are simple strings used to grant access to this object. A permission is used
- when a |clock|n is checked that contains the |wperm|n or |wpperm|n lock functions. Certain
- permissions belong in the |cpermission hierarchy|n together with the |Wperm()|n lock
- function.
-
-{current}
- """.format(
- current=_get_current_value(
- caller,
- "permissions",
- comparer=_currentcmp,
- formatter=lambdalst:"\n"+"\n".join(prmforprminlst),
- only_inherit=True,
- )
- )
- _set_actioninfo(caller,_format_list_actions("examine","remove",prefix="Actions: "))
-
- helptext="""
- Any string can act as a permission as long as a lock is set to look for it. Depending on the
- lock, having a permission could even be negative (i.e. the lock is only passed if you
- |wdon't|n have the 'permission'). The most common permissions are the hierarchical
- permissions:
-
-{permissions}.
-
- For example, a |clock|n string like "edit:perm(Builder)" will grant access to accessors
- having the |cpermission|n "Builder" or higher.
- """.format(
- permissions=", ".join(settings.PERMISSION_HIERARCHY)
- )
-
- text=(text,helptext)
-
- options=_wizard_options("permissions","locks","location")
- options.append({"key":"_default","goto":_permissions_actions})
-
- returntext,options
-
-
-# location node
-
-
-
[docs]defnode_location(caller):
-
- text="""
- The |cLocation|n of this object in the world. If not given, the object will spawn in the
- inventory of |c{caller}|n by default.
-
-{current}
- """.format(
- caller=caller.key,current=_get_current_value(caller,"location")
- )
-
- helptext="""
- You get the most control by not specifying the location - you can then teleport the spawned
- objects as needed later. Setting the location may be useful for quickly populating a given
- location. One could also consider randomizing the location using a $protfunc.
-
- |c$protfuncs|n
-{pfuncs}
- """.format(
- pfuncs=_format_protfuncs()
- )
-
- text=(text,helptext)
-
- options=_wizard_options("location","permissions","home",search=True)
- options.append(
- {
- "key":"_default",
- "goto":(_set_property,dict(prop="location",processor=lambdas:s.strip())),
- }
- )
- returntext,options
-
-
-# home node
-
-
-
[docs]defnode_home(caller):
-
- text="""
- The |cHome|n location of an object is often only used as a backup - this is where the object
- will be moved to if its location is deleted. The home location can also be used as an actual
- home for characters to quickly move back to.
-
- If unset, the global home default (|w{default}|n) will be used.
-
-{current}
- """.format(
- default=settings.DEFAULT_HOME,current=_get_current_value(caller,"home")
- )
- helptext="""
- The home can be given as a #dbref but can also be specified using the protfunc
- '$obj(name)'. Use |wSE|nearch to find objects in the database.
-
- The home location is commonly not used except as a backup; using the global default is often
- enough.
-
- |c$protfuncs|n
-{pfuncs}
- """.format(
- pfuncs=_format_protfuncs()
- )
-
- text=(text,helptext)
-
- options=_wizard_options("home","location","destination",search=True)
- options.append(
- {
- "key":"_default",
- "goto":(_set_property,dict(prop="home",processor=lambdas:s.strip())),
- }
- )
- returntext,options
-
-
-# destination node
-
-
-
[docs]defnode_destination(caller):
-
- text="""
- The object's |cDestination|n is generally only used by Exit-like objects to designate where
- the exit 'leads to'. It's usually unset for all other types of objects.
-
-{current}
- """.format(
- current=_get_current_value(caller,"destination")
- )
-
- helptext="""
- The destination can be given as a #dbref but can also be specified using the protfunc
- '$obj(name)'. Use |wSEearch to find objects in the database.
-
- |c$protfuncs|n
-{pfuncs}
- """.format(
- pfuncs=_format_protfuncs()
- )
-
- text=(text,helptext)
-
- options=_wizard_options("destination","home","prototype_desc",search=True)
- options.append(
- {
- "key":"_default",
- "goto":(_set_property,dict(prop="destination",processor=lambdas:s.strip())),
- }
- )
- returntext,options
-
-
-# prototype_desc node
-
-
-
[docs]defnode_prototype_desc(caller):
-
- text="""
- The |cPrototype-Description|n briefly describes the prototype when it's viewed in listings.
-
-{current}
- """.format(
- current=_get_current_value(caller,"prototype_desc")
- )
-
- helptext="""
- Giving a brief description helps you and others to locate the prototype for use later.
- """
-
- text=(text,helptext)
-
- options=_wizard_options("prototype_desc","prototype_key","prototype_tags")
- options.append(
- {
- "key":"_default",
- "goto":(
- _set_property,
- dict(
- prop="prototype_desc",
- processor=lambdas:s.strip(),
- next_node="node_prototype_desc",
- ),
- ),
- }
- )
-
- returntext,options
-
-
-# prototype_tags node
-
-
-def_caller_prototype_tags(caller):
- prototype=_get_menu_prototype(caller)
- tags=prototype.get("prototype_tags",[])
- tags=[tag[0]ifisinstance(tag,tuple)elsetagfortagintags]
- returntags
-
-
-def_add_prototype_tag(caller,tag_string,**kwargs):
- """
- Add prototype_tags to the system. We only support straight tags, no
- categories (category is assigned automatically).
-
- Args:
- caller (Object): Caller of menu.
- tag_string (str): Input from user - only tagname
-
- Keyword Args:
- delete (str): If this is set, tag_string is considered
- the name of the tag to delete.
-
- Returns:
- result (str): Result string of action.
-
- """
- tag=tag_string.strip().lower()
-
- iftag:
- tags=_caller_prototype_tags(caller)
- exists=tagintags
-
- if"delete"inkwargs:
- ifexists:
- tags.pop(tags.index(tag))
- text="Removed Prototype-Tag '{}'.".format(tag)
- else:
- text="Found no Prototype-Tag to remove."
- elifnotexists:
- # a fresh, new tag
- tags.append(tag)
- text="Added Prototype-Tag '{}'.".format(tag)
- else:
- text="Prototype-Tag already added."
-
- _set_prototype_value(caller,"prototype_tags",tags)
- else:
- text="No Prototype-Tag specified."
-
- returntext
-
-
-def_prototype_tag_select(caller,tagname):
- caller.msg("Prototype-Tag: {}".format(tagname))
- return"node_prototype_tags"
-
-
-def_prototype_tags_actions(caller,raw_inp,**kwargs):
- """Parse actions for tags listing"""
- choices=kwargs.get("available_choices",[])
- tagname,action=_default_parse(raw_inp,choices,("remove","r","delete","d"))
-
- iftagname:
- ifaction=="remove":
- res=_add_prototype_tag(caller,tagname,delete=True)
- caller.msg(res)
- else:
- res=_add_prototype_tag(caller,raw_inp.lower().strip())
- caller.msg(res)
- return"node_prototype_tags"
-
-
-@list_node(_caller_prototype_tags,_prototype_tag_select)
-defnode_prototype_tags(caller):
-
- text="""
- |cPrototype-Tags|n can be used to classify and find prototypes in listings Tag names are not
- case-sensitive and can have not have a custom category.
-
-{current}
- """.format(
- current=_get_current_value(
- caller,
- "prototype_tags",
- formatter=lambdalst:", ".join(tgfortginlst),
- only_inherit=True,
- )
- )
- _set_actioninfo(
- caller,_format_list_actions("remove",prefix="|w<text>|n|W to add Tag. Other Action:|n ")
- )
- helptext="""
- Using prototype-tags is a good way to organize and group large numbers of prototypes by
- genre, type etc. Under the hood, prototypes' tags will all be stored with the category
- '{tagmetacategory}'.
- """.format(
- tagmetacategory=protlib._PROTOTYPE_TAG_META_CATEGORY
- )
-
- text=(text,helptext)
-
- options=_wizard_options("prototype_tags","prototype_desc","prototype_locks")
- options.append({"key":"_default","goto":_prototype_tags_actions})
-
- returntext,options
-
-
-# prototype_locks node
-
-
-def_caller_prototype_locks(caller):
- locks=_get_menu_prototype(caller).get("prototype_locks","")
- return[lckforlckinlocks.split(";")iflck]
-
-
-def_prototype_lock_select(caller,lockstr):
- return(
- "node_examine_entity",
- {"text":_locks_display(caller,lockstr),"back":"prototype_locks"},
- )
-
-
-def_prototype_lock_add(caller,lock,**kwargs):
- locks=_caller_prototype_locks(caller)
-
- try:
- locktype,lockdef=lock.split(":",1)
- exceptValueError:
- return"Lockstring lacks ':'."
-
- locktype=locktype.strip().lower()
-
- if"delete"inkwargs:
- try:
- ind=locks.index(lock)
- locks.pop(ind)
- _set_prototype_value(caller,"prototype_locks",";".join(locks),parse=False)
- ret="Prototype-lock {} deleted.".format(lock)
- exceptValueError:
- ret="No Prototype-lock found to delete."
- returnret
- try:
- locktypes=[lck.split(":",1)[0].strip().lower()forlckinlocks]
- ind=locktypes.index(locktype)
- locks[ind]=lock
- ret="Prototype-lock with locktype '{}' updated.".format(locktype)
- exceptValueError:
- locks.append(lock)
- ret="Added Prototype-lock '{}'.".format(lock)
- _set_prototype_value(caller,"prototype_locks",";".join(locks))
- returnret
-
-
-def_prototype_locks_actions(caller,raw_inp,**kwargs):
- choices=kwargs.get("available_choices",[])
- lock,action=_default_parse(
- raw_inp,choices,("examine","e"),("remove","r","delete","d")
- )
-
- iflock:
- ifaction=="examine":
- return("node_examine_entity",{"text":_locks_display(caller,lock),"back":"locks"})
- elifaction=="remove":
- ret=_prototype_lock_add(caller,lock.strip(),delete=True)
- caller.msg(ret)
- else:
- ret=_prototype_lock_add(caller,raw_inp.strip())
- caller.msg(ret)
-
- return"node_prototype_locks"
-
-
-@list_node(_caller_prototype_locks,_prototype_lock_select)
-defnode_prototype_locks(caller):
-
- text="""
- |cPrototype-Locks|n are used to limit access to this prototype when someone else is trying
- to access it. By default any prototype can be edited only by the creator and by Admins while
- they can be used by anyone with access to the spawn command. There are two valid lock types
- the prototype access tools look for:
-
- - 'edit': Who can edit the prototype.
- - 'spawn': Who can spawn new objects with this prototype.
-
- If unsure, keep the open defaults.
-
-{current}
- """.format(
- current=_get_current_value(
- caller,
- "prototype_locks",
- formatter=lambdalstring:"\n".join(
- _locks_display(caller,lstr)forlstrinlstring.split(";")
- ),
- only_inherit=True,
- )
- )
- _set_actioninfo(caller,_format_list_actions("examine","remove",prefix="Actions: "))
-
- helptext="""
- Prototype locks can be used to vary access for different tiers of builders. It also allows
- developers to produce 'base prototypes' only meant for builders to inherit and expand on
- rather than tweak in-place.
- """
-
- text=(text,helptext)
-
- options=_wizard_options("prototype_locks","prototype_tags","index")
- options.append({"key":"_default","goto":_prototype_locks_actions})
-
- returntext,options
-
-
-# update existing objects node
-
-
-def_apply_diff(caller,**kwargs):
- """update existing objects"""
- prototype=kwargs["prototype"]
- objects=kwargs["objects"]
- back_node=kwargs["back_node"]
- diff=kwargs.get("diff",None)
- num_changed=spawner.batch_update_objects_with_prototype(prototype,diff=diff,objects=objects,
- caller=caller)
- caller.msg("|g{num} objects were updated successfully.|n".format(num=num_changed))
- returnback_node
-
-
-def_keep_diff(caller,**kwargs):
- """Change to KEEP setting for a given section of a diff"""
- # from evennia import set_trace;set_trace(term_size=(182, 50))
- path=kwargs["path"]
- diff=kwargs["diff"]
- tmp=diff
- forkeyinpath[:-1]:
- tmp=tmp[key]
- tmp[path[-1]]=tuple(list(tmp[path[-1]][:-1])+["KEEP"])
-
-
-def_format_diff_text_and_options(diff,minimal=True,**kwargs):
- """
- Reformat the diff in a way suitable for the olc menu.
-
- Args:
- diff (dict): A diff as produced by `prototype_diff`.
- minimal (bool, optional): Don't show KEEPs.
-
- Keyword Args:
- any (any): Forwarded into the generated options as arguments to the callable.
-
- Returns:
- texts (list): List of texts.
- options (list): List of options dict.
-
- """
- valid_instructions=("KEEP","REMOVE","ADD","UPDATE")
-
- def_visualize(obj,rootname,get_name=False):
- ifutils.is_iter(obj):
- ifnotobj:
- returnstr(obj)
- ifget_name:
- returnobj[0]ifobj[0]else"<unset>"
- ifrootname=="attrs":
- return"{} |W=|n {} |W(category:|n {}|W, locks:|n {}|W)|n".format(*obj)
- elifrootname=="tags":
- return"{} |W(category:|n {}|W)|n".format(obj[0],obj[1])
-
- return"{}".format(obj)
-
- def_parse_diffpart(diffpart,optnum,*args):
- typ=type(diffpart)
- texts=[]
- options=[]
- iftyp==tupleandlen(diffpart)==3anddiffpart[2]invalid_instructions:
- rootname=args[0]
- old,new,instruction=diffpart
- ifinstruction=="KEEP":
- ifnotminimal:
- texts.append(" |gKEEP|W:|n {old}".format(old=_visualize(old,rootname)))
- else:
- # instructions we should be able to revert by a menu choice
- vold=_visualize(old,rootname)
- vnew=_visualize(new,rootname)
- vsep=""iflen(vold)<78else"\n"
-
- ifinstruction=="ADD":
- texts.append(
- " |c[{optnum}] |yADD|n: {new}".format(
- optnum=optnum,new=_visualize(new,rootname)
- )
- )
- elifinstruction=="REMOVE"andnotnew:
- ifrootname=="tags"andold[1]==protlib.PROTOTYPE_TAG_CATEGORY:
- # special exception for the prototype-tag mechanism
- # this is added post-spawn automatically and should
- # not be listed as REMOVE.
- returntexts,options,optnum
-
- texts.append(
- " |c[{optnum}] |rREMOVE|n: {old}".format(
- optnum=optnum,old=_visualize(old,rootname)
- )
- )
- else:
- vinst="|y{}|n".format(instruction)
- texts.append(
- " |c[{num}] {inst}|W:|n {old} |W->|n{sep}{new}".format(
- inst=vinst,num=optnum,old=vold,sep=vsep,new=vnew
- )
- )
- options.append(
- {
- "key":str(optnum),
- "desc":"|gKEEP|n ({}) {}".format(
- rootname,_visualize(old,args[-1],get_name=True)
- ),
- "goto":(_keep_diff,dict((("path",args),("diff",diff)),**kwargs)),
- }
- )
- optnum+=1
- else:
- forkeyinsorted(list(diffpart.keys())):
- subdiffpart=diffpart[key]
- text,option,optnum=_parse_diffpart(subdiffpart,optnum,*(args+(key,)))
- texts.extend(text)
- options.extend(option)
- returntexts,options,optnum
-
- texts=[]
- options=[]
- # we use this to allow for skipping full KEEP instructions
- optnum=1
-
- forroot_keyinsorted(diff):
- diffpart=diff[root_key]
- text,option,optnum=_parse_diffpart(diffpart,optnum,root_key)
- heading="- |w{}:|n ".format(root_key)
- iftext:
- text=[heading+text[0]]+text[1:]
- else:
- text=[heading]
-
- texts.extend(text)
- options.extend(option)
-
- returntexts,options
-
-
-
[docs]defnode_apply_diff(caller,**kwargs):
- """Offer options for updating objects"""
-
- def_keep_option(keyname,prototype,base_obj,obj_prototype,diff,objects,back_node):
- """helper returning an option dict"""
- options={
- "desc":"Keep {} as-is".format(keyname),
- "goto":(
- _keep_diff,
- {
- "key":keyname,
- "prototype":prototype,
- "base_obj":base_obj,
- "obj_prototype":obj_prototype,
- "diff":diff,
- "objects":objects,
- "back_node":back_node,
- },
- ),
- }
- returnoptions
-
- prototype=kwargs.get("prototype",None)
- update_objects=kwargs.get("objects",None)
- back_node=kwargs.get("back_node","node_index")
- obj_prototype=kwargs.get("obj_prototype",None)
- base_obj=kwargs.get("base_obj",None)
- diff=kwargs.get("diff",None)
- custom_location=kwargs.get("custom_location",None)
-
- ifnotupdate_objects:
- text="There are no existing objects to update."
- options={"key":"_default","goto":back_node}
- returntext,options
-
- ifnotdiff:
- # use one random object as a reference to calculate a diff
- base_obj=choice(update_objects)
-
- diff,obj_prototype=spawner.prototype_diff_from_object(prototype,base_obj)
-
- helptext="""
- This will go through all existing objects and apply the changes you accept.
-
- Be careful with this operation! The upgrade mechanism will try to automatically estimate
- what changes need to be applied. But the estimate is |wonly based on the analysis of one
- randomly selected object|n among all objects spawned by this prototype. If that object
- happens to be unusual in some way the estimate will be off and may lead to unexpected
- results for other objects. Always test your objects carefully after an upgrade and consider
- being conservative (switch to KEEP) for things you are unsure of. For complex upgrades it
- may be better to get help from an administrator with access to the `@py` command for doing
- this manually.
-
- Note that the `location` will never be auto-adjusted because it's so rare to want to
- homogenize the location of all object instances."""
-
- ifnotcustom_location:
- diff.pop("location",None)
-
- txt,options=_format_diff_text_and_options(
- diff,objects=update_objects,base_obj=base_obj,prototype=prototype
- )
-
- ifoptions:
- text=[
- "Suggested changes to {} objects. ".format(len(update_objects)),
- "Showing random example obj to change: {name} ({dbref}))\n".format(
- name=base_obj.key,dbref=base_obj.dbref
- ),
- ]+txt
- options.extend(
- [
- {
- "key":("|wu|Wpdate {} objects".format(len(update_objects)),"update","u"),
- "desc":"Update {} objects".format(len(update_objects)),
- "goto":(
- _apply_diff,
- {
- "prototype":prototype,
- "objects":update_objects,
- "back_node":back_node,
- "diff":diff,
- "base_obj":base_obj,
- },
- ),
- },
- {
- "key":("|wr|Weset changes","reset","r"),
- "goto":(
- "node_apply_diff",
- {"prototype":prototype,"back_node":back_node,"objects":update_objects},
- ),
- },
- ]
- )
- else:
- text=[
- "Analyzed a random sample object (out of {}) - "
- "found no changes to apply.".format(len(update_objects))
- ]
-
- options.extend(_wizard_options("update_objects",back_node[5:],None))
- options.append({"key":"_default","goto":back_node})
-
- text="\n".join(text)
- text=(text,helptext)
-
- returntext,options
-
-
-# prototype save node
-
-
-
[docs]defnode_prototype_save(caller,**kwargs):
- """Save prototype to disk """
- # these are only set if we selected 'yes' to save on a previous pass
- prototype=kwargs.get("prototype",None)
- # set to True/False if answered, None if first pass
- accept_save=kwargs.get("accept_save",None)
-
- ifaccept_saveandprototype:
- # we already validated and accepted the save, so this node acts as a goto callback and
- # should now only return the next node
- prototype_key=prototype.get("prototype_key")
- try:
- protlib.save_prototype(prototype)
- exceptExceptionasexc:
- text="|rCould not save:|n {}\n(press Return to continue)".format(exc)
- options={"key":"_default","goto":"node_index"}
- returntext,options
-
- spawned_objects=protlib.search_objects_with_prototype(prototype_key)
- nspawned=spawned_objects.count()
-
- text=["|gPrototype saved.|n"]
-
- ifnspawned:
- text.append(
- "\nDo you want to update {} object(s) "
- "already using this prototype?".format(nspawned)
- )
- options=(
- {
- "key":("|wY|Wes|n","yes","y"),
- "desc":"Go to updating screen",
- "goto":(
- "node_apply_diff",
- {
- "accept_update":True,
- "objects":spawned_objects,
- "prototype":prototype,
- "back_node":"node_prototype_save",
- },
- ),
- },
- {"key":("[|wN|Wo|n]","n"),"desc":"Return to index","goto":"node_index"},
- {"key":"_default","goto":"node_index"},
- )
- else:
- text.append("(press Return to continue)")
- options={"key":"_default","goto":"node_index"}
-
- text="\n".join(text)
-
- helptext="""
- Updating objects means that the spawner will find all objects previously created by this
- prototype. You will be presented with a list of the changes the system will try to apply to
- each of these objects and you can choose to customize that change if needed. If you have
- done a lot of manual changes to your objects after spawning, you might want to update those
- objects manually instead.
- """
-
- text=(text,helptext)
-
- returntext,options
-
- # not validated yet
- prototype=_get_menu_prototype(caller)
- error,text=_validate_prototype(prototype)
-
- text=[text]
-
- iferror:
- # abort save
- text.append(
- "\n|yValidation errors were found. They need to be corrected before this prototype "
- "can be saved (or used to spawn).|n"
- )
- options=_wizard_options("prototype_save","index",None)
- options.append({"key":"_default","goto":"node_index"})
- return"\n".join(text),options
-
- prototype_key=prototype["prototype_key"]
- ifprotlib.search_prototype(prototype_key):
- text.append(
- "\nDo you want to save/overwrite the existing prototype '{name}'?".format(
- name=prototype_key
- )
- )
- else:
- text.append("\nDo you want to save the prototype as '{name}'?".format(name=prototype_key))
-
- text="\n".join(text)
-
- helptext="""
- Saving the prototype makes it available for use later. It can also be used to inherit from,
- by name. Depending on |cprototype-locks|n it also makes the prototype usable and/or
- editable by others. Consider setting good |cPrototype-tags|n and to give a useful, brief
- |cPrototype-desc|n to make the prototype easy to find later.
-
- """
-
- text=(text,helptext)
-
- options=(
- {
- "key":("[|wY|Wes|n]","yes","y"),
- "desc":"Save prototype",
- "goto":("node_prototype_save",{"accept_save":True,"prototype":prototype}),
- },
- {"key":("|wN|Wo|n","n"),"desc":"Abort and return to Index","goto":"node_index"},
- {
- "key":"_default",
- "goto":("node_prototype_save",{"accept_save":True,"prototype":prototype}),
- },
- )
-
- returntext,options
-"""
-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(args, kwargs)"
-
- ... }
-
-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)
-
-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.
-
-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).
-
-"""
-
-fromevennia.utilsimportfuncparser
-
-
-
[docs]defprotfunc_callable_protkey(*args,**kwargs):
- """
- 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.
-
- """
- ifnotargs:
- return""
-
- prototype=kwargs.get("prototype",{})
- fieldname=args[0]
- prot_value=None
- iffieldnameinprototype:
- prot_value=prototype[fieldname]
- else:
- # check if it's an attribute
- forattrtupleinprototype.get('attrs',[]):
- ifattrtuple[0]==fieldname:
- prot_value=attrtuple[1]
- break
- else:
- raiseAttributeError(f"{fieldname} not found in prototype\n{prototype}\n"
- "(neither as prototype-field or as an Attribute")
- ifcallable(prot_value):
- raiseRuntimeError(f"Error in prototype\n{prototype}\n$protkey can only reference static "
- f"values/attributes (found {prot_value})")
- try:
- returnfuncparser.funcparser_callable_eval(prot_value,**kwargs)
- exceptfuncparser.ParsingError:
- returnprot_value
-
-
-# this is picked up by FuncParser
-FUNCPARSER_CALLABLES={
- "protkey":protfunc_callable_protkey,
- **funcparser.FUNCPARSER_CALLABLES,
- **funcparser.SEARCHING_CALLABLES,
-}
-
[docs]defhomogenize_prototype(prototype,custom_keys=None):
- """
- 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
- the default reserved keys.
-
- Returns:
- homogenized (dict): Prototype where all non-identified keys grouped as attributes and other
- homogenizations like adding missing prototype_keys and setting a default typeclass.
-
- """
- ifnotprototypeorisinstance(prototype,str):
- returnprototype
-
- reserved=_PROTOTYPE_RESERVED_KEYS+(custom_keysor())
-
- # correct cases of setting None for certain values
- forprotkeyinprototype:
- ifprototype[protkey]isNone:
- ifprotkeyin("attrs","tags","prototype_tags"):
- prototype[protkey]=[]
- elifprotkeyin("prototype_key","prototype_desc"):
- prototype[protkey]=""
-
- homogenized={}
- homogenized_tags=[]
- homogenized_attrs=[]
- homogenized_parents=[]
-
- forkey,valinprototype.items():
- ifkeyinreserved:
- # check all reserved keys
- ifkey=="tags":
- # tags must be on form [(tag, category, data), ...]
- tags=make_iter(prototype.get("tags",[]))
- fortagintags:
- ifnotis_iter(tag):
- homogenized_tags.append((tag,None,None))
- eliftag:
- ntag=len(tag)
- ifntag==1:
- homogenized_tags.append((tag[0],None,None))
- elifntag==2:
- homogenized_tags.append((tag[0],tag[1],None))
- else:
- homogenized_tags.append(tag[:3])
-
- elifkey=="attrs":
- attrs=list(prototype.get("attrs",[]))# break reference
- forattrinattrs:
- # attrs must be on form [(key, value, category, lockstr)]
- ifnotis_iter(attr):
- logger.log_error("Prototype's 'attr' field must "
- f"be a list of tuples: {prototype}")
- elifattr:
- nattr=len(attr)
- ifnattr==1:
- # we assume a None-value
- homogenized_attrs.append((attr[0],None,None,""))
- elifnattr==2:
- homogenized_attrs.append((attr[0],attr[1],None,""))
- elifnattr==3:
- homogenized_attrs.append((attr[0],attr[1],attr[2],""))
- else:
- homogenized_attrs.append(attr[:4])
-
- elifkey=="prototype_parent":
- # homogenize any prototype-parents embedded directly as dicts
- protparents=prototype.get('prototype_parent',[])
- ifisinstance(protparents,dict):
- protparents=[protparents]
- forparentinmake_iter(protparents):
- ifisinstance(parent,dict):
- # recursively homogenize directly embedded prototype parents
- homogenized_parents.append(
- homogenize_prototype(parent,custom_keys=custom_keys))
- else:
- # normal prototype-parent names are added as-is
- homogenized_parents.append(parent)
-
- else:
- # another reserved key
- homogenized[key]=val
- else:
- # unreserved keys -> attrs
- homogenized_attrs.append((key,val,None,""))
- ifhomogenized_attrs:
- homogenized["attrs"]=homogenized_attrs
- ifhomogenized_tags:
- homogenized["tags"]=homogenized_tags
- ifhomogenized_parents:
- homogenized['prototype_parent']=homogenized_parents
-
- # add required missing parts that had defaults before
-
- homogenized["prototype_key"]=homogenized.get(
- "prototype_key",
- # assign a random hash as key
- "prototype-{}".format(hashlib.md5(bytes(str(time.time()),"utf-8")).hexdigest()[:7]),
- )
- homogenized["prototype_tags"]=homogenized.get("prototype_tags",[])
- homogenized["prototype_locks"]=homogenized.get("prototype_lock",_PROTOTYPE_FALLBACK_LOCK)
- homogenized["prototype_desc"]=homogenized.get("prototype_desc","")
- if"typeclass"notinprototypeand"prototype_parent"notinprototype:
- homogenized["typeclass"]=settings.BASE_OBJECT_TYPECLASS
-
- returnhomogenized
-
-
-# module/dict-based prototypes
-
-
[docs]defload_module_prototypes(*mod_or_prototypes,override=True):
- """
- Load module prototypes. Also prototype-dicts passed directly to this function are considered
- 'module' prototypes (they are impossible to change) but will have a module of None.
-
- Args:
- *mod_or_prototypes (module or dict): Each arg should be a separate module or
- prototype-dict to load. If none are given, `settings.PROTOTYPE_MODULES` will be used.
- override (bool, optional): If prototypes should override existing ones already loaded.
- Disabling this can allow for injecting prototypes into the system dynamically while
- still allowing same prototype-keys to be overridden from settings (even though settings
- is usually loaded before dynamic loading).
-
- Note:
- This is called (without arguments) by `evennia.__init__` as Evennia initializes. It's
- important to do this late so as to not interfere with evennia initialization. But it can
- also be used later to add more prototypes to the library on the fly. This is requried
- before a module-based prototype can be accessed by prototype-key.
-
- """
- global_MODULE_PROTOTYPE_MODULES,_MODULE_PROTOTYPES
-
- def_prototypes_from_module(mod):
- """
- Load prototypes from a module, first by looking for a global list PROTOTYPE_LIST (a list of
- dict-prototypes), and if not found, assuming all global-level dicts in the module are
- prototypes.
-
- Args:
- mod (module): The module to load from.evennia
-
- Returns:
- list: A list of tuples `(prototype_key, prototype-dict)` where the prototype
- has been homogenized.
-
- """
- prots=[]
- 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}")
- continue
- prots.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)))
- returnprots
-
- def_cleanup_prototype(prototype_key,prototype,mod=None):
- """
- We need to handle externally determined prototype-keys and to make sure
- the prototype contains all needed meta information.
-
- Args:
- prototype_key (str): The determined name of the prototype.
- prototype (dict): The prototype itself.
- mod (module, optional): The module the prototype was loaded from, if any.
-
- Returns:
- dict: The cleaned up prototype.
-
- """
- actual_prot_key=prototype.get("prototype_key",prototype_key).lower()
- prototype.update(
- {
- "prototype_key":actual_prot_key,
- "prototype_desc":(
- prototype["prototype_desc"]if"prototype_desc"inprototypeelse(modor"N/A")),
- "prototype_locks":(
- prototype["prototype_locks"]
- if"prototype_locks"inprototype
- else"use:all();edit:false()"
- ),
- "prototype_tags":list(
- set(list(make_iter(prototype.get("prototype_tags",[])))+["module"])
- ),
- }
- )
- returnprototype
-
- ifnotmod_or_prototypes:
- # in principle this means PROTOTYPE_MODULES could also contain prototypes, but that is
- # rarely useful ...
- mod_or_prototypes=settings.PROTOTYPE_MODULES
-
- formod_or_dictinmod_or_prototypes:
-
- ifisinstance(mod_or_dict,dict):
- # a single prototype; we must make sure it has its key
- prototype_key=mod_or_dict.get('prototype_key')
- ifnotprototype_key:
- raiseValidationError(f"The prototype {mod_or_prototype} does not contain a 'prototype_key'")
- prots=[(prototype_key,mod_or_dict)]
- mod=None
- else:
- # a module (or path to module). This can contain many prototypes; they can be keyed by
- # variable-name too
- prots=_prototypes_from_module(mod_or_dict)
- mod=repr(mod_or_dict)
-
- # store all found prototypes
- forprototype_key,protinprots:
- prototype=_cleanup_prototype(prototype_key,prot,mod=mod)
- # the key can change since in-proto key is given prio over variable-name-based keys
- actual_prototype_key=prototype['prototype_key']
-
- ifactual_prototype_keyin_MODULE_PROTOTYPESandnotoverride:
- # don't override - useful to still let settings replace dynamic inserts
- continue
-
- # make sure the prototype contains all meta info
- _MODULE_PROTOTYPES[actual_prototype_key]=prototype
- # track module path for display purposes
- _MODULE_PROTOTYPE_MODULES[actual_prototype_key.lower()]=mod
-
-
-# Db-based prototypes
-
-
-
[docs]classDbPrototype(DefaultScript):
- """
- This stores a single prototype, in an Attribute `prototype`.
-
- """
-
-
[docs]defat_script_creation(self):
- self.key="empty prototype"# prototype_key
- self.desc="A prototype"# prototype_desc (.tags are used for prototype_tags)
- self.db.prototype={}# actual prototype
[docs]defsave_prototype(prototype):
- """
- Create/Store a prototype persistently.
-
- Args:
- prototype (dict): The prototype to save. A `prototype_key` key is
- required.
-
- Returns:
- prototype (dict or None): The prototype stored using the given kwargs, None if deleting.
-
- Raises:
- prototypes.ValidationError: If prototype does not validate.
-
- Note:
- No edit/spawn locks will be checked here - if this function is called the caller
- is expected to have valid permissions.
-
- """
- in_prototype=prototype
- in_prototype=homogenize_prototype(in_prototype)
-
- def_to_batchtuple(inp,*args):
- "build tuple suitable for batch-creation"
- ifis_iter(inp):
- # already a tuple/list, use as-is
- returninp
- return(inp,)+args
-
- prototype_key=in_prototype.get("prototype_key")
- ifnotprototype_key:
- raiseValidationError(_("Prototype requires a prototype_key"))
-
- prototype_key=str(prototype_key).lower()
-
- # we can't edit a prototype defined in a module
- ifprototype_keyin_MODULE_PROTOTYPES:
- mod=_MODULE_PROTOTYPE_MODULES.get(prototype_key)
- ifmod:
- err=_("{protkey} is a read-only prototype (defined as code in {module}).")
- else:
- err=_("{protkey} is a read-only prototype (passed directly as a dict).")
- raisePermissionError(err.format(protkey=prototype_key,module=mod))
-
- # make sure meta properties are included with defaults
- in_prototype["prototype_desc"]=in_prototype.get(
- "prototype_desc",prototype.get("prototype_desc","")
- )
- prototype_locks=in_prototype.get(
- "prototype_locks",prototype.get("prototype_locks",_PROTOTYPE_FALLBACK_LOCK)
- )
- is_valid,err=validate_lockstring(prototype_locks)
- ifnotis_valid:
- raiseValidationError("Lock error: {}".format(err))
- in_prototype["prototype_locks"]=prototype_locks
-
- prototype_tags=[
- _to_batchtuple(tag,_PROTOTYPE_TAG_META_CATEGORY)
- fortaginmake_iter(
- in_prototype.get("prototype_tags",prototype.get("prototype_tags",[]))
- )
- ]
- in_prototype["prototype_tags"]=prototype_tags
-
- stored_prototype=DbPrototype.objects.filter(db_key=prototype_key)
- ifstored_prototype:
- # edit existing prototype
- stored_prototype=stored_prototype[0]
- stored_prototype.desc=in_prototype["prototype_desc"]
- ifprototype_tags:
- stored_prototype.tags.clear(category=PROTOTYPE_TAG_CATEGORY)
- stored_prototype.tags.batch_add(*in_prototype["prototype_tags"])
- stored_prototype.locks.add(in_prototype["prototype_locks"])
- stored_prototype.attributes.add("prototype",in_prototype)
- else:
- # create a new prototype
- stored_prototype=create_script(
- DbPrototype,
- key=prototype_key,
- desc=in_prototype["prototype_desc"],
- persistent=True,
- locks=prototype_locks,
- tags=in_prototype["prototype_tags"],
- attributes=[("prototype",in_prototype)],
- )
- returnstored_prototype.prototype
-
-
-create_prototype=save_prototype# alias
-
-
-
[docs]defdelete_prototype(prototype_key,caller=None):
- """
- Delete a stored prototype
-
- Args:
- key (str): The persistent prototype to delete.
- caller (Account or Object, optionsl): Caller aiming to delete a prototype.
- Note that no locks will be checked if`caller` is not passed.
- Returns:
- success (bool): If deletion worked or not.
- Raises:
- PermissionError: If 'edit' lock was not passed or deletion failed for some other reason.
-
- """
- ifprototype_keyin_MODULE_PROTOTYPES:
- mod=_MODULE_PROTOTYPE_MODULES.get(prototype_key)
- ifmod:
- err=_("{protkey} is a read-only prototype (defined as code in {module}).")
- else:
- err=_("{protkey} is a read-only prototype (passed directly as a dict).")
- raisePermissionError(err.format(protkey=prototype_key,module=mod))
-
- stored_prototype=DbPrototype.objects.filter(db_key__iexact=prototype_key)
-
- ifnotstored_prototype:
- raisePermissionError(_("Prototype {prototype_key} was not found.").format(
- prototype_key=prototype_key))
-
- stored_prototype=stored_prototype[0]
- ifcaller:
- ifnotstored_prototype.access(caller,"edit"):
- raisePermissionError(
- _("{caller} needs explicit 'edit' permissions to "
- "delete prototype {prototype_key}.").format(
- caller=caller,prototype_key=prototype_key)
- )
- stored_prototype.delete()
- returnTrue
-
-
-
[docs]defsearch_prototype(key=None,tags=None,require_single=False,return_iterators=False,
- no_db=False):
- """
- Find prototypes based on key and/or tags, or all prototypes.
-
- Keyword Args:
- key (str): An exact or partial key to query for.
- tags (str or list): Tag key or keys to query for. These
- will always be applied with the 'db_protototype'
- tag category.
- require_single (bool): If set, raise KeyError if the result
- was not found or if there are multiple matches.
- return_iterators (bool): Optimized return for large numbers of db-prototypes.
- If set, separate returns of module based prototypes and paginate
- the db-prototype return.
- no_db (bool): Optimization. If set, skip querying for database-generated prototypes and only
- include module-based prototypes. This can lead to a dramatic speedup since
- module-prototypes are static and require no db-lookup.
-
- Return:
- matches (list): Default return, all found prototype dicts. Empty list if
- no match was found. Note that if neither `key` nor `tags`
- were given, *all* available prototypes will be returned.
- list, queryset: If `return_iterators` are found, this is a list of
- module-based prototypes followed by a *paginated* queryset of
- db-prototypes.
-
- Raises:
- KeyError: If `require_single` is True and there are 0 or >1 matches.
-
- Note:
- The available prototypes is a combination of those supplied in
- PROTOTYPE_MODULES and those stored in the database. Note that if
- tags are given and the prototype has no tags defined, it will not
- be found as a match.
-
- """
- # This will load the prototypes the first time they are searched
- ifnot_MODULE_PROTOTYPE_MODULES:
- load_module_prototypes()
-
- # prototype keys are always in lowecase
- ifkey:
- key=key.lower()
-
- # search module prototypes
-
- mod_matches={}
- iftags:
- # use tags to limit selection
- tagset=set(tags)
- mod_matches={
- prototype_key:prototype
- forprototype_key,prototypein_MODULE_PROTOTYPES.items()
- iftagset.intersection(prototype.get("prototype_tags",[]))
- }
- else:
- mod_matches=_MODULE_PROTOTYPES
-
- allow_fuzzy=True
- ifkey:
- ifkeyinmod_matches:
- # exact match
- module_prototypes=[mod_matches[key].copy()]
- allow_fuzzy=False
- else:
- # fuzzy matching
- module_prototypes=[
- prototype
- forprototype_key,prototypeinmod_matches.items()
- ifkeyinprototype_key
- ]
- else:
- # note - we return a copy of the prototype dict, otherwise using this with e.g.
- # prototype_from_object will modify the base prototype for every object
- module_prototypes=[match.copy()formatchinmod_matches.values()]
-
- ifno_db:
- db_matches=[]
- else:
- # search db-stored prototypes
- iftags:
- # exact match on tag(s)
- tags=make_iter(tags)
- tag_categories=["db_prototype"for_intags]
- db_matches=DbPrototype.objects.get_by_tag(tags,tag_categories)
- else:
- db_matches=DbPrototype.objects.all()
-
- ifkey:
- # exact or partial match on key
- exact_match=db_matches.filter(Q(db_key__iexact=key)).order_by("db_key")
- ifnotexact_matchandallow_fuzzy:
- # try with partial match instead
- db_matches=db_matches.filter(Q(db_key__icontains=key)).order_by("db_key")
- else:
- db_matches=exact_match
-
- # convert to prototype
- db_ids=db_matches.values_list("id",flat=True)
- db_matches=(
- Attribute.objects.filter(scriptdb__pk__in=db_ids,db_key="prototype")
- .values_list("db_value",flat=True)
- .order_by("scriptdb__db_key")
- )
-
- ifkeyandrequire_single:
- nmodules=len(module_prototypes)
- ndbprots=db_matches.count()ifdb_matcheselse0
- ifnmodules+ndbprots!=1:
- raiseKeyError(_(
- "Found {num} matching prototypes among {module_prototypes}.").format(
- num=nmodules+ndbprots,
- module_prototypes=module_prototypes)
- )
-
- ifreturn_iterators:
- # trying to get the entire set of prototypes - we must paginate
- # the result instead of trying to fetch the entire set at once
- returndb_matches,module_prototypes
- else:
- # full fetch, no pagination (compatibility mode)
- returnlist(db_matches)+module_prototypes
-
-
-
[docs]defsearch_objects_with_prototype(prototype_key):
- """
- Retrieve all object instances created by a given prototype.
-
- Args:
- prototype_key (str): The exact (and unique) prototype identifier to query for.
-
- Returns:
- matches (Queryset): All matching objects spawned from this prototype.
-
- """
- returnObjectDB.objects.get_by_tag(key=prototype_key,category=PROTOTYPE_TAG_CATEGORY)
-
-
-
[docs]classPrototypeEvMore(EvMore):
- """
- 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
-
- """
- 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)
-
-
[docs]definit_pages(self,inp):
- """
- This will be initialized with a tuple (mod_prototype_list, paginated_db_query)
- 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
- # to account for long descs etc
- dbprot_paged=Paginator(dbprot_query,max(1,int(self.height/2)))
-
- # we separate the different types of data, so we track how many pages there are
- # of each.
- n_mod=len(modprot_list)
- self._npages_mod=n_mod//self.height+(0ifn_mod%self.height==0else1)
- self._db_count=dbprot_paged.count
- self._npages_db=dbprot_paged.num_pagesifself._db_count>0else0
- # total number of pages
- self._npages=self._npages_mod+self._npages_db
- self._data=(dbprot_paged,modprot_list)
- self._paginator=self.prototype_paginator
-
-
[docs]defprototype_paginator(self,pageno):
- """
- 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
-
- ifself._db_countandpageno<self._npages_db:
- returndbprot_pages.page(pageno+1)
- else:
- # get the correct slice, adjusted for the db-prototypes
- pageno=max(0,pageno-self._npages_db)
- returnmodprot_list[pageno*self.height:pageno*self.height+self.height]
[docs]deflist_prototypes(
- caller,key=None,tags=None,show_non_use=False,show_non_edit=True,session=None
-):
- """
- Collate a list of found prototypes based on search criteria and access.
-
- Args:
- caller (Account or Object): The object requesting the list.
- key (str, optional): Exact or partial prototype key to query for.
- tags (str or list, optional): Tag key or keys to query for.
- show_non_use (bool, optional): Show also prototypes the caller may not use.
- show_non_edit (bool, optional): Show also prototypes the caller may not edit.
- session (Session, optional): If given, this is used for display formatting.
- Returns:
- PrototypeEvMore: An EvMore subclass optimized for prototype listings.
- None: If no matches were found. In this case the caller has already been notified.
-
- """
- # this allows us to pass lists of empty strings
- tags=[tagfortaginmake_iter(tags)iftag]
-
- dbprot_query,modprot_list=search_prototype(key,tags,return_iterators=True)
-
- ifnotdbprot_queryandnotmodprot_list:
- caller.msg(_("No prototypes found."),session=session)
- returnNone
-
- # get specific prototype (one value or exception)
- returnPrototypeEvMore(
- caller,
- (dbprot_query,modprot_list),
- session=session,
- show_non_use=show_non_use,
- show_non_edit=show_non_edit,
- )
-
-
-
[docs]defvalidate_prototype(
- prototype,protkey=None,protparents=None,is_prototype_base=True,strict=True,_flags=None
-):
- """
- Run validation on a prototype, checking for inifinite regress.
-
- Args:
- prototype (dict): Prototype to validate.
- protkey (str, optional): The name of the prototype definition. If not given, the prototype
- dict needs to have the `prototype_key` field set.
- protpartents (dict, optional): The available prototype parent library. If
- note given this will be determined from settings/database.
- is_prototype_base (bool, optional): We are trying to create a new object *based on this
- object*. This means we can't allow 'mixin'-style prototypes without typeclass/parent
- etc.
- strict (bool, optional): If unset, don't require needed keys, only check against infinite
- recursion etc.
- _flags (dict, optional): Internal work dict that should not be set externally.
- Raises:
- RuntimeError: If prototype has invalid structure.
- RuntimeWarning: If prototype has issues that would make it unsuitable to build an object
- with (it may still be useful as a mix-in prototype).
-
- """
- assertisinstance(prototype,dict)
-
- if_flagsisNone:
- _flags={"visited":[],"depth":0,"typeclass":False,"errors":[],"warnings":[]}
-
- ifnotprotparents:
- protparents={
- prototype.get("prototype_key","").lower():prototype
- forprototypeinsearch_prototype()
- }
-
- protkey=protkeyandprotkey.lower()orprototype.get("prototype_key",None)
-
- ifstrictandnotbool(protkey):
- _flags["errors"].append(_("Prototype lacks a 'prototype_key'."))
- protkey="[UNSET]"
-
- typeclass=prototype.get("typeclass")
- prototype_parent=prototype.get("prototype_parent",[])
-
- ifstrictandnot(typeclassorprototype_parent):
- ifis_prototype_base:
- _flags["errors"].append(
- _("Prototype {protkey} requires `typeclass` ""or 'prototype_parent'.").format(
- protkey=protkey)
- )
- else:
- _flags["warnings"].append(
- _("Prototype {protkey} can only be used as a mixin since it lacks "
- "'typeclass' or 'prototype_parent' keys.").format(protkey=protkey)
- )
-
- ifstrictandtypeclass:
- try:
- class_from_module(typeclass)
- exceptImportErroraserr:
- _flags["errors"].append(
- _("{err}: Prototype {protkey} is based on typeclass {typeclass}, "
- "which could not be imported!").format(
- err=err,protkey=protkey,typeclass=typeclass)
- )
-
- ifprototype_parentandisinstance(prototype_parent,dict):
- # the protparent is already embedded as a dict;
- prototype_parent=[prototype_parent]
-
- # recursively traverse prototype_parent chain
- forprotstringinmake_iter(prototype_parent):
- ifisinstance(protstring,dict):
- # an already embedded prototype_parent
- protparent=protstring
- protstring=None
- else:
- protstring=protstring.lower()
- ifprotkeyisnotNoneandprotstring==protkey:
- _flags["errors"].append(_("Prototype {protkey} tries to parent itself.").format(
- protkey=protkey))
- protparent=protparents.get(protstring)
- ifnotprotparent:
- _flags["errors"].append(
- _("Prototype {protkey}'s `prototype_parent` (named '{parent}') "
- "was not found.").format(protkey=protkey,parent=protstring)
- )
-
- # check for infinite recursion
- ifid(prototype)in_flags["visited"]:
- _flags["errors"].append(
- _("{protkey} has infinite nesting of prototypes.").format(
- protkey=protkeyorprototype)
- )
-
- if_flags["errors"]:
- raiseRuntimeError(f"{_ERRSTR}: "+f"\n{_ERRSTR}: ".join(_flags["errors"]))
- _flags["visited"].append(id(prototype))
- _flags["depth"]+=1
-
- # next step of recursive validation
- validate_prototype(
- protparent,protstring,protparents,is_prototype_base=is_prototype_base,_flags=_flags
- )
-
- _flags["visited"].pop()
- _flags["depth"]-=1
-
- iftypeclassandnot_flags["typeclass"]:
- _flags["typeclass"]=typeclass
-
- # 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 {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(f"{_ERRSTR}:_"+f"\n{_ERRSTR}: ".join(_flags["errors"]))
- if_flags["warnings"]:
- raiseRuntimeWarning(f"{_WARNSTR}: "+f"\n{_WARNSTR}: ".join(_flags["warnings"]))
-
- # make sure prototype_locks are set to defaults
- prototype_locks=[
- lstring.split(":",1)
- forlstringinprototype.get("prototype_locks","").split(";")
- if":"inlstring
- ]
- locktypes=[tup[0].strip()fortupinprototype_locks]
- if"spawn"notinlocktypes:
- prototype_locks.append(("spawn","all()"))
- if"edit"notinlocktypes:
- prototype_locks.append(("edit","all()"))
- prototype_locks=";".join(":".join(tup)fortupinprototype_locks)
- 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.
-
- Available protfuncs are specified as callables in one of the modules of
- `settings.PROTFUNC_MODULES`, or specified on the command line.
-
- Args:
- value (any): The value to test for a parseable protfunc. Only strings will be parsed for
- 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.
- 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:
- 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
-
- result=FUNC_PARSER.parse(value,raise_errors=True,return_str=False,caller=caller,**kwargs)
-
- returnresult
-
-
-# Various prototype utilities
-
-
-
[docs]defformat_available_protfuncs():
- """
- Get all protfuncs in a pretty-formatted form.
-
- Args:
- clr (str, optional): What coloration tag to use.
- """
- out=[]
- forprotfunc_name,protfuncinFUNC_PARSER.callables.items():
- out.append(
- "- |c${name}|n - |W{docs}".format(
- name=protfunc_name,docs=protfunc.__doc__.strip().replace("\n","")
- )
- )
- returnjustify("\n".join(out),indent=8)
[docs]defcheck_permission(prototype_key,action,default=True):
- """
- Helper function to check access to actions on given prototype.
-
- Args:
- prototype_key (str): The prototype to affect.
- action (str): One of "spawn" or "edit".
- default (str): If action is unknown or prototype has no locks
-
- Returns:
- passes (bool): If permission for action is granted or not.
-
- """
- ifaction=="edit":
- ifprototype_keyin_MODULE_PROTOTYPES:
- mod=_MODULE_PROTOTYPE_MODULES.get(prototype_key)
- ifmod:
- err=_("{protkey} is a read-only prototype (defined as code in {module}).")
- else:
- err=_("{protkey} is a read-only prototype (passed directly as a dict).")
- logger.log_err(err.format(protkey=prototype_key,module=mod))
- returnFalse
-
- prototype=search_prototype(key=prototype_key)
- ifnotprototype:
- logger.log_err("Prototype {} not found.".format(prototype_key))
- returnFalse
-
- lockstring=prototype.get("prototype_locks")
-
- iflockstring:
- returncheck_lockstring(None,lockstring,default=default,access_type=action)
- returndefault
-
-
-
[docs]definit_spawn_value(value,validator=None,caller=None,prototype=None):
- """
- Analyze the prototype value and produce a value useful at the point of spawning.
-
- Args:
- value (any): This can be:
- callable - will be called as callable()
- (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.
- prototype (dict): Prototype this is to be used for. Necessary for certain protfuncs.
-
- Returns:
- any (any): The (potentially pre-processed value to use for this prototype key)
-
- """
- validator=validatorifvalidatorelselambdao:o
- ifcallable(value):
- value=validator(value())
- elifvalueandisinstance(value,(list,tuple))andcallable(value[0]):
- # a structure (callable, (args, ))
- args=value[1:]
- value=validator(value[0](*make_iter(args)))
- else:
- value=validator(value)
- result=protfunc_parser(value,caller=caller,prototype=prototype)
- ifresult!=value:
- returnvalidator(result)
- returnresult
-"""
-Spawner
-
-The spawner takes input files containing object definitions in
-dictionary forms. These use a prototype architecture to define
-unique objects without having to make a Typeclass for each.
-
-There main function is `spawn(*prototype)`, where the `prototype`
-is a dictionary like this:
-
-```python
-from evennia.prototypes import prototypes, spawner
-
-prot = {
- "prototype_key": "goblin",
- "typeclass": "types.objects.Monster",
- "key": "goblin grunt",
- "health": lambda: randint(20,30),
- "resists": ["cold", "poison"],
- "attacks": ["fists"],
- "weaknesses": ["fire", "light"]
- "tags": ["mob", "evil", ('greenskin','mob')]
- "attrs": [("weapon", "sword")]
-}
-# spawn something with the prototype
-goblin = spawner.spawn(prot)
-
-# make this into a db-saved prototype (optional)
-prot = prototypes.create_prototype(prot)
-
-```
-
-Possible keywords are:
- prototype_key (str): name of this prototype. This is used when storing prototypes and should
- be unique. This should always be defined but for prototypes defined in modules, the
- variable holding the prototype dict will become the prototype_key if it's not explicitly
- given.
- prototype_desc (str, optional): describes prototype in listings
- prototype_locks (str, optional): locks for restricting access to this prototype. Locktypes
- 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: Deprecated. Same meaning as 'parent'.
-
- typeclass (str or callable, optional): if not set, will use typeclass of parent prototype or use
- `settings.BASE_OBJECT_TYPECLASS`
- key (str or callable, optional): the name of the spawned object. If not given this will set to a
- random hash
- location (obj, str or callable, optional): location of the object - a valid object or #dbref
- home (obj, str or callable, optional): valid object or #dbref
- destination (obj, str or callable, optional): only valid for exits (object or #dbref)
-
- permissions (str, list or callable, optional): which permissions for spawned object to have
- locks (str or callable, optional): lock-string for the spawned object
- aliases (str, list or callable, optional): Aliases for the spawned object
- exec (str or callable, optional): this is a string of python code to execute or a list of such
- codes. This can be used e.g. to trigger custom handlers on the object. The execution
- namespace contains 'evennia' for the library and 'obj'. All default spawn commands limit
- this functionality to Developer/superusers. Usually it's better to use callables or
- prototypefuncs instead of this.
- tags (str, tuple, list or callable, optional): string or list of strings or tuples
- `(tagstr, category)`. Plain strings will be result in tags with no category (default tags).
- attrs (tuple, list or callable, optional): tuple or list of tuples of Attributes to add. This
- form allows more complex Attributes to be set. Tuples at least specify `(key, value)`
- but can also specify up to `(key, value, category, lockstring)`. If you want to specify a
- lockstring but not a category, set the category to `None`.
- ndb_<name> (any): value of a nattribute (ndb_ is stripped) - this is of limited use.
- other (any): any other name is interpreted as the key of an Attribute with
- its value. Such Attributes have no categories.
-
-Each value can also be a callable that takes no arguments. It should
-return the value to enter into the field and will be called every time
-the prototype is used to spawn an object. Note, if you want to store
-a callable in an Attribute, embed it in a tuple to the `args` keyword.
-
-By specifying the "prototype_parent" key, the prototype becomes a child of
-the given prototype, inheritng all prototype slots it does not explicitly
-define itself, while overloading those that it does specify.
-
-```python
-import random
-
-
-{
- "prototype_key": "goblin_wizard",
- "prototype_parent": "GOBLIN",
- "key": "goblin wizard",
- "spells": ["fire ball", "lighting bolt"]
- }
-
-GOBLIN_ARCHER = {
- "prototype_parent": "GOBLIN",
- "key": "goblin archer",
- "attack_skill": (random, (5, 10))"
- "attacks": ["short bow"]
-}
-```
-
-One can also have multiple prototypes. These are inherited from the
-left, with the ones further to the right taking precedence.
-
-```python
-ARCHWIZARD = {
- "attack": ["archwizard staff", "eye of doom"]
-
-GOBLIN_ARCHWIZARD = {
- "key" : "goblin archwizard"
- "prototype_parent": ("GOBLIN_WIZARD", "ARCHWIZARD"),
-}
-```
-
-The *goblin archwizard* will have some different attacks, but will
-otherwise have the same spells as a *goblin wizard* who in turn shares
-many traits with a normal *goblin*.
-
-
-Storage mechanism:
-
-This sets up a central storage for prototypes. The idea is to make these
-available in a repository for buildiers to use. Each prototype is stored
-in a Script so that it can be tagged for quick sorting/finding and locked for limiting
-access.
-
-This system also takes into consideration prototypes defined and stored in modules.
-Such prototypes are considered 'read-only' to the system and can only be modified
-in code. To replace a default prototype, add the same-name prototype in a
-custom module read later in the settings.PROTOTYPE_MODULES list. To remove a default
-prototype, override its name with an empty dict.
-
-
-"""
-
-
-importcopy
-importhashlib
-importtime
-
-fromdjango.confimportsettings
-fromdjango.utils.translationimportgettextas_
-
-importevennia
-fromevennia.objects.modelsimportObjectDB
-fromevennia.utilsimportlogger
-fromevennia.utils.utilsimportmake_iter,is_iter
-fromevennia.prototypesimportprototypesasprotlib
-fromevennia.prototypes.prototypesimport(
- value_to_obj,
- value_to_obj_or_any,
- init_spawn_value,
- PROTOTYPE_TAG_CATEGORY,
-)
-
-
-_CREATE_OBJECT_KWARGS=("key","location","home","destination")
-_PROTOTYPE_META_NAMES=("prototype_key","prototype_desc","prototype_tags",
- "prototype_locks","prototype_parent")
-_PROTOTYPE_ROOT_NAMES=(
- "typeclass",
- "key",
- "aliases",
- "attrs",
- "tags",
- "locks",
- "permissions",
- "location",
- "home",
- "destination",
-)
-_NON_CREATE_KWARGS=_CREATE_OBJECT_KWARGS+_PROTOTYPE_META_NAMES
-
-
-
-
-
-# Helper
-
-
-def_get_prototype(inprot,protparents,uninherited=None,_workprot=None):
- """
- Recursively traverse a prototype dictionary, including multiple
- inheritance. Use validate_prototype before this, we don't check
- for infinite recursion here.
-
- Args:
- inprot (dict): Prototype dict (the individual prototype, with no inheritance included).
- protparents (dict): Available protparents, keyed by prototype_key.
- uninherited (dict): Parts of prototype to not inherit.
- _workprot (dict, optional): Work dict for the recursive algorithm.
-
- Returns:
- merged (dict): A prototype where parent's have been merged as needed (the
- `prototype_parent` key is removed).
-
- """
-
- def_inherit_tags(old_tags,new_tags):
- old={(tup[0],tup[1]):tupfortupinold_tags}
- new={(tup[0],tup[1]):tupfortupinnew_tags}
- old.update(new)
- returnlist(old.values())
-
- def_inherit_attrs(old_attrs,new_attrs):
- old={(tup[0],tup[2]):tupfortupinold_attrs}
- new={(tup[0],tup[2]):tupfortupinnew_attrs}
- old.update(new)
- returnlist(old.values())
-
- _workprot={}if_workprotisNoneelse_workprot
- if"prototype_parent"ininprot:
- # move backwards through the inheritance
-
- prototype_parents=inprot["prototype_parent"]
- ifisinstance(prototype_parents,dict):
- # protparent already embedded as-is
- prototype_parents=[prototype_parents]
-
- forprototypeinmake_iter(prototype_parents):
- ifisinstance(prototype,dict):
- # protparent already embedded as-is
- parent_prototype=prototype
- else:
- # protparent given by-name
- parent_prototype=protparents.get(prototype.lower(),{})
-
- # Build the prot dictionary in reverse order, overloading
- new_prot=_get_prototype(
- parent_prototype,protparents,_workprot=_workprot
- )
-
- # attrs, tags have internal structure that should be inherited separately
- new_prot["attrs"]=_inherit_attrs(
- _workprot.get("attrs",{}),new_prot.get("attrs",[])
- )
- new_prot["tags"]=_inherit_tags(_workprot.get("tags",[]),new_prot.get("tags",[]))
-
- _workprot.update(new_prot)
- # the inprot represents a higher level (a child prot), which should override parents
-
- inprot["attrs"]=_inherit_attrs(_workprot.get("attrs",[]),inprot.get("attrs",[]))
- inprot["tags"]=_inherit_tags(_workprot.get("tags",[]),inprot.get("tags",[]))
- _workprot.update(inprot)
- ifuninherited:
- # put back the parts that should not be inherited
- _workprot.update(uninherited)
- _workprot.pop("prototype_parent",None)# we don't need this for spawning
- return_workprot
-
-
-
[docs]defflatten_prototype(prototype,validate=False,no_db=False):
- """
- Produce a 'flattened' prototype, where all prototype parents in the inheritance tree have been
- merged into a final prototype.
-
- Args:
- prototype (dict): Prototype to flatten. Its `prototype_parent` field will be parsed.
- validate (bool, optional): Validate for valid keys etc.
- no_db (bool, optional): Don't search db-based prototypes. This can speed up
- searching dramatically since module-based prototypes are static.
-
- Returns:
- flattened (dict): The final, flattened prototype.
-
- """
-
- ifprototype:
- prototype=protlib.homogenize_prototype(prototype)
- protparents={prot["prototype_key"].lower():prot
- forprotinprotlib.search_prototype(no_db=no_db)}
- protlib.validate_prototype(
- prototype,None,protparents,is_prototype_base=validate,strict=validate
- )
- return_get_prototype(
- prototype,protparents,uninherited={"prototype_key":prototype.get("prototype_key")}
- )
- return{}
-
-
-# obj-related prototype functions
-
-
-
[docs]defprototype_from_object(obj):
- """
- Guess a minimal prototype from an existing object.
-
- Args:
- obj (Object): An object to analyze.
-
- Returns:
- prototype (dict): A prototype estimating the current state of the object.
-
- """
- # first, check if this object already has a prototype
-
- prot=obj.tags.get(category=PROTOTYPE_TAG_CATEGORY,return_list=True)
- ifprot:
- prot=protlib.search_prototype(prot[0])
-
- ifnotprotorlen(prot)>1:
- # no unambiguous prototype found - build new prototype
- prot={}
- prot["prototype_key"]="From-Object-{}-{}".format(
- obj.key,hashlib.md5(bytes(str(time.time()),"utf-8")).hexdigest()[:7]
- )
- prot["prototype_desc"]="Built from {}".format(str(obj))
- prot["prototype_locks"]="spawn:all();edit:all()"
- prot["prototype_tags"]=[]
- else:
- prot=prot[0]
-
- prot["key"]=obj.db_keyorhashlib.md5(bytes(str(time.time()),"utf-8")).hexdigest()[:6]
- prot["typeclass"]=obj.db_typeclass_path
-
- location=obj.db_location
- iflocation:
- prot["location"]=location.dbref
- home=obj.db_home
- ifhome:
- prot["home"]=home.dbref
- destination=obj.db_destination
- ifdestination:
- prot["destination"]=destination.dbref
- locks=obj.locks.all()
- iflocks:
- prot["locks"]=";".join(locks)
- perms=obj.permissions.get(return_list=True)
- ifperms:
- prot["permissions"]=make_iter(perms)
- aliases=obj.aliases.get(return_list=True)
- ifaliases:
- prot["aliases"]=aliases
- tags=sorted(
- [(tag.db_key,tag.db_category,tag.db_data)fortaginobj.tags.all(return_objs=True)],
- key=lambdatup:(str(tup[0]),tup[1]or'',tup[2]or'')
- )
- iftags:
- prot["tags"]=tags
- attrs=sorted(
- [
- (attr.key,attr.value,attr.category,";".join(attr.locks.all()))
- forattrinobj.attributes.all()
- ],
- key=lambdatup:(str(tup[0]),tup[1]or'',tup[2]or'',tup[3])
- )
- ifattrs:
- prot["attrs"]=attrs
-
- returnprot
-
-
-
[docs]defprototype_diff(prototype1,prototype2,maxdepth=2,homogenize=False,implicit_keep=False):
- """
- A 'detailed' diff specifies differences down to individual sub-sections
- of the prototype, like individual attributes, permissions etc. It is used
- by the menu to allow a user to customize what should be kept.
-
- Args:
- prototype1 (dict): Original prototype.
- prototype2 (dict): Comparison prototype.
- maxdepth (int, optional): The maximum depth into the diff we go before treating the elements
- of iterables as individual entities to compare. This is important since a single
- attr/tag (for example) are represented by a tuple.
- homogenize (bool, optional): Auto-homogenize both prototypes for the best comparison.
- 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.
-
- Returns:
- diff (dict): A structure detailing how to convert prototype1 to prototype2. All
- nested structures are dicts with keys matching either the prototype's matching
- key or the first element in the tuple describing the prototype value (so for
- a tag tuple `(tagname, category)` the second-level key in the diff would be tagname).
- The the bottom level of the diff consist of tuples `(old, new, instruction)`, where
- instruction can be one of "REMOVE", "ADD", "UPDATE" or "KEEP".
-
- """
- _unset=Unset()
-
- def_recursive_diff(old,new,depth=0):
-
- old_type=type(old)
- new_type=type(new)
-
- ifold_type==new_typeandnot(oldornew):
- # both old and new are unset, like [] or None
- return(None,None,"KEEP")
- ifold_type!=new_type:
- ifoldandnotnew:
- ifdepth<maxdepthandold_type==dict:
- return{key:(part,None,"REMOVE")forkey,partinold.items()}
- elifdepth<maxdepthandis_iter(old):
- return{
- part[0]ifis_iter(part)elsepart:(part,None,"REMOVE")forpartinold
- }
- ifisinstance(new,Unset)andimplicit_keep:
- # the new does not define any change, use implicit-keep
- return(old,None,"KEEP")
- return(old,new,"REMOVE")
- elifnotoldandnew:
- ifdepth<maxdepthandnew_type==dict:
- return{key:(None,part,"ADD")forkey,partinnew.items()}
- elifdepth<maxdepthandis_iter(new):
- return{part[0]ifis_iter(part)elsepart:(None,part,"ADD")forpartinnew}
- return(old,new,"ADD")
- else:
- # this condition should not occur in a standard diff
- return(old,new,"UPDATE")
- elifdepth<maxdepthandnew_type==dict:
- all_keys=set(list(old.keys())+list(new.keys()))
- return{
- key:_recursive_diff(old.get(key,_unset),new.get(key,_unset),depth=depth+1)
- forkeyinall_keys
- }
- elifdepth<maxdepthandis_iter(new):
- old_map={part[0]ifis_iter(part)elsepart:partforpartinold}
- new_map={part[0]ifis_iter(part)elsepart:partforpartinnew}
- all_keys=set(list(old_map.keys())+list(new_map.keys()))
- return{
- key:_recursive_diff(
- old_map.get(key,_unset),new_map.get(key,_unset),depth=depth+1
- )
- forkeyinall_keys
- }
- elifold!=new:
- return(old,new,"UPDATE")
- else:
- return(old,new,"KEEP")
-
- prot1=protlib.homogenize_prototype(prototype1)ifhomogenizeelseprototype1
- prot2=protlib.homogenize_prototype(prototype2)ifhomogenizeelseprototype2
-
- diff=_recursive_diff(prot1,prot2)
-
- returndiff
-
-
-
[docs]defflatten_diff(diff):
- """
- For spawning, a 'detailed' diff is not necessary, rather we just want instructions on how to
- handle each root key.
-
- Args:
- diff (dict): Diff produced by `prototype_diff` and
- possibly modified by the user. Note that also a pre-flattened diff will come out
- unchanged by this function.
-
- Returns:
- flattened_diff (dict): A flat structure detailing how to operate on each
- root component of the prototype.
-
- Notes:
- The flattened diff has the following possible instructions:
- UPDATE, REPLACE, REMOVE
- Many of the detailed diff's values can hold nested structures with their own
- individual instructions. A detailed diff can have the following instructions:
- REMOVE, ADD, UPDATE, KEEP
- Here's how they are translated:
- - All REMOVE -> REMOVE
- - All ADD|UPDATE -> UPDATE
- - All KEEP -> KEEP
- - Mix KEEP, UPDATE, ADD -> UPDATE
- - Mix REMOVE, KEEP, UPDATE, ADD -> REPLACE
- """
-
- valid_instructions=("KEEP","REMOVE","ADD","UPDATE")
-
- def_get_all_nested_diff_instructions(diffpart):
- "Started for each root key, returns all instructions nested under it"
- out=[]
- typ=type(diffpart)
- iftyp==tupleandlen(diffpart)==3anddiffpart[2]invalid_instructions:
- out=[diffpart[2]]
- eliftyp==dict:
- # all other are dicts
- forvalindiffpart.values():
- out.extend(_get_all_nested_diff_instructions(val))
- else:
- raiseRuntimeError(
- _("Diff contains non-dicts that are not on the "
- "form (old, new, action_to_take): {diffpart}").format(diffpart)
- )
- returnout
-
- flat_diff={}
-
- # flatten diff based on rules
- forrootkey,diffpartindiff.items():
- insts=_get_all_nested_diff_instructions(diffpart)
- ifall(inst=="KEEP"forinstininsts):
- rootinst="KEEP"
- elifall(instin("ADD","UPDATE")forinstininsts):
- rootinst="UPDATE"
- elifall(inst=="REMOVE"forinstininsts):
- rootinst="REMOVE"
- elif"REMOVE"ininsts:
- rootinst="REPLACE"
- else:
- rootinst="UPDATE"
-
- flat_diff[rootkey]=rootinst
-
- returnflat_diff
-
-
-
[docs]defprototype_diff_from_object(prototype,obj,implicit_keep=True):
- """
- Get a simple diff for a prototype compared to an object which may or may not already have a
- prototype (or has one but changed locally). For more complex migratations a manual diff may be
- needed.
-
- Args:
- prototype (dict): New prototype.
- obj (Object): Object to compare prototype against.
-
- Returns:
- diff (dict): Mapping for every prototype key: {"keyname": "REMOVE|UPDATE|KEEP", ...}
- obj_prototype (dict): The prototype calculated for the given object. The diff is how to
- convert this prototype into the new prototype.
- implicit_keep (bool, optional): This is usually what one wants for object updating. When
- set, this means the prototype diff will assume KEEP on differences
- between the object-generated prototype and that which is not explicitly set in the
- new prototype. This means e.g. that even though the object has a location, and the
- prototype does not specify the location, it will not be unset.
-
- Notes:
- The `diff` is on the following form:
-
- {"key": (old, new, "KEEP|REPLACE|UPDATE|REMOVE"),
- "attrs": {"attrkey": (old, new, "KEEP|REPLACE|UPDATE|REMOVE"),
- "attrkey": (old, new, "KEEP|REPLACE|UPDATE|REMOVE"), ...},
- "aliases": {"aliasname": (old, new, "KEEP...", ...},
- ... }
-
- """
- obj_prototype=prototype_from_object(obj)
- diff=prototype_diff(
- obj_prototype,protlib.homogenize_prototype(prototype),implicit_keep=implicit_keep
- )
- returndiff,obj_prototype
[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.
-
- Args:
- prototype (str or dict): Either the `prototype_key` to use or the
- prototype dict itself.
- diff (dict, optional): This a diff structure that describes how to update the protototype.
- If not given this will be constructed from the first object found.
- objects (list, optional): List of objects to update. If not given, query for these
- objects using the prototype's `prototype_key`.
- exact (bool, optional): By default (`False`), keys not explicitly in the prototype will
- not be applied to the object, but will be retained as-is. This is usually what is
- expected - for example, one usually do not want to remove the object's location even
- 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.
-
- """
- prototype=protlib.homogenize_prototype(prototype)
-
- ifisinstance(prototype,str):
- new_prototype=protlib.search_prototype(prototype)
- else:
- new_prototype=prototype
-
- prototype_key=new_prototype["prototype_key"]
-
- ifnotobjects:
- objects=ObjectDB.objects.get_by_tag(prototype_key,category=PROTOTYPE_TAG_CATEGORY)
-
- ifnotobjects:
- return0
-
- ifnotdiff:
- diff,_=prototype_diff_from_object(new_prototype,objects[0])
-
- # make sure the diff is flattened
- diff=flatten_diff(diff)
-
- changed=0
- forobjinobjects:
- do_save=False
-
- old_prot_key=obj.tags.get(category=PROTOTYPE_TAG_CATEGORY,return_list=True)
- old_prot_key=old_prot_key[0]ifold_prot_keyelseNone
-
- try:
- forkey,directiveindiff.items():
-
- ifkeynotinnew_prototypeandnotexact:
- # we don't update the object if the prototype does not actually
- # contain the key (the diff will report REMOVE but we ignore it
- # since exact=False)
- continue
-
- ifdirectivein("UPDATE","REPLACE"):
-
- ifkeyin_PROTOTYPE_META_NAMES:
- # prototype meta keys are not stored on-object
- continue
-
- val=new_prototype[key]
- do_save=True
-
- def_init(val,typ):
- returninit_spawn_value(val,str,caller=caller,prototype=new_prototype)
-
- ifkey=="key":
- obj.db_key=_init(val,str)
- elifkey=="typeclass":
- obj.db_typeclass_path=_init(val,str)
- elifkey=="location":
- obj.db_location=_init(val,value_to_obj)
- elifkey=="home":
- obj.db_home=_init(val,value_to_obj)
- elifkey=="destination":
- obj.db_destination=_init(val,value_to_obj)
- elifkey=="locks":
- ifdirective=="REPLACE":
- obj.locks.clear()
- obj.locks.add(_init(val,str))
- elifkey=="permissions":
- ifdirective=="REPLACE":
- obj.permissions.clear()
- obj.permissions.batch_add(*(_init(perm,str)forperminval))
- elifkey=="aliases":
- ifdirective=="REPLACE":
- obj.aliases.clear()
- obj.aliases.batch_add(*(_init(alias,str)foraliasinval))
- elifkey=="tags":
- ifdirective=="REPLACE":
- obj.tags.clear()
- obj.tags.batch_add(
- *(
- (_init(ttag,str),tcategory,tdata)
- forttag,tcategory,tdatainval
- )
- )
- elifkey=="attrs":
- ifdirective=="REPLACE":
- obj.attributes.clear()
- obj.attributes.batch_add(
- *(
- (
- _init(akey,str),
- _init(aval,value_to_obj),
- acategory,
- alocks,
- )
- forakey,aval,acategory,alocksinval
- )
- )
- elifkey=="exec":
- # we don't auto-rerun exec statements, it would be huge security risk!
- pass
- else:
- obj.attributes.add(key,_init(val,value_to_obj))
- elifdirective=="REMOVE":
- do_save=True
- ifkey=="key":
- obj.db_key=""
- elifkey=="typeclass":
- # fall back to default
- obj.db_typeclass_path=settings.BASE_OBJECT_TYPECLASS
- elifkey=="location":
- obj.db_location=None
- elifkey=="home":
- obj.db_home=None
- elifkey=="destination":
- obj.db_destination=None
- elifkey=="locks":
- obj.locks.clear()
- elifkey=="permissions":
- obj.permissions.clear()
- elifkey=="aliases":
- obj.aliases.clear()
- elifkey=="tags":
- obj.tags.clear()
- elifkey=="attrs":
- obj.attributes.clear()
- elifkey=="exec":
- # we don't auto-rerun exec statements, it would be huge security risk!
- pass
- else:
- obj.attributes.remove(key)
- exceptException:
- logger.log_trace(f"Failed to apply prototype '{prototype_key}' to {obj}.")
- finally:
- # we must always make sure to re-add the prototype tag
- obj.tags.clear(category=PROTOTYPE_TAG_CATEGORY)
- obj.tags.add(prototype_key,category=PROTOTYPE_TAG_CATEGORY)
-
- ifdo_save:
- changed+=1
- obj.save()
-
- returnchanged
-
-
-
[docs]defbatch_create_object(*objparams):
- """
- This is a cut-down version of the create_object() function,
- optimized for speed. It does NOT check and convert various input
- so make sure the spawned Typeclass works before using this!
-
- Args:
- objsparams (tuple): Each paremter tuple will create one object instance using the parameters
- within.
- The parameters should be given in the following order:
- - `create_kwargs` (dict): For use as new_obj = `ObjectDB(**create_kwargs)`.
- - `permissions` (str): Permission string used with `new_obj.batch_add(permission)`.
- - `lockstring` (str): Lockstring used with `new_obj.locks.add(lockstring)`.
- - `aliases` (list): A list of alias strings for
- adding with `new_object.aliases.batch_add(*aliases)`.
- - `nattributes` (list): list of tuples `(key, value)` to be loop-added to
- add with `new_obj.nattributes.add(*tuple)`.
- - `attributes` (list): list of tuples `(key, value[,category[,lockstring]])` for
- adding with `new_obj.attributes.batch_add(*attributes)`.
- - `tags` (list): list of tuples `(key, category)` for adding
- with `new_obj.tags.batch_add(*tags)`.
- - `execs` (list): Code strings to execute together with the creation
- of each object. They will be executed with `evennia` and `obj`
- (the newly created object) available in the namespace. Execution
- will happend after all other properties have been assigned and
- is intended for calling custom handlers etc.
-
- Returns:
- objects (list): A list of created objects
-
- Notes:
- The `exec` list will execute arbitrary python code so don't allow this to be available to
- unprivileged users!
-
- """
-
- # bulk create all objects in one go
-
- # unfortunately this doesn't work since bulk_create doesn't creates pks;
- # the result would be duplicate objects at the next stage, so we comment
- # it out for now:
- # dbobjs = _ObjectDB.objects.bulk_create(dbobjs)
-
- objs=[]
- forobjparaminobjparams:
-
- obj=ObjectDB(**objparam[0])
-
- # setup
- obj._createdict={
- "permissions":make_iter(objparam[1]),
- "locks":objparam[2],
- "aliases":make_iter(objparam[3]),
- "nattributes":objparam[4],
- "attributes":objparam[5],
- "tags":make_iter(objparam[6]),
- }
- # this triggers all hooks
- obj.save()
- # run eventual extra code
- forcodeinobjparam[7]:
- ifcode:
- exec(code,{},{"evennia":evennia,"obj":obj})
- objs.append(obj)
- returnobjs
-
-
-# Spawner mechanism
-
-
-
[docs]defspawn(*prototypes,caller=None,**kwargs):
- """
- Spawn a number of prototyped objects.
-
- Args:
- prototypes (str or dict): Each argument should either be a
- 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
- prototypes. If not given, it will instead look for modules
- defined by settings.PROTOTYPE_MODULES.
- prototype_parents (dict): A dictionary holding a custom
- prototype-parent dictionary. Will overload same-named
- prototypes from prototype_modules.
- return_parents (bool): Return a dict of the entire prototype-parent tree
- available to this prototype (no object creation happens). This is a
- merged result between the globally found protparents and whatever
- custom `prototype_parents` are given to this function.
- only_validate (bool): Only run validation of prototype/parents
- (no object creation) and return the create-kwargs.
-
- Returns:
- object (Object, dict or list): Spawned object(s). If `only_validate` is given, return
- a list of the creation kwargs to build the object(s) without actually creating it. If
- `return_parents` is set, instead return dict of prototype parents.
-
- """
- # search string (=prototype_key) from input
- prototypes=[
- protlib.search_prototype(prot,require_single=True)[0]ifisinstance(prot,str)elseprot
- forprotinprototypes
- ]
-
- # get available protparents
- protparents={prot["prototype_key"].lower():protforprotinprotlib.search_prototype()}
-
- ifnotkwargs.get("only_validate"):
- # homogenization to be more lenient about prototype format when entering the prototype
- # manually
- prototypes=[protlib.homogenize_prototype(prot)forprotinprototypes]
-
- # overload module's protparents with specifically given protparents
- # we allow prototype_key to be the key of the protparent dict, to allow for module-level
- # prototype imports. We need to insert prototype_key in this case
- forkey,protparentinkwargs.get("prototype_parents",{}).items():
- key=str(key).lower()
- protparent["prototype_key"]=str(protparent.get("prototype_key",key)).lower()
- protparents[key]=protlib.homogenize_prototype(protparent)
-
- if"return_parents"inkwargs:
- # only return the parents
- returncopy.deepcopy(protparents)
-
- objsparams=[]
- forprototypeinprototypes:
-
- protlib.validate_prototype(prototype,None,protparents,is_prototype_base=True)
- prot=_get_prototype(
- prototype,protparents,uninherited={"prototype_key":prototype.get("prototype_key")}
- )
- ifnotprot:
- continue
-
- # extract the keyword args we need to create the object itself. If we get a callable,
- # call that to get the value (don't catch errors)
- create_kwargs={}
- # we must always add a key, so if not given we use a shortened md5 hash. There is a (small)
- # chance this is not unique but it should usually not be a problem.
- val=prot.pop(
- "key",
- "Spawned-{}".format(hashlib.md5(bytes(str(time.time()),"utf-8")).hexdigest()[:6]),
- )
- create_kwargs["db_key"]=init_spawn_value(val,str,caller=caller,prototype=prototype)
-
- val=prot.pop("location",None)
- create_kwargs["db_location"]=init_spawn_value(
- val,value_to_obj,caller=caller,prototype=prototype)
-
- val=prot.pop("home",None)
- ifval:
- create_kwargs["db_home"]=init_spawn_value(val,value_to_obj,caller=caller,
- prototype=prototype)
- else:
- try:
- create_kwargs["db_home"]=init_spawn_value(
- settings.DEFAULT_HOME,value_to_obj,caller=caller,prototype=prototype)
- exceptObjectDB.DoesNotExist:
- # settings.DEFAULT_HOME not existing is common for unittests
- pass
-
- val=prot.pop("destination",None)
- create_kwargs["db_destination"]=init_spawn_value(val,value_to_obj,caller=caller,
- prototype=prototype)
-
- val=prot.pop("typeclass",settings.BASE_OBJECT_TYPECLASS)
- create_kwargs["db_typeclass_path"]=init_spawn_value(val,str,caller=caller,
- prototype=prototype)
-
- # extract calls to handlers
- val=prot.pop("permissions",[])
- permission_string=init_spawn_value(val,make_iter,caller=caller,prototype=prototype)
- val=prot.pop("locks","")
- lock_string=init_spawn_value(val,str,caller=caller,prototype=prototype)
- val=prot.pop("aliases",[])
- alias_string=init_spawn_value(val,make_iter,caller=caller,prototype=prototype)
-
- val=prot.pop("tags",[])
- tags=[]
- for(tag,category,*data)inval:
- tags.append((init_spawn_value(tag,str,caller=caller,prototype=prototype),
- category,data[0]ifdataelseNone))
-
- prototype_key=prototype.get("prototype_key",None)
- ifprototype_key:
- # we make sure to add a tag identifying which prototype created this object
- tags.append((prototype_key,PROTOTYPE_TAG_CATEGORY))
-
- val=prot.pop("exec","")
- execs=init_spawn_value(val,make_iter,caller=caller,prototype=prototype)
-
- # extract ndb assignments
- nattributes=dict(
- (key.split("_",1)[1],init_spawn_value(val,value_to_obj,caller=caller,
- prototype=prototype))
- forkey,valinprot.items()
- ifkey.startswith("ndb_")
- )
-
- # the rest are attribute tuples (attrname, value, category, locks)
- val=make_iter(prot.pop("attrs",[]))
- attributes=[]
- for(attrname,value,*rest)inval:
- attributes.append(
- (attrname,init_spawn_value(value,caller=caller,prototype=prototype),
- rest[0]ifrestelseNone,rest[1]iflen(rest)>1elseNone))
-
- simple_attributes=[]
- forkey,valuein(
- (key,value)forkey,valueinprot.items()ifnot(key.startswith("ndb_"))
- ):
- # we don't support categories, nor locks for simple attributes
- ifkeyin_PROTOTYPE_META_NAMES:
- continue
- else:
- simple_attributes.append(
- (key,init_spawn_value(value,value_to_obj_or_any,caller=caller,
- prototype=prototype),None,None)
- )
-
- attributes=attributes+simple_attributes
- attributes=[tupfortupinattributesifnottup[0]in_NON_CREATE_KWARGS]
-
- # pack for call into _batch_create_object
- objsparams.append(
- (
- create_kwargs,
- permission_string,
- lock_string,
- alias_string,
- nattributes,
- attributes,
- tags,
- execs,
- )
- )
-
- ifkwargs.get("only_validate"):
- returnobjsparams
- returnbatch_create_object(*objsparams)
[docs]classScriptDBManager(TypedObjectManager):
- """
- This Scriptmanager implements methods for searching
- and manipulating Scripts directly from the database.
-
- Evennia-specific search methods (will return Typeclasses or
- lists of Typeclasses, whereas Django-general methods will return
- Querysets or database objects).
-
- dbref (converter)
- dbref_search
- get_dbref_range
- object_totals
- typeclass_search
- get_all_scripts_on_obj
- get_all_scripts
- delete_script
- remove_non_persistent
- validate
- script_search (equivalent to evennia.search_script)
- copy_script
-
- """
-
-
[docs]defget_all_scripts_on_obj(self,obj,key=None):
- """
- Find all Scripts related to a particular object.
-
- Args:
- obj (Object): Object whose Scripts we are looking for.
- key (str, optional): Script identifier - can be given as a
- dbref or name string. If given, only scripts matching the
- key on the object will be returned.
- Returns:
- matches (list): Matching scripts.
-
- """
- ifnotobj:
- return[]
- account=_GA(_GA(obj,"__dbclass__"),"__name__")=="AccountDB"
- ifkey:
- dbref=self.dbref(key)
- ifdbrefordbref==0:
- ifaccount:
- returnself.filter(db_account=obj,id=dbref)
- else:
- returnself.filter(db_obj=obj,id=dbref)
- elifaccount:
- returnself.filter(db_account=obj,db_key=key)
- else:
- returnself.filter(db_obj=obj,db_key=key)
- elifaccount:
- returnself.filter(db_account=obj)
- else:
- returnself.filter(db_obj=obj)
-
-
[docs]defget_all_scripts(self,key=None):
- """
- Get all scripts in the database.
-
- Args:
- key (str or int, optional): Restrict result to only those
- with matching key or dbref.
-
- Returns:
- scripts (list): All scripts found, or those matching `key`.
-
- """
- ifkey:
- dbref=self.dbref(key)
- ifdbref:
- returnself.filter(id=dbref)
- returnself.filter(db_key__iexact=key.strip())
- returnself.all()
-
-
[docs]defdelete_script(self,dbref):
- """
- This stops and deletes a specific script directly from the
- script database.
-
- Args:
- dbref (int): Database unique id.
-
- Notes:
- This might be needed for global scripts not tied to a
- specific game object
-
- """
- scripts=self.get_id(dbref)
- forscriptinmake_iter(scripts):
- script.stop()
- script.delete()
[docs]defsearch_script(self,ostring,obj=None,only_timed=False,typeclass=None):
- """
- Search for a particular script.
-
- Args:
- ostring (str): Search criterion - a script dbef or key.
- obj (Object, optional): Limit search to scripts defined on
- this object
- only_timed (bool): Limit search only to scripts that run
- on a timer.
- typeclass (class or str): Typeclass or path to typeclass.
-
- Returns:
- Queryset: An iterable with 0, 1 or more results.
-
- """
-
- ostring=ostring.strip()
-
- dbref=self.dbref(ostring)
- ifdbref:
- # this is a dbref, try to find the script directly
- dbref_match=self.dbref_search(dbref)
- ifdbref_match:
- dmatch=dbref_match[0]
- ifnot(objandobj!=dmatch.obj)or(only_timedanddmatch.interval):
- returndbref_match
-
- iftypeclass:
- ifcallable(typeclass):
- typeclass="%s.%s"%(typeclass.__module__,typeclass.__name__)
- else:
- typeclass="%s"%typeclass
-
- # not a dbref; normal search
- obj_restriction=objandQ(db_obj=obj)orQ()
- timed_restriction=only_timedandQ(db_interval__gt=0)orQ()
- typeclass_restriction=typeclassandQ(db_typeclass_path=typeclass)orQ()
- scripts=self.filter(
- timed_restriction&obj_restriction&typeclass_restriction&Q(db_key__iexact=ostring)
- )
- returnscripts
-
- # back-compatibility alias
- script_search=search_script
-
-
[docs]defcopy_script(self,original_script,new_key=None,new_obj=None,new_locks=None):
- """
- Make an identical copy of the original_script.
-
- Args:
- original_script (Script): The Script to copy.
- new_key (str, optional): Rename the copy.
- new_obj (Object, optional): Place copy on different Object.
- new_locks (str, optional): Give copy different locks from
- the original.
-
- Returns:
- script_copy (Script): A new Script instance, copied from
- the original.
- """
- typeclass=original_script.typeclass_path
- new_key=new_keyifnew_keyisnotNoneelseoriginal_script.key
- new_obj=new_objifnew_objisnotNoneelseoriginal_script.obj
- new_locks=new_locksifnew_locksisnotNoneelseoriginal_script.db_lock_storage
-
- fromevennia.utilsimportcreate
-
- new_script=create.create_script(
- typeclass,key=new_key,obj=new_obj,locks=new_locks,autostart=True
- )
- returnnew_script
-
-
[docs]defcreate_script(
- self,
- typeclass=None,
- key=None,
- obj=None,
- account=None,
- locks=None,
- interval=None,
- start_delay=None,
- repeats=None,
- persistent=None,
- autostart=True,
- report_to=None,
- desc=None,
- tags=None,
- attributes=None,
- ):
- """
- Create a new script. All scripts are a combination of a database
- object that communicates with the database, and an typeclass that
- 'decorates' the database object into being different types of
- scripts. It's behaviour is similar to the game objects except
- scripts has a time component and are more limited in scope.
-
- 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.
- obj (Object): The entity on which this Script sits. If this
- is `None`, we are creating a "global" script.
- account (Account): The account on which this Script sits. It is
- exclusiv to `obj`.
- locks (str): one or more lockstrings, separated by semicolons.
- interval (int): The triggering interval for this Script, in
- seconds. If unset, the Script will not have a timing
- component.
- start_delay (bool): If `True`, will wait `interval` seconds
- before triggering the first time.
- repeats (int): The number of times to trigger before stopping.
- If unset, will repeat indefinitely.
- persistent (bool): If this Script survives a server shutdown
- or not (all Scripts will survive a reload).
- autostart (bool): If this Script will start immediately when
- created or if the `start` method must be called explicitly.
- 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 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
-
- See evennia.scripts.manager for methods to manipulate existing
- scripts in the database.
-
- """
- global_ObjectDB,_AccountDB
- ifnot_ObjectDB:
- fromevennia.objects.modelsimportObjectDBas_ObjectDB
- fromevennia.accounts.modelsimportAccountDBas_AccountDB
-
- typeclass=typeclassiftypeclasselsesettings.BASE_SCRIPT_TYPECLASS
-
- ifisinstance(typeclass,str):
- # a path is given. Load the actual typeclass
- typeclass=class_from_module(typeclass,settings.TYPECLASS_PATHS)
-
- # validate input
- kwarg={}
- ifkey:
- kwarg["db_key"]=key
- ifaccount:
- kwarg["db_account"]=dbid_to_obj(account,_AccountDB)
- ifobj:
- kwarg["db_obj"]=dbid_to_obj(obj,_ObjectDB)
- ifinterval:
- kwarg["db_interval"]=max(0,interval)
- ifstart_delay:
- kwarg["db_start_delay"]=start_delay
- ifrepeats:
- kwarg["db_repeats"]=max(0,repeats)
- ifpersistent:
- kwarg["db_persistent"]=persistent
- ifdesc:
- kwarg["db_desc"]=desc
- tags=make_iter(tags)iftagsisnotNoneelseNone
- attributes=make_iter(attributes)ifattributesisnotNoneelseNone
-
- # create new instance
- new_script=typeclass(**kwarg)
-
- # store the call signature for the signal
- new_script._createdict=dict(
- key=key,
- obj=obj,
- account=account,
- locks=locks,
- interval=interval,
- start_delay=start_delay,
- repeats=repeats,
- persistent=persistent,
- autostart=autostart,
- report_to=report_to,
- desc=desc,
- tags=tags,
- attributes=attributes,
- )
- # this will trigger the save signal which in turn calls the
- # at_first_save hook on the typeclass, where the _createdict
- # can be used.
- new_script.save()
-
- ifnotnew_script.id:
- # this happens in the case of having a repeating script with `repeats=1` and
- # `start_delay=False` - the script will run once and immediately stop before
- # save is over.
- returnNone
-
- signals.SIGNAL_SCRIPT_POST_CREATE.send(sender=new_script)
-
- returnnew_script
-"""
-Scripts are entities that perform some sort of action, either only
-once or repeatedly. They can be directly linked to a particular
-Evennia Object or be stand-alonw (in the latter case it is considered
-a 'global' script). Scripts can indicate both actions related to the
-game world as well as pure behind-the-scenes events and effects.
-Everything that has a time component in the game (i.e. is not
-hard-coded at startup or directly created/controlled by players) is
-handled by Scripts.
-
-Scripts have to check for themselves that they should be applied at a
-particular moment of time; this is handled by the is_valid() hook.
-Scripts can also implement at_start and at_end hooks for preparing and
-cleaning whatever effect they have had on the game object.
-
-Common examples of uses of Scripts:
-
-- Load the default cmdset to the account object's cmdhandler
- when logging in.
-- Switch to a different state, such as entering a text editor,
- start combat or enter a dark room.
-- Merge a new cmdset with the default one for changing which
- commands are available at a particular time
-- Give the account/object a time-limited bonus/effect
-
-"""
-fromdjango.confimportsettings
-fromdjango.dbimportmodels
-fromdjango.core.exceptionsimportObjectDoesNotExist
-fromevennia.typeclasses.modelsimportTypedObject
-fromevennia.scripts.managerimportScriptDBManager
-fromevennia.utils.utilsimportdbref,to_str
-
-__all__=("ScriptDB",)
-_GA=object.__getattribute__
-_SA=object.__setattr__
-
-
-# ------------------------------------------------------------
-#
-# ScriptDB
-#
-# ------------------------------------------------------------
-
-
-
[docs]classScriptDB(TypedObject):
- """
- The Script database representation.
-
- The TypedObject supplies the following (inherited) 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 ScriptDB adds the following properties:
- desc - optional description of script
- obj - the object the script is linked to, if any
- account - the account the script is linked to (exclusive with obj)
- interval - how often script should run
- start_delay - if the script should start repeating right away
- repeats - how many times the script should repeat
- persistent - if script should survive a server reboot
- is_active - bool if script is currently running
-
- """
-
- #
- # ScriptDB Database Model setup
- #
- # These database fields are all set using their corresponding properties,
- # named same as the field, but withtou the db_* prefix.
-
- # inherited fields (from TypedObject):
- # db_key, db_typeclass_path, db_date_created, db_permissions
-
- # optional description.
- db_desc=models.CharField("desc",max_length=255,blank=True)
- # A reference to the database object affected by this Script, if any.
- db_obj=models.ForeignKey(
- "objects.ObjectDB",
- null=True,
- blank=True,
- on_delete=models.CASCADE,
- verbose_name="scripted object",
- help_text="the object to store this script on, if not a global script.",
- )
- db_account=models.ForeignKey(
- "accounts.AccountDB",
- null=True,
- blank=True,
- on_delete=models.CASCADE,
- verbose_name="scripted account",
- help_text="the account to store this script on (should not be set if db_obj is set)",
- )
-
- # how often to run Script (secs). -1 means there is no timer
- db_interval=models.IntegerField(
- "interval",default=-1,help_text="how often to repeat script, in seconds. <= 0 means off."
- )
- # start script right away or wait interval seconds first
- db_start_delay=models.BooleanField(
- "start delay",default=False,help_text="pause interval seconds before starting."
- )
- # 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=True)
- # defines if this script has already been started in this session
- db_is_active=models.BooleanField("script active",default=False)
-
- # Database manager
- objects=ScriptDBManager()
-
- # defaults
- __settingsclasspath__=settings.BASE_SCRIPT_TYPECLASS
- __defaultclasspath__="evennia.scripts.scripts.DefaultScript"
- __applabel__="scripts"
-
- classMeta(object):
- "Define Django meta options"
- verbose_name="Script"
-
- #
- #
- # ScriptDB class properties
- #
- #
-
- # obj property
- def__get_obj(self):
- """
- Property wrapper that homogenizes access to either the
- db_account or db_obj field, using the same object property
- name.
-
- """
- obj=_GA(self,"db_account")
- ifnotobj:
- obj=_GA(self,"db_obj")
- returnobj
-
- def__set_obj(self,value):
- """
- Set account or obj to their right database field. If
- a dbref is given, assume ObjectDB.
-
- """
- try:
- value=_GA(value,"dbobj")
- exceptAttributeError:
- # deprecated ...
- pass
- ifisinstance(value,(str,int)):
- fromevennia.objects.modelsimportObjectDB
-
- value=to_str(value)
- ifvalue.isdigit()orvalue.startswith("#"):
- dbid=dbref(value,reqhash=False)
- ifdbid:
- try:
- value=ObjectDB.objects.get(id=dbid)
- exceptObjectDoesNotExist:
- # maybe it is just a name that happens to look like a dbid
- pass
- ifvalue.__class__.__name__=="AccountDB":
- fname="db_account"
- _SA(self,fname,value)
- else:
- fname="db_obj"
- _SA(self,fname,value)
- # saving the field
- _GA(self,"save")(update_fields=[fname])
-
- obj=property(__get_obj,__set_obj)
- object=property(__get_obj,__set_obj)
-"""
-Monitors - catch changes to model fields and Attributes.
-
-The MONITOR_HANDLER singleton from this module offers the following
-functionality:
-
-- Field-monitor - track a object's specific database field and perform
- an action whenever that field *changes* for whatever reason.
-- Attribute-monitor tracks an object's specific Attribute and perform
- an action whenever that Attribute *changes* for whatever reason.
-
-"""
-importinspect
-
-fromcollectionsimportdefaultdict
-fromevennia.server.modelsimportServerConfig
-fromevennia.utils.dbserializeimportdbserialize,dbunserialize
-fromevennia.utilsimportlogger
-fromevennia.utilsimportvariable_from_module
-
-_SA=object.__setattr__
-_GA=object.__getattribute__
-_DA=object.__delattr__
-
-
-
[docs]classMonitorHandler(object):
- """
- This is a resource singleton that allows for registering
- callbacks for when a field or Attribute is updated (saved).
-
- """
-
-
[docs]defsave(self):
- """
- Store our monitors to the database. This is called
- by the server process.
-
- Since dbserialize can't handle defaultdicts, we convert to an
- intermediary save format ((obj,fieldname, idstring, callback, kwargs), ...)
-
- """
- savedata=[]
- ifself.monitors:
- forobjinself.monitors:
- forfieldnameinself.monitors[obj]:
- foridstring,(callback,persistent,kwargs)inself.monitors[obj][
- fieldname
- ].items():
- path="%s.%s"%(callback.__module__,callback.__name__)
- savedata.append((obj,fieldname,idstring,path,persistent,kwargs))
- savedata=dbserialize(savedata)
- ServerConfig.objects.conf(key=self.savekey,value=savedata)
-
-
[docs]defrestore(self,server_reload=True):
- """
- Restore our monitors after a reload. This is called
- by the server process.
-
- Args:
- server_reload (bool, optional): If this is False, it means
- the server went through a cold reboot and all
- non-persistent tickers must be killed.
-
- """
- self.monitors=defaultdict(lambda:defaultdict(dict))
- restored_monitors=ServerConfig.objects.conf(key=self.savekey)
- ifrestored_monitors:
- restored_monitors=dbunserialize(restored_monitors)
- for(obj,fieldname,idstring,path,persistent,kwargs)inrestored_monitors:
- try:
- ifnotserver_reloadandnotpersistent:
- # this monitor will not be restarted
- continue
- if"session"inkwargsandnotkwargs["session"]:
- # the session was removed because it no longer
- # exists. Don't restart the monitor.
- continue
- modname,varname=path.rsplit(".",1)
- callback=variable_from_module(modname,varname)
-
- ifobjandhasattr(obj,fieldname):
- self.monitors[obj][fieldname][idstring]=(callback,persistent,kwargs)
- exceptException:
- continue
- # make sure to clean data from database
- ServerConfig.objects.conf(key=self.savekey,delete=True)
-
- def_attr_category_fieldname(self,fieldname,category):
- """
- Modify the saved fieldname to make sure to differentiate between Attributes
- with different categories.
-
- """
- returnf"{fieldname}[{category}]"ifcategoryelsefieldname
-
-
[docs]defat_update(self,obj,fieldname):
- """
- Called by the field/attribute as it saves.
-
- """
- # if this an Attribute with a category we should differentiate
- fieldname=self._attr_category_fieldname(
- fieldname,obj.db_category
- iffieldname=="db_value"andhasattr(obj,"db_category")elseNone
- )
-
- to_delete=[]
- ifobjinself.monitorsandfieldnameinself.monitors[obj]:
- foridstring,(callback,persistent,kwargs)inself.monitors[obj][fieldname].items():
- try:
- callback(obj=obj,fieldname=fieldname,**kwargs)
- exceptException:
- to_delete.append((obj,fieldname,idstring))
- logger.log_trace("Monitor callback was removed.")
- # we cleanup non-found monitors (has to be done after loop)
- for(obj,fieldname,idstring)into_delete:
- delself.monitors[obj][fieldname][idstring]
-
-
[docs]defadd(self,obj,fieldname,callback,idstring="",persistent=False,
- category=None,**kwargs):
- """
- Add monitoring to a given field or Attribute. A field must
- be specified with the full db_* name or it will be assumed
- to be an Attribute (so `db_key`, not just `key`).
-
- Args:
- obj (Typeclassed Entity): The entity on which to monitor a
- field or Attribute.
- fieldname (str): Name of field (db_*) or Attribute to monitor.
- callback (callable): A callable on the form `callable(**kwargs),
- where kwargs holds keys fieldname and obj.
- idstring (str, optional): An id to separate this monitor from other monitors
- of the same field and object.
- persistent (bool, optional): If False, the monitor will survive
- a server reload but not a cold restart. This is default.
- category (str, optional): This is only used if `fieldname` refers to
- an Attribute (i.e. it does not start with `db_`). You must specify this
- if you want to target an Attribute with a category.
-
- Keyword Args:
- session (Session): If this keyword is given, the monitorhandler will
- correctly analyze it and remove the monitor if after a reload/reboot
- the session is no longer valid.
- any (any): Any other kwargs are passed on to the callback. Remember that
- all kwargs must be possible to pickle!
-
- """
- ifnotfieldname.startswith("db_")ornothasattr(obj,fieldname):
- # an Attribute - we track its db_value field
- obj=obj.attributes.get(fieldname,return_obj=True)
- ifnotobj:
- return
- fieldname=self._attr_category_fieldname("db_value",category)
-
- # we try to serialize this data to test it's valid. Otherwise we won't accept it.
- try:
- ifnotinspect.isfunction(callback):
- raiseTypeError("callback is not a function.")
- dbserialize((obj,fieldname,callback,idstring,persistent,kwargs))
- exceptException:
- err="Invalid monitor definition: \n"" (%s, %s, %s, %s, %s, %s)"%(
- obj,
- fieldname,
- callback,
- idstring,
- persistent,
- kwargs,
- )
- logger.log_trace(err)
- else:
- self.monitors[obj][fieldname][idstring]=(callback,persistent,kwargs)
[docs]defall(self,obj=None):
- """
- List all monitors or all monitors of a given object.
-
- Args:
- obj (Object): The object on which to list all monitors.
-
- Returns:
- monitors (list): The handled monitors.
-
- """
- output=[]
- objs=[obj]ifobjelseself.monitors
-
- forobjinobjs:
- forfieldnameinself.monitors[obj]:
- foridstring,(callback,persistent,kwargs)inself.monitors[obj][
- fieldname
- ].items():
- output.append((obj,fieldname,idstring,persistent,kwargs))
- returnoutput
-"""
-The script handler makes sure to check through all stored scripts to
-make sure they are still relevant. A scripthandler is automatically
-added to all game objects. You access it through the property
-`scripts` on the game object.
-
-"""
-fromevennia.scripts.modelsimportScriptDB
-fromevennia.utilsimportcreate
-fromevennia.utilsimportlogger
-
-fromdjango.utils.translationimportgettextas_
-
-
-
[docs]classScriptHandler(object):
- """
- Implements the handler. This sits on each game object.
-
- """
-
-
[docs]def__init__(self,obj):
- """
- Set up internal state.
-
- Args:
- obj (Object): A reference to the object this handler is
- attached to.
-
- """
- self.obj=obj
[docs]defadd(self,scriptclass,key=None,autostart=True):
- """
- Add a script to this object.
-
- Args:
- scriptclass (Scriptclass, Script or str): Either a class
- object inheriting from DefaultScript, an instantiated
- script object or a python path to such a class object.
- key (str, optional): Identifier for the script (often set
- in script definition and listings)
- autostart (bool, optional): Start the script upon adding it.
-
- """
- ifself.obj.__dbclass__.__name__=="AccountDB":
- # we add to an Account, not an Object
- script=create.create_script(
- scriptclass,key=key,account=self.obj,autostart=autostart
- )
- else:
- # the normal - adding to an Object. We wait to autostart so we can differentiate
- # a failing creation from a script that immediately starts/stops.
- script=create.create_script(scriptclass,key=key,obj=self.obj,autostart=False)
- ifnotscript:
- logger.log_err("Script %s failed to be created/started."%scriptclass)
- returnFalse
- ifautostart:
- script.start()
- ifnotscript.id:
- # this can happen if the script has repeats=1 or calls stop() in at_repeat.
- logger.log_info(
- "Script %s started and then immediately stopped; "
- "it could probably be a normal function."%scriptclass
- )
- returnTrue
-
-
[docs]defstart(self,key):
- """
- Find scripts and force-start them
-
- Args:
- key (str): The script's key or dbref.
-
- Returns:
- nr_started (int): The number of started scripts found.
-
- """
- scripts=ScriptDB.objects.get_all_scripts_on_obj(self.obj,key=key)
- num=0
- forscriptinscripts:
- script.start()
- num+=1
- returnnum
-
-
[docs]defget(self,key):
- """
- Search scripts on this object.
-
- Args:
- key (str): Search criterion, the script's key or dbref.
-
- Returns:
- scripts (list): The found scripts matching `key`.
-
- """
- returnlist(ScriptDB.objects.get_all_scripts_on_obj(self.obj,key=key))
-
-
[docs]defdelete(self,key=None):
- """
- Forcibly delete a script from this object.
-
- Args:
- key (str, optional): A script key or the path to a script (in the
- latter case all scripts with this path will be deleted!)
- If no key is given, delete *all* scripts on the object!
-
- """
- delscripts=ScriptDB.objects.get_all_scripts_on_obj(self.obj,key=key)
- ifnotdelscripts:
- delscripts=[
- script
- forscriptinScriptDB.objects.get_all_scripts_on_obj(self.obj)
- ifscript.path==key
- ]
- num=0
- forscriptindelscripts:
- script.delete()
- num+=1
- returnnum
-
- # alias to delete
- stop=delete
-
-
[docs]defall(self):
- """
- Get all scripts stored in this handler.
-
- """
- returnScriptDB.objects.get_all_scripts_on_obj(self.obj)
-"""
-This module defines Scripts, out-of-character entities that can store
-data both on themselves and on other objects while also having the
-ability to run timers.
-
-"""
-
-fromtwisted.internet.deferimportDeferred,maybeDeferred
-fromtwisted.internet.taskimportLoopingCall
-fromdjango.utils.translationimportgettextas_
-fromevennia.typeclasses.modelsimportTypeclassBase
-fromevennia.scripts.modelsimportScriptDB
-fromevennia.scripts.managerimportScriptManager
-fromevennia.utilsimportcreate,logger
-
-__all__=["DefaultScript","DoNothing","Store"]
-
-
-classExtendedLoopingCall(LoopingCall):
- """
- 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.
-
- """
-
- start_delay=None
- callcount=0
-
- defstart(self,interval,now=True,start_delay=None,count_start=0):
- """
- Start running function every interval seconds.
-
- This overloads the LoopingCall default by offering the
- start_delay keyword and ability to repeat.
-
- Args:
- interval (int): Repeat interval in seconds.
- now (bool, optional): Whether to start immediately or after
- `start_delay` seconds.
- 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.
-
- Raises:
- AssertError: if trying to start a task which is already running.
- ValueError: If interval is set to an invalid value < 0.
-
- Notes:
- As opposed to Twisted's inbuilt count mechanism, this
- system will count also if force_repeat() was called rather
- than just the number of `interval` seconds since the start.
- This allows us to force-step through a limited number of
- steps if we want.
-
- """
- assertnotself.running,"Tried to start an already running ExtendedLoopingCall."
- ifinterval<0:
- raiseValueError("interval must be >= 0")
- self.running=True
- deferred=self._deferred=Deferred()
- self.starttime=self.clock.seconds()
- self.interval=interval
- self._runAtStart=now
- self.callcount=max(0,count_start)
- self.start_delay=start_delayifstart_delayisNoneelsemax(0,start_delay)
-
- ifnow:
- # run immediately
- self()
- elifstart_delayisnotNoneandstart_delay>=0:
- # start after some time: for this to work we need to
- # trick _scheduleFrom by temporarily setting a different
- # self.interval for it to check.
- real_interval,self.interval=self.interval,start_delay
- self._scheduleFrom(self.starttime)
- # re-set the actual interval (this will be picked up
- # next time it runs
- self.interval=real_interval
- else:
- self._scheduleFrom(self.starttime)
- returndeferred
-
- def__call__(self):
- """
- Tick one step. We update callcount (tracks number of calls) as
- well as null start_delay (needed in order to correctly
- estimate next_call_time at all times).
-
- """
- self.callcount+=1
- ifself.start_delay:
- self.start_delay=None
- self.starttime=self.clock.seconds()
- ifself._deferred:
- LoopingCall.__call__(self)
-
- defforce_repeat(self):
- """
- Force-fire the callback
-
- Raises:
- AssertionError: When trying to force a task that is not
- running.
-
- """
- assertself.running,"Tried to fire an ExtendedLoopingCall that was not running."
- self.call.cancel()
- self.call=None
- self.starttime=self.clock.seconds()
- self()
-
- defnext_call_time(self):
- """
- Get the next call time. This also takes the eventual effect
- of start_delay into account.
-
- Returns:
- 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.
-
- """
- ifself.runningandself.interval>0:
- total_runtime=self.clock.seconds()-self.starttime
- interval=self.start_delayorself.interval
- returnmax(0,interval-(total_runtime%self.interval))
-
-
-classScriptBase(ScriptDB,metaclass=TypeclassBase):
- """
- 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()
-
- def__str__(self):
- return"<{cls}{key}>".format(cls=self.__class__.__name__,key=self.key)
-
- def__repr__(self):
- returnstr(self)
-
- defat_idmapper_flush(self):
- """
- 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 point
- self.ndb._task=ExtendedLoopingCall(self._step_task)
-
- self._unpause_task(
- interval=interval,
- start_delay=start_delay,
- auto_unpause=auto_unpause,
- old_interval=old_interval,
- )
-
- ifnotself.ndb._task.running:
- # if not unpausing started it, start script anew with the new values
- self.ndb._task.start(self.db_interval,now=notself.db_start_delay)
-
- self.at_start(**kwargs)
-
- def_pause_task(self,auto_pause=False,**kwargs):
- """
- 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
- )
- self.db._paused_time=None
- self.db._paused_callcount=None
- self.db._manually_paused=None
-
- self.at_start(**kwargs)
-
- def_stop_task(self,**kwargs):
- """
- Stop task runner and delete the task.
-
- """
- task=self.ndb._task
- iftaskandtask.running:
- task.stop()
- self.ndb._task=None
- self.db_is_active=False
-
- # make sure this is not confused as a paused script
- self.db._paused_time=None
- self.db._paused_callcount=None
- self.db._manually_paused=None
-
- self.save(update_fields=["db_is_active"])
- self.at_stop(**kwargs)
-
- def_step_errback(self,e):
- """
- Callback for runner errors
-
- """
- cname=self.__class__.__name__
- estring=_(
- "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:
- # we must not crash inside the errback, even if db_obj is None.
- pass
- logger.log_err(estring)
-
- def_step_callback(self):
- """
- Step task runner. No try..except needed due to defer wrap.
-
- """
- ifnotself.ndb._task:
- # if there is no task, we have no business using this method
- return
-
- ifnotself.is_valid():
- self.stop()
- return
-
- # call hook
- self.at_repeat()
-
- # check repeats
- ifself.ndb._task:
- # we need to check for the task in case stop() was called
- # inside at_repeat() and it already went away.
- callcount=self.ndb._task.callcount
- maxcount=self.db_repeats
- ifmaxcount>0andmaxcount<=callcount:
- self.stop()
-
- def_step_task(self):
- """
- Step task. This groups error handling.
- """
- try:
- returnmaybeDeferred(self._step_callback).addErrback(self._step_errback)
- exceptException:
- logger.log_trace()
- returnNone
-
- # Access methods / hooks
-
- defat_first_save(self,**kwargs):
- """
- This is called after very first time this object is saved.
- Generally, you don't need to overload this, but only the hooks
- called by this method.
-
- Args:
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- """
- self.at_script_creation()
-
- ifhasattr(self,"_createdict"):
- # this will only be set if the utils.create_script
- # function was used to create the object. We want
- # the create call's kwargs to override the values
- # set by hooks.
- cdict=self._createdict
- updates=[]
- ifnotcdict.get("key"):
- ifnotself.db_key:
- self.db_key="#%i"%self.dbid
- updates.append("db_key")
- elifself.db_key!=cdict["key"]:
- self.db_key=cdict["key"]
- updates.append("db_key")
- ifcdict.get("interval")andself.interval!=cdict["interval"]:
- self.db_interval=max(0,cdict["interval"])
- updates.append("db_interval")
- ifcdict.get("start_delay")andself.start_delay!=cdict["start_delay"]:
- self.db_start_delay=cdict["start_delay"]
- updates.append("db_start_delay")
- ifcdict.get("repeats")andself.repeats!=cdict["repeats"]:
- self.db_repeats=max(0,cdict["repeats"])
- updates.append("db_repeats")
- ifcdict.get("persistent")andself.persistent!=cdict["persistent"]:
- self.db_persistent=cdict["persistent"]
- updates.append("db_persistent")
- ifcdict.get("desc")andself.desc!=cdict["desc"]:
- self.db_desc=cdict["desc"]
- updates.append("db_desc")
- ifupdates:
- self.save(update_fields=updates)
-
- ifcdict.get("permissions"):
- self.permissions.batch_add(*cdict["permissions"])
- ifcdict.get("locks"):
- self.locks.add(cdict["locks"])
- ifcdict.get("tags"):
- # this should be a list of tags, tuples (key, category) or (key, category, data)
- self.tags.batch_add(*cdict["tags"])
- ifcdict.get("attributes"):
- # this should be tuples (key, val, ...)
- self.attributes.batch_add(*cdict["attributes"])
- ifcdict.get("nattributes"):
- # this should be a dict of nattrname:value
- forkey,valueincdict["nattributes"]:
- self.nattributes.add(key,value)
-
- ifcdict.get("autostart"):
- # autostart the script
- self._start_task(force_restart=True)
-
- defdelete(self,stop_task=True):
- """
- Delete the Script. Normally stops any timer task. This fires at_script_delete before
- deletion.
-
- Args:
- stop_task (bool, optional): If unset, the task will not be stopped
- when this method is called. The main reason for setting this to False
- is if wanting to delete the script from the at_stop method - setting
- this will then avoid an infinite recursion.
-
- Returns:
- bool: If deletion was successful or not. Only time this can fail would be if
- the script was already previously deleted, or `at_script_delete` returns
- False.
-
- """
- ifnotself.pkornotself.at_script_delete():
- returnFalse
- ifstop_task:
- self._stop_task()
- super().delete()
- returnTrue
-
- defat_script_creation(self):
- """
- Should be overridden in child.
-
- """
- pass
-
- defat_script_delete(self):
- """
- Called when script is deleted, before the script timer stops.
-
- Returns:
- bool: If False, deletion is aborted.
-
- """
- returnTrue
-
- 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]classDefaultScript(ScriptBase):
- """
- This is the base TypeClass for all Scripts. Scripts describe
- events, timers and states in game, they can have a time component
- or describe a state that changes under certain conditions.
-
- """
-
-
[docs]@classmethod
- defcreate(cls,key,**kwargs):
- """
- Provides a passthrough interface to the utils.create_script() function.
-
- Args:
- key (str): Name of the new object.
-
- Returns:
- object (Object): A newly created object of the given typeclass.
- errors (list): A list of errors in string form, if any.
-
- """
- errors=[]
- obj=None
-
- kwargs["key"]=key
-
- # If no typeclass supplied, use this class
- kwargs["typeclass"]=kwargs.pop("typeclass",cls)
-
- try:
- obj=create.create_script(**kwargs)
- exceptException:
- logger.log_trace()
- errors.append("The script '%s' encountered errors and could not be created."%key)
-
- returnobj,errors
-
-
[docs]defat_script_creation(self):
- """
- Only called once, when script is first created.
-
- """
- pass
-
-
[docs]defis_valid(self):
- """
- 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.
-
- """
- returnTrue
-
-
[docs]defat_start(self,**kwargs):
- """
- 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
- overriding the call (unused by default).
-
- """
- pass
-
-
[docs]defat_repeat(self,**kwargs):
- """
- Called repeatedly if this Script is set to repeat regularly.
-
- Args:
- **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's timer to stop (either
- because is_valid returned False, it ran out of iterations or it was manuallys
- stopped.
-
- 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, before stopping the timer.
-
- Returns:
- bool: If False, the deletion is aborted.
-
- """
- returnTrue
-
-
[docs]defat_server_reload(self):
- """
- This hook is called whenever the server is shutting down for
- restart/reboot. If you want to, for example, save
- non-persistent properties across a restart, this is the place
- to do it.
- """
- pass
-
-
[docs]defat_server_shutdown(self):
- """
- 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
-
-
-# Some useful default Script types used by Evennia.
-
-
-
[docs]classDoNothing(DefaultScript):
- """
- A script that does nothing. Used as default fallback.
- """
-
-
[docs]defat_script_creation(self):
- """
- Setup the script
- """
- self.key="sys_do_nothing"
- self.desc="This is an empty placeholder script."
[docs]classTaskHandlerTask:
- """An object to represent a single TaskHandler task.
-
- Instance Attributes:
- task_id (int): the global id for this task
- deferred (deferred): a reference to this task's deferred
- Property Attributes:
- paused (bool): check if the deferred instance of a task has been paused.
- called(self): A task attribute to check if the deferred instance of a task has been called.
-
- Methods:
- 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.
- active(): Check if a task is active (has not been called yet).
- exists(): Check if a task exists.
- get_id(): Returns the global id for this task. For use with
-
- """
-
-
[docs]defget_deferred(self):
- """Return the instance of the deferred the task id is using.
-
- Returns:
- bool or deferred: An instance of a deferred or False if there is no task with the id.
- None is returned if there is no deferred affiliated with this id.
-
- """
- returnTASK_HANDLER.get_deferred(self.task_id)
-
-
[docs]defpause(self):
- """
- Pause the callback of a task.
- To resume use `TaskHandlerTask.unpause`.
-
- """
- d=self.deferred
- ifd:
- d.pause()
-
-
[docs]defunpause(self):
- """
- Unpause a task, run the task if it has passed delay time.
-
- """
- d=self.deferred
- ifd:
- d.unpause()
-
- @property
- defpaused(self):
- """
- 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.
-
- Returns:
- bool or None: True if the task was properly paused. None if the task does not have
- a deferred instance.
-
- """
- d=self.deferred
- ifd:
- returnd.paused
- else:
- returnNone
-
-
[docs]defdo_task(self):
- """
- 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.
-
- Returns:
- bool or any: Set to `False` if the task does not exist in task
- handler. Otherwise it will be the return of the task's callback.
-
- """
- returnTASK_HANDLER.do_task(self.task_id)
-
-
[docs]defcall(self):
- """
- 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.
-
- Returns:
- bool or any: Set to `False` if the task does not exist in task
- handler. Otherwise it will be the return of the task's callback.
-
- """
- returnTASK_HANDLER.call_task(self.task_id)
-
-
[docs]defremove(self):
- """Remove a task without executing it.
- Deletes the instance of the task's deferred.
-
- Args:
- task_id (int): an existing task ID.
-
- Returns:
- bool: True if the removal completed successfully.
-
- """
- returnTASK_HANDLER.remove(self.task_id)
-
-
[docs]defcancel(self):
- """Stop a task from automatically executing.
- This will not remove the task.
-
- Returns:
- bool: True if the cancel completed successfully.
- False if the cancel did not complete successfully.
-
- """
- returnTASK_HANDLER.cancel(self.task_id)
-
-
[docs]defactive(self):
- """Check if a task is active (has not been called yet).
-
- Returns:
- bool: True if a task is active (has not been called yet). False if
- it is not (has been called) or if the task does not exist.
-
- """
- returnTASK_HANDLER.active(self.task_id)
-
- @property
- defcalled(self):
- """
- A task attribute to check if the deferred instance of a task has been called.
-
- This exists to mock usage of a twisted deferred object.
- It will not set to True if Task.call has been called. This only happens if
- task's deferred instance calls the callback.
-
- Returns:
- bool: True if the deferred instance of this task has called the callback.
- False if the deferred instnace of this task has not called the callback.
-
- """
- d=self.deferred
- ifd:
- returnd.called
- else:
- returnNone
-
-
[docs]defexists(self):
- """
- Check if a task exists.
- Most task handler methods check for existence for you.
-
- Returns:
- bool: True the task exists False if it does not.
-
- """
- returnTASK_HANDLER.exists(self.task_id)
-
-
[docs]defget_id(self):
- """
- Returns the global id for this task. For use with
- `evennia.scripts.taskhandler.TASK_HANDLER`.
-
- Returns:
- task_id (int): global task id for this task.
-
- """
- returnself.task_id
-
-
-
[docs]classTaskHandler(object):
-
- """A light singleton wrapper allowing to access permanent tasks.
-
- When `utils.delay` is called, the task handler is used to create
- the task.
-
- Task handler will automatically remove uncalled but canceled from task
- handler. By default this will not occur until a canceled task
- has been uncalled for 60 second after the time it should have been called.
- To adjust this time use TASK_HANDLER.stale_timeout. If stale_timeout is 0
- stale tasks will not be automatically removed.
- This is not done on a timer. I is done as new tasks are added or the load method is called.
-
- """
-
-
[docs]def__init__(self):
- self.tasks={}
- self.to_save={}
- self.clock=reactor
- # number of seconds before an uncalled canceled task is removed from TaskHandler
- self.stale_timeout=60
- self._now=False# used in unit testing to manually set now time
-
-
[docs]defload(self):
- """Load from the ServerConfig.
-
- This should be automatically called when Evennia starts.
- It populates `self.tasks` according to the ServerConfig.
-
- """
- to_save=False
- value=ServerConfig.objects.conf("delayed_tasks",default={})
- ifisinstance(value,str):
- tasks=dbunserialize(value)
- else:
- tasks=value
-
- # At this point, `tasks` contains a dictionary of still-serialized tasks
- fortask_id,valueintasks.items():
- date,callback,args,kwargs=dbunserialize(value)
- ifisinstance(callback,tuple):
- # `callback` can be an object and name for instance methods
- obj,method=callback
- ifobjisNone:
- to_save=True
- continue
-
- callback=getattr(obj,method)
- self.tasks[task_id]=(date,callback,args,kwargs,True,None)
-
- ifself.stale_timeout>0:# cleanup stale tasks.
- self.clean_stale_tasks()
- ifto_save:
- self.save()
-
-
[docs]defclean_stale_tasks(self):
- """remove uncalled but canceled from task handler.
-
- By default this will not occur until a canceled task
- has been uncalled for 60 second after the time it should have been called.
- To adjust this time use TASK_HANDLER.stale_timeout.
-
- """
- clean_ids=[]
- fortask_id,(date,callback,args,kwargs,persistent,_)inself.tasks.items():
- ifnotself.active(task_id):
- stale_date=date+timedelta(seconds=self.stale_timeout)
- # if a now time is provided use it (intended for unit testing)
- now=self._nowifself._nowelsedatetime.now()
- # the task was canceled more than stale_timeout seconds ago
- ifnow>stale_date:
- clean_ids.append(task_id)
- fortask_idinclean_ids:
- self.remove(task_id)
- returnTrue
-
-
[docs]defsave(self):
- """
- Save the tasks in ServerConfig.
-
- """
-
- fortask_id,(date,callback,args,kwargs,persistent,_)inself.tasks.items():
- iftask_idinself.to_save:
- continue
- ifnotpersistent:
- continue
-
- safe_callback=callback
- ifgetattr(callback,"__self__",None):
- # `callback` is an instance method
- obj=callback.__self__
- name=callback.__name__
- safe_callback=(obj,name)
-
- # Check if callback can be pickled. args and kwargs have been checked
- 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))
-
- ServerConfig.objects.conf("delayed_tasks",self.to_save)
-
-
[docs]defadd(self,timedelay,callback,*args,**kwargs):
- """
- Add a new task.
-
- If the persistent kwarg is truthy:
- The callback, args and values for kwarg will be serialized. Type
- and attribute errors during the serialization will be logged,
- but will not throw exceptions.
- For persistent tasks do not use memory references in the callback
- function or arguments. After a restart those memory references are no
- longer accurate.
-
- Args:
- timedelay (int or float): time in seconds before calling the callback.
- callback (function or instance method): the callback itself
- any (any): any additional positional arguments to send to the callback
- *args: positional arguments to pass to callback.
- **kwargs: keyword arguments to pass to callback.
- - persistent (bool, optional): persist the task (stores it).
- Persistent key and value is removed from kwargs it will
- not be passed to callback.
-
- Returns:
- TaskHandlerTask: An object to represent a task.
- Reference evennia.scripts.taskhandler.TaskHandlerTask for complete details.
-
- """
- # set the completion time
- # Only used on persistent tasks after a restart
- now=datetime.now()
- delta=timedelta(seconds=timedelay)
- comp_time=now+delta
- # get an open task id
- used_ids=list(self.tasks.keys())
- task_id=1
- whiletask_idinused_ids:
- task_id+=1
-
- # record the task to the tasks dictionary
- persistent=kwargs.get("persistent",False)
- if"persistent"inkwargs:
- delkwargs["persistent"]
- ifpersistent:
- safe_args=[]
- safe_kwargs={}
-
- # Check that args and kwargs contain picklable information
- forarginargs:
- try:
- dbserialize(arg)
- except(TypeError,AttributeError,PickleError):
- log_err(
- "The positional argument {} cannot be "
- "pickled and will not be present in the arguments "
- "fed to the callback {}".format(arg,callback)
- )
- else:
- safe_args.append(arg)
-
- forkey,valueinkwargs.items():
- try:
- dbserialize(value)
- except(TypeError,AttributeError,PickleError):
- log_err(
- "The {} keyword argument {} cannot be "
- "pickled and will not be present in the arguments "
- "fed to the callback {}".format(key,value,callback)
- )
- else:
- safe_kwargs[key]=value
-
- self.tasks[task_id]=(comp_time,callback,safe_args,safe_kwargs,persistent,None)
- self.save()
- else:# this is a non-persitent task
- self.tasks[task_id]=(comp_time,callback,args,kwargs,persistent,None)
-
- # defer the task
- callback=self.do_task
- args=[task_id]
- kwargs={}
- d=deferLater(self.clock,timedelay,callback,*args,**kwargs)
- d.addErrback(handle_error)
-
- # some tasks may complete before the deferred can be added
- iftask_idinself.tasks:
- task=self.tasks.get(task_id)
- task=list(task)
- task[4]=persistent
- task[5]=d
- self.tasks[task_id]=task
- else:# the task already completed
- returnFalse
- ifself.stale_timeout>0:
- self.clean_stale_tasks()
- returnTaskHandlerTask(task_id)
-
-
[docs]defexists(self,task_id):
- """
- Check if a task exists.
- Most task handler methods check for existence for you.
-
- Args:
- task_id (int): an existing task ID.
-
- Returns:
- bool: True the task exists False if it does not.
-
- """
- iftask_idinself.tasks:
- returnTrue
- else:
- returnFalse
-
-
[docs]defactive(self,task_id):
- """
- Check if a task is active (has not been called yet).
-
- Args:
- task_id (int): an existing task ID.
-
- Returns:
- bool: True if a task is active (has not been called yet). False if
- it is not (has been called) or if the task does not exist.
-
- """
- iftask_idinself.tasks:
- # if the task has not been run, cancel it
- deferred=self.get_deferred(task_id)
- returnnot(deferredanddeferred.called)
- else:
- returnFalse
-
-
[docs]defcancel(self,task_id):
- """
- Stop a task from automatically executing.
- This will not remove the task.
-
- Args:
- task_id (int): an existing task ID.
-
- Returns:
- bool: True if the cancel completed successfully.
- False if the cancel did not complete successfully.
-
- """
- iftask_idinself.tasks:
- # if the task has not been run, cancel it
- d=self.get_deferred(task_id)
- ifd:# it is remotely possible for a task to not have a deferred
- ifd.called:
- returnFalse
- else:# the callback has not been called yet.
- d.cancel()
- returnTrue
- else:# this task has no deferred instance
- returnFalse
- else:
- returnFalse
-
-
[docs]defremove(self,task_id):
- """
- Remove a task without executing it.
- Deletes the instance of the task's deferred.
-
- Args:
- task_id (int): an existing task ID.
-
- Returns:
- bool: True if the removal completed successfully.
-
- """
- d=None
- # delete the task from the tasks dictionary
- iftask_idinself.tasks:
- # if the task has not been run, cancel it
- self.cancel(task_id)
- delself.tasks[task_id]# delete the task from the tasks dictionary
- # remove the task from the persistent dictionary and ServerConfig
- iftask_idinself.to_save:
- delself.to_save[task_id]
- self.save()# remove from ServerConfig.objects
- # delete the instance of the deferred
- ifd:
- deld
- returnTrue
-
-
[docs]defclear(self,save=True,cancel=True):
- """
- 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.
- cancel=True (bool): Cancel scheduled tasks before removing it from task handler.
-
- Returns:
- True (bool): if the removal completed successfully.
-
- """
- ifself.tasks:
- fortask_idinself.tasks.keys():
- ifcancel:
- self.cancel(task_id)
- self.tasks={}
- ifself.to_save:
- self.to_save={}
- ifsave:
- self.save()
- returnTrue
-
-
[docs]defcall_task(self,task_id):
- """
- 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.
-
- Args:
- task_id (int): an existing task ID.
-
- Returns:
- bool or any: Set to `False` if the task does not exist in task
- handler. Otherwise it will be the return of the task's callback.
-
- """
- iftask_idinself.tasks:
- date,callback,args,kwargs,persistent,d=self.tasks.get(task_id)
- else:# the task does not exist
- returnFalse
- returncallback(*args,**kwargs)
-
-
[docs]defdo_task(self,task_id):
- """
- 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.
-
- Args:
- task_id (int): a valid task ID.
-
- Returns:
- bool or any: Set to `False` if the task does not exist in task
- handler. Otherwise it will be the return of the task's callback.
-
- """
- callback_return=False
- iftask_idinself.tasks:
- date,callback,args,kwargs,persistent,d=self.tasks.get(task_id)
- else:# the task does not exist
- returnFalse
- ifd:# it is remotely possible for a task to not have a deferred
- ifnotd.called:# the task's deferred has not been called yet
- d.cancel()# cancel the automated callback
- else:# this task has no deferred, and should not be called
- returnFalse
- callback_return=callback(*args,**kwargs)
- self.remove(task_id)
- returncallback_return
-
-
[docs]defget_deferred(self,task_id):
- """
- Return the instance of the deferred the task id is using.
-
- Args:
- task_id (int): a valid task ID.
-
- Returns:
- bool or deferred: An instance of a deferred or False if there is no task with the id.
- None is returned if there is no deferred affiliated with this id.
-
- """
- iftask_idinself.tasks:
- returnself.tasks[task_id][5]
- else:
- returnNone
-
-
[docs]defcreate_delays(self):
- """
- Create the delayed tasks for the persistent tasks.
- This method should be automatically called when Evennia starts.
-
- """
- now=datetime.now()
- fortask_id,(date,callback,args,kwargs,_,_)inself.tasks.items():
- self.tasks[task_id]=date,callback,args,kwargs,True,None
- seconds=max(0,(date-now).total_seconds())
- d=deferLater(self.clock,seconds,self.do_task,task_id)
- d.addErrback(handle_error)
- # some tasks may complete before the deferred can be added
- ifself.tasks.get(task_id,False):
- self.tasks[task_id]=date,callback,args,kwargs,True,d
-
-
-# Create the soft singleton
-TASK_HANDLER=TaskHandler()
-
-"""
-TickerHandler
-
-This implements an efficient Ticker which uses a subscription
-model to 'tick' subscribed objects at regular intervals.
-
-The ticker mechanism is used by importing and accessing
-the instantiated TICKER_HANDLER instance in this module. This
-instance is run by the server; it will save its status across
-server reloads and be started automaticall on boot.
-
-Example:
-
-```python
- from evennia.scripts.tickerhandler import TICKER_HANDLER
-
- # call tick myobj.at_tick(*args, **kwargs) every 15 seconds
- TICKER_HANDLER.add(15, myobj.at_tick, *args, **kwargs)
-```
-
-You supply the interval to tick and a callable to call regularly with
-any extra args/kwargs. The callable should either be a stand-alone
-function in a module *or* the method on a *typeclassed* entity (that
-is, on an object that can be safely and stably returned from the
-database). Functions that are dynamically created or sits on
-in-memory objects cannot be used by the tickerhandler (there is no way
-to reference them safely across reboots and saves).
-
-The handler will transparently set
-up and add new timers behind the scenes to tick at given intervals,
-using a TickerPool - all callables with the same interval will share
-the interval ticker.
-
-To remove:
-
-```python
- TICKER_HANDLER.remove(15, myobj.at_tick)
-```
-
-Both interval and callable must be given since a single object can be subscribed
-to many different tickers at the same time. You can also supply `idstring`
-as an identifying string if you ever want to tick the callable at the same interval
-but with different arguments (args/kwargs are not used for identifying the ticker). There
-is also `persistent=False` if you don't want to make a ticker that don't survive a reload.
-If either or both `idstring` or `persistent` has been changed from their defaults, they
-must be supplied to the `TICKER_HANDLER.remove` call to properly identify the ticker
-to remove.
-
-The TickerHandler's functionality can be overloaded by modifying the
-Ticker class and then changing TickerPool and TickerHandler to use the
-custom classes
-
-```python
-class MyTicker(Ticker):
- # [doing custom stuff]
-
-class MyTickerPool(TickerPool):
- ticker_class = MyTicker
-class MyTickerHandler(TickerHandler):
- ticker_pool_class = MyTickerPool
-```
-
-If one wants to duplicate TICKER_HANDLER's auto-saving feature in
-a custom handler one can make a custom `AT_STARTSTOP_MODULE` entry to
-call the handler's `save()` and `restore()` methods when the server reboots.
-
-"""
-importinspect
-
-fromtwisted.internet.deferimportinlineCallbacks
-fromdjango.core.exceptionsimportObjectDoesNotExist
-fromevennia.scripts.scriptsimportExtendedLoopingCall
-fromevennia.server.modelsimportServerConfig
-fromevennia.utils.loggerimportlog_trace,log_err
-fromevennia.utils.dbserializeimportdbserialize,dbunserialize,pack_dbobj
-fromevennia.utilsimportvariable_from_module,inherits_from
-
-_GA=object.__getattribute__
-_SA=object.__setattr__
-
-
-_ERROR_ADD_TICKER="""TickerHandler: Tried to add an invalid ticker:
-{store_key}
-Ticker was not added."""
-
-_ERROR_ADD_TICKER_SUB_SECOND="""You are trying to add a ticker running faster
-than once per second. This is not supported and also probably not useful:
-Spamming messages to the user faster than once per second serves no purpose in
-a text-game, and if you want to update some property, consider doing so
-on-demand rather than using a ticker.
-"""
-
-
[docs]classTicker(object):
- """
- Represents a repeatedly running task that calls
- hooks repeatedly. Overload `_callback` to change the
- way it operates.
- """
-
- @inlineCallbacks
- def_callback(self):
- """
- This will be called repeatedly every `self.interval` seconds.
- `self.subscriptions` contain tuples of (obj, args, kwargs) for
- each subscribing object.
-
- If overloading, this callback is expected to handle all
- subscriptions when it is triggered. It should not return
- anything and should not traceback on poorly designed hooks.
- The callback should ideally work under @inlineCallbacks so it
- can yield appropriately.
-
- The _hook_key, which is passed down through the handler via
- kwargs is used here to identify which hook method to call.
-
- """
- self._to_add=[]
- self._to_remove=[]
- self._is_ticking=True
- forstore_key,(args,kwargs)inself.subscriptions.items():
- callback=yieldkwargs.pop("_callback","at_tick")
- obj=yieldkwargs.pop("_obj",None)
- try:
- ifcallable(callback):
- # call directly
- yieldcallback(*args,**kwargs)
- continue
- # try object method
- ifnotobjornotobj.pk:
- # object was deleted between calls
- self._to_remove.append(store_key)
- continue
- else:
- yield_GA(obj,callback)(*args,**kwargs)
- exceptObjectDoesNotExist:
- log_trace("Removing ticker.")
- self._to_remove.append(store_key)
- exceptException:
- log_trace()
- finally:
- # make sure to re-store
- kwargs["_callback"]=callback
- kwargs["_obj"]=obj
- # cleanup - we do this here to avoid changing the subscription dict while it loops
- self._is_ticking=False
- forstore_keyinself._to_remove:
- self.remove(store_key)
- forstore_key,(args,kwargs)inself._to_add:
- self.add(store_key,*args,**kwargs)
- self._to_remove=[]
- self._to_add=[]
-
-
[docs]def__init__(self,interval):
- """
- Set up the ticker
-
- Args:
- interval (int): The stepping interval.
-
- """
- self.interval=interval
- self.subscriptions={}
- self._is_ticking=False
- self._to_remove=[]
- self._to_add=[]
- # set up a twisted asynchronous repeat call
- self.task=ExtendedLoopingCall(self._callback)
-
-
[docs]defvalidate(self,start_delay=None):
- """
- Start/stop the task depending on how many subscribers we have
- using it.
-
- Args:
- start_delay (int): Time to way before starting.
-
- """
- subs=self.subscriptions
- ifself.task.running:
- ifnotsubs:
- self.task.stop()
- elifsubs:
- self.task.start(self.interval,now=False,start_delay=start_delay)
-
-
[docs]defadd(self,store_key,*args,**kwargs):
- """
- Sign up a subscriber to this ticker.
- Args:
- store_key (str): Unique storage hash for this ticker subscription.
- args (any, optional): Arguments to call the hook method with.
-
- Keyword Args:
- _start_delay (int): If set, this will be
- used to delay the start of the trigger instead of
- `interval`.
-
- """
- ifself._is_ticking:
- # protects the subscription dict from
- # updating while it is looping
- self._to_add.append((store_key,(args,kwargs)))
- else:
- start_delay=kwargs.pop("_start_delay",None)
- self.subscriptions[store_key]=(args,kwargs)
- self.validate(start_delay=start_delay)
-
-
[docs]defremove(self,store_key):
- """
- Unsubscribe object from this ticker
-
- Args:
- store_key (str): Unique store key.
-
- """
- ifself._is_ticking:
- # this protects the subscription dict from
- # updating while it is looping
- self._to_remove.append(store_key)
- else:
- self.subscriptions.pop(store_key,False)
- self.validate()
-
-
[docs]defstop(self):
- """
- Kill the Task, regardless of subscriptions.
-
- """
- self.subscriptions={}
- self.validate()
-
-
-
[docs]classTickerPool(object):
- """
- This maintains a pool of
- `evennia.scripts.scripts.ExtendedLoopingCall` tasks for calling
- subscribed objects at given times.
-
- """
-
- ticker_class=Ticker
-
-
[docs]def__init__(self):
- """
- Initialize the pool.
-
- """
- self.tickers={}
[docs]defstop(self,interval=None):
- """
- Stop all scripts in pool. This is done at server reload since
- restoring the pool will automatically re-populate the pool.
-
- Args:
- interval (int, optional): Only stop tickers with this
- interval.
-
- """
- ifintervalandintervalinself.tickers:
- self.tickers[interval].stop()
- else:
- fortickerinself.tickers.values():
- ticker.stop()
-
-
-
[docs]classTickerHandler(object):
- """
- The Tickerhandler maintains a pool of tasks for subscribing
- objects to various tick rates. The pool maintains creation
- instructions and and re-applies them at a server restart.
-
- """
-
- ticker_pool_class=TickerPool
-
-
[docs]def__init__(self,save_name="ticker_storage"):
- """
- Initialize handler
-
- save_name (str, optional): The name of the ServerConfig
- instance to store the handler state persistently.
-
- """
- self.ticker_storage={}
- self.save_name=save_name
- self.ticker_pool=self.ticker_pool_class()
-
- def_get_callback(self,callback):
- """
- Analyze callback and determine its consituents
-
- Args:
- callback (function or method): This is either a stand-alone
- function or class method on a typeclassed entitye (that is,
- an entity that can be saved to the database).
- Returns:
- ret (tuple): This is a tuple of the form `(obj, path, callfunc)`,
- where `obj` is the database object the callback is defined on
- if it's a method (otherwise `None`) and vice-versa, `path` is
- the python-path to the stand-alone function (`None` if a method).
- The `callfunc` is either the name of the method to call or the
- callable function object itself.
- Raises:
- TypeError: If the callback is of an unsupported type.
-
- """
- outobj,outpath,outcallfunc=None,None,None
- ifcallable(callback):
- ifinspect.ismethod(callback):
- outobj=callback.__self__
- outcallfunc=callback.__func__.__name__
- elifinspect.isfunction(callback):
- outpath="%s.%s"%(callback.__module__,callback.__name__)
- outcallfunc=callback
- else:
- raiseTypeError(f"{callback} is not a method or function.")
- else:
- raiseTypeError(f"{callback} is not a callable function or method.")
-
- ifoutobjandnotinherits_from(outobj,"evennia.typeclasses.models.TypedObject"):
- raiseTypeError(
- f"{callback} is a method on a normal object - it must "
- "be either a method on a typeclass, or a stand-alone function."
- )
-
- returnoutobj,outpath,outcallfunc
-
- def_store_key(self,obj,path,interval,callfunc,idstring="",persistent=True):
- """
- Tries to create a store_key for the object.
-
- Args:
- obj (Object, tuple or None): Subscribing object if any. If a tuple, this is
- a packed_obj tuple from dbserialize.
- path (str or None): Python-path to callable, if any.
- interval (int): Ticker interval. Floats will be converted to
- nearest lower integer value.
- callfunc (callable or str): This is either the callable function or
- the name of the method to call. Note that the callable is never
- stored in the key; that is uniquely identified with the python-path.
- idstring (str, optional): Additional separator between
- different subscription types.
- persistent (bool, optional): If this ticker should survive a system
- shutdown or not.
-
- Returns:
- store_key (tuple): A tuple `(packed_obj, methodname, outpath, interval,
- idstring, persistent)` that uniquely identifies the
- ticker. Here, `packed_obj` is the unique string representation of the
- object or `None`. The `methodname` is the string name of the method on
- `packed_obj` to call, or `None` if `packed_obj` is unset. `path` is
- the Python-path to a non-method callable, or `None`. Finally, `interval`
- `idstring` and `persistent` are integers, strings and bools respectively.
-
- """
- ifinterval<1:
- raiseRuntimeError(_ERROR_ADD_TICKER_SUB_SECOND)
-
- interval=int(interval)
- persistent=bool(persistent)
- packed_obj=pack_dbobj(obj)
- methodname=callfuncifcallfuncandisinstance(callfunc,str)elseNone
- outpath=pathifpathandisinstance(path,str)elseNone
- return(packed_obj,methodname,outpath,interval,idstring,persistent)
-
-
[docs]defsave(self):
- """
- Save ticker_storage as a serialized string into a temporary
- ServerConf field. Whereas saving is done on the fly, if called
- by server when it shuts down, the current timer of each ticker
- will be saved so it can start over from that point.
-
- """
- ifself.ticker_storage:
- # get the current times so the tickers can be restarted with a delay later
- start_delays=dict(
- (interval,ticker.task.next_call_time())
- forinterval,tickerinself.ticker_pool.tickers.items()
- )
-
- # remove any subscriptions that lost its object in the interim
- to_save={
- store_key:(args,kwargs)
- forstore_key,(args,kwargs)inself.ticker_storage.items()
- if(
- (
- store_key[1]
- and("_obj"inkwargsandkwargs["_obj"].pk)
- andhasattr(kwargs["_obj"],store_key[1])
- )
- orstore_key[2]# a valid method with existing obj
- )
- }# a path given
-
- # update the timers for the tickers
- forstore_key,(args,kwargs)into_save.items():
- interval=store_key[1]
- # this is a mutable, so it's updated in-place in ticker_storage
- kwargs["_start_delay"]=start_delays.get(interval,None)
- ServerConfig.objects.conf(key=self.save_name,value=dbserialize(to_save))
- else:
- # make sure we have nothing lingering in the database
- ServerConfig.objects.conf(key=self.save_name,delete=True)
-
-
[docs]defrestore(self,server_reload=True):
- """
- Restore ticker_storage from database and re-initialize the
- handler from storage. This is triggered by the server at
- restart.
-
- Args:
- server_reload (bool, optional): If this is False, it means
- the server went through a cold reboot and all
- non-persistent tickers must be killed.
-
- """
- # load stored command instructions and use them to re-initialize handler
- restored_tickers=ServerConfig.objects.conf(key=self.save_name)
- ifrestored_tickers:
- # the dbunserialize will convert all serialized dbobjs to real objects
-
- restored_tickers=dbunserialize(restored_tickers)
- self.ticker_storage={}
- forstore_key,(args,kwargs)inrestored_tickers.items():
- try:
- # at this point obj is the actual object (or None) due to how
- # the dbunserialize works
- obj,callfunc,path,interval,idstring,persistent=store_key
- ifnotpersistentandnotserver_reload:
- # this ticker will not be restarted
- continue
- ifisinstance(callfunc,str)andnotobj:
- # methods must have an existing object
- continue
- # we must rebuild the store_key here since obj must not be
- # stored as the object itself for the store_key to be hashable.
- store_key=self._store_key(obj,path,interval,callfunc,idstring,persistent)
-
- ifobjandcallfunc:
- kwargs["_callback"]=callfunc
- kwargs["_obj"]=obj
- elifpath:
- modname,varname=path.rsplit(".",1)
- callback=variable_from_module(modname,varname)
- kwargs["_callback"]=callback
- kwargs["_obj"]=None
- else:
- # Neither object nor path - discard this ticker
- log_err("Tickerhandler: Removing malformed ticker: %s"%str(store_key))
- continue
- exceptException:
- # this suggests a malformed save or missing objects
- log_trace("Tickerhandler: Removing malformed ticker: %s"%str(store_key))
- continue
- # if we get here we should create a new ticker
- self.ticker_storage[store_key]=(args,kwargs)
- self.ticker_pool.add(store_key,*args,**kwargs)
-
-
[docs]defadd(self,interval=60,callback=None,idstring="",persistent=True,*args,**kwargs):
- """
- Add subscription to tickerhandler
-
- Args:
- interval (int, optional): Interval in seconds between calling
- `callable(*args, **kwargs)`
- callable (callable function or method, optional): This
- should either be a stand-alone function or a method on a
- typeclassed entity (that is, one that can be saved to the
- database).
- idstring (str, optional): Identifier for separating
- this ticker-subscription from others with the same
- interval. Allows for managing multiple calls with
- the same time interval and callback.
- persistent (bool, optional): A ticker will always survive
- a server reload. If this is unset, the ticker will be
- deleted by a server shutdown.
- args, kwargs (optional): These will be passed into the
- callback every time it is called. This must be data possible
- to pickle!
-
- Returns:
- store_key (tuple): The immutable store-key for this ticker. This can
- be stored and passed into `.remove(store_key=store_key)` later to
- easily stop this ticker later.
-
- Notes:
- The callback will be identified by type and stored either as
- as combination of serialized database object + methodname or
- as a python-path to the module + funcname. These strings will
- be combined iwth `interval` and `idstring` to define a
- unique storage key for saving. These must thus all be supplied
- when wanting to modify/remove the ticker later.
-
- """
- obj,path,callfunc=self._get_callback(callback)
- store_key=self._store_key(obj,path,interval,callfunc,idstring,persistent)
- kwargs["_obj"]=obj
- kwargs["_callback"]=callfunc# either method-name or callable
- self.ticker_storage[store_key]=(args,kwargs)
- self.ticker_pool.add(store_key,*args,**kwargs)
- self.save()
- returnstore_key
-
-
[docs]defremove(self,interval=60,callback=None,idstring="",persistent=True,store_key=None):
- """
- Remove ticker subscription from handler.
-
- Args:
- interval (int, optional): Interval of ticker to remove.
- callback (callable function or method): Either a function or
- the method of a typeclassed object.
- idstring (str, optional): Identifier id of ticker to remove.
- persistent (bool, optional): Whether this ticker is persistent or not.
- store_key (str, optional): If given, all other kwargs are ignored and only
- this is used to identify the ticker.
-
- Raises:
- KeyError: If no matching ticker was found to remove.
-
- Notes:
- The store-key is normally built from the interval/callback/idstring/persistent values;
- but if the `store_key` is explicitly given, this is used instead.
-
- """
- ifisinstance(callback,int):
- raiseRuntimeError(
- "TICKER_HANDLER.remove has changed: "
- "the interval is now the first argument, callback the second."
- )
- ifnotstore_key:
- obj,path,callfunc=self._get_callback(callback)
- store_key=self._store_key(obj,path,interval,callfunc,idstring,persistent)
- to_remove=self.ticker_storage.pop(store_key,None)
- ifto_remove:
- self.ticker_pool.remove(store_key)
- self.save()
- else:
- raiseKeyError(f"No Ticker was found matching the store-key {store_key}.")
-
-
[docs]defclear(self,interval=None):
- """
- Stop/remove tickers from handler.
-
- Args:
- interval (int): Only stop tickers with this interval.
-
- Notes:
- This is the only supported way to kill tickers related to
- non-db objects.
-
- """
- self.ticker_pool.stop(interval)
- ifinterval:
- self.ticker_storage=dict(
- (store_key,store_key)
- forstore_keyinself.ticker_storage
- ifstore_key[1]!=interval
- )
- else:
- self.ticker_storage={}
- self.save()
-
-
[docs]defall(self,interval=None):
- """
- Get all subscriptions.
-
- Args:
- interval (int): Limit match to tickers with this interval.
-
- Returns:
- tickers (list): If `interval` was given, this is a list of
- tickers using that interval.
- tickerpool_layout (dict): If `interval` was *not* given,
- this is a dict {interval1: [ticker1, ticker2, ...], ...}
-
- """
- ifintervalisNone:
- # return dict of all, ordered by interval
- returndict(
- (interval,ticker.subscriptions)
- forinterval,tickerinself.ticker_pool.tickers.items()
- )
- else:
- # get individual interval
- ticker=self.ticker_pool.tickers.get(interval,None)
- ifticker:
- return{interval:ticker.subscriptions}
- returnNone
-
-
[docs]defall_display(self):
- """
- Get all tickers on an easily displayable form.
-
- Returns:
- tickers (dict): A list of all storekeys
-
- """
- store_keys=[]
- fortickerinself.ticker_pool.tickers.values():
- for(
- (objtup,callfunc,path,interval,idstring,persistent),
- (args,kwargs),
- )inticker.subscriptions.items():
- store_keys.append(
- (kwargs.get("_obj",None),callfunc,path,interval,idstring,persistent)
- )
- returnstore_keys
-
-
-# main tickerhandler
-TICKER_HANDLER=TickerHandler()
-
-"""
-The Evennia Server service acts as an AMP-client when talking to the
-Portal. This module sets up the Client-side communication.
-
-"""
-
-importos
-fromdjango.confimportsettings
-fromevennia.server.portalimportamp
-fromtwisted.internetimportprotocol
-fromevennia.utilsimportlogger
-fromevennia.utils.utilsimportclass_from_module
-
-
-
[docs]classAMPClientFactory(protocol.ReconnectingClientFactory):
- """
- This factory creates an instance of an AMP client connection. This handles communication from
- the be the Evennia 'Server' service to the 'Portal'. The client will try to auto-reconnect on a
- connection error.
-
- """
-
- # Initial reconnect delay in seconds.
- initialDelay=1
- factor=1.5
- maxDelay=1
- noisy=False
-
-
[docs]def__init__(self,server):
- """
- Initializes the client factory.
-
- Args:
- server (server): server instance.
-
- """
- self.server=server
- 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
- self.broadcasts=[]
-
-
[docs]defstartedConnecting(self,connector):
- """
- Called when starting to try to connect to the Portal AMP server.
-
- Args:
- connector (Connector): Twisted Connector instance representing
- this connection.
-
- """
- pass
-
-
[docs]defbuildProtocol(self,addr):
- """
- Creates an AMPProtocol instance when connecting to the AMP server.
-
- Args:
- addr (str): Connection address. Not used.
-
- """
- self.resetDelay()
- self.server.amp_protocol=AMPServerClientProtocol()
- self.server.amp_protocol.factory=self
- returnself.server.amp_protocol
-
-
[docs]defclientConnectionLost(self,connector,reason):
- """
- Called when the AMP connection to the MUD server is lost.
-
- Args:
- connector (Connector): Twisted Connector instance representing
- this connection.
- reason (str): Eventual text describing why connection was lost.
-
- """
- logger.log_info("Server disconnected from the portal.")
- protocol.ReconnectingClientFactory.clientConnectionLost(self,connector,reason)
-
-
[docs]defclientConnectionFailed(self,connector,reason):
- """
- Called when an AMP connection attempt to the MUD server fails.
-
- Args:
- connector (Connector): Twisted Connector instance representing
- this connection.
- reason (str): Eventual text describing why connection failed.
-
- """
- logger.log_msg("Attempting to reconnect to Portal ...")
- protocol.ReconnectingClientFactory.clientConnectionFailed(self,connector,reason)
-
-
-
[docs]classAMPServerClientProtocol(amp.AMPMultiConnectionProtocol):
- """
- This protocol describes the Server service (acting as an AMP-client)'s communication with the
- Portal (which acts as the AMP-server)
-
- """
-
- # sending AMP data
-
-
[docs]defconnectionMade(self):
- """
- Called when a new connection is established.
-
- """
- # print("AMPClient new connection {}".format(self))
- info_dict=self.factory.server.get_info_dict()
- super(AMPServerClientProtocol,self).connectionMade()
- # first thing we do is to request the Portal to sync all sessions
- # back with the Server side. We also need the startup mode (reload, reset, shutdown)
- self.send_AdminServer2Portal(
- amp.DUMMYSESSION,operation=amp.PSYNC,spid=os.getpid(),info_dict=info_dict
- )
- # run the intial setup if needed
- self.factory.server.run_initial_setup()
-
-
[docs]defdata_to_portal(self,command,sessid,**kwargs):
- """
- Send data across the wire to the Portal
-
- Args:
- command (AMP Command): A protocol send command.
- sessid (int): A unique Session id.
- kwargs (any): Any data to pickle into the command.
-
- Returns:
- deferred (deferred or None): A deferred with an errback.
-
- Notes:
- Data will be sent across the wire pickled as a tuple
- (sessid, kwargs).
-
- """
- # print("server data_to_portal: {}, {}, {}".format(command, sessid, kwargs))
- returnself.callRemote(command,packed_data=amp.dumps((sessid,kwargs))).addErrback(
- self.errback,command.key
- )
-
-
[docs]defsend_MsgServer2Portal(self,session,**kwargs):
- """
- Access method - executed on the Server for sending data
- to Portal.
-
- Args:
- session (Session): Unique Session.
- kwargs (any, optiona): Extra data.
-
- """
- returnself.data_to_portal(amp.MsgServer2Portal,session.sessid,**kwargs)
-
-
[docs]defsend_AdminServer2Portal(self,session,operation="",**kwargs):
- """
- Administrative access method called by the Server to send an
- instruction to the Portal.
-
- Args:
- session (Session): Session.
- operation (char, optional): Identifier for the server
- operation, as defined by the global variables in
- `evennia/server/amp.py`.
- kwargs (dict, optional): Data going into the adminstrative.
-
- """
- returnself.data_to_portal(
- amp.AdminServer2Portal,session.sessid,operation=operation,**kwargs
- )
[docs]defask_continue(self):
- "'Press return to continue'-prompt"
- input(" (Press return to continue)")
-
-
[docs]defask_node(self,options,prompt="Enter choice: ",default=None):
- """
- Retrieve options and jump to different menu nodes
-
- Args:
- options (dict): Node options on the form {key: (desc, callback), }
- prompt (str, optional): Question to ask
- default (str, optional): Default value to use if user hits return.
-
- """
-
- opt_txt="\n".join(f" {key}: {desc}"forkey,(desc,_,_)inoptions.items())
- self.display(opt_txt+"\n")
-
- whileTrue:
- resp=input(prompt).strip()
-
- ifnotresp:
- ifdefault:
- resp=str(default)
-
- ifresp.lower()inoptions:
- # self.display(f" Selected '{resp}'.")
- desc,callback,kwargs=options[resp.lower()]
- callback(self,**kwargs)
- elifresp.lower()in("quit","q"):
- sys.exit()
- elifresp:
- # input, but nothing was recognized
- self.display(" Choose one of: {}".format(list_to_string(list(options))))
-
-
[docs]defask_yesno(self,prompt,default="yes"):
- """
- Ask a yes/no question inline.
-
- Keyword Args:
- prompt (str): The prompt to ask.
- default (str): "yes" or "no", used if pressing return.
- Returns:
- reply (str): Either 'yes' or 'no'.
-
- """
- prompt=prompt+(" [Y]/N? "ifdefault=="yes"else" Y/[N]? ")
-
- whileTrue:
- resp=input(prompt).lstrip().lower()
- ifnotresp:
- resp=default.lower()
- ifrespin("yes","y"):
- self.display(" Answered Yes.")
- return"yes"
- elifrespin("no","n"):
- self.display(" Answered No.")
- return"no"
- elifresp.lower()in("quit","q"):
- sys.exit()
-
-
[docs]defask_choice(self,prompt=" > ",options=None,default=None):
- """
- Ask multiple-choice question, get response inline.
-
- Keyword Args:
- prompt (str): Input prompt.
- options (list): List of options. Will be indexable by sequence number 1...
- default (int): The list index+1 of the default choice, if any
- Returns:
- reply (str): The answered reply.
-
- """
- opt_txt="\n".join(f" {ind+1}: {desc}"forind,descinenumerate(options))
- self.display(opt_txt+"\n")
-
- whileTrue:
- resp=input(prompt).strip()
-
- ifnotresp:
- ifdefault:
- returnoptions[int(default)]
- ifresp.lower()in("quit","q"):
- sys.exit()
- ifresp.isdigit():
- resp=int(resp)-1
- if0<=resp<len(options):
- selection=options[resp]
- self.display(f" Selected '{selection}'.")
- returnselection
- self.display(" Select one of the given options.")
-
-
[docs]defask_input(self,prompt=" > ",default=None,validator=None):
- """
- Get arbitrary input inline.
-
- Keyword Args:
- prompt (str): The display prompt.
- default (str): If empty input, use this.
- validator (callable): If given, the input will be passed
- into this callable. It should return True unless validation
- fails (and is expected to echo why if so).
-
- Returns:
- inp (str): The input given, or default.
-
- """
- whileTrue:
- resp=input(prompt).strip()
-
- ifnotrespanddefault:
- resp=str(default)
-
- ifresp.lower()in("q","quit"):
- sys.exit()
-
- ifresp.lower()=="none":
- resp=""
-
- ifvalidatorandnotvalidator(resp):
- continue
-
- ok=input("\n Leave blank? [Y]/N: ")
- ifok.lower()in("n","no"):
- continue
- elifok.lower()in("q","quit"):
- sys.exit()
- returnresp
-
- ifvalidatorandnotvalidator(resp):
- continue
-
- self.display(resp)
- ok=input("\n Is this correct? [Y]/N: ")
- ifok.lower()in("n","no"):
- continue
- elifok.lower()in("q","quit"):
- sys.exit()
- returnresp
-
-
-
[docs]defnode_start(wizard):
- text="""
- This wizard helps to attach your Evennia server to external networks. It
- will save to a file `server/conf/connection_settings.py` that will be
- imported from the bottom of your game settings file. Once generated you can
- also modify that file directly.
-
- Make sure you have at least started the game once before continuing!
-
- Use `quit` at any time to abort and throw away unsaved changes.
- """
- options={
- "1":(
- "Add your game to the Evennia game index (also for closed-dev games)",
- node_game_index_start,
- {},
- ),
- "2":("MSSP setup (for mud-list crawlers)",node_mssp_start,{}),
- # "3": ("Add Grapevine listing",
- # node_grapevine_start, {}),
- # "4": ("Add IRC link",
- # "node_irc_start", {}),
- # "5" ("Add RSS feed",
- # "node_rss_start", {}),
- "s":("View and (optionally) Save created settings",node_view_and_apply_settings,{}),
- "q":("Quit",lambda*args:sys.exit(),{}),
- }
-
- wizard.display(text)
- wizard.ask_node(options)
-
-
-# Evennia game index
-
-
-
[docs]defnode_game_index_start(wizard,**kwargs):
- text="""
- The Evennia game index (http://games.evennia.com) lists both active Evennia
- games as well as games in various stages of development.
-
- You can put up your game in the index also if you are not (yet) open for
- players. If so, put 'None' for the connection details - you are just telling
- us that you are out there, making us excited about your upcoming game!
-
- Please check the listing online first to see that your exact game name is
- not colliding with an existing game-name in the list (be nice!).
- """
-
- wizard.display(text)
- ifwizard.ask_yesno("Continue adding/editing an Index entry?")=="yes":
- node_game_index_fields(wizard)
- else:
- node_start(wizard)
-
-
-
[docs]defnode_game_index_fields(wizard,status=None):
-
- # reset the listing if needed
- ifnothasattr(wizard,"game_index_listing"):
- wizard.game_index_listing=settings.GAME_INDEX_LISTING
-
- # game status
-
- status_default=wizard.game_index_listing["game_status"]
- text=f"""
- What is the status of your game?
- - pre-alpha: a game in its very early stages, mostly unfinished or unstarted
- - alpha: a working concept, probably lots of bugs and incomplete features
- - beta: a working game, but expect bugs and changing features
- - launched: a full, working game (that may still be expanded upon and improved later)
-
- Current value (return to keep):
-{status_default}
- """
-
- options=["pre-alpha","alpha","beta","launched"]
-
- wizard.display(text)
- wizard.game_index_listing["game_status"]=wizard.ask_choice("Select one: ",options)
-
- # game name
-
- name_default=settings.SERVERNAME
- text=f"""
- Your game's name should usually be the same as `settings.SERVERNAME`, but
- you can set it to something else here if you want.
-
- Current value:
-{name_default}
- """
-
- defname_validator(inp):
- tmax=80
- tlen=len(inp)
- iftlen>tmax:
- print(f"The name must be shorter than {tmax} characters (was {tlen}).")
- wizard.ask_continue()
- returnFalse
- returnTrue
-
- wizard.display(text)
- wizard.game_index_listing["game_name"]=wizard.ask_input(
- default=name_default,validator=name_validator
- )
-
- # short desc
-
- sdesc_default=wizard.game_index_listing.get("short_description",None)
-
- text=f"""
- Enter a short description of your game. Make it snappy and interesting!
- This should be at most one or two sentences (255 characters) to display by
- '{settings.SERVERNAME}' in the main game list. Line breaks will be ignored.
-
- Current value:
-{sdesc_default}
- """
-
- defsdesc_validator(inp):
- tmax=255
- tlen=len(inp)
- iftlen>tmax:
- print(f"The short desc must be shorter than {tmax} characters (was {tlen}).")
- wizard.ask_continue()
- returnFalse
- returnTrue
-
- wizard.display(text)
- wizard.game_index_listing["short_description"]=wizard.ask_input(
- default=sdesc_default,validator=sdesc_validator
- )
-
- # long desc
-
- long_default=wizard.game_index_listing.get("long_description",None)
-
- text=f"""
- Enter a longer, full-length description. This will be shown when clicking
- on your game's listing. You can use \\n to create line breaks and may use
- Markdown formatting like *bold*, _italic_, [linkname](http://link) etc.
-
- Current value:
-{long_default}
- """
-
- wizard.display(text)
- wizard.game_index_listing["long_description"]=wizard.ask_input(default=long_default)
-
- # listing contact
-
- listing_default=wizard.game_index_listing.get("listing_contact",None)
- text=f"""
- Enter a listing email-contact. This will not be visible in the listing, but
- allows us to get in touch with you should there be some listing issue (like
- a name collision) or some bug with the listing (us actually using this is
- likely to be somewhere between super-rarely and never).
-
- Current value:
-{listing_default}
- """
-
- defcontact_validator(inp):
- ifnotinpor"@"notininp:
- print("This should be an email and cannot be blank.")
- wizard.ask_continue()
- returnFalse
- returnTrue
-
- wizard.display(text)
- wizard.game_index_listing["listing_contact"]=wizard.ask_input(
- default=listing_default,validator=contact_validator
- )
-
- # telnet hostname
-
- hostname_default=wizard.game_index_listing.get("telnet_hostname",None)
- text=f"""
- Enter the hostname to which third-party telnet mud clients can connect to
- your game. This would be the name of the server your game is hosted on,
- like `coolgame.games.com`, or `mygreatgame.se`.
-
- Write 'None' if you are not offering public telnet connections at this time.
-
- Current value:
-{hostname_default}
- """
-
- wizard.display(text)
- wizard.game_index_listing["telnet_hostname"]=wizard.ask_input(default=hostname_default)
-
- # telnet port
-
- port_default=wizard.game_index_listing.get("telnet_port",None)
- text=f"""
- Enter the main telnet port. The Evennia default is 4000. You can change
- this with the TELNET_PORTS server setting.
-
- Write 'None' if you are not offering public telnet connections at this time.
-
- Current value:
-{port_default}
- """
-
- wizard.display(text)
- wizard.game_index_listing["telnet_port"]=wizard.ask_input(default=port_default)
-
- # website
-
- website_default=wizard.game_index_listing.get("game_website",None)
- text=f"""
- Evennia is its own web server and runs your game's website. Enter the
- URL of the website here, like http://yourwebsite.com, here.
-
- Write 'None' if you are not offering a publicly visible website at this time.
-
- Current value:
-{website_default}
- """
-
- wizard.display(text)
- wizard.game_index_listing["game_website"]=wizard.ask_input(default=website_default)
-
- # webclient
-
- webclient_default=wizard.game_index_listing.get("web_client_url",None)
- text=f"""
- Evennia offers its own native webclient. Normally it will be found from the
- game homepage at something like http://yourwebsite.com/webclient. Enter
- your specific URL here (when clicking this link you should launch into the
- web client)
-
- Write 'None' if you don't want to list a publicly accessible webclient.
-
- Current value:
-{webclient_default}
- """
-
- wizard.display(text)
- wizard.game_index_listing["web_client_url"]=wizard.ask_input(default=webclient_default)
-
- ifnot(
- wizard.game_index_listing.get("web_client_url")
- or(wizard.game_index_listing.get("telnet_host"))
- ):
- wizard.display(
- "\nNote: You have not specified any connection options. This means "
- "your game \nwill be marked as being in 'closed development' in "
- "the index."
- )
-
- wizard.display("\nDon't forget to inspect and save your changes.")
-
- node_start(wizard)
-
-
-# MSSP
-
-
-
[docs]defnode_mssp_start(wizard):
-
- mssp_module=mod_import(settings.MSSP_META_MODULEor"server.conf.mssp")
- try:
- filename=mssp_module.__file__
- exceptAttributeError:
- filename="server/conf/mssp.py"
-
- text=f"""
- MSSP (Mud Server Status Protocol) has a vast amount of options so it must
- be modified outside this wizard by directly editing its config file here:
-
- '{filename}'
-
- MSSP allows traditional online MUD-listing sites/crawlers to continuously
- monitor your game and list information about it. Some of this, like active
- player-count, Evennia will automatically add for you, whereas most fields
- you need to set manually.
-
- To use MSSP you should generally have a publicly open game that external
- players can connect to. You also need to register at a MUD listing site to
- tell them to crawl your game.
- """
-
- wizard.display(text)
- wizard.ask_continue()
- node_start(wizard)
-
-
-# Admin
-
-
-def_save_changes(wizard):
- """
- Perform the save
- """
-
- # add import statement to settings file
- import_stanza="from .connection_settings import *"
- setting_module=mod_import("server.conf.settings")
- withopen(setting_module.__file__,"r+")asf:
- txt=f.read()# moves pointer to end of file
- ifimport_stanzanotintxt:
- # add to the end of the file
- f.write(
- "\n\n"
- "try:\n"
- " # Created by the `evennia connections` wizard\n"
- f" {import_stanza}\n"
- "except ImportError:\n"
- " pass"
- )
-
- connect_settings_file=path.join(settings.GAME_DIR,"server","conf","connection_settings.py")
- withopen(connect_settings_file,"w")asf:
- f.write(
- "# This file is auto-generated by the `evennia connections` wizard.\n"
- "# Don't edit manually, your changes will be overwritten.\n\n"
- )
-
- f.write(wizard.save_output)
- wizard.display(f"saving to {connect_settings_file} ...")
-
-
-
[docs]defnode_view_and_apply_settings(wizard):
- """
- Inspect and save the data gathered in the other nodes
-
- """
- pp=pprint.PrettyPrinter(indent=4)
- saves=False
-
- # game index
- game_index_save_text=""
- game_index_listing=(
- wizard.game_index_listingifhasattr(wizard,"game_index_listing")elseNone
- )
- ifnotgame_index_listingandsettings.GAME_INDEX_ENABLED:
- game_index_listing=settings.GAME_INDEX_LISTING
- ifgame_index_listing:
- game_index_save_text=(
- "GAME_INDEX_ENABLED = True\n"
- "GAME_INDEX_LISTING = \\\n"+pp.pformat(game_index_listing)
- )
- saves=True
- else:
- game_index_save_text="# No Game Index settings found."
-
- # potentially add other wizards in the future
- text=game_index_save_text
-
- wizard.display(f"Settings to save:\n\n{text}")
-
- ifsaves:
- ifwizard.ask_yesno("\nDo you want to save these settings?")=="yes":
- wizard.save_output=text
- _save_changes(wizard)
- wizard.display("... saved!\nThe changes will apply after you reload your server.")
- else:
- wizard.display("... cancelled.")
- wizard.ask_continue()
- node_start(wizard)
-"""
-This module contains historical deprecations that the Evennia launcher
-checks for.
-
-These all print to the terminal.
-"""
-importos
-
-
[docs]defcheck_errors(settings):
- """
- Check for deprecations that are critical errors and should stop
- the launcher.
-
- Args:
- settings (Settings): The Django settings file
-
- Raises:
- DeprecationWarning if a critical deprecation is found.
-
- """
- deprstring=(
- "settings.%s should be renamed to %s. If defaults are used, "
- "their path/classname must be updated "
- "(see evennia/settings_default.py)."
- )
- ifhasattr(settings,"CMDSET_DEFAULT"):
- raiseDeprecationWarning(deprstring%("CMDSET_DEFAULT","CMDSET_CHARACTER"))
- ifhasattr(settings,"CMDSET_OOC"):
- raiseDeprecationWarning(deprstring%("CMDSET_OOC","CMDSET_ACCOUNT"))
- ifsettings.WEBSERVER_ENABLEDandnotisinstance(settings.WEBSERVER_PORTS[0],tuple):
- raiseDeprecationWarning(
- "settings.WEBSERVER_PORTS must be on the form ""[(proxyport, serverport), ...]"
- )
- ifhasattr(settings,"BASE_COMM_TYPECLASS"):
- raiseDeprecationWarning(deprstring%("BASE_COMM_TYPECLASS","BASE_CHANNEL_TYPECLASS"))
- ifhasattr(settings,"COMM_TYPECLASS_PATHS"):
- raiseDeprecationWarning(deprstring%("COMM_TYPECLASS_PATHS","CHANNEL_TYPECLASS_PATHS"))
- ifhasattr(settings,"CHARACTER_DEFAULT_HOME"):
- raiseDeprecationWarning(
- "settings.CHARACTER_DEFAULT_HOME should be renamed to "
- "DEFAULT_HOME. See also settings.START_LOCATION "
- "(see evennia/settings_default.py)."
- )
- deprstring=(
- "settings.%s is now merged into settings.TYPECLASS_PATHS. ""Update your settings file."
- )
- ifhasattr(settings,"OBJECT_TYPECLASS_PATHS"):
- raiseDeprecationWarning(deprstring%"OBJECT_TYPECLASS_PATHS")
- ifhasattr(settings,"SCRIPT_TYPECLASS_PATHS"):
- raiseDeprecationWarning(deprstring%"SCRIPT_TYPECLASS_PATHS")
- ifhasattr(settings,"ACCOUNT_TYPECLASS_PATHS"):
- raiseDeprecationWarning(deprstring%"ACCOUNT_TYPECLASS_PATHS")
- ifhasattr(settings,"CHANNEL_TYPECLASS_PATHS"):
- raiseDeprecationWarning(deprstring%"CHANNEL_TYPECLASS_PATHS")
- ifhasattr(settings,"SEARCH_MULTIMATCH_SEPARATOR"):
- raiseDeprecationWarning(
- "settings.SEARCH_MULTIMATCH_SEPARATOR was replaced by "
- "SEARCH_MULTIMATCH_REGEX and SEARCH_MULTIMATCH_TEMPLATE. "
- "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,"
- "TIME_HOUR_PER_DAY, TIME_DAY_PER_WEEK, \n"
- "TIME_WEEK_PER_MONTH and TIME_MONTH_PER_YEAR "
- "are no longer supported. Remove them from your "
- "settings file to continue.\nIf you want to use "
- "and manipulate these time units, the tools from utils.gametime "
- "are now found in contrib/convert_gametime.py instead."
- )
- ifany(
- hasattr(settings,value)
- forvaluein(
- "TIME_SEC_PER_MIN",
- "TIME_MIN_PER_HOUR",
- "TIME_HOUR_PER_DAY",
- "TIME_DAY_PER_WEEK",
- "TIME_WEEK_PER_MONTH",
- "TIME_MONTH_PER_YEAR",
- )
- ):
- raiseDeprecationWarning(gametime_deprecation)
-
- game_directory_deprecation=(
- "The setting GAME_DIRECTORY_LISTING was removed. It must be "
- "renamed to GAME_INDEX_LISTING instead."
- )
- ifhasattr(settings,"GAME_DIRECTORY_LISTING"):
- raiseDeprecationWarning(game_directory_deprecation)
-
- chan_connectinfo=settings.CHANNEL_CONNECTINFO
- ifchan_connectinfoisnotNoneandnotisinstance(chan_connectinfo,dict):
- raiseDeprecationWarning(
- "settings.CHANNEL_CONNECTINFO has changed. It "
- "must now be either None or a dict "
- "specifying the properties of the channel to create."
- )
- ifhasattr(settings,"CYCLE_LOGFILES"):
- raiseDeprecationWarning(
- "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."
- )
-
-
-
[docs]defcheck_warnings(settings):
- """
- Check conditions and deprecations that should produce warnings but which
- does not stop launch.
- """
- ifsettings.DEBUG:
- print(" [Devel: settings.DEBUG is True. Important to turn off in production.]")
- ifsettings.IN_GAME_ERRORS:
- print(" [Devel: settings.IN_GAME_ERRORS is True. Turn off in production.]")
- ifsettings.ALLOWED_HOSTS==["*"]:
- print(" [Devel: settings.ALLOWED_HOSTS set to '*' (all). Limit in production.]")
- ifsettings.SERVER_HOSTNAME=="localhost":
- print(" [Devel: settings.SERVER_HOSTNAME is set to 'localhost'. "
- "Update to the actual hostname in production.]")
-
- fordbentryinsettings.DATABASES.values():
- if"psycopg"indbentry.get("ENGINE",""):
- print(
- 'Deprecation: postgresql_psycopg2 backend is deprecated". '
- "Switch settings.DATABASES to use "
- '"ENGINE": "django.db.backends.postgresql instead"'
- )
-#!/usr/bin/python
-"""
-Evennia launcher program
-
-This is the start point for running Evennia.
-
-Sets the appropriate environmental variables for managing an Evennia game. It will start and connect
-to the Portal, through which the Server is also controlled. This pprogram
-
-Run the script with the -h flag to see usage information.
-
-"""
-
-importos
-importsys
-importre
-importsignal
-importshutil
-importimportlib
-importpickle
-fromdistutils.versionimportLooseVersion
-fromargparseimportArgumentParser
-importargparse
-fromsubprocessimportPopen,check_output,call,CalledProcessError,STDOUT
-
-fromtwisted.protocolsimportamp
-fromtwisted.internetimportreactor,endpoints
-importdjango
-fromdjango.core.managementimportexecute_from_command_line
-fromdjango.db.utilsimportProgrammingError
-
-# Signal processing
-SIG=signal.SIGINT
-CTRL_C_EVENT=0# Windows SIGINT-like signal
-
-# Set up the main python paths to Evennia
-EVENNIA_ROOT=os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-
-importevennia# noqa
-
-EVENNIA_LIB=os.path.join(os.path.dirname(os.path.abspath(evennia.__file__)))
-EVENNIA_SERVER=os.path.join(EVENNIA_LIB,"server")
-EVENNIA_TEMPLATE=os.path.join(EVENNIA_LIB,"game_template")
-EVENNIA_PROFILING=os.path.join(EVENNIA_SERVER,"profiling")
-EVENNIA_DUMMYRUNNER=os.path.join(EVENNIA_PROFILING,"dummyrunner.py")
-
-TWISTED_BINARY="twistd"
-
-# Game directory structure
-SETTINGFILE="settings.py"
-SERVERDIR="server"
-CONFDIR=os.path.join(SERVERDIR,"conf")
-SETTINGS_PATH=os.path.join(CONFDIR,SETTINGFILE)
-SETTINGS_DOTPATH="server.conf.settings"
-CURRENT_DIR=os.getcwd()
-GAMEDIR=CURRENT_DIR
-
-# Operational setup
-
-SERVER_LOGFILE=None
-PORTAL_LOGFILE=None
-HTTP_LOGFILE=None
-
-SERVER_PIDFILE=None
-PORTAL_PIDFILE=None
-
-SERVER_PY_FILE=None
-PORTAL_PY_FILE=None
-
-SPROFILER_LOGFILE=None
-PPROFILER_LOGFILE=None
-
-TEST_MODE=False
-ENFORCED_SETTING=False
-
-REACTOR_RUN=False
-NO_REACTOR_STOP=False
-
-# communication constants
-
-AMP_PORT=None
-AMP_HOST=None
-AMP_INTERFACE=None
-AMP_CONNECTION=None
-
-SRELOAD=chr(14)# server reloading (have portal start a new server)
-SSTART=chr(15)# server start
-PSHUTD=chr(16)# portal (+server) shutdown
-SSHUTD=chr(17)# server-only shutdown
-PSTATUS=chr(18)# ping server or portal status
-SRESET=chr(19)# shutdown server in reset mode
-
-# requirements
-PYTHON_MIN="3.7"
-TWISTED_MIN="20.3.0"
-DJANGO_MIN="3.2.0"
-DJANGO_LT="4.0"
-
-try:
- sys.path[1]=EVENNIA_ROOT
-exceptIndexError:
- sys.path.append(EVENNIA_ROOT)
-
-# ------------------------------------------------------------
-#
-# Messages
-#
-# ------------------------------------------------------------
-
-CREATED_NEW_GAMEDIR="""
- Welcome to Evennia!
- Created a new Evennia game directory '{gamedir}'.
-
- You can now optionally edit your new settings file
- at {settings_path}. If you don't, the defaults
- will work out of the box. When ready to continue, 'cd' to your
- game directory and run:
-
- evennia migrate
-
- This initializes the database. To start the server for the first
- time, run:
-
- evennia start
-
- Make sure to create a superuser when asked for it (the email is optional)
- You should now be able to connect to your server on 'localhost', port 4000
- using a telnet/mud client or http://localhost:4001 using your web browser.
- If things don't work, check the log with `evennia --log`. Also make sure
- ports are open.
-
- (Finally, why not run `evennia connections` and make the world aware of
- your new Evennia project!)
- """
-
-ERROR_INPUT="""
- Command
-{args}{kwargs}
- raised an error: '{traceback}'.
-"""
-
-ERROR_NO_ALT_GAMEDIR="""
- The path '{gamedir}' could not be found.
-"""
-
-ERROR_NO_GAMEDIR="""
- ERROR: No Evennia settings file was found. Evennia looks for the
- file in your game directory as ./server/conf/settings.py.
-
- You must run this command from somewhere inside a valid game
- directory first created with
-
- evennia --init mygamename
-
- If you are in a game directory but is missing a settings.py file,
- it may be because you have git-cloned an existing game directory.
- The settings.py file is not cloned by git (it's in .gitignore)
- since it can contain sensitive and/or server-specific information.
- You can create a new, empty settings file with
-
- evennia --initsettings
-
- If cloning the settings file is not a problem you could manually
- copy over the old settings file or remove its entry in .gitignore
-
- """
-
-WARNING_MOVING_SUPERUSER="""
- WARNING: Evennia expects an Account superuser with id=1. No such
- Account was found. However, another superuser ('{other_key}',
- id={other_id}) was found in the database. If you just created this
- superuser and still see this text it is probably due to the
- database being flushed recently - in this case the database's
- internal auto-counter might just start from some value higher than
- one.
-
- We will fix this by assigning the id 1 to Account '{other_key}'.
- Please confirm this is acceptable before continuing.
- """
-
-WARNING_RUNSERVER="""
- WARNING: There is no need to run the Django development
- webserver to test out Evennia web features (the web client
- will in fact not work since the Django test server knows
- nothing about MUDs). Instead, just start Evennia with the
- webserver component active (this is the default).
- """
-
-ERROR_SETTINGS="""
- ERROR: There was an error importing Evennia's config file
-{settingspath}.
- There is usually one of three reasons for this:
- 1) You are not running this command from your game directory.
- Change directory to your game directory and try again (or
- create a new game directory using evennia --init <dirname>)
- 2) The settings file contains a syntax error. If you see a
- traceback above, review it, resolve the problem and try again.
- 3) Django is not correctly installed. This usually shows as
- errors mentioning 'DJANGO_SETTINGS_MODULE'. If you run a
- virtual machine, it might be worth to restart it to see if
- this resolves the issue.
- """.format(
- settingspath=SETTINGS_PATH
-)
-
-ERROR_INITSETTINGS="""
- ERROR: 'evennia --initsettings' must be called from the root of
- your game directory, since it tries to (re)create the new
- settings.py file in a subfolder server/conf/.
- """
-
-RECREATED_SETTINGS="""
- (Re)created an empty settings file in server/conf/settings.py.
-
- Note that if you were using an existing database, the password
- salt of this new settings file will be different from the old one.
- This means that any existing accounts may not be able to log in to
- their accounts with their old passwords.
- """
-
-ERROR_INITMISSING="""
- ERROR: 'evennia --initmissing' must be called from the root of
- your game directory, since it tries to create any missing files
- in the server/ subfolder.
- """
-
-RECREATED_MISSING="""
- (Re)created any missing directories or files. Evennia should
- be ready to run now!
- """
-
-ERROR_DATABASE="""
- ERROR: Your database does not exist or is not set up correctly.
- (error was '{traceback}')
-
- If you think your database should work, make sure you are running your
- commands from inside your game directory. If this error persists, run
-
- evennia migrate
-
- to initialize/update the database according to your settings.
- """
-
-ERROR_WINDOWS_WIN32API="""
- ERROR: Unable to import win32api, which Twisted requires to run.
- You may download it from:
-
- http://sourceforge.net/projects/pywin32/files/pywin32/
-
- If you are running in a virtual environment, browse to the
- location of the latest win32api exe file for your computer and
- Python version and copy the url to it; then paste it into a call
- to easy_install:
-
- easy_install http://<url to win32api exe>
- """
-
-INFO_WINDOWS_BATFILE="""
- INFO: Since you are running Windows, a file 'twistd.bat' was
- created for you. This is a simple batch file that tries to call
- the twisted executable. Evennia determined this to be:
-
-{twistd_path}
-
- If you run into errors at startup you might need to edit
- twistd.bat to point to the actual location of the Twisted
- executable (usually called twistd.py) on your machine.
-
- This procedure is only done once. Run `evennia` again when you
- are ready to start the server.
- """
-
-CMDLINE_HELP="""Starts, initializes, manages and operates the Evennia MU* server.
-Most standard django management commands are also accepted."""
-
-
-VERSION_INFO="""
- Evennia {version}
- OS: {os}
- Python: {python}
- Twisted: {twisted}
- Django: {django}{about}
- """
-
-ABOUT_INFO="""
- Evennia MUD/MUX/MU* development system
-
- Licence: BSD 3-Clause Licence
- Web: http://www.evennia.com
- Chat: https://discord.gg/AJJpcRUhtF
- Forum: http://www.evennia.com/discussions
- Maintainer (2006-10): Greg Taylor
- Maintainer (2010-): Griatch (griatch AT gmail DOT com)
-
- Use -h for command line options.
- """
-
-HELP_ENTRY="""
- Evennia has two processes, the 'Server' and the 'Portal'.
- External users connect to the Portal while the Server runs the
- game/database. Restarting the Server will refresh code but not
- disconnect users.
-
- To start a new game, use 'evennia --init mygame'.
- For more ways to operate and manage Evennia, see 'evennia -h'.
-
- If you want to add unit tests to your game, see
- https://github.com/evennia/evennia/wiki/Unit-Testing
-
- Evennia's manual is found here:
- https://github.com/evennia/evennia/wiki
- """
-
-MENU="""
- +----Evennia Launcher-------------------------------------------+
-{gameinfo}
- +--- Common operations -----------------------------------------+
- | 1) Start (also restart stopped Server) |
- | 2) Reload (stop/start Server in 'reload' mode) |
- | 3) Stop (shutdown Portal and Server) |
- | 4) Reboot (shutdown then restart) |
- +--- Other operations ------------------------------------------+
- | 5) Reset (stop/start Server in 'shutdown' mode) |
- | 6) Stop Server only |
- | 7) Kill Server only (send kill signal to process) |
- | 8) Kill Portal + Server |
- +--- Information -----------------------------------------------+
- | 9) Tail log files (quickly see errors - Ctrl-C to exit) |
- | 10) Status |
- | 11) Port info |
- +--- Testing ---------------------------------------------------+
- | 12) Test gamedir (run gamedir test suite, if any) |
- | 13) Test Evennia (run Evennia test suite) |
- +---------------------------------------------------------------+
- | h) Help i) About info q) Abort |
- +---------------------------------------------------------------+"""
-
-ERROR_AMP_UNCONFIGURED="""
- Can't find server info for connecting. Either run this command from
- the game dir (it will then use the game's settings file) or specify
- the path to your game's settings file manually with the --settings
- option.
- """
-
-ERROR_LOGDIR_MISSING="""
- ERROR: One or more log-file directory locations could not be
- found:
-
-{logfiles}
-
- This is simple to fix: Just manually create the missing log
- directory (or directories) and re-launch the server (the log files
- will be created automatically).
-
- (Explanation: Evennia creates the log directory automatically when
- initializing a new game directory. This error usually happens if
- you used git to clone a pre-created game directory - since log
- files are in .gitignore they will not be cloned, which leads to
- the log directory also not being created.)
- """
-
-ERROR_PYTHON_VERSION="""
- ERROR: Python {pversion} used. Evennia requires version
-{python_min} or higher.
- """
-
-ERROR_TWISTED_VERSION="""
- ERROR: Twisted {tversion} found. Evennia requires
- version {twisted_min} or higher.
- """
-
-ERROR_NOTWISTED="""
- ERROR: Twisted does not seem to be installed.
- """
-
-ERROR_DJANGO_MIN="""
- ERROR: Django {dversion} found. Evennia requires at least version {django_min} (but
- no higher than {django_lt}).
-
- If you are using a virtualenv, use the command `pip install --upgrade -e evennia` where
- `evennia` is the folder to where you cloned the Evennia library. If not
- in a virtualenv you can install django with for example `pip install --upgrade django`
- or with `pip install django=={django_min}` to get a specific version.
-
- It's also a good idea to run `evennia migrate` after this upgrade. Ignore
- any warnings and don't run `makemigrate` even if told to.
- """
-
-NOTE_DJANGO_NEW="""
- NOTE: Django {dversion} found. This is newer than Evennia's
- recommended version ({django_rec}). It might work, but is new
- enough to not be fully tested yet. Report any issues.
- """
-
-ERROR_NODJANGO="""
- ERROR: Django does not seem to be installed.
- """
-
-NOTE_KEYBOARDINTERRUPT="""
- STOP: Caught keyboard interrupt while in interactive mode.
- """
-
-NOTE_TEST_DEFAULT="""
- TESTING: Using Evennia's default settings file (evennia.settings_default).
- (use 'evennia test --settings settings.py .' to run only your custom game tests)
- """
-
-NOTE_TEST_CUSTOM="""
- TESTING: Using specified settings file '{settings_dotpath}'.
-
- OBS: Evennia's full test suite may not pass if the settings are very
- different from the default (use 'evennia test evennia' to run core tests)
- """
-
-PROCESS_ERROR="""
-{component} process error: {traceback}.
- """
-
-PORTAL_INFO="""{servername} Portal {version}
- external ports:
-{telnet}
-{telnet_ssl}
-{ssh}
-{webserver_proxy}
-{webclient}
- internal_ports (to Server):
-{webserver_internal}
-{amp}
-"""
-
-
-SERVER_INFO="""{servername} Server {version}
- internal ports (to Portal):
-{webserver}
-{amp}
-{irc_rss}
-{info}
-{errors}"""
-
-
-ARG_OPTIONS="""Actions on installed server. One of:
- start - launch server+portal if not running
- reload - restart server in 'reload' mode
- stop - shutdown server+portal
- reboot - shutdown server+portal, then start again
- reset - restart server in 'shutdown' mode
- istart - start server in foreground (until reload)
- ipstart - start portal in foreground
- sstop - stop only server
- kill - send kill signal to portal+server (force)
- skill - send kill signal only to server
- status - show server and portal run state
- info - show server and portal port info
- menu - show a menu of options
- connections - show connection wizard
-Others, like migrate, test and shell is passed on to Django."""
-
-# ------------------------------------------------------------
-#
-# Private helper functions
-#
-# ------------------------------------------------------------
-
-
-def_is_windows():
- returnos.name=="nt"
-
-
-def_file_names_compact(filepath1,filepath2):
- "Compact the output of filenames with same base dir"
- dirname1=os.path.dirname(filepath1)
- dirname2=os.path.dirname(filepath2)
- ifdirname1==dirname2:
- name2=os.path.basename(filepath2)
- return"{} and {}".format(filepath1,name2)
- else:
- return"{} and {}".format(filepath1,filepath2)
-
-
-def_print_info(portal_info_dict,server_info_dict):
- """
- Format info dicts from the Portal/Server for display
-
- """
- ind=" "*8
-
- def_prepare_dict(dct):
- out={}
- forkey,valueindct.items():
- ifisinstance(value,list):
- value="\n{}".format(ind).join(str(val)forvalinvalue)
- out[key]=value
- returnout
-
- def_strip_empty_lines(string):
- return"\n".join(lineforlineinstring.split("\n")ifline.strip())
-
- pstr,sstr="",""
- ifportal_info_dict:
- pdict=_prepare_dict(portal_info_dict)
- pstr=_strip_empty_lines(PORTAL_INFO.format(**pdict))
-
- ifserver_info_dict:
- sdict=_prepare_dict(server_info_dict)
- sstr=_strip_empty_lines(SERVER_INFO.format(**sdict))
-
- info=pstr+("\n\n"+sstrifsstrelse"")
- maxwidth=max(len(line)forlineininfo.split("\n"))
- top_border="-"*(maxwidth-11)+" Evennia "+"---"
- border="-"*(maxwidth+1)
- print(top_border+"\n"+info+"\n"+border)
-
-
-def_parse_status(response):
- "Unpack the status information"
- returnpickle.loads(response["status"])
-
-
-def_get_twistd_cmdline(pprofiler,sprofiler):
- """
- Compile the command line for starting a Twisted application using the 'twistd' executable.
-
- """
- portal_cmd=[TWISTED_BINARY,"--python={}".format(PORTAL_PY_FILE)]
- server_cmd=[TWISTED_BINARY,"--python={}".format(SERVER_PY_FILE)]
-
- ifos.name!="nt":
- # PID files only for UNIX
- portal_cmd.append("--pidfile={}".format(PORTAL_PIDFILE))
- server_cmd.append("--pidfile={}".format(SERVER_PIDFILE))
-
- ifpprofiler:
- portal_cmd.extend(
- ["--savestats","--profiler=cprofile","--profile={}".format(PPROFILER_LOGFILE)]
- )
- ifsprofiler:
- server_cmd.extend(
- ["--savestats","--profiler=cprofile","--profile={}".format(SPROFILER_LOGFILE)]
- )
-
- returnportal_cmd,server_cmd
-
-
-def_reactor_stop():
- ifnotNO_REACTOR_STOP:
- reactor.stop()
-
-
-# ------------------------------------------------------------
-#
-# Protocol Evennia launcher - Portal/Server communication
-#
-# ------------------------------------------------------------
-
-
-
[docs]defwait_for_status_reply(callback):
- """
- Wait for an explicit STATUS signal to be sent back from Evennia.
- """
- ifAMP_CONNECTION:
- AMP_CONNECTION.wait_for_status(callback)
- else:
- print("No Evennia connection established.")
-
-
-
[docs]defwait_for_status(
- portal_running=True,server_running=True,callback=None,errback=None,rate=0.5,retries=20
-):
- """
- Repeat the status ping until the desired state combination is achieved.
-
- Args:
- portal_running (bool or None): Desired portal run-state. If None, any state
- is accepted.
- server_running (bool or None): Desired server run-state. If None, any state
- is accepted. The portal must be running.
- callback (callable): Will be called with portal_state, server_state when
- condition is fulfilled.
- errback (callable): Will be called with portal_state, server_state if the
- request is timed out.
- rate (float): How often to retry.
- retries (int): How many times to retry before timing out and calling `errback`.
- """
-
- def_callback(response):
- prun,srun,_,_,_,_=_parse_status(response)
- if(portal_runningisNoneorprun==portal_running)and(
- server_runningisNoneorsrun==server_running
- ):
- # the correct state was achieved
- ifcallback:
- callback(prun,srun)
- else:
- _reactor_stop()
- else:
- ifretries<=0:
- iferrback:
- errback(prun,srun)
- else:
- print("Connection to Evennia timed out. Try again.")
- _reactor_stop()
- else:
- reactor.callLater(
- rate,
- wait_for_status,
- portal_running,
- server_running,
- callback,
- errback,
- rate,
- retries-1,
- )
-
- def_errback(fail):
- """
- Portal not running
- """
- ifnotportal_running:
- # this is what we want
- ifcallback:
- callback(portal_running,server_running)
- else:
- _reactor_stop()
- else:
- ifretries<=0:
- iferrback:
- errback(portal_running,server_running)
- else:
- print("Connection to Evennia timed out. Try again.")
- _reactor_stop()
- else:
- reactor.callLater(
- rate,
- wait_for_status,
- portal_running,
- server_running,
- callback,
- errback,
- rate,
- retries-1,
- )
-
- returnsend_instruction(PSTATUS,None,_callback,_errback)
[docs]defcollectstatic():
- "Run the collectstatic django command"
- django.core.management.call_command("collectstatic",interactive=False,verbosity=0)
-
-
-
[docs]defstart_evennia(pprofiler=False,sprofiler=False):
- """
- This will start Evennia anew by launching the Evennia Portal (which in turn
- will start the Server)
-
- """
- portal_cmd,server_cmd=_get_twistd_cmdline(pprofiler,sprofiler)
-
- def_fail(fail):
- print(fail)
- _reactor_stop()
-
- def_server_started(response):
- print("... Server started.\nEvennia running.")
- ifresponse:
- _,_,_,_,pinfo,sinfo=response
- _print_info(pinfo,sinfo)
- _reactor_stop()
-
- def_portal_started(*args):
- print(
- "... Portal started.\nServer starting {} ...".format(
- "(under cProfile)"ifsprofilerelse""
- )
- )
- wait_for_status_reply(_server_started)
- send_instruction(SSTART,server_cmd)
-
- def_portal_running(response):
- prun,srun,ppid,spid,_,_=_parse_status(response)
- print("Portal is already running as process {pid}. Not restarted.".format(pid=ppid))
- ifsrun:
- print("Server is already running as process {pid}. Not restarted.".format(pid=spid))
- _reactor_stop()
- else:
- print("Server starting {}...".format("(under cProfile)"ifsprofilerelse""))
- send_instruction(SSTART,server_cmd,_server_started,_fail)
-
- def_portal_not_running(fail):
- print("Portal starting {}...".format("(under cProfile)"ifpprofilerelse""))
- try:
- if_is_windows():
- # Windows requires special care
- create_no_window=0x08000000
- Popen(portal_cmd,env=getenv(),bufsize=-1,creationflags=create_no_window)
- else:
- Popen(portal_cmd,env=getenv(),bufsize=-1)
- exceptExceptionase:
- print(PROCESS_ERROR.format(component="Portal",traceback=e))
- _reactor_stop()
- wait_for_status(True,None,_portal_started)
-
- collectstatic()
- send_instruction(PSTATUS,None,_portal_running,_portal_not_running)
-
-
-
[docs]defreload_evennia(sprofiler=False,reset=False):
- """
- This will instruct the Portal to reboot the Server component. We
- do this manually by telling the server to shutdown (in reload mode)
- and wait for the portal to report back, at which point we start the
- server again. This way we control the process exactly.
-
- """
- _,server_cmd=_get_twistd_cmdline(False,sprofiler)
-
- def_server_restarted(*args):
- print("... Server re-started.")
- _reactor_stop()
-
- def_server_reloaded(status):
- print("... Server {}.".format("reset"ifresetelse"reloaded"))
- _reactor_stop()
-
- def_server_stopped(status):
- wait_for_status_reply(_server_reloaded)
- send_instruction(SSTART,server_cmd)
-
- def_portal_running(response):
- _,srun,_,_,_,_=_parse_status(response)
- ifsrun:
- print("Server {}...".format("resetting"ifresetelse"reloading"))
- wait_for_status_reply(_server_stopped)
- send_instruction(SRESETifresetelseSRELOAD,{})
- else:
- print("Server down. Re-starting ...")
- wait_for_status_reply(_server_restarted)
- send_instruction(SSTART,server_cmd)
-
- def_portal_not_running(fail):
- print("Evennia not running. Starting up ...")
- start_evennia()
-
- collectstatic()
- send_instruction(PSTATUS,None,_portal_running,_portal_not_running)
-
-
-
[docs]defstop_evennia():
- """
- This instructs the Portal to stop the Server and then itself.
-
- """
-
- def_portal_stopped(*args):
- print("... Portal stopped.\nEvennia shut down.")
- _reactor_stop()
-
- def_server_stopped(*args):
- print("... Server stopped.\nStopping Portal ...")
- send_instruction(PSHUTD,{})
- wait_for_status(False,None,_portal_stopped)
-
- def_portal_running(response):
- prun,srun,ppid,spid,_,_=_parse_status(response)
- ifsrun:
- print("Server stopping ...")
- send_instruction(SSHUTD,{})
- wait_for_status_reply(_server_stopped)
- else:
- print("Server already stopped.\nStopping Portal ...")
- send_instruction(PSHUTD,{})
- wait_for_status(False,None,_portal_stopped)
-
- def_portal_not_running(fail):
- print("Evennia not running.")
- _reactor_stop()
-
- send_instruction(PSTATUS,None,_portal_running,_portal_not_running)
-
-
-
[docs]defreboot_evennia(pprofiler=False,sprofiler=False):
- """
- This is essentially an evennia stop && evennia start except we make sure
- the system has successfully shut down before starting it again.
-
- If evennia was not running, start it.
-
- """
- globalAMP_CONNECTION
-
- def_portal_stopped(*args):
- print("... Portal stopped. Evennia shut down. Rebooting ...")
- globalAMP_CONNECTION
- AMP_CONNECTION=None
- start_evennia(pprofiler,sprofiler)
-
- def_server_stopped(*args):
- print("... Server stopped.\nStopping Portal ...")
- send_instruction(PSHUTD,{})
- wait_for_status(False,None,_portal_stopped)
-
- def_portal_running(response):
- prun,srun,ppid,spid,_,_=_parse_status(response)
- ifsrun:
- print("Server stopping ...")
- send_instruction(SSHUTD,{})
- wait_for_status_reply(_server_stopped)
- else:
- print("Server already stopped.\nStopping Portal ...")
- send_instruction(PSHUTD,{})
- wait_for_status(False,None,_portal_stopped)
-
- def_portal_not_running(fail):
- print("Evennia not running. Starting up ...")
- start_evennia()
-
- collectstatic()
- send_instruction(PSTATUS,None,_portal_running,_portal_not_running)
-
-
-
[docs]defstart_only_server():
- """
- Tell portal to start server (debug)
- """
- portal_cmd,server_cmd=_get_twistd_cmdline(False,False)
- print("launcher: Sending to portal: SSTART + {}".format(server_cmd))
- collectstatic()
- send_instruction(SSTART,server_cmd)
-
-
-
[docs]defstart_server_interactive():
- """
- Start the Server under control of the launcher process (foreground)
-
- """
-
- def_iserver():
- _,server_twistd_cmd=_get_twistd_cmdline(False,False)
- server_twistd_cmd.append("--nodaemon")
- print("Starting Server in interactive mode (stop with Ctrl-C)...")
- try:
- Popen(server_twistd_cmd,env=getenv(),stderr=STDOUT).wait()
- exceptKeyboardInterrupt:
- print("... Stopped Server with Ctrl-C.")
- else:
- print("... Server stopped (leaving interactive mode).")
-
- collectstatic()
- stop_server_only(when_stopped=_iserver,interactive=True)
-
-
-
[docs]defstart_portal_interactive():
- """
- Start the Portal under control of the launcher process (foreground)
-
- Notes:
- In a normal start, the launcher waits for the Portal to start, then
- tells it to start the Server. Since we can't do this here, we instead
- start the Server first and then starts the Portal - the Server will
- auto-reconnect to the Portal. To allow the Server to be reloaded, this
- relies on a fixed server server-cmdline stored as a fallback on the
- portal application in evennia/server/portal/portal.py.
-
- """
-
- def_iportal(fail):
- portal_twistd_cmd,server_twistd_cmd=_get_twistd_cmdline(False,False)
- portal_twistd_cmd.append("--nodaemon")
-
- # starting Server first - it will auto-connect once Portal comes up
- if_is_windows():
- # Windows requires special care
- create_no_window=0x08000000
- Popen(server_twistd_cmd,env=getenv(),bufsize=-1,creationflags=create_no_window)
- else:
- Popen(server_twistd_cmd,env=getenv(),bufsize=-1)
-
- print("Starting Portal in interactive mode (stop with Ctrl-C)...")
- try:
- Popen(portal_twistd_cmd,env=getenv(),stderr=STDOUT).wait()
- exceptKeyboardInterrupt:
- print("... Stopped Portal with Ctrl-C.")
- else:
- print("... Portal stopped (leaving interactive mode).")
-
- def_portal_running(response):
- print("Evennia must be shut down completely before running Portal in interactive mode.")
- _reactor_stop()
-
- send_instruction(PSTATUS,None,_portal_running,_iportal)
-
-
-
[docs]defstop_server_only(when_stopped=None,interactive=False):
- """
- Only stop the Server-component of Evennia (this is not useful except for debug)
-
- Args:
- when_stopped (callable): This will be called with no arguments when Server has stopped (or
- if it had already stopped when this is called).
- interactive (bool, optional): Set if this is called as part of the interactive reload
- mechanism.
-
- """
-
- def_server_stopped(*args):
- ifwhen_stopped:
- when_stopped()
- else:
- print("... Server stopped.")
- _reactor_stop()
-
- def_portal_running(response):
- _,srun,_,_,_,_=_parse_status(response)
- ifsrun:
- print("Server stopping ...")
- wait_for_status_reply(_server_stopped)
- ifinteractive:
- send_instruction(SRELOAD,{})
- else:
- send_instruction(SSHUTD,{})
- else:
- ifwhen_stopped:
- when_stopped()
- else:
- print("Server is not running.")
- _reactor_stop()
-
- def_portal_not_running(fail):
- print("Evennia is not running.")
- ifinteractive:
- print("Start Evennia normally first, then use `istart` to switch to interactive mode.")
- _reactor_stop()
-
- send_instruction(PSTATUS,None,_portal_running,_portal_not_running)
-
-
-
[docs]defquery_info():
- """
- Display the info strings from the running Evennia
-
- """
-
- def_got_status(status):
- _,_,_,_,pinfo,sinfo=_parse_status(status)
- _print_info(pinfo,sinfo)
- _reactor_stop()
-
- def_portal_running(response):
- query_status(_got_status)
-
- def_portal_not_running(fail):
- print("Evennia is not running.")
-
- send_instruction(PSTATUS,None,_portal_running,_portal_not_running)
-
-
-
[docs]deftail_log_files(filename1,filename2,start_lines1=20,start_lines2=20,rate=1):
- """
- Tail two logfiles interactively, combining their output to stdout
-
- When first starting, this will display the tail of the log files. After
- that it will poll the log files repeatedly and display changes.
-
- Args:
- filename1 (str): Path to first log file.
- filename2 (str): Path to second log file.
- start_lines1 (int): How many lines to show from existing first log.
- start_lines2 (int): How many lines to show from existing second log.
- rate (int, optional): How often to poll the log file.
-
- """
- globalREACTOR_RUN
-
- def_file_changed(filename,prev_size):
- "Get size of file in bytes, get diff compared with previous size"
- try:
- new_size=os.path.getsize(filename)
- exceptFileNotFoundError:
- returnFalse,0
- returnnew_size!=prev_size,new_size
-
- def_get_new_lines(filehandle,old_linecount):
- "count lines, get the ones not counted before"
-
- def_block(filehandle,size=65536):
- "File block generator for quick traversal"
- whileTrue:
- dat=filehandle.read(size)
- ifnotdat:
- break
- yielddat
-
- # count number of lines in file
- new_linecount=sum(blck.count("\n")forblckin_block(filehandle))
-
- ifnew_linecount<old_linecount:
- # this happens if the file was cycled or manually deleted/edited.
- print(
- " ** Log file {filename} has cycled or been edited. "
- "Restarting log. ".format(filename=filehandle.name)
- )
- new_linecount=0
- old_linecount=0
-
- lines_to_get=max(0,new_linecount-old_linecount)
-
- ifnotlines_to_get:
- return[],old_linecount
-
- lines_found=[]
- buffer_size=4098
- block_count=-1
-
- whilelen(lines_found)<lines_to_get:
- try:
- # scan backwards in file, starting from the end
- filehandle.seek(block_count*buffer_size,os.SEEK_END)
- exceptIOError:
- # file too small for current seek, include entire file
- filehandle.seek(0)
- lines_found=filehandle.readlines()
- break
- lines_found=filehandle.readlines()
- block_count-=1
-
- # only actually return the new lines
- returnlines_found[-lines_to_get:],new_linecount
-
- def_tail_file(filename,file_size,line_count,max_lines=None):
- """This will cycle repeatedly, printing new lines"""
-
- # poll for changes
- has_changed,file_size=_file_changed(filename,file_size)
-
- ifhas_changed:
- try:
- withopen(filename,"r")asfilehandle:
- new_lines,line_count=_get_new_lines(filehandle,line_count)
- exceptIOError:
- # the log file might not exist yet. Wait a little, then try again ...
- pass
- else:
- ifmax_lines==0:
- # don't show any lines from old file
- new_lines=[]
- elifmax_lines:
- # show some lines from first startup
- new_lines=new_lines[-max_lines:]
-
- # print to stdout without line break (log has its own line feeds)
- sys.stdout.write("".join(new_lines))
- sys.stdout.flush()
-
- # set up the next poll
- reactor.callLater(rate,_tail_file,filename,file_size,line_count,max_lines=100)
-
- reactor.callLater(0,_tail_file,filename1,0,0,max_lines=start_lines1)
- reactor.callLater(0,_tail_file,filename2,0,0,max_lines=start_lines2)
-
- REACTOR_RUN=True
[docs]defevennia_version():
- """
- Get the Evennia version info from the main package.
-
- """
- version="Unknown"
- try:
- version=evennia.__version__
- exceptImportError:
- # even if evennia is not found, we should not crash here.
- pass
- try:
- rev=(
- check_output("git rev-parse --short HEAD",shell=True,cwd=EVENNIA_ROOT,stderr=STDOUT)
- .strip()
- .decode()
- )
- version="%s (rev %s)"%(version,rev)
- except(IOError,CalledProcessError,OSError):
- # move on if git is not answering
- pass
- returnversion
-
-
-EVENNIA_VERSION=evennia_version()
-
-
-
[docs]defcheck_main_evennia_dependencies():
- """
- Checks and imports the Evennia dependencies. This must be done
- already before the paths are set up.
-
- Returns:
- not_error (bool): True if no dependency error was found.
-
- """
- error=False
-
- # Python
- pversion=".".join(str(num)fornuminsys.version_infoifisinstance(num,int))
- ifLooseVersion(pversion)<LooseVersion(PYTHON_MIN):
- print(ERROR_PYTHON_VERSION.format(pversion=pversion,python_min=PYTHON_MIN))
- error=True
- # Twisted
- try:
- importtwisted
-
- tversion=twisted.version.short()
- ifLooseVersion(tversion)<LooseVersion(TWISTED_MIN):
- print(ERROR_TWISTED_VERSION.format(tversion=tversion,twisted_min=TWISTED_MIN))
- error=True
- exceptImportError:
- print(ERROR_NOTWISTED)
- error=True
- # Django
- try:
- dversion=".".join(str(num)fornumindjango.VERSIONifisinstance(num,int))
- # only the main version (1.5, not 1.5.4.0)
- dversion_main=".".join(dversion.split(".")[:2])
- ifLooseVersion(dversion)<LooseVersion(DJANGO_MIN):
- print(
- ERROR_DJANGO_MIN.format(
- dversion=dversion_main,django_min=DJANGO_MIN,django_lt=DJANGO_LT
- )
- )
- error=True
- elifLooseVersion(DJANGO_LT)<=LooseVersion(dversion_main):
- print(NOTE_DJANGO_NEW.format(dversion=dversion_main,django_rec=DJANGO_LT))
- exceptImportError:
- print(ERROR_NODJANGO)
- error=True
- iferror:
- sys.exit()
-
- # return True/False if error was reported or not
- returnnoterror
-
-
-
[docs]defset_gamedir(path):
- """
- Set GAMEDIR based on path, by figuring out where the setting file
- is inside the directory tree. This allows for running the launcher
- from elsewhere than the top of the gamedir folder.
-
- """
- globalGAMEDIR
-
- Ndepth=10
- settings_path=os.path.join("server","conf","settings.py")
- os.chdir(GAMEDIR)
- foriinrange(Ndepth):
- gpath=os.getcwd()
- if"server"inos.listdir(gpath):
- ifos.path.isfile(settings_path):
- GAMEDIR=gpath
- return
- os.chdir(os.pardir)
- print(ERROR_NO_GAMEDIR)
- sys.exit()
[docs]defcreate_settings_file(init=True,secret_settings=False):
- """
- Uses the template settings file to build a working settings file.
-
- Args:
- init (bool): This is part of the normal evennia --init
- operation. If false, this function will copy a fresh
- template file in (asking if it already exists).
- secret_settings (bool, optional): If False, create settings.py, otherwise
- create the secret_settings.py file.
-
- """
- ifsecret_settings:
- settings_path=os.path.join(GAMEDIR,"server","conf","secret_settings.py")
- setting_dict={"secret_key":"'%s'"%create_secret_key()}
- else:
- settings_path=os.path.join(GAMEDIR,"server","conf","settings.py")
- setting_dict={
- "settings_default":os.path.join(EVENNIA_LIB,"settings_default.py"),
- "servername":'"%s"'%GAMEDIR.rsplit(os.path.sep,1)[1],
- "secret_key":"'%s'"%create_secret_key(),
- }
-
- ifnotinit:
- # if not --init mode, settings file may already exist from before
- ifos.path.exists(settings_path):
- inp=input("%s already exists. Do you want to reset it? y/[N]> "%settings_path)
- ifnotinp.lower()=="y":
- print("Aborted.")
- sys.exit()
- else:
- print("Reset the settings file.")
-
- ifsecret_settings:
- default_settings_path=os.path.join(
- EVENNIA_TEMPLATE,"server","conf","secret_settings.py"
- )
- else:
- default_settings_path=os.path.join(EVENNIA_TEMPLATE,"server","conf","settings.py")
- shutil.copy(default_settings_path,settings_path)
-
- withopen(settings_path,"r")asf:
- settings_string=f.read()
-
- settings_string=settings_string.format(**setting_dict)
-
- withopen(settings_path,"w")asf:
- f.write(settings_string)
-
-
-
[docs]defcreate_game_directory(dirname):
- """
- Initialize a new game directory named dirname
- at the current path. This means copying the
- template directory from evennia's root.
-
- Args:
- dirname (str): The directory name to create.
-
- """
- globalGAMEDIR
- GAMEDIR=os.path.abspath(os.path.join(CURRENT_DIR,dirname))
- ifos.path.exists(GAMEDIR):
- print("Cannot create new Evennia game dir: '%s' already exists."%dirname)
- sys.exit()
- # copy template directory
- shutil.copytree(EVENNIA_TEMPLATE,GAMEDIR)
- # rename gitignore to .gitignore
- os.rename(os.path.join(GAMEDIR,"gitignore"),os.path.join(GAMEDIR,".gitignore"))
-
- # pre-build settings file in the new GAMEDIR
- create_settings_file()
- create_settings_file(secret_settings=True)
-
-
-
[docs]defcreate_superuser():
- """
- Create the superuser account
-
- """
- print(
- "\nCreate a superuser below. The superuser is Account #1, the 'owner' "
- "account of the server. Email is optional and can be empty.\n"
- )
- 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):
- """
- Check so the database exists.
-
- Args:
- always_return (bool, optional): If set, will always return True/False
- also on critical errors. No output will be printed.
- Returns:
- exists (bool): `True` if the database exists, otherwise `False`.
-
-
- """
- # Check so a database exists and is accessible
- fromdjango.dbimportconnection
-
- tables=connection.introspection.get_table_list(connection.cursor())
- ifnottablesornotisinstance(tables[0],str):# django 1.8+
- tables=[tableinfo.namefortableinfointables]
- iftablesand"accounts_accountdb"intables:
- # database exists and seems set up. Initialize evennia.
- evennia._init()
- # Try to get Account#1
- fromevennia.accounts.modelsimportAccountDB
-
- try:
- AccountDB.objects.get(id=1)
- except(django.db.utils.OperationalError,ProgrammingError)ase:
- ifalways_return:
- returnFalse
- print(ERROR_DATABASE.format(traceback=e))
- sys.exit()
- exceptAccountDB.DoesNotExist:
- # no superuser yet. We need to create it.
-
- other_superuser=AccountDB.objects.filter(is_superuser=True)
- ifother_superuser:
- # Another superuser was found, but not with id=1. This may
- # happen if using flush (the auto-id starts at a higher
- # value). Wwe copy this superuser into id=1. To do
- # this we must deepcopy it, delete it then save the copy
- # with the new id. This allows us to avoid the UNIQUE
- # constraint on usernames.
- other=other_superuser[0]
- other_id=other.id
- other_key=other.username
- print(WARNING_MOVING_SUPERUSER.format(other_key=other_key,other_id=other_id))
- res=""
- whileres.upper()!="Y":
- # ask for permission
- res=eval(input("Continue [Y]/N: "))
- ifres.upper()=="N":
- sys.exit()
- elifnotres:
- break
- # continue with the
- fromcopyimportdeepcopy
-
- new=deepcopy(other)
- other.delete()
- new.id=1
- new.save()
- else:
- create_superuser()
- check_database(always_return=always_return)
- returnTrue
-
-
-
[docs]defgetenv():
- """
- Get current environment and add PYTHONPATH.
-
- Returns:
- env (dict): Environment global dict.
-
- """
- sep=";"if_is_windows()else":"
- env=os.environ.copy()
- env["PYTHONPATH"]=sep.join(sys.path)
- returnenv
-
-
-
[docs]defget_pid(pidfile,default=None):
- """
- Get the PID (Process ID) by trying to access an PID file.
-
- Args:
- pidfile (str): The path of the pid file.
- default (int, optional): What to return if file does not exist.
-
- Returns:
- pid (str): The process id or `default`.
-
- """
- ifos.path.exists(pidfile):
- withopen(pidfile,"r")asf:
- pid=f.read()
- returnpid
- returndefault
-
-
-
[docs]defdel_pid(pidfile):
- """
- The pidfile should normally be removed after a process has
- finished, but when sending certain signals they remain, so we need
- to clean them manually.
-
- Args:
- pidfile (str): The path of the pid file.
-
- """
- ifos.path.exists(pidfile):
- os.remove(pidfile)
-
-
-
[docs]defkill(pidfile,component="Server",callback=None,errback=None,killsignal=SIG):
- """
- Send a kill signal to a process based on PID. A customized
- success/error message will be returned. If clean=True, the system
- will attempt to manually remove the pid file. On Windows, no arguments
- are useful since Windows has no ability to direct signals except to all
- children of a console.
-
- Args:
- pidfile (str): The path of the pidfile to get the PID from. This is ignored
- on Windows.
- component (str, optional): Usually one of 'Server' or 'Portal'. This is
- ignored on Windows.
- errback (callable, optional): Called if signal failed to send. This
- is ignored on Windows.
- callback (callable, optional): Called if kill signal was sent successfully.
- This is ignored on Windows.
- killsignal (int, optional): Signal identifier for signal to send. This is
- ignored on Windows.
-
- """
- if_is_windows():
- # Windows signal sending is very limited.
- fromwin32apiimportGenerateConsoleCtrlEvent,SetConsoleCtrlHandler
-
- try:
- # Windows can only send a SIGINT-like signal to
- # *every* process spawned off the same console, so we must
- # avoid killing ourselves here.
- SetConsoleCtrlHandler(None,True)
- GenerateConsoleCtrlEvent(CTRL_C_EVENT,0)
- exceptKeyboardInterrupt:
- # We must catch and ignore the interrupt sent.
- pass
- print("Sent kill signal to all spawned processes")
-
- else:
- # Linux/Unix/Mac can send kill signal directly to specific PIDs.
- pid=get_pid(pidfile)
- ifpid:
- if_is_windows():
- os.remove(pidfile)
- try:
- os.kill(int(pid),killsignal)
- exceptOSError:
- print(
- "{component} ({pid}) cannot be stopped. "
- "The PID file '{pidfile}' seems stale. "
- "Try removing it manually.".format(
- component=component,pid=pid,pidfile=pidfile
- )
- )
- return
- ifcallback:
- callback()
- else:
- print("Sent kill signal to {component}.".format(component=component))
- return
- iferrback:
- errback()
- else:
- print(
- "Could not send kill signal - {component} does "
- "not appear to be running.".format(component=component)
- )
-
-
-
[docs]defshow_version_info(about=False):
- """
- Display version info.
-
- Args:
- about (bool): Include ABOUT info as well as version numbers.
-
- Returns:
- version_info (str): A complete version info string.
-
- """
- importsys
- importtwisted
-
- returnVERSION_INFO.format(
- version=EVENNIA_VERSION,
- about=ABOUT_INFOifaboutelse"",
- os=os.name,
- python=sys.version.split()[0],
- twisted=twisted.version.short(),
- django=django.get_version(),
- )
-
-
-
[docs]deferror_check_python_modules(show_warnings=False):
- """
- Import settings modules in settings. This will raise exceptions on
- pure python-syntax issues which are hard to catch gracefully with
- exceptions in the engine (since they are formatting errors in the
- python source files themselves). Best they fail already here
- before we get any further.
-
- Keyword Args:
- show_warnings (bool): If non-fatal warning messages should be shown.
-
- """
-
- fromdjango.confimportsettings
-
- def_imp(path,split=True):
- "helper method"
- mod,fromlist=path,"None"
- ifsplit:
- mod,fromlist=path.rsplit(".",1)
- __import__(mod,fromlist=[fromlist])
-
- # check the historical deprecations
- fromevennia.serverimportdeprecations
-
- try:
- deprecations.check_errors(settings)
- exceptDeprecationWarningaserr:
- print(err)
- sys.exit()
-
- ifshow_warnings:
- deprecations.check_warnings(settings)
-
- # core modules
- _imp(settings.COMMAND_PARSER)
- _imp(settings.SEARCH_AT_RESULT)
- _imp(settings.CONNECTION_SCREEN_MODULE)
- # imp(settings.AT_INITIAL_SETUP_HOOK_MODULE, split=False)
- forpathinsettings.LOCK_FUNC_MODULES:
- _imp(path,split=False)
-
- fromevennia.commandsimportcmdsethandler
-
- ifnotcmdsethandler.import_cmdset(settings.CMDSET_UNLOGGEDIN,None):
- print("Warning: CMDSET_UNLOGGED failed to load!")
- ifnotcmdsethandler.import_cmdset(settings.CMDSET_CHARACTER,None):
- print("Warning: CMDSET_CHARACTER failed to load")
- ifnotcmdsethandler.import_cmdset(settings.CMDSET_ACCOUNT,None):
- print("Warning: CMDSET_ACCOUNT failed to load")
- # typeclasses
- _imp(settings.BASE_ACCOUNT_TYPECLASS)
- _imp(settings.BASE_OBJECT_TYPECLASS)
- _imp(settings.BASE_CHARACTER_TYPECLASS)
- _imp(settings.BASE_ROOM_TYPECLASS)
- _imp(settings.BASE_EXIT_TYPECLASS)
- _imp(settings.BASE_SCRIPT_TYPECLASS)
[docs]definit_game_directory(path,check_db=True,need_gamedir=True):
- """
- Try to analyze the given path to find settings.py - this defines
- the game directory and also sets PYTHONPATH as well as the django
- path.
-
- Args:
- path (str): Path to new game directory, including its name.
- check_db (bool, optional): Check if the databae exists.
- need_gamedir (bool, optional): set to False if Evennia doesn't require to
- be run in a valid game directory.
-
- """
- # set the GAMEDIR path
- ifneed_gamedir:
- set_gamedir(path)
-
- # Add gamedir to python path
- sys.path.insert(0,GAMEDIR)
-
- ifTEST_MODEornotneed_gamedir:
- ifENFORCED_SETTING:
- print(NOTE_TEST_CUSTOM.format(settings_dotpath=SETTINGS_DOTPATH))
- os.environ["DJANGO_SETTINGS_MODULE"]=SETTINGS_DOTPATH
- else:
- print(NOTE_TEST_DEFAULT)
- os.environ["DJANGO_SETTINGS_MODULE"]="evennia.settings_default"
- else:
- os.environ["DJANGO_SETTINGS_MODULE"]=SETTINGS_DOTPATH
-
- # required since django1.7
- django.setup()
-
- # test existence of the settings module
- try:
- fromdjango.confimportsettings
- exceptExceptionasex:
- ifnotstr(ex).startswith("No module named"):
- importtraceback
-
- print(traceback.format_exc().strip())
- print(ERROR_SETTINGS)
- sys.exit()
-
- # this will both check the database and initialize the evennia dir.
- ifcheck_db:
- check_database()
-
- # if we don't have to check the game directory, return right away
- ifnotneed_gamedir:
- return
-
- # set up the Evennia executables and log file locations
- globalAMP_PORT,AMP_HOST,AMP_INTERFACE
- globalSERVER_PY_FILE,PORTAL_PY_FILE
- globalSERVER_LOGFILE,PORTAL_LOGFILE,HTTP_LOGFILE
- globalSERVER_PIDFILE,PORTAL_PIDFILE
- globalSPROFILER_LOGFILE,PPROFILER_LOGFILE
- globalEVENNIA_VERSION
-
- AMP_PORT=settings.AMP_PORT
- AMP_HOST=settings.AMP_HOST
- AMP_INTERFACE=settings.AMP_INTERFACE
-
- SERVER_PY_FILE=os.path.join(EVENNIA_LIB,"server","server.py")
- PORTAL_PY_FILE=os.path.join(EVENNIA_LIB,"server","portal","portal.py")
-
- SERVER_PIDFILE=os.path.join(GAMEDIR,SERVERDIR,"server.pid")
- PORTAL_PIDFILE=os.path.join(GAMEDIR,SERVERDIR,"portal.pid")
-
- SPROFILER_LOGFILE=os.path.join(GAMEDIR,SERVERDIR,"logs","server.prof")
- PPROFILER_LOGFILE=os.path.join(GAMEDIR,SERVERDIR,"logs","portal.prof")
-
- SERVER_LOGFILE=settings.SERVER_LOG_FILE
- PORTAL_LOGFILE=settings.PORTAL_LOG_FILE
- HTTP_LOGFILE=settings.HTTP_LOG_FILE
-
- # verify existence of log file dir (this can be missing e.g.
- # if the game dir itself was cloned since log files are in .gitignore)
- logdirs=[
- logfile.rsplit(os.path.sep,1)forlogfilein(SERVER_LOGFILE,PORTAL_LOGFILE,HTTP_LOGFILE)
- ]
- ifnotall(os.path.isdir(pathtup[0])forpathtupinlogdirs):
- errstr="\n ".join(
- "%s (log file %s)"%(pathtup[0],pathtup[1])
- forpathtupinlogdirs
- ifnotos.path.isdir(pathtup[0])
- )
- print(ERROR_LOGDIR_MISSING.format(logfiles=errstr))
- sys.exit()
-
- if_is_windows():
- # We need to handle Windows twisted separately. We create a
- # batchfile in game/server, linking to the actual binary
-
- globalTWISTED_BINARY
- # Windows requires us to use the absolute path for the bat file.
- server_path=os.path.dirname(os.path.abspath(__file__))
- TWISTED_BINARY=os.path.join(server_path,"twistd.bat")
-
- # add path so system can find the batfile
- sys.path.insert(1,os.path.join(GAMEDIR,SERVERDIR))
-
- try:
- importlib.import_module("win32api")
- exceptImportError:
- print(ERROR_WINDOWS_WIN32API)
- sys.exit()
-
- batpath=os.path.join(EVENNIA_SERVER,TWISTED_BINARY)
- ifnotos.path.exists(batpath):
- # Test for executable twisted batch file. This calls the
- # twistd.py executable that is usually not found on the
- # path in Windows. It's not enough to locate
- # scripts.twistd, what we want is the executable script
- # C:\PythonXX/Scripts/twistd.py. Alas we cannot hardcode
- # this location since we don't know if user has Python in
- # a non-standard location. So we try to figure it out.
- twistd=importlib.import_module("twisted.scripts.twistd")
- twistd_dir=os.path.dirname(twistd.__file__)
-
- # note that we hope the twistd package won't change here, since we
- # try to get to the executable by relative path.
- # Update: In 2016, it seems Twisted 16 has changed the name of
- # of its executable from 'twistd.py' to 'twistd.exe'.
- twistd_path=os.path.abspath(
- os.path.join(
- twistd_dir,os.pardir,os.pardir,os.pardir,os.pardir,"scripts","twistd.exe"
- )
- )
-
- withopen(batpath,"w")asbat_file:
- # build a custom bat file for windows
- bat_file.write('@"%s" %%*'%twistd_path)
-
- print(INFO_WINDOWS_BATFILE.format(twistd_path=twistd_path))
-
-
-
[docs]defrun_dummyrunner(number_of_dummies):
- """
- Start an instance of the dummyrunner
-
- Args:
- number_of_dummies (int): The number of dummy accounts to start.
-
- Notes:
- The dummy accounts' behavior can be customized by adding a
- `dummyrunner_settings.py` config file in the game's conf/
- directory.
-
- """
- number_of_dummies=str(int(number_of_dummies))ifnumber_of_dummieselse1
- cmdstr=[sys.executable,EVENNIA_DUMMYRUNNER,"-N",number_of_dummies]
- config_file=os.path.join(SETTINGS_PATH,"dummyrunner_settings.py")
- ifos.path.exists(config_file):
- cmdstr.extend(["--config",config_file])
- try:
- call(cmdstr,env=getenv())
- exceptKeyboardInterrupt:
- # this signals the dummyrunner to stop cleanly and should
- # not lead to a traceback here.
- pass
-
-
-
[docs]defrun_connect_wizard():
- """
- Run the linking wizard, for adding new external connections.
-
- """
- from.connection_wizardimportConnectionWizard,node_start
-
- wizard=ConnectionWizard()
- node_start(wizard)
-
-
-
[docs]deflist_settings(keys):
- """
- Display the server settings. We only display the Evennia specific
- settings here. The result will be printed to the terminal.
-
- Args:
- keys (str or list): Setting key or keys to inspect.
-
- """
- fromimportlibimportimport_module
- fromevennia.utilsimportevtable
-
- evsettings=import_module(SETTINGS_DOTPATH)
- iflen(keys)==1andkeys[0].upper()=="ALL":
- # show a list of all keys
- # a specific key
- table=evtable.EvTable()
- confs=[keyforkeyinsorted(evsettings.__dict__)ifkey.isupper()]
- foriinrange(0,len(confs),4):
- table.add_row(*confs[i:i+4])
- else:
- # a specific key
- table=evtable.EvTable(width=131)
- keys=[key.upper()forkeyinkeys]
- confs=dict((key,var)forkey,varinevsettings.__dict__.items()ifkeyinkeys)
- forkey,valinconfs.items():
- table.add_row(key,str(val))
- print(table)
-
-
-
[docs]defrun_custom_commands(option,*args):
- """
- Inject a custom option into the evennia launcher command chain.
-
- Args:
- option (str): Incoming option - the first argument after `evennia` on
- the command line.
- *args: All args will passed to a found callable.__dict__
-
- Returns:
- bool: If a custom command was found and handled the option.
-
- Notes:
- Provide new commands in settings with
-
- CUSTOM_EVENNIA_LAUNCHER_COMMANDS = {"mycmd": "path.to.callable", ...}
-
- The callable will be passed any `*args` given on the command line and is expected to
- handle/validate the input correctly. Use like any other evennia command option on
- in the terminal/console, for example:
-
- evennia mycmd foo bar
-
- """
- fromdjango.confimportsettings
- importimportlib
-
- try:
- # a dict of {option: callable(*args), ...}
- custom_commands=settings.EXTRA_LAUNCHER_COMMANDS
- exceptAttributeError:
- returnFalse
- cmdpath=custom_commands.get(option)
- ifcmdpath:
- modpath,*cmdname=cmdpath.rsplit('.',1)
- ifcmdname:
- cmdname=cmdname[0]
- mod=importlib.import_module(modpath)
- command=mod.__dict__.get(cmdname)
- ifcommand:
- command(*args)
- returnTrue
- returnFalse
Source code for evennia.server.game_index_client.client
-"""
-The client for sending data to the Evennia Game Index
-
-"""
-importurllib.request,urllib.parse,urllib.error
-importplatform
-importwarnings
-
-importdjango
-fromdjango.confimportsettings
-fromtwisted.internetimportdefer
-fromtwisted.internetimportprotocol
-fromtwisted.internetimportreactor
-fromtwisted.internet.deferimportinlineCallbacks
-fromtwisted.web.clientimportAgent,_HTTP11ClientFactory,HTTPConnectionPool
-fromtwisted.web.http_headersimportHeaders
-fromtwisted.web.iwebimportIBodyProducer
-fromzope.interfaceimportimplementer
-
-fromevennia.accounts.modelsimportAccountDB
-fromevennia.server.sessionhandlerimportSESSIONS
-fromevennia.utilsimportget_evennia_version,logger
-
-_EGI_HOST="http://evennia-game-index.appspot.com"
-_EGI_REPORT_PATH="/api/v1/game/check_in"
-
-
-
[docs]classEvenniaGameIndexClient(object):
- """
- This client class is used for gathering and sending game details to the
- Evennia Game Index. Since EGI is in the early goings, this isn't
- incredibly configurable as far as to what is being sent.
- """
-
-
[docs]def__init__(self,on_bad_request=None):
- """
- :param on_bad_request: Optional callable to trigger when a bad request
- was sent. This is almost always going to be due to bad config.
- """
- self.report_host=_EGI_HOST
- self.report_path=_EGI_REPORT_PATH
- self.report_url=self.report_host+self.report_path
- self.logged_first_connect=False
-
- self._on_bad_request=on_bad_request
- # Oh, the humanity. Silence the factory start/stop messages.
- self._conn_pool=HTTPConnectionPool(reactor)
- self._conn_pool._factory=QuietHTTP11ClientFactory
-
-
[docs]@inlineCallbacks
- defsend_game_details(self):
- """
- This is where the magic happens. Send details about the game to the
- Evennia Game Index.
- """
- status_code,response_body=yieldself._form_and_send_request()
- ifstatus_code==200:
- ifnotself.logged_first_connect:
- logger.log_infomsg("Successfully sent game details to Evennia Game Index.")
- self.logged_first_connect=True
- return
- # At this point, either EGD is having issues or the payload we sent
- # is improperly formed (probably due to mis-configuration).
- logger.log_errmsg(
- "Failed to send game details to Evennia Game Index. HTTP "
- "status code was %s. Message was: %s"%(status_code,response_body)
- )
-
- ifstatus_code==400andself._on_bad_request:
- # Improperly formed request. Defer to the callback as far as what
- # to do. Probably not a great idea to continue attempting to send
- # to EGD, though.
- self._on_bad_request()
-
- def_form_and_send_request(self):
- """
- Build the request to send to the index.
-
- """
- agent=Agent(reactor,pool=self._conn_pool)
- headers={
- b"User-Agent":[b"Evennia Game Index Client"],
- b"Content-Type":[b"application/x-www-form-urlencoded"],
- }
- egi_config=settings.GAME_INDEX_LISTING
- # We are using `or` statements below with dict.get() to avoid sending
- # stringified 'None' values to the server.
- try:
- values={
- # Game listing stuff
- "game_name":egi_config.get("game_name",settings.SERVERNAME),
- "game_status":egi_config["game_status"],
- "game_website":egi_config.get("game_website",""),
- "short_description":egi_config["short_description"],
- "long_description":egi_config.get("long_description",""),
- "listing_contact":egi_config["listing_contact"],
- # How to play
- "telnet_hostname":egi_config.get("telnet_hostname",""),
- "telnet_port":egi_config.get("telnet_port",""),
- "web_client_url":egi_config.get("web_client_url",""),
- # Game stats
- "connected_account_count":SESSIONS.account_count(),
- "total_account_count":AccountDB.objects.num_total_accounts()or0,
- # System info
- "evennia_version":get_evennia_version(),
- "python_version":platform.python_version(),
- "django_version":django.get_version(),
- "server_platform":platform.platform(),
- }
- exceptKeyErroraserr:
- raiseKeyError(f"Error loading GAME_INDEX_LISTING: {err}")
-
- data=urllib.parse.urlencode(values)
-
- d=agent.request(
- b"POST",
- bytes(self.report_url,"utf-8"),
- headers=Headers(headers),
- bodyProducer=StringProducer(data),
- )
-
- d.addCallback(self.handle_egd_response)
- returnd
-
-
[docs]defhandle_egd_response(self,response):
- if200<=response.code<300:
- d=defer.succeed((response.code,"OK"))
- else:
- # Go through the horrifying process of getting the response body
- # out of Twisted's plumbing.
- d=defer.Deferred()
- response.deliverBody(SimpleResponseReceiver(response.code,d))
- returnd
-
-
-
[docs]classSimpleResponseReceiver(protocol.Protocol):
- """
- Used for pulling the response body out of an HTTP response.
- """
-
-
Source code for evennia.server.game_index_client.service
-"""
-Service for integrating the Evennia Game Index client into Evennia.
-
-"""
-fromtwisted.internetimportreactor
-fromtwisted.internet.taskimportLoopingCall
-fromtwisted.application.serviceimportService
-
-fromevennia.utilsimportlogger
-from.clientimportEvenniaGameIndexClient
-
-# How many seconds to wait before triggering the first EGI check-in.
-_FIRST_UPDATE_DELAY=10
-# How often to sync to the server
-_CLIENT_UPDATE_RATE=60*30
-
-
-
[docs]classEvenniaGameIndexService(Service):
- """
- Twisted Service that contains a LoopingCall for regularly sending game details
- to the Evennia Game Index.
-
- """
-
- # We didn't stick the Evennia prefix on here because it'd get marked as
- # a core system service.
- name="GameIndexClient"
-
-
[docs]defstartService(self):
- super().startService()
- # Check to make sure that the client is configured.
- # Start the loop, but only after a short delay. This allows the
- # portal and the server time to sync up as far as total player counts.
- # Prevents always reporting a count of 0.
- reactor.callLater(_FIRST_UPDATE_DELAY,self.loop.start,_CLIENT_UPDATE_RATE)
-
-
[docs]defstopService(self):
- ifself.running==0:
- # reload errors if we've stopped this service.
- return
- super().stopService()
- ifself.loop.running:
- self.loop.stop()
-
- def_die_on_bad_request(self):
- """
- If it becomes apparent that our configuration is generating improperly
- formed messages to EGI, we don't want to keep sending bad messages.
- Stop the service so we're not wasting resources.
- """
- logger.log_infomsg(
- "Shutting down Evennia Game Index client service due to ""invalid configuration."
- )
- self.stopService()
-"""
-This module handles initial database propagation, which is only run the first time the game starts.
-It will create some default objects (notably give #1 its evennia-specific properties, and create the
-Limbo room). It will also hooks, and then perform an initial restart.
-
-Everything starts at handle_setup()
-"""
-
-
-importtime
-fromdjango.confimportsettings
-fromdjango.utils.translationimportgettextas_
-fromevennia.accounts.modelsimportAccountDB
-fromevennia.server.modelsimportServerConfig
-fromevennia.utilsimportcreate,logger
-
-
-ERROR_NO_SUPERUSER="""
- No superuser exists yet. The superuser is the 'owner' account on
- the Evennia server. Create a new superuser using the command
-
- evennia createsuperuser
-
- Follow the prompts, then restart the server.
- """
-
-
-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="""
- PostgreSQL-psycopg2 compatibility fix:
- The in-game channels {chan1}, {chan2} and {chan3} were created,
- 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.
-"""
-
-
-def_get_superuser_account():
- """
- Get the superuser (created at the command line) and don't take no for an answer.
-
- Returns:
- Account: The first superuser (User #1).
-
- Raises:
- AccountDB.DoesNotExist: If the superuser couldn't be found.
-
- """
- try:
- superuser=AccountDB.objects.get(id=1)
- exceptAccountDB.DoesNotExist:
- raiseAccountDB.DoesNotExist(ERROR_NO_SUPERUSER)
- returnsuperuser
-
-
-
[docs]defcreate_objects():
- """
- Creates the #1 account and Limbo room.
-
- """
-
- logger.log_info("Initial setup: Creating objects (Account #1 and Limbo room) ...")
-
- # Set the initial User's account object's username on the #1 object.
- # This object is pure django and only holds name, email and password.
- superuser=_get_superuser_account()
- fromevennia.objects.modelsimportObjectDB
-
- # Create an Account 'user profile' object to hold eventual
- # mud-specific settings for the AccountDB object.
- account_typeclass=settings.BASE_ACCOUNT_TYPECLASS
-
- # run all creation hooks on superuser (we must do so manually
- # since the manage.py command does not)
- superuser.swap_typeclass(account_typeclass,clean_attributes=True)
- superuser.basetype_setup()
- superuser.at_account_creation()
- superuser.locks.add(
- "examine:perm(Developer);edit:false();delete:false();boot:false();msg:all()"
- )
- # this is necessary for quelling to work correctly.
- superuser.permissions.add("Developer")
-
- # Limbo is the default "nowhere" starting room
-
- # Create the in-game god-character for account #1 and set
- # it to exist in Limbo.
- character_typeclass=settings.BASE_CHARACTER_TYPECLASS
- try:
- superuser_character=ObjectDB.objects.get(id=1)
- exceptObjectDB.DoesNotExist:
- superuser_character=create.create_object(
- character_typeclass,key=superuser.username,nohome=True)
-
- superuser_character.db_typeclass_path=character_typeclass
- superuser_character.db.desc=_("This is User #1.")
- superuser_character.locks.add(
- "examine:perm(Developer);edit:false();delete:false();boot:false();msg:all();puppet:false()"
- )
- # we set this low so that quelling is more useful
- superuser_character.permissions.add("Player")
- superuser_character.save()
-
- superuser.attributes.add("_first_login",True)
- superuser.attributes.add("_last_puppet",superuser_character)
-
- try:
- superuser.db._playable_characters.append(superuser_character)
- exceptAttributeError:
- superuser.db_playable_characters=[superuser_character]
-
- room_typeclass=settings.BASE_ROOM_TYPECLASS
- try:
- limbo_obj=ObjectDB.objects.get(id=2)
- exceptObjectDB.DoesNotExist:
- limbo_obj=create.create_object(room_typeclass,_("Limbo"),nohome=True)
-
- limbo_obj.db_typeclass_path=room_typeclass
- limbo_obj.db.desc=LIMBO_DESC.strip()
- limbo_obj.save()
-
- # Now that Limbo exists, try to set the user up in Limbo (unless
- # the creation hooks already fixed this).
- ifnotsuperuser_character.location:
- superuser_character.location=limbo_obj
- ifnotsuperuser_character.home:
- superuser_character.home=limbo_obj
-
-
[docs]defat_initial_setup():
- """
- Custom hook for users to overload some or all parts of the initial
- setup. Called very last in the sequence. It tries to import and
- srun a module settings.AT_INITIAL_SETUP_HOOK_MODULE and will fail
- silently if this does not exist or fails to load.
-
- """
- modname=settings.AT_INITIAL_SETUP_HOOK_MODULE
- ifnotmodname:
- return
- try:
- mod=__import__(modname,fromlist=[None])
- except(ImportError,ValueError):
- return
- logger.log_info("Initial setup: Running at_initial_setup() hook.")
- ifmod.__dict__.get("at_initial_setup",None):
- mod.at_initial_setup()
-
-
-
[docs]defcollectstatic():
- """
- Run collectstatic to make sure all web assets are loaded.
-
- """
- fromdjango.core.managementimportcall_command
-
- logger.log_info("Initial setup: Gathering static resources using 'collectstatic'")
- call_command("collectstatic","--noinput")
-
-
-
[docs]defreset_server():
- """
- We end the initialization by resetting the server. This makes sure
- the first login is the same as all the following ones,
- particularly it cleans all caches for the special objects. It
- also checks so the warm-reset mechanism works as it should.
-
- """
- ServerConfig.objects.conf("server_epoch",time.time())
- fromevennia.server.sessionhandlerimportSESSIONS
-
- logger.log_info("Initial setup complete. Restarting Server once.")
- SESSIONS.portal_reset_server()
-
-
-
[docs]defhandle_setup(last_step=None):
- """
- Main logic for the module. It allows for restarting the
- initialization at any point if one of the modules should crash.
-
- Args:
- last_step (str, None): The last stored successful step, for starting
- over on errors. None if starting from scratch. If this is 'done',
- the function will exit immediately.
-
- """
- iflast_stepin('done',-1):
- # this means we don't need to handle setup since
- # it already ran sucessfully once. -1 is the legacy
- # value for existing databases.
- return
-
- # setup sequence
- setup_sequence={
- 'create_objects':create_objects,
- 'at_initial_setup':at_initial_setup,
- 'collectstatic':collectstatic,
- 'done':reset_server,
- }
-
- # determine the sequence so we can skip ahead
- steps=list(setup_sequence)
- steps=steps[steps.index(last_step)+1iflast_stepisnotNoneelse0:]
-
- # step through queue from last completed function. Once completed,
- # the 'done' key should be set.
- forstepnameinsteps:
- try:
- setup_sequence[stepname]()
- exceptException:
- # we re-raise to make sure to stop startup
- raise
- else:
- # save the step
- ServerConfig.objects.conf("last_initial_setup_step",stepname)
- ifstepname=='done':
- # always exit on 'done'
- break
-"""
-Functions for processing input commands.
-
-All global functions in this module whose name does not start with "_"
-is considered an inputfunc. Each function must have the following
-callsign (where inputfunc name is always lower-case, no matter what the
-OOB input name looked like):
-
- inputfunc(session, *args, **kwargs)
-
-Where "options" is always one of the kwargs, containing eventual
-protocol-options.
-There is one special function, the "default" function, which is called
-on a no-match. It has this callsign:
-
- default(session, cmdname, *args, **kwargs)
-
-Evennia knows which modules to use for inputfuncs by
-settings.INPUT_FUNC_MODULES.
-
-"""
-
-importimportlib
-fromcodecsimportlookupascodecs_lookup
-fromdjango.confimportsettings
-fromevennia.commands.cmdhandlerimportcmdhandler
-fromevennia.accounts.modelsimportAccountDB
-fromevennia.utils.loggerimportlog_err
-fromevennia.utils.utilsimportto_str
-
-BrowserSessionStore=importlib.import_module(settings.SESSION_ENGINE).SessionStore
-
-
-# always let "idle" work since we use this in the webclient
-_IDLE_COMMAND=settings.IDLE_COMMAND
-_IDLE_COMMAND=(_IDLE_COMMAND,)if_IDLE_COMMAND=="idle"else(_IDLE_COMMAND,"idle")
-_GA=object.__getattribute__
-_SA=object.__setattr__
-
-
-_STRIP_INCOMING_MXP=settings.MXP_ENABLEDandsettings.MXP_OUTGOING_ONLY
-_STRIP_MXP=None
-
-
-
-def_NA(o):
- return"N/A"
-
-
-def_maybe_strip_incoming_mxp(txt):
- global_STRIP_MXP
- if_STRIP_INCOMING_MXP:
- ifnot_STRIP_MXP:
- fromevennia.utils.ansiimportstrip_mxpas_STRIP_MXP
- return_STRIP_MXP(txt)
- returntxt
-
-
-_ERROR_INPUT="Inputfunc {name}({session}): Wrong/unrecognized input: {inp}"
-
-
-# All global functions are inputfuncs available to process inputs
-
-
-
[docs]deftext(session,*args,**kwargs):
- """
- Main text input from the client. This will execute a command
- string on the server.
-
- Args:
- session (Session): The active Session to receive the input.
- text (str): First arg is used as text-command input. Other
- arguments are ignored.
-
- """
-
- # from evennia.server.profiling.timetrace import timetrace
- # text = timetrace(text, "ServerSession.data_in")
-
- txt=args[0]ifargselseNone
-
- # explicitly check for None since text can be an empty string, which is
- # also valid
- iftxtisNone:
- return
- # this is treated as a command input
- # handle the 'idle' command
- iftxt.strip()in_IDLE_COMMAND:
- session.update_session_counters(idle=True)
- return
-
- txt=_maybe_strip_incoming_mxp(txt)
-
- ifsession.account:
- # nick replacement
- puppet=session.puppet
- ifpuppet:
- txt=puppet.nicks.nickreplace(
- txt,categories=("inputline"),include_account=True
- )
- else:
- txt=session.account.nicks.nickreplace(
- txt,categories=("inputline"),include_account=False
- )
- kwargs.pop("options",None)
- cmdhandler(session,txt,callertype="session",session=session,**kwargs)
- session.update_session_counters()
-
-
-
[docs]defbot_data_in(session,*args,**kwargs):
- """
- Text input from the IRC and RSS bots.
- This will trigger the execute_cmd method on the bots in-game counterpart.
-
- Args:
- session (Session): The active Session to receive the input.
- text (str): First arg is text input. Other arguments are ignored.
-
- """
-
- txt=args[0]ifargselseNone
-
- # Explicitly check for None since text can be an empty string, which is
- # also valid
- iftxtisNone:
- return
- # this is treated as a command input
- # handle the 'idle' command
- iftxt.strip()in_IDLE_COMMAND:
- session.update_session_counters(idle=True)
- return
-
- txt=_maybe_strip_incoming_mxp(txt)
-
- kwargs.pop("options",None)
- # Trigger the execute_cmd method of the corresponding bot.
- session.account.execute_cmd(session=session,txt=txt,**kwargs)
- session.update_session_counters()
-
-
-
[docs]defecho(session,*args,**kwargs):
- """
- Echo test function
- """
- if_STRIP_INCOMING_MXP:
- txt=strip_mxp(txt)
-
- session.data_out(text="Echo returns: %s"%args)
-
-
-
[docs]defdefault(session,cmdname,*args,**kwargs):
- """
- Default catch-function. This is like all other input functions except
- it will get `cmdname` as the first argument.
-
- """
- err=(
- "Session {sessid}: Input command not recognized:\n"
- " name: '{cmdname}'\n"
- " args, kwargs: {args}, {kwargs}".format(
- sessid=session.sessid,cmdname=cmdname,args=args,kwargs=kwargs
- )
- )
- ifsession.protocol_flags.get("INPUTDEBUG",False):
- session.msg(err)
- log_err(err)
[docs]defclient_options(session,*args,**kwargs):
- """
- This allows the client an OOB way to inform us about its name and capabilities.
- This will be integrated into the session settings
-
- Keyword Args:
- get (bool): If this is true, return the settings as a dict
- (ignore all other kwargs).
- client (str): A client identifier, like "mushclient".
- version (str): A client version
- ansi (bool): Supports ansi colors
- xterm256 (bool): Supports xterm256 colors or not
- mxp (bool): Supports MXP or not
- utf-8 (bool): Supports UTF-8 or not
- screenreader (bool): Screen-reader mode on/off
- mccp (bool): MCCP compression on/off
- screenheight (int): Screen height in lines
- screenwidth (int): Screen width in characters
- inputdebug (bool): Debug input functions
- nocolor (bool): Strip color
- raw (bool): Turn off parsing
- localecho (bool): Turn on server-side echo (for clients not supporting it)
-
- """
- old_flags=session.protocol_flags
- ifnotkwargsorkwargs.get("get",False):
- # return current settings
- options=dict((key,old_flags[key])forkeyinold_flagsifkey.upper()in_CLIENT_OPTIONS)
- session.msg(client_options=options)
- return
-
- defvalidate_encoding(val):
- # helper: change encoding
- try:
- codecs_lookup(val)
- exceptLookupError:
- raiseRuntimeError("The encoding '|w%s|n' is invalid. "%val)
- returnval
-
- defvalidate_size(val):
- return{0:int(val)}
-
- defvalidate_bool(val):
- ifisinstance(val,str):
- returnTrueifval.lower()in("true","on","1")elseFalse
- returnbool(val)
-
- flags={}
- forkey,valueinkwargs.items():
- key=key.lower()
- ifkey=="client":
- flags["CLIENTNAME"]=to_str(value)
- elifkey=="version":
- if"CLIENTNAME"inflags:
- flags["CLIENTNAME"]="%s%s"%(flags["CLIENTNAME"],to_str(value))
- elifkey=="ENCODING":
- flags["ENCODING"]=validate_encoding(value)
- elifkey=="ansi":
- flags["ANSI"]=validate_bool(value)
- elifkey=="xterm256":
- flags["XTERM256"]=validate_bool(value)
- elifkey=="mxp":
- flags["MXP"]=validate_bool(value)
- elifkey=="utf-8":
- flags["UTF-8"]=validate_bool(value)
- elifkey=="screenreader":
- flags["SCREENREADER"]=validate_bool(value)
- elifkey=="mccp":
- flags["MCCP"]=validate_bool(value)
- elifkey=="screenheight":
- flags["SCREENHEIGHT"]=validate_size(value)
- elifkey=="screenwidth":
- flags["SCREENWIDTH"]=validate_size(value)
- elifkey=="inputdebug":
- flags["INPUTDEBUG"]=validate_bool(value)
- elifkey=="nocolor":
- flags["NOCOLOR"]=validate_bool(value)
- elifkey=="raw":
- flags["RAW"]=validate_bool(value)
- elifkey=="nogoahead":
- flags["NOGOAHEAD"]=validate_bool(value)
- elifkey=="localecho":
- flags["LOCALECHO"]=validate_bool(value)
- elifkeyin(
- "Char 1",
- "Char.Skills 1",
- "Char.Items 1",
- "Room 1",
- "IRE.Rift 1",
- "IRE.Composer 1",
- ):
- # ignore mudlet's default send (aimed at IRE games)
- pass
- elifkeynotin("options","cmdid"):
- err=_ERROR_INPUT.format(name="client_settings",session=session,inp=key)
- session.msg(text=err)
-
- session.protocol_flags.update(flags)
- # we must update the protocol flags on the portal session copy as well
- session.sessionhandler.session_portal_partial_sync({session.sessid:{"protocol_flags":flags}})
-
-
-
[docs]defget_client_options(session,*args,**kwargs):
- """
- Alias wrapper for getting options.
- """
- client_options(session,get=True)
-
-
-
[docs]defget_inputfuncs(session,*args,**kwargs):
- """
- Get the keys of all available inputfuncs. Note that we don't get
- it from this module alone since multiple modules could be added.
- So we get it from the sessionhandler.
- """
- inputfuncsdict=dict(
- (key,func.__doc__)forkey,funcinsession.sessionhandler.get_inputfuncs().items()
- )
- session.msg(get_inputfuncs=inputfuncsdict)
-
-
-
[docs]deflogin(session,*args,**kwargs):
- """
- Peform a login. This only works if session is currently not logged
- in. This will also automatically throttle too quick attempts.
-
- Keyword Args:
- name (str): Account name
- password (str): Plain-text password
-
- """
- ifnotsession.logged_inand"name"inkwargsand"password"inkwargs:
- fromevennia.commands.default.unloggedinimportcreate_normal_account
-
- account=create_normal_account(session,kwargs["name"],kwargs["password"])
- ifaccount:
- session.sessionhandler.login(session,account)
[docs]defget_value(session,*args,**kwargs):
- """
- Return the value of a given attribute or db_property on the
- session's current account or character.
-
- Keyword Args:
- name (str): Name of info value to return. Only names
- in the _gettable dictionary earlier in this module
- are accepted.
-
- """
- name=kwargs.get("name","")
- obj=session.puppetorsession.account
- ifnamein_gettable:
- session.msg(get_value={"name":name,"value":_gettable[name](obj)})
-
-
-def_testrepeat(**kwargs):
- """
- This is a test function for using with the repeat
- inputfunc.
-
- Keyword Args:
- session (Session): Session to return to.
- """
- importtime
-
- kwargs["session"].msg(repeat="Repeat called: %s"%time.time())
-
-
-_repeatable={"test1":_testrepeat,"test2":_testrepeat}# example only # "
-
-
-
[docs]defrepeat(session,*args,**kwargs):
- """
- Call a named function repeatedly. Note that
- this is meant as an example of limiting the number of
- possible call functions.
-
- Keyword Args:
- callback (str): The function to call. Only functions
- from the _repeatable dictionary earlier in this
- module are available.
- interval (int): How often to call function (s).
- Defaults to once every 60 seconds with a minimum
- of 5 seconds.
- stop (bool): Stop a previously assigned ticker with
- the above settings.
-
- """
- fromevennia.scripts.tickerhandlerimportTICKER_HANDLER
-
- name=kwargs.get("callback","")
- interval=max(5,int(kwargs.get("interval",60)))
-
- ifnamein_repeatable:
- ifkwargs.get("stop",False):
- TICKER_HANDLER.remove(
- interval,_repeatable[name],idstring=session.sessid,persistent=False
- )
- else:
- TICKER_HANDLER.add(
- interval,
- _repeatable[name],
- idstring=session.sessid,
- persistent=False,
- session=session,
- )
- else:
- session.msg("Allowed repeating functions are: %s"%(", ".join(_repeatable)))
-
-
-
[docs]defunrepeat(session,*args,**kwargs):
- "Wrapper for OOB use"
- kwargs["stop"]=True
- repeat(session,*args,**kwargs)
-
-
-_monitorable={"name":"db_key","location":"db_location","desc":"desc"}
-
-
-def_on_monitor_change(**kwargs):
- fieldname=kwargs["fieldname"]
- obj=kwargs["obj"]
- name=kwargs["name"]
- session=kwargs["session"]
- outputfunc_name=kwargs["outputfunc_name"]
-
- # the session may be None if the char quits and someone
- # else then edits the object
-
- ifsession:
- callsign={outputfunc_name:{"name":name,"value":_GA(obj,fieldname)}}
- session.msg(**callsign)
-
-
-
[docs]defmonitor(session,*args,**kwargs):
- """
- Adds monitoring to a given property or Attribute.
-
- Keyword Args:
- name (str): The name of the property or Attribute
- to report. No db_* prefix is needed. Only names
- in the _monitorable dict earlier in this module
- are accepted.
- stop (bool): Stop monitoring the above name.
- outputfunc_name (str, optional): Change the name of
- the outputfunc name. This is used e.g. by MSDP which
- has its own specific output format.
-
- """
- fromevennia.scripts.monitorhandlerimportMONITOR_HANDLER
-
- name=kwargs.get("name",None)
- outputfunc_name=kwargs("outputfunc_name","monitor")
- ifnameandnamein_monitorableandsession.puppet:
- field_name=_monitorable[name]
- obj=session.puppet
- ifkwargs.get("stop",False):
- MONITOR_HANDLER.remove(obj,field_name,idstring=session.sessid)
- else:
- # the handler will add fieldname and obj to the kwargs automatically
- MONITOR_HANDLER.add(
- obj,
- field_name,
- _on_monitor_change,
- idstring=session.sessid,
- persistent=False,
- name=name,
- session=session,
- outputfunc_name=outputfunc_name,
- )
-
-
-
[docs]defunmonitor(session,*args,**kwargs):
- """
- Wrapper for turning off monitoring
- """
- kwargs["stop"]=True
- monitor(session,*args,**kwargs)
-
-
-
[docs]defmonitored(session,*args,**kwargs):
- """
- Report on what is being monitored
-
- """
- fromevennia.scripts.monitorhandlerimportMONITOR_HANDLER
-
- obj=session.puppet
- monitors=MONITOR_HANDLER.all(obj=obj)
- session.msg(monitored=(monitors,{}))
-
-
-def_on_webclient_options_change(**kwargs):
- """
- Called when the webclient options stored on the account changes.
- Inform the interested clients of this change.
- """
- session=kwargs["session"]
- obj=kwargs["obj"]
- fieldname=kwargs["fieldname"]
- clientoptions=_GA(obj,fieldname)
-
- # the session may be None if the char quits and someone
- # else then edits the object
- ifsession:
- session.msg(webclient_options=clientoptions)
-
-
-
[docs]defwebclient_options(session,*args,**kwargs):
- """
- Handles retrieving and changing of options related to the webclient.
-
- If kwargs is empty (or contains just a "cmdid"), the saved options will be
- sent back to the session.
- A monitor handler will be created to inform the client of any future options
- that changes.
-
- If kwargs is not empty, the key/values stored in there will be persisted
- to the account object.
-
- Keyword Args:
- <option name>: an option to save
- """
- account=session.account
-
- clientoptions=account.db._saved_webclient_options
- ifnotclientoptions:
- # No saved options for this account, copy and save the default.
- account.db._saved_webclient_options=settings.WEBCLIENT_OPTIONS.copy()
- # Get the _SaverDict created by the database.
- clientoptions=account.db._saved_webclient_options
-
- # The webclient adds a cmdid to every kwargs, but we don't need it.
- try:
- delkwargs["cmdid"]
- exceptKeyError:
- pass
-
- ifnotkwargs:
- # No kwargs: we are getting the stored options
- # Convert clientoptions to regular dict for sending.
- session.msg(webclient_options=dict(clientoptions))
-
- # Create a monitor. If a monitor already exists then it will replace
- # the previous one since it would use the same idstring
- fromevennia.scripts.monitorhandlerimportMONITOR_HANDLER
-
- MONITOR_HANDLER.add(
- account,
- "_saved_webclient_options",
- _on_webclient_options_change,
- idstring=session.sessid,
- persistent=False,
- session=session,
- )
- else:
- # kwargs provided: persist them to the account object.
- clientoptions.update(kwargs)
-
-
-# OOB protocol-specific aliases and wrappers
-
-# GMCP aliases
-hello=client_options
-supports_set=client_options
-
-
-# MSDP aliases (some of the the generic MSDP commands defined in the MSDP spec are prefixed
-# by msdp_ at the protocol level)
-# See https://tintin.sourceforge.io/protocols/msdp/
-
-
-
-
-
-# client specific
-
-
-def_not_implemented(session,*args,**kwargs):
- """
- Dummy used to swallow missing-inputfunc errors for
- common clients.
- """
- pass
-
-
-# GMCP External.Discord.Hello is sent by Mudlet as a greeting
-# (see https://wiki.mudlet.org/w/Manual:Technical_Manual)
-external_discord_hello=_not_implemented
-
-
-# GMCP Client.Gui is sent by Mudlet for gui setup.
-client_gui=_not_implemented
-
[docs]classServerConfigManager(models.Manager):
- """
- This ServerConfigManager implements methods for searching and
- manipulating ServerConfigs directly from the database.
-
- These methods will all return database objects (or QuerySets)
- directly.
-
- ServerConfigs are used to store certain persistent settings for
- the server at run-time.
-
- """
-
-
[docs]defconf(self,key=None,value=None,delete=False,default=None):
- """
- Add, retrieve and manipulate config values.
-
- Args:
- key (str, optional): Name of config.
- value (str, optional): Data to store in this config value.
- delete (bool, optional): If `True`, delete config with `key`.
- default (str, optional): Use when retrieving a config value
- by a key that does not exist.
- Returns:
- all (list): If `key` was not given - all stored config values.
- value (str): If `key` was given, this is the stored value, or
- `default` if no matching `key` was found.
-
- """
- ifnotkey:
- returnself.all()
- elifdeleteisTrue:
- forconfinself.filter(db_key=key):
- conf.delete()
- elifvalueisnotNone:
- conf=self.filter(db_key=key)
- ifconf:
- conf=conf[0]
- else:
- conf=self.model(db_key=key)
- conf.value=value# this will pickle
- else:
- conf=self.filter(db_key=key)
- ifnotconf:
- returndefault
- returnconf[0].value
- returnNone
-"""
-
-Server Configuration flags
-
-This holds persistent server configuration flags.
-
-Config values should usually be set through the
-manager's conf() method.
-
-"""
-fromdjango.dbimportmodels
-
-fromevennia.utils.idmapper.modelsimportWeakSharedMemoryModel
-fromevennia.utilsimportlogger,utils
-fromevennia.utils.dbserializeimportto_pickle,from_pickle
-fromevennia.server.managerimportServerConfigManager
-fromevennia.utilsimportpicklefield
-
-
-# ------------------------------------------------------------
-#
-# ServerConfig
-#
-# ------------------------------------------------------------
-
-
-
[docs]classServerConfig(WeakSharedMemoryModel):
- """
- On-the fly storage of global settings.
-
- Properties defined on ServerConfig:
-
- - key: Main identifier
- - value: Value stored in key. This is a pickled storage.
-
- """
-
- #
- # ServerConfig database model setup
- #
- #
- # These database fields are all set using their corresponding properties,
- # named same as the field, but without the db_* prefix.
-
- # main name of the database entry
- db_key=models.CharField(max_length=64,unique=True)
- # config value
- # db_value = models.BinaryField(blank=True)
-
- db_value=picklefield.PickledObjectField(
- "value",
- null=True,
- help_text="The data returned when the config value is accessed. Must be "
- "written as a Python literal if editing through the admin "
- "interface. Attribute values which are not Python literals "
- "cannot be edited through the admin interface.",
- )
-
- objects=ServerConfigManager()
- _is_deleted=False
-
- # Wrapper properties to easily set database fields. These are
- # @property decorators that allows to access these fields using
- # normal python operations (without having to remember to save()
- # etc). So e.g. a property 'attr' has a get/set/del decorator
- # defined that allows the user to do self.attr = value,
- # value = self.attr and del self.attr respectively (where self
- # is the object in question).
-
- # key property (wraps db_key)
- # @property
- def__key_get(self):
- "Getter. Allows for value = self.key"
- returnself.db_key
-
- # @key.setter
- def__key_set(self,value):
- "Setter. Allows for self.key = value"
- self.db_key=value
- self.save()
-
- # @key.deleter
- def__key_del(self):
- "Deleter. Allows for del self.key. Deletes entry."
- self.delete()
-
- key=property(__key_get,__key_set,__key_del)
-
- # value property (wraps db_value)
- # @property
- def__value_get(self):
- "Getter. Allows for value = self.value"
- returnfrom_pickle(self.db_value,db_obj=self)
-
- # @value.setter
- def__value_set(self,value):
- "Setter. Allows for self.value = value"
- ifutils.has_parent("django.db.models.base.Model",value):
- # we have to protect against storing db objects.
- logger.log_err("ServerConfig cannot store db objects! (%s)"%value)
- return
- self.db_value=to_pickle(value)
- self.save()
-
- # @value.deleter
- def__value_del(self):
- "Deleter. Allows for del self.value. Deletes entry."
- self.delete()
-
- value=property(__value_get,__value_set,__value_del)
-
- classMeta:
- "Define Django meta options"
- verbose_name="Server Config value"
- verbose_name_plural="Server Config values"
-
- #
- # ServerConfig other methods
- #
- def__repr__(self):
- return"<{}{}>".format(self.__class__.__name__,self.key)
-
-
[docs]defstore(self,key,value):
- """
- Wrap the storage.
-
- Args:
- key (str): The name of this store.
- value (str): The data to store with this `key`.
-
- """
- self.key=key
- self.value=value
-"""
-The AMP (Asynchronous Message Protocol)-communication commands and constants used by Evennia.
-
-This module acts as a central place for AMP-servers and -clients to get commands to use.
-
-"""
-
-fromfunctoolsimportwraps
-importtime
-fromtwisted.protocolsimportamp
-fromcollectionsimportdefaultdict,namedtuple
-fromioimportBytesIO
-fromitertoolsimportcount
-importzlib# Used in Compressed class
-importpickle
-
-fromtwisted.internet.deferimportDeferredList,Deferred
-fromevennia.utils.utilsimportvariable_from_module
-
-# delayed import
-_LOGGER=None
-
-# communication bits
-# (chr(9) and chr(10) are \t and \n, so skipping them)
-
-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 creating new connection (for irc bots and etc)
-PCONNSYNC=chr(12)# portal post-syncing a session
-PDISCONNALL=chr(13)# portal session disconnect all
-SRELOAD=chr(14)# server shutdown in reload mode
-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
-
-NUL=b"\x00"
-NULNUL=b"\x00\x00"
-
-AMP_MAXLEN=amp.MAX_VALUE_LENGTH# max allowed data length in AMP protocol (cannot be changed)
-
-# amp internal
-ASK=b'_ask'
-ANSWER=b'_answer'
-ERROR=b'_error'
-ERROR_CODE=b'_error_code'
-ERROR_DESCRIPTION=b'_error_description'
-UNKNOWN_ERROR_CODE=b'UNKNOWN'
-
-# buffers
-_SENDBATCH=defaultdict(list)
-_MSGBUFFER=defaultdict(list)
-
-# resources
-
-DUMMYSESSION=namedtuple("DummySession",["sessid"])(0)
-
-
-_HTTP_WARNING=bytes(
- """
-HTTP/1.1 200 OK
-Content-Type: text/html
-
-<html>
- <body>
- This is Evennia's internal AMP port. It handles communication
- between Evennia's different processes.
- <p>
- <h3>This port should NOT be publicly visible.</h3>
- </p>
- </body>
-</html>""".strip(),
- "utf-8",
-)
-
-
-# Helper functions for pickling.
-
-
-
-
-
-def_get_logger():
- """
- Delay import of logger until absolutely necessary
-
- """
- global_LOGGER
- ifnot_LOGGER:
- fromevennia.utilsimportloggeras_LOGGER
- return_LOGGER
-
-
-@wraps
-defcatch_traceback(func):
- """
- Helper decorator
-
- """
-
- defdecorator(*args,**kwargs):
- try:
- func(*args,**kwargs)
- exceptExceptionaserr:
- _get_logger().log_trace()
- raise# make sure the error is visible on the other side of the connection too
- print(err)
-
- returndecorator
-
-
-# AMP Communication Command types
-
-
-
[docs]classCompressed(amp.String):
- """
- This is a custom AMP command Argument that both handles too-long
- sends as well as uses zlib for compression across the wire. The
- batch-grouping of too-long sends is borrowed from the "mediumbox"
- recipy at twisted-hacks's ~glyph/+junk/amphacks/mediumbox.
-
- """
-
-
[docs]deffromBox(self,name,strings,objects,proto):
- """
- Converts from box string representation to python. We read back too-long batched data and
- put it back together here.
-
- """
-
- value=BytesIO()
- value.write(self.fromStringProto(strings.get(name),proto))
- forcounterincount(2):
- # count from 2 upwards
- chunk=strings.get(b"%s.%d"%(name,counter))
- ifchunkisNone:
- break
- value.write(self.fromStringProto(chunk,proto))
- objects[str(name,"utf-8")]=value.getvalue()
-
-
[docs]deftoBox(self,name,strings,objects,proto):
- """
- Convert from python object to string box representation.
- we break up too-long data snippets into multiple batches here.
-
- """
-
- # print("toBox: name={}, strings={}, objects={}, proto{}".format(name, strings, objects, proto))
-
- value=BytesIO(objects[str(name,"utf-8")])
- strings[name]=self.toStringProto(value.read(AMP_MAXLEN),proto)
-
- # print("toBox strings[name] = {}".format(strings[name]))
-
- forcounterincount(2):
- chunk=value.read(AMP_MAXLEN)
- ifnotchunk:
- break
- strings[b"%s.%d"%(name,counter)]=self.toStringProto(chunk,proto)
-
-
[docs]deftoString(self,inObject):
- """
- Convert to send as a bytestring on the wire, with compression.
-
- Note: In Py3 this is really a byte stream.
-
- """
- returnzlib.compress(super(Compressed,self).toString(inObject),9)
-
-
[docs]deffromString(self,inString):
- """
- Convert (decompress) from the string-representation on the wire to Python.
-
- """
- returnsuper(Compressed,self).fromString(zlib.decompress(inString))
[docs]classAdminPortal2Server(amp.Command):
- """
- Administration Portal -> Server
-
- Sent when the portal needs to perform admin operations on the
- server, such as when a new session connects or resyncs
-
- """
-
- key="AdminPortal2Server"
- arguments=[(b"packed_data",Compressed())]
- errors={Exception:b"EXCEPTION"}
- response=[]
-
-
-
[docs]classAdminServer2Portal(amp.Command):
- """
- Administration Server -> Portal
-
- Sent when the server needs to perform admin operations on the
- portal.
-
- """
-
- key="AdminServer2Portal"
- arguments=[(b"packed_data",Compressed())]
- errors={Exception:b"EXCEPTION"}
- response=[]
[docs]classFunctionCall(amp.Command):
- """
- Bidirectional Server <-> Portal
-
- Sent when either process needs to call an arbitrary function in
- the other. This does not use the batch-send functionality.
-
- """
-
- key="FunctionCall"
- arguments=[
- (b"module",amp.String()),
- (b"function",amp.String()),
- (b"args",amp.String()),
- (b"kwargs",amp.String()),
- ]
- errors={Exception:b"EXCEPTION"}
- response=[(b"result",amp.String())]
-
-
-# -------------------------------------------------------------
-# Core AMP protocol for communication Server <-> Portal
-# -------------------------------------------------------------
-
-
-
[docs]classAMPMultiConnectionProtocol(amp.AMP):
- """
- AMP protocol that safely handle multiple connections to the same
- server without dropping old ones - new clients will receive
- all server returns (broadcast). Will also correctly handle
- erroneous HTTP requests on the port and return a HTTP error response.
-
- """
-
- # helper methods
-
-
[docs]def__init__(self,*args,**kwargs):
- """
- Initialize protocol with some things that need to be in place
- already before connecting both on portal and server.
-
- """
- self.send_batch_counter=0
- self.send_reset_time=time.time()
- self.send_mode=True
- self.send_task=None
- self.multibatches=0
- # later twisted amp has its own __init__
- super(AMPMultiConnectionProtocol,self).__init__(*args,**kwargs)
-
- def_commandReceived(self,box):
- """
- This overrides the default Twisted AMP error handling which is not
- passing enough of the traceback through to the other side. Instead we
- add a specific log of the problem on the erroring side.
-
- """
- defformatAnswer(answerBox):
- answerBox[ANSWER]=box[ASK]
- returnanswerBox
-
- defformatError(error):
- iferror.check(amp.RemoteAmpError):
- code=error.value.errorCode
- desc=error.value.description
-
- # Evennia extra logging
- desc+=" (error logged on other side)"
- _get_logger().log_err(f"AMP caught exception ({desc}):\n{error.value}")
-
- ifisinstance(desc,str):
- desc=desc.encode("utf-8","replace")
- iferror.value.fatal:
- errorBox=amp.QuitBox()
- else:
- errorBox=amp.AmpBox()
- else:
- errorBox=amp.QuitBox()
- _get_logger().log_err(error)# server-side logging if unhandled error
- code=UNKNOWN_ERROR_CODE
- desc=b"Unknown Error"
- errorBox[ERROR]=box[ASK]
- errorBox[ERROR_DESCRIPTION]=desc
- errorBox[ERROR_CODE]=code
- returnerrorBox
- deferred=self.dispatchCommand(box)
- ifASKinbox:
- deferred.addCallbacks(formatAnswer,formatError)
- deferred.addCallback(self._safeEmit)
- deferred.addErrback(self.unhandledError)
-
-
[docs]defdataReceived(self,data):
- """
- Handle non-AMP messages, such as HTTP communication.
-
- """
- # print("dataReceived: {}".format(data))
- ifdata[:1]==NUL:
- # an AMP communication
- ifdata[-2:]!=NULNUL:
- # an incomplete AMP box means more batches are forthcoming.
- self.multibatches+=1
- try:
- super(AMPMultiConnectionProtocol,self).dataReceived(data)
- exceptKeyError:
- _get_logger().log_trace(
- "Discarded incoming partial (packed) data (len {})".format(len(data))
- )
- elifself.multibatches:
- # invalid AMP, but we have a pending multi-batch that is not yet complete
- ifdata[-2:]==NULNUL:
- # end of existing multibatch
- self.multibatches=max(0,self.multibatches-1)
- try:
- super(AMPMultiConnectionProtocol,self).dataReceived(data)
- exceptKeyError:
- _get_logger().log_trace(
- "Discarded incoming multi-batch (packed) data (len {})".format(len(data))
- )
- else:
- # not an AMP communication, return warning
- self.transport.write(_HTTP_WARNING)
- self.transport.loseConnection()
- print("HTTP received (the AMP port should not receive http, only AMP!) %s"%data)
-
-
[docs]defmakeConnection(self,transport):
- """
- Swallow connection log message here. Copied from original
- in the amp protocol.
-
- """
- # copied from original, removing the log message
- ifnotself._ampInitialized:
- amp.AMP.__init__(self)
- self._transportPeer=transport.getPeer()
- self._transportHost=transport.getHost()
- amp.BinaryBoxProtocol.makeConnection(self,transport)
-
-
[docs]defconnectionMade(self):
- """
- This is called when an AMP connection is (re-)established. AMP calls it on both sides.
-
- """
- # print("connectionMade: {}".format(self))
- self.factory.broadcasts.append(self)
-
-
[docs]defconnectionLost(self,reason):
- """
- We swallow connection errors here. The reason is that during a
- normal reload/shutdown there will almost always be cases where
- either the portal or server shuts down before a message has
- returned its (empty) return, triggering a connectionLost error
- 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:
- self.factory.broadcasts.remove(self)
- exceptValueError:
- pass
-
- # Error handling
-
-
[docs]deferrback(self,err,info):
- """
- Error callback.
- Handles errors to avoid dropping connections on server tracebacks.
-
- Args:
- err (Failure): Deferred error instance.
- info (str): Error string.
-
- """
- err.trap(Exception)
- _get_logger().log_err(
- "AMP Error from {info}: {trcbck}{err}".format(
- info=info,trcbck=err.getTraceback(),err=err.getErrorMessage()
- )
- )
[docs]defbroadcast(self,command,sessid,**kwargs):
- """
- Send data across the wire to all connections.
-
- Args:
- command (AMP Command): A protocol send command.
- sessid (int): A unique Session id.
-
- Returns:
- deferred (deferred or None): A deferred with an errback.
-
- Notes:
- Data will be sent across the wire pickled as a tuple
- (sessid, kwargs).
-
- """
- deferreds=[]
- # print("broadcast: {} {}: {}".format(command, sessid, kwargs))
-
- forprotclinself.factory.broadcasts:
- deferreds.append(
- protcl.callRemote(command,**kwargs).addErrback(self.errback,command.key)
- )
-
- returnDeferredList(deferreds)
-
- # generic function send/recvs
-
-
[docs]defsend_FunctionCall(self,modulepath,functionname,*args,**kwargs):
- """
- Access method called by either process. This will call an arbitrary
- function on the other process (On Portal if calling from Server and
- vice versa).
-
- Inputs:
- modulepath (str) - python path to module holding function to call
- functionname (str) - name of function in given module
- *args, **kwargs will be used as arguments/keyword args for the
- remote function call
- Returns:
- A deferred that fires with the return value of the remote
- function call
-
- """
- return(
- self.callRemote(
- FunctionCall,
- module=modulepath,
- function=functionname,
- args=dumps(args),
- kwargs=dumps(kwargs),
- )
- .addCallback(lambdar:loads(r["result"]))
- .addErrback(self.errback,"FunctionCall")
- )
-
-
[docs]@FunctionCall.responder
- @catch_traceback
- defreceive_functioncall(self,module,function,func_args,func_kwargs):
- """
- This allows Portal- and Server-process to call an arbitrary
- function in the other process. It is intended for use by
- plugin modules.
-
- Args:
- module (str or module): The module containing the
- `function` to call.
- function (str): The name of the function to call in
- `module`.
- func_args (str): Pickled args tuple for use in `function` call.
- func_kwargs (str): Pickled kwargs dict for use in `function` call.
-
- """
- args=loads(func_args)
- kwargs=loads(func_kwargs)
-
- # call the function (don't catch tracebacks here)
- result=variable_from_module(module,function)(*args,**kwargs)
-
- ifisinstance(result,Deferred):
- # if result is a deferred, attach handler to properly
- # wrap the return value
- result.addCallback(lambdar:{"result":dumps(r)})
- returnresult
- else:
- return{"result":dumps(result)}
-"""
-The Evennia Portal service acts as an AMP-server, handling AMP
-communication to the AMP clients connecting to it (by default
-these are the Evennia Server and the evennia launcher).
-
-"""
-importos
-importsys
-fromtwisted.internetimportprotocol
-fromevennia.server.portalimportamp
-fromdjango.confimportsettings
-fromsubprocessimportPopen,STDOUT
-fromevennia.utilsimportlogger
-fromevennia.utils.utilsimportclass_from_module
-
-
-def_is_windows():
- returnos.name=="nt"
-
-
-
[docs]defgetenv():
- """
- Get current environment and add PYTHONPATH.
-
- Returns:
- env (dict): Environment global dict.
-
- """
- sep=";"if_is_windows()else":"
- env=os.environ.copy()
- env["PYTHONPATH"]=sep.join(sys.path)
- returnenv
-
-
-
[docs]classAMPServerFactory(protocol.ServerFactory):
-
- """
- This factory creates AMP Server connection. This acts as the 'Portal'-side communication to the
- 'Server' process.
-
- """
-
- noisy=False
-
-
[docs]deflogPrefix(self):
- """
- How this is named in logs
-
- """
- return"AMP"
-
-
[docs]def__init__(self,portal):
- """
- Initialize the factory. This is called as the Portal service starts.
-
- Args:
- portal (Portal): The Evennia Portal service instance.
- protocol (Protocol): The protocol the factory creates
- instances of.
-
- """
- self.portal=portal
- self.protocol=class_from_module(settings.AMP_SERVER_PROTOCOL_CLASS)
- self.broadcasts=[]
- self.server_connection=None
- self.launcher_connection=None
- self.disconnect_callbacks={}
- self.server_connect_callbacks=[]
-
-
[docs]defbuildProtocol(self,addr):
- """
- Start a new connection, and store it on the service object.
-
- Args:
- addr (str): Connection address. Not used.
-
- Returns:
- protocol (Protocol): The created protocol.
-
- """
- self.portal.amp_protocol=self.protocol()
- self.portal.amp_protocol.factory=self
- returnself.portal.amp_protocol
-
-
-
[docs]classAMPServerProtocol(amp.AMPMultiConnectionProtocol):
- """
- Protocol subclass for the AMP-server run by the Portal.
-
- """
-
-
[docs]defconnectionLost(self,reason):
- """
- Set up a simple callback mechanism to let the amp-server wait for a connection to close.
-
- """
- # wipe broadcast and data memory
- super(AMPServerProtocol,self).connectionLost(reason)
- ifself.factory.server_connection==self:
- self.factory.server_connection=None
- self.factory.portal.server_info_dict={}
- ifself.factory.launcher_connection==self:
- self.factory.launcher_connection=None
-
- callback,args,kwargs=self.factory.disconnect_callbacks.pop(self,(None,None,None))
- ifcallback:
- try:
- callback(*args,**kwargs)
- exceptException:
- logger.log_trace()
-
-
[docs]defget_status(self):
- """
- Return status for the Evennia infrastructure.
-
- Returns:
- status (tuple): The portal/server status and pids
- (portal_live, server_live, portal_PID, server_PID).
-
- """
- server_connected=bool(
- self.factory.server_connectionandself.factory.server_connection.transport.connected
- )
- portal_info_dict=self.factory.portal.get_info_dict()
- server_info_dict=self.factory.portal.server_info_dict
- server_pid=self.factory.portal.server_process_id
- portal_pid=os.getpid()
- return(True,server_connected,portal_pid,server_pid,portal_info_dict,server_info_dict)
-
-
[docs]defdata_to_server(self,command,sessid,**kwargs):
- """
- Send data across the wire to the Server.
-
- Args:
- command (AMP Command): A protocol send command.
- sessid (int): A unique Session id.
- kwargs (any): Data to send. This will be pickled.
-
- Returns:
- deferred (deferred or None): A deferred with an errback.
-
- Notes:
- Data will be sent across the wire pickled as a tuple
- (sessid, kwargs).
-
- """
- # print("portal data_to_server: {}, {}, {}".format(command, sessid, kwargs))
- ifself.factory.server_connection:
- returnself.factory.server_connection.callRemote(
- command,packed_data=amp.dumps((sessid,kwargs))
- ).addErrback(self.errback,command.key)
- else:
- # if no server connection is available, broadcast
- returnself.broadcast(command,sessid,packed_data=amp.dumps((sessid,kwargs)))
-
-
[docs]defstart_server(self,server_twistd_cmd):
- """
- (Re-)Launch the Evennia server.
-
- Args:
- server_twisted_cmd (list): The server start instruction
- to pass to POpen to start the server.
-
- """
- # start the Server
- print("Portal starting server ... ")
- process=None
- withopen(settings.SERVER_LOG_FILE,"a")aslogfile:
- # we link stdout to a file in order to catch
- # eventual errors happening before the Server has
- # opened its logger.
- try:
- if_is_windows():
- # Windows requires special care
- create_no_window=0x08000000
- process=Popen(
- server_twistd_cmd,
- env=getenv(),
- bufsize=-1,
- stdout=logfile,
- stderr=STDOUT,
- creationflags=create_no_window,
- )
-
- else:
- process=Popen(
- server_twistd_cmd,env=getenv(),bufsize=-1,stdout=logfile,stderr=STDOUT
- )
- exceptException:
- logger.log_trace()
-
- self.factory.portal.server_twistd_cmd=server_twistd_cmd
- logfile.flush()
- ifprocessandnot_is_windows():
- # avoid zombie-process on Unix/BSD
- process.wait()
- return
-
-
[docs]defwait_for_disconnect(self,callback,*args,**kwargs):
- """
- Add a callback for when this connection is lost.
-
- Args:
- callback (callable): Will be called with *args, **kwargs
- once this protocol is disconnected.
-
- """
- self.factory.disconnect_callbacks[self]=(callback,args,kwargs)
-
-
[docs]defwait_for_server_connect(self,callback,*args,**kwargs):
- """
- Add a callback for when the Server is sure to have connected.
-
- Args:
- callback (callable): Will be called with *args, **kwargs
- once the Server handshake with Portal is complete.
-
- """
- self.factory.server_connect_callbacks.append((callback,args,kwargs))
-
-
[docs]defstop_server(self,mode="shutdown"):
- """
- Shut down server in one or more modes.
-
- Args:
- mode (str): One of 'shutdown', 'reload' or 'reset'.
-
- """
- ifmode=="reload":
- self.send_AdminPortal2Server(amp.DUMMYSESSION,operation=amp.SRELOAD)
- elifmode=="reset":
- self.send_AdminPortal2Server(amp.DUMMYSESSION,operation=amp.SRESET)
- elifmode=="shutdown":
- self.send_AdminPortal2Server(amp.DUMMYSESSION,operation=amp.SSHUTD)
- self.factory.portal.server_restart_mode=mode
-
- # sending amp data
-
-
[docs]defsend_Status2Launcher(self):
- """
- Send a status stanza to the launcher.
-
- """
- # print("send status to launcher")
- # print("self.get_status(): {}".format(self.get_status()))
- ifself.factory.launcher_connection:
- self.factory.launcher_connection.callRemote(
- amp.MsgStatus,status=amp.dumps(self.get_status())
- ).addErrback(self.errback,amp.MsgStatus.key)
-
-
[docs]defsend_MsgPortal2Server(self,session,**kwargs):
- """
- Access method called by the Portal and executed on the Portal.
-
- Args:
- session (session): Session
- kwargs (any, optional): Optional data.
-
- Returns:
- deferred (Deferred): Asynchronous return.
-
- """
- returnself.data_to_server(amp.MsgPortal2Server,session.sessid,**kwargs)
-
-
[docs]defsend_AdminPortal2Server(self,session,operation="",**kwargs):
- """
- Send Admin instructions from the Portal to the Server.
- Executed on the Portal.
-
- Args:
- session (Session): Session.
- operation (char, optional): Identifier for the server operation, as defined by the
- global variables in `evennia/server/amp.py`.
- data (str or dict, optional): Data used in the administrative operation.
-
- """
- returnself.data_to_server(
- amp.AdminPortal2Server,session.sessid,operation=operation,**kwargs
- )
-
- # receive amp data
-
- @amp.MsgStatus.responder
- @amp.catch_traceback
- defportal_receive_status(self,status):
- """
- Returns run-status for the server/portal.
-
- Args:
- status (str): Not used.
- Returns:
- status (dict): The status is a tuple
- (portal_running, server_running, portal_pid, server_pid).
-
- """
- # print('Received PSTATUS request')
- return{"status":amp.dumps(self.get_status())}
-
- @amp.MsgLauncher2Portal.responder
- @amp.catch_traceback
- defportal_receive_launcher2portal(self,operation,arguments):
- """
- Receives message arriving from evennia_launcher.
- This method is executed on the Portal.
-
- Args:
- operation (str): The action to perform.
- arguments (str): Possible argument to the instruction, or the empty string.
-
- Returns:
- result (dict): The result back to the launcher.
-
- Notes:
- This is the entrypoint for controlling the entire Evennia system from the evennia
- launcher. It can obviously only accessed when the Portal is already up and running.
-
- """
- # Since the launcher command uses amp.String() we need to convert from byte here.
- operation=str(operation,"utf-8")
- self.factory.launcher_connection=self
- _,server_connected,_,_,_,_=self.get_status()
-
- # logger.log_msg("Evennia Launcher->Portal operation %s:%s received" % (ord(operation), arguments))
-
- # logger.log_msg("operation == amp.SSTART: {}: {}".format(operation == amp.SSTART, amp.loads(arguments)))
-
- ifoperation==amp.SSTART:# portal start #15
- # first, check if server is already running
- ifnotserver_connected:
- self.wait_for_server_connect(self.send_Status2Launcher)
- self.start_server(amp.loads(arguments))
-
- elifoperation==amp.SRELOAD:# reload server #14
- ifserver_connected:
- # We let the launcher restart us once they get the signal
- self.factory.server_connection.wait_for_disconnect(self.send_Status2Launcher)
- self.stop_server(mode="reload")
- else:
- self.wait_for_server_connect(self.send_Status2Launcher)
- self.start_server(amp.loads(arguments))
-
- elifoperation==amp.SRESET:# reload server #19
- ifserver_connected:
- self.factory.server_connection.wait_for_disconnect(self.send_Status2Launcher)
- self.stop_server(mode="reset")
- else:
- self.wait_for_server_connect(self.send_Status2Launcher)
- self.start_server(amp.loads(arguments))
-
- elifoperation==amp.SSHUTD:# server-only shutdown #17
- ifserver_connected:
- self.factory.server_connection.wait_for_disconnect(self.send_Status2Launcher)
- self.stop_server(mode="shutdown")
-
- elifoperation==amp.PSHUTD:# portal + server shutdown #16
- ifserver_connected:
- self.factory.server_connection.wait_for_disconnect(self.factory.portal.shutdown)
- else:
- self.factory.portal.shutdown()
-
- else:
- logger.log_err("Operation {} not recognized".format(operation))
- raiseException("operation %(op)s not recognized."%{"op":operation})
-
- return{}
-
- @amp.MsgServer2Portal.responder
- @amp.catch_traceback
- defportal_receive_server2portal(self,packed_data):
- """
- Receives message arriving to Portal from Server.
- This method is executed on the Portal.
-
- Args:
- packed_data (str): Pickled data (sessid, kwargs) coming over the wire.
-
- """
- try:
- sessid,kwargs=self.data_in(packed_data)
- session=self.factory.portal.sessions.get(sessid,None)
- ifsession:
- self.factory.portal.sessions.data_out(session,**kwargs)
- exceptException:
- logger.log_trace("packed_data len {}".format(len(packed_data)))
- return{}
-
- @amp.AdminServer2Portal.responder
- @amp.catch_traceback
- defportal_receive_adminserver2portal(self,packed_data):
- """
-
- Receives and handles admin operations sent to the Portal
- This is executed on the Portal.
-
- Args:
- packed_data (str): Data received, a pickled tuple (sessid, kwargs).
-
- """
- self.factory.server_connection=self
-
- sessid,kwargs=self.data_in(packed_data)
-
- # logger.log_msg("Evennia Server->Portal admin data %s:%s received" % (sessid, kwargs))
-
- operation=kwargs.pop("operation")
- portal_sessionhandler=self.factory.portal.sessions
-
- ifoperation==amp.SLOGIN:# server_session_login
- # a session has authenticated; sync it.
- session=portal_sessionhandler.get(sessid)
- ifsession:
- portal_sessionhandler.server_logged_in(session,kwargs.get("sessiondata"))
-
- elifoperation==amp.SDISCONN:# server_session_disconnect
- # the server is ordering to disconnect the session
- session=portal_sessionhandler.get(sessid)
- ifsession:
- portal_sessionhandler.server_disconnect(session,reason=kwargs.get("reason"))
-
- elifoperation==amp.SDISCONNALL:# server_session_disconnect_all
- # server orders all sessions to disconnect
- portal_sessionhandler.server_disconnect_all(reason=kwargs.get("reason"))
-
- elifoperation==amp.SRELOAD:# server reload
- self.factory.server_connection.wait_for_disconnect(
- self.start_server,self.factory.portal.server_twistd_cmd
- )
- self.stop_server(mode="reload")
-
- elifoperation==amp.SRESET:# server reset
- self.factory.server_connection.wait_for_disconnect(
- self.start_server,self.factory.portal.server_twistd_cmd
- )
- self.stop_server(mode="reset")
-
- elifoperation==amp.SSHUTD:# server-only shutdown
- self.stop_server(mode="shutdown")
-
- elifoperation==amp.PSHUTD:# full server+server shutdown
- self.factory.server_connection.wait_for_disconnect(self.factory.portal.shutdown)
- self.stop_server(mode="shutdown")
-
- elifoperation==amp.PSYNC:# portal sync
- # Server has (re-)connected and wants the session data from portal
- self.factory.portal.server_info_dict=kwargs.get("info_dict",{})
- self.factory.portal.server_process_id=kwargs.get("spid",None)
- # this defaults to 'shutdown' or whatever value set in server_stop
- server_restart_mode=self.factory.portal.server_restart_mode
-
- sessdata=self.factory.portal.sessions.get_all_sync_data()
- self.send_AdminPortal2Server(
- amp.DUMMYSESSION,
- amp.PSYNC,
- server_restart_mode=server_restart_mode,
- sessiondata=sessdata,
- portal_start_time=self.factory.portal.start_time,
- )
- self.factory.portal.sessions.at_server_connection()
-
- ifself.factory.server_connection:
- # this is an indication the server has successfully connected, so
- # we trigger any callbacks (usually to tell the launcher server is up)
- forcallback,args,kwargsinself.factory.server_connect_callbacks:
- try:
- callback(*args,**kwargs)
- exceptException:
- logger.log_trace()
- self.factory.server_connect_callbacks=[]
-
- elifoperation==amp.SSYNC:# server_session_sync
- # server wants to save session data to the portal,
- # maybe because it's about to shut down.
- portal_sessionhandler.server_session_sync(
- kwargs.get("sessiondata"),kwargs.get("clean",True)
- )
-
- # set a flag in case we are about to shut down soon
- self.factory.server_restart_mode=True
-
- elifoperation==amp.SCONN:# server_force_connection (for irc/etc)
- portal_sessionhandler.server_connect(**kwargs)
-
- else:
- raiseException("operation %(op)s not recognized."%{"op":operation})
- return{}
-"""
-Grapevine network connection
-
-This is an implementation of the Grapevine Websocket protocol v 1.0.0 as
-outlined here: https://grapevine.haus/docs
-
-This will allow the linked game to transfer status as well as connects
-the grapevine client to in-game channels.
-
-"""
-
-importjson
-fromtwisted.internetimportprotocol
-fromdjango.confimportsettings
-fromevennia.server.sessionimportSession
-fromevennia.utilsimportget_evennia_version
-fromevennia.utils.loggerimportlog_info,log_err
-fromautobahn.twisted.websocketimportWebSocketClientProtocol,WebSocketClientFactory,connectWS
-
-# There is only one at this time
-GRAPEVINE_URI="wss://grapevine.haus/socket"
-
-GRAPEVINE_CLIENT_ID=settings.GRAPEVINE_CLIENT_ID
-GRAPEVINE_CLIENT_SECRET=settings.GRAPEVINE_CLIENT_SECRET
-GRAPEVINE_CHANNELS=settings.GRAPEVINE_CHANNELS
-
-# defined error codes
-CLOSE_NORMAL=1000
-GRAPEVINE_AUTH_ERROR=4000
-GRAPEVINE_HEARTBEAT_FAILURE=4001
-
-
-
[docs]classRestartingWebsocketServerFactory(WebSocketClientFactory,protocol.ReconnectingClientFactory):
- """
- A variant of the websocket-factory that auto-reconnects.
-
- """
-
- initialDelay=1
- factor=1.5
- maxDelay=60
-
-
[docs]defbuildProtocol(self,addr):
- """
- Build new instance of protocol
-
- Args:
- addr (str): Not used, using factory/settings data
-
- """
- protocol=GrapevineClient()
- protocol.factory=self
- protocol.channel=self.channel
- protocol.sessionhandler=self.sessionhandler
- returnprotocol
-
-
[docs]defstartedConnecting(self,connector):
- """
- Tracks reconnections for debugging.
-
- Args:
- connector (Connector): Represents the connection.
-
- """
- log_info("(re)connecting to grapevine channel '%s'"%self.channel)
-
-
[docs]defclientConnectionFailed(self,connector,reason):
- """
- Called when Client failed to connect.
-
- Args:
- connector (Connection): Represents the connection.
- reason (str): The reason for the failure.
-
- """
- protocol.ReconnectingClientFactory.clientConnectionLost(self,connector,reason)
-
-
[docs]defclientConnectionLost(self,connector,reason):
- """
- Called when Client loses connection.
-
- Args:
- connector (Connection): Represents the connection.
- reason (str): The reason for the failure.
-
- """
- ifnot(self.botor(self.botandself.bot.stopping)):
- self.retry(connector)
-
-
[docs]defreconnect(self):
- """
- Force a reconnection of the bot protocol. This requires
- de-registering the session and then reattaching a new one,
- otherwise you end up with an ever growing number of bot
- sessions.
-
- """
- self.bot.stopping=True
- self.bot.transport.loseConnection()
- self.sessionhandler.server_disconnect(self.bot)
- self.start()
-
-
[docs]defstart(self):
- "Connect protocol to remote server"
-
- try:
- fromtwisted.internetimportssl
- exceptImportError:
- log_err("To use Grapevine, The PyOpenSSL module must be installed.")
- else:
- context_factory=ssl.ClientContextFactory()ifself.isSecureelseNone
- connectWS(self,context_factory)
[docs]defonMessage(self,payload,isBinary):
- """
- Callback fired when a complete WebSocket message was received.
-
- Args:
- payload (bytes): The WebSocket message received.
- isBinary (bool): Flag indicating whether payload is binary or
- UTF-8 encoded text.
-
- """
- ifnotisBinary:
- data=json.loads(str(payload,"utf-8"))
- self.data_in(data=data)
- self.retry_task=None
-
-
[docs]defonClose(self,wasClean,code=None,reason=None):
- """
- This is executed when the connection is lost for whatever
- reason. it can also be called directly, from the disconnect
- method.
-
- Args:
- wasClean (bool): ``True`` if the WebSocket was closed cleanly.
- code (int or None): Close status as sent by the WebSocket peer.
- reason (str or None): Close reason as sent by the WebSocket peer.
-
- """
- self.disconnect(reason)
-
- ifcode==GRAPEVINE_HEARTBEAT_FAILURE:
- log_err("Grapevine connection lost (Heartbeat error)")
- elifcode==GRAPEVINE_AUTH_ERROR:
- log_err("Grapevine connection lost (Auth error)")
- elifself.restart_downtime:
- # server previously warned us about downtime and told us to be
- # ready to reconnect.
- log_info("Grapevine connection lost (Server restart).")
-
- def_send_json(self,data):
- """
- Send (json-) data to client.
-
- Args:
- data (str): Text to send.
-
- """
- returnself.sendMessage(json.dumps(data).encode("utf-8"))
-
-
[docs]defdisconnect(self,reason=None):
- """
- Generic hook for the engine to call in order to
- disconnect this protocol.
-
- Args:
- reason (str or None): Motivation for the disconnection.
-
- """
- self.sessionhandler.disconnect(self)
- # autobahn-python: 1000 for a normal close, 3000-4999 for app. specific,
- # in case anyone wants to expose this functionality later.
- #
- # sendClose() under autobahn/websocket/interfaces.py
- self.sendClose(CLOSE_NORMAL,reason)
-
- # send_* method are automatically callable through .msg(heartbeat={}) etc
-
-
[docs]defsend_subscribe(self,channelname,*args,**kwargs):
- """
- Subscribe to new grapevine channel
-
- Use with session.msg(subscribe="channelname")
- """
- data={"event":"channels/subscribe","payload":{"channel":channelname}}
- self._send_json(data)
-
-
[docs]defsend_unsubscribe(self,channelname,*args,**kwargs):
- """
- Un-subscribe to a grapevine channel
-
- Use with session.msg(unsubscribe="channelname")
- """
- data={"event":"channels/unsubscribe","payload":{"channel":channelname}}
- self._send_json(data)
-
-
[docs]defsend_channel(self,text,channel,sender,*args,**kwargs):
- """
- Send text type Evennia -> grapevine
-
- This is the channels/send message type
-
- Use with session.msg(channel=(message, channel, sender))
-
- """
-
- data={
- "event":"channels/send",
- "payload":{"message":text,"channel":channel,"name":sender},
- }
- self._send_json(data)
[docs]defparse_irc_to_ansi(string):
- """
- Parse IRC mIRC color syntax and replace with Evennia ANSI color markers
-
- Args:
- string (str): String to parse for IRC colors.
-
- Returns:
- parsed_string (str): String with replaced IRC colors.
-
- """
-
- def_sub_to_ansi(irc_match):
- returnANSI_COLOR_MAP.get(irc_match.group(),"")
-
- in_string=utils.to_str(string)
- pstring=RE_IRC_COLOR.sub(_sub_to_ansi,in_string)
- returnpstring
-
-
-# IRC bot
-
-
-
[docs]classIRCBot(irc.IRCClient,Session):
- """
- An IRC bot that tracks activity in a channel as well
- as sends text to it when prompted
-
- """
-
- lineRate=1
-
- # assigned by factory at creation
-
- nickname=None
- logger=None
- factory=None
- channel=None
- sourceURL="http://code.evennia.com"
-
-
[docs]defsignedOn(self):
- """
- This is called when we successfully connect to the network. We
- make sure to now register with the game as a full session.
-
- """
- self.join(self.channel)
- self.stopping=False
- self.factory.bot=self
- address="%s@%s"%(self.channel,self.network)
- self.init_session("ircbot",address,self.factory.sessionhandler)
- # we link back to our bot and log in
- self.uid=int(self.factory.uid)
- self.logged_in=True
- self.factory.sessionhandler.connect(self)
- logger.log_info(
- "IRC bot '%s' connected to %s at %s:%s."
- %(self.nickname,self.channel,self.network,self.port)
- )
-
-
[docs]defdisconnect(self,reason=""):
- """
- Called by sessionhandler to disconnect this protocol.
-
- Args:
- reason (str): Motivation for the disconnect.
-
- """
- self.sessionhandler.disconnect(self)
- self.stopping=True
- self.transport.loseConnection()
[docs]defprivmsg(self,user,channel,msg):
- """
- Called when the connected channel receives a message.
-
- Args:
- user (str): User name sending the message.
- channel (str): Channel name seeing the message.
- msg (str): The message arriving from channel.
-
- """
- ifchannel==self.nickname:
- # private message
- user=user.split("!",1)[0]
- self.data_in(text=msg,type="privmsg",user=user,channel=channel)
- elifnotmsg.startswith("***"):
- # channel message
- user=user.split("!",1)[0]
- user=ansi.raw(user)
- self.data_in(text=msg,type="msg",user=user,channel=channel)
-
-
[docs]defaction(self,user,channel,msg):
- """
- Called when an action is detected in channel.
-
- Args:
- user (str): User name sending the message.
- channel (str): Channel name seeing the message.
- msg (str): The message arriving from channel.
-
- """
- ifnotmsg.startswith("**"):
- user=user.split("!",1)[0]
- self.data_in(text=msg,type="action",user=user,channel=channel)
-
-
[docs]defget_nicklist(self):
- """
- Retrieve name list from the channel. The return
- is handled by the catch methods below.
-
- """
- ifnotself.nicklist:
- self.sendLine("NAMES %s"%self.channel)
[docs]defirc_RPL_ENDOFNAMES(self,prefix,params):
- """Called when the nicklist has finished being returned."""
- channel=params[1].lower()
- ifchannel!=self.channel.lower():
- return
- self.data_in(
- text="",type="nicklist",user="server",channel=channel,nicklist=self.nicklist
- )
- self.nicklist=[]
-
-
[docs]defpong(self,user,time):
- """
- Called with the return timing from a PING.
-
- Args:
- user (str): Name of user
- time (float): Ping time in secs.
-
- """
- self.data_in(text="",type="ping",user="server",channel=self.channel,timing=time)
-
-
[docs]defdata_in(self,text=None,**kwargs):
- """
- Data IRC -> Server.
-
- Keyword Args:
- text (str): Ingoing text.
- kwargs (any): Other data from protocol.
-
- """
- self.sessionhandler.data_in(self,bot_data_in=[parse_irc_to_ansi(text),kwargs])
-
-
[docs]defsend_channel(self,*args,**kwargs):
- """
- Send channel text to IRC channel (visible to all). Note that
- we don't handle the "text" send (it's rerouted to send_default
- which does nothing) - this is because the IRC bot is a normal
- session and would otherwise report anything that happens to it
- to the IRC channel (such as it seeing server reload messages).
-
- Args:
- text (str): Outgoing text
-
- """
- text=args[0]ifargselse""
- iftext:
- text=parse_ansi_to_irc(text)
- self.say(self.channel,text)
-
-
[docs]defsend_privmsg(self,*args,**kwargs):
- """
- Send message only to specific user.
-
- Args:
- text (str): Outgoing text.
-
- Keyword Args:
- user (str): the nick to send
- privately to.
-
- """
- text=args[0]ifargselse""
- user=kwargs.get("user",None)
- iftextanduser:
- text=parse_ansi_to_irc(text)
- self.msg(user,text)
-
-
[docs]defsend_request_nicklist(self,*args,**kwargs):
- """
- Send a request for the channel nicklist. The return (handled
- by `self.irc_RPL_ENDOFNAMES`) will be sent back as a message
- with type `nicklist'.
- """
- self.get_nicklist()
-
-
[docs]defsend_ping(self,*args,**kwargs):
- """
- Send a ping. The return (handled by `self.pong`) will be sent
- back as a message of type 'ping'.
- """
- self.ping(self.nickname)
-
-
[docs]defsend_reconnect(self,*args,**kwargs):
- """
- The server instructs us to rebuild the connection by force,
- probably because the client silently lost connection.
- """
- self.factory.reconnect()
-
-
[docs]defsend_default(self,*args,**kwargs):
- """
- Ignore other types of sends.
-
- """
- pass
-
-
-
[docs]classIRCBotFactory(protocol.ReconnectingClientFactory):
- """
- Creates instances of IRCBot, connecting with a staggered
- increase in delay
-
- """
-
- # scaling reconnect time
- initialDelay=1
- factor=1.5
- maxDelay=60
-
-
[docs]def__init__(
- self,
- sessionhandler,
- uid=None,
- botname=None,
- channel=None,
- network=None,
- port=None,
- ssl=None,
- ):
- """
- Storing some important protocol properties.
-
- Args:
- sessionhandler (SessionHandler): Reference to the main Sessionhandler.
-
- Keyword Args:
- uid (int): Bot user id.
- botname (str): Bot name (seen in IRC channel).
- channel (str): IRC channel to connect to.
- network (str): Network address to connect to.
- port (str): Port of the network.
- ssl (bool): Indicates SSL connection.
-
- """
- self.sessionhandler=sessionhandler
- self.uid=uid
- self.nickname=str(botname)
- self.channel=str(channel)
- self.network=str(network)
- self.port=port
- self.ssl=ssl
- self.bot=None
- self.nicklists={}
-
-
[docs]defbuildProtocol(self,addr):
- """
- Build the protocol and assign it some properties.
-
- Args:
- addr (str): Not used; using factory data.
-
- """
- protocol=IRCBot()
- protocol.factory=self
- protocol.nickname=self.nickname
- protocol.channel=self.channel
- protocol.network=self.network
- protocol.port=self.port
- protocol.ssl=self.ssl
- protocol.nicklist=[]
- returnprotocol
-
-
[docs]defstartedConnecting(self,connector):
- """
- Tracks reconnections for debugging.
-
- Args:
- connector (Connector): Represents the connection.
-
- """
- logger.log_info("(re)connecting to %s"%self.channel)
-
-
[docs]defclientConnectionFailed(self,connector,reason):
- """
- Called when Client failed to connect.
-
- Args:
- connector (Connection): Represents the connection.
- reason (str): The reason for the failure.
-
- """
- self.retry(connector)
-
-
[docs]defclientConnectionLost(self,connector,reason):
- """
- Called when Client loses connection.
-
- Args:
- connector (Connection): Represents the connection.
- reason (str): The reason for the failure.
-
- """
- ifnot(self.botor(self.botandself.bot.stopping)):
- self.retry(connector)
-
-
[docs]defreconnect(self):
- """
- Force a reconnection of the bot protocol. This requires
- de-registering the session and then reattaching a new one,
- otherwise you end up with an ever growing number of bot
- sessions.
-
- """
- self.bot.stopping=True
- self.bot.transport.loseConnection()
- self.sessionhandler.server_disconnect(self.bot)
- self.start()
-
-
[docs]defstart(self):
- """
- Connect session to sessionhandler.
-
- """
- ifself.port:
- ifself.ssl:
- try:
- fromtwisted.internetimportssl
-
- service=reactor.connectSSL(
- self.network,int(self.port),self,ssl.ClientContextFactory()
- )
- exceptImportError:
- logger.log_err("To use SSL, the PyOpenSSL module must be installed.")
- else:
- service=internet.TCPClient(self.network,int(self.port),self)
- self.sessionhandler.portal.services.addService(service)
-"""
-
-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
-fromdjango.confimportsettings
-
-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
- ifsettings.MXP_ENABLED:
- 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.
-
- """
- ifsettings.MXP_ENABLED:
- self.protocol.protocol_flags["MXP"]=True
- self.protocol.requestNegotiation(MXP,b"")
- else:
- self.protocol.wont(MXP)
- 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)
-
Source code for evennia.server.portal.portalsessionhandler
-"""
-Sessionhandler for portal sessions.
-
-"""
-
-
-importtime
-fromcollectionsimportdeque,namedtuple
-fromtwisted.internetimportreactor
-fromdjango.confimportsettings
-fromevennia.server.sessionhandlerimportSessionHandler
-fromevennia.server.portal.ampimportPCONN,PDISCONN,PCONNSYNC,PDISCONNALL
-fromevennia.utils.loggerimportlog_trace
-fromevennia.utils.utilsimportclass_from_module
-fromdjango.utils.translationimportgettextas_
-
-# module import
-_MOD_IMPORT=None
-
-# global throttles
-_MAX_CONNECTION_RATE=float(settings.MAX_CONNECTION_RATE)
-# per-session throttles
-_MAX_COMMAND_RATE=float(settings.MAX_COMMAND_RATE)
-_MAX_CHAR_LIMIT=int(settings.MAX_CHAR_LIMIT)
-
-_MIN_TIME_BETWEEN_CONNECTS=1.0/float(_MAX_CONNECTION_RATE)
-_MIN_TIME_BETWEEN_COMMANDS=1.0/float(_MAX_COMMAND_RATE)
-
-_ERROR_COMMAND_OVERFLOW=settings.COMMAND_RATE_WARNING
-_ERROR_MAX_CHAR=settings.MAX_CHAR_LIMIT_WARNING
-
-_CONNECTION_QUEUE=deque()
-
-DUMMYSESSION=namedtuple("DummySession",["sessid"])(0)
-
-# -------------------------------------------------------------
-# Portal-SessionHandler class
-# -------------------------------------------------------------
-
-DOS_PROTECTION_MSG=_("{servername} DoS protection is active."
- "You are queued to connect in {num} seconds ...")
-
-
-
[docs]classPortalSessionHandler(SessionHandler):
- """
- This object holds the sessions connected to the portal at any time.
- It is synced with the server's equivalent SessionHandler over the AMP
- connection.
-
- Sessions register with the handler using the connect() method. This
- will assign a new unique sessionid to the session and send that sessid
- to the server using the AMP connection.
-
- """
-
-
[docs]defat_server_connection(self):
- """
- Called when the Portal establishes connection with the Server.
- At this point, the AMP connection is already established.
-
- """
- self.connection_time=time.time()
-
-
[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
- authenticated session using an ever-increasing counter for
- sessid.
-
- Args:
- session (PortalSession): The Session connecting.
-
- Notes:
- We implement a throttling mechanism here to limit the speed at
- which new connections are accepted - this is both a stop
- against DoS attacks as well as helps using the Dummyrunner
- tester with a large number of connector dummies.
-
- """
- global_CONNECTION_QUEUE
-
- ifsession:
- # assign if we are first-connectors
- ifnotsession.sessid:
- # if the session already has a sessid (e.g. being inherited in the
- # case of a webclient auto-reconnect), keep it
- session.sessid=self.generate_sessid()
- session.server_connected=False
- _CONNECTION_QUEUE.appendleft(session)
- iflen(_CONNECTION_QUEUE)>1:
- session.data_out(
- text=(
- (DOS_PROTECTION_MSG.format(
- servername=settings.SERVERNAME,
- num=len(_CONNECTION_QUEUE)*_MIN_TIME_BETWEEN_CONNECTS),),
- {},
- )
- )
- now=time.time()
- if(
- now-self.connection_last<_MIN_TIME_BETWEEN_CONNECTS
- )ornotself.portal.amp_protocol:
- ifnotsessionornotself.connection_task:
- self.connection_task=reactor.callLater(
- _MIN_TIME_BETWEEN_CONNECTS,self.connect,None
- )
- self.connection_last=now
- return
- elifnotsession:
- if_CONNECTION_QUEUE:
- # keep launching tasks until queue is empty
- self.connection_task=reactor.callLater(
- _MIN_TIME_BETWEEN_CONNECTS,self.connect,None
- )
- else:
- self.connection_task=None
- self.connection_last=now
-
- if_CONNECTION_QUEUE:
- # sync with server-side
- session=_CONNECTION_QUEUE.pop()
- sessdata=session.get_sync_data()
-
- self[session.sessid]=session
- session.server_connected=True
- self.portal.amp_protocol.send_AdminPortal2Server(
- session,operation=PCONN,sessiondata=sessdata
- )
-
-
[docs]defsync(self,session):
- """
- Called by the protocol of an already connected session. This
- can be used to sync the session info in a delayed manner, such
- as when negotiation and handshakes are delayed.
-
- Args:
- session (PortalSession): Session to sync.
-
- """
- ifsession.sessidandsession.server_connected:
- # only use if session already has sessid and has already connected
- # once to the server - if so we must re-sync woth the server, otherwise
- # we skip this step.
- sessdata=session.get_sync_data()
- ifself.portal.amp_protocol:
- # we only send sessdata that should not have changed
- # at the server level at this point
- sessdata=dict(
- (key,val)
- forkey,valinsessdata.items()
- ifkey
- in(
- "protocol_key",
- "address",
- "sessid",
- "csessid",
- "conn_time",
- "protocol_flags",
- "server_data",
- )
- )
- self.portal.amp_protocol.send_AdminPortal2Server(
- session,operation=PCONNSYNC,sessiondata=sessdata
- )
-
-
[docs]defdisconnect(self,session):
- """
- Called from portal when the connection is closed from the
- portal side.
-
- Args:
- session (PortalSession): Session to disconnect.
- delete (bool, optional): Delete the session from
- the handler. Only time to not do this is when
- this is called from a loop, such as from
- self.disconnect_all().
-
- """
- global_CONNECTION_QUEUE
- ifsessionin_CONNECTION_QUEUE:
- # connection was already dropped before we had time
- # to forward this to the Server, so now we just remove it.
- _CONNECTION_QUEUE.remove(session)
- return
-
- ifsession.sessidinselfandnothasattr(self,"_disconnect_all"):
- # if this was called directly from the protocol, the
- # connection is already dead and we just need to cleanup
- delself[session.sessid]
-
- # Tell the Server to disconnect its version of the Session as well.
- self.portal.amp_protocol.send_AdminPortal2Server(session,operation=PDISCONN)
-
-
[docs]defdisconnect_all(self):
- """
- Disconnect all sessions, informing the Server.
-
- """
-
- def_callback(result,sessionhandler):
- # we set a watchdog to stop self.disconnect from deleting
- # sessions while we are looping over them.
- sessionhandler._disconnect_all=True
- forsessioninsessionhandler.values():
- session.disconnect()
- delsessionhandler._disconnect_all
-
- # inform Server; wait until finished sending before we continue
- # removing all the sessions.
- self.portal.amp_protocol.send_AdminPortal2Server(
- DUMMYSESSION,operation=PDISCONNALL
- ).addCallback(_callback,self)
-
-
[docs]defserver_connect(self,protocol_path="",config=dict()):
- """
- Called by server to force the initialization of a new protocol
- instance. Server wants this instance to get a unique sessid and to be
- connected back as normal. This is used to initiate irc/rss etc
- connections.
-
- Args:
- protocol_path (str): Full python path to the class factory
- for the protocol used, eg
- 'evennia.server.portal.irc.IRCClientFactory'
- config (dict): Dictionary of configuration options, fed as
- `**kwarg` to protocol class `__init__` method.
-
- Raises:
- RuntimeError: If The correct factory class is not found.
-
- Notes:
- The called protocol class must have a method start()
- that calls the portalsession.connect() as a normal protocol.
-
- """
- global_MOD_IMPORT
- ifnot_MOD_IMPORT:
- fromevennia.utils.utilsimportvariable_from_moduleas_MOD_IMPORT
- path,clsname=protocol_path.rsplit(".",1)
- cls=_MOD_IMPORT(path,clsname)
- ifnotcls:
- raiseRuntimeError("ServerConnect: protocol factory '%s' not found."%protocol_path)
- protocol=cls(self,**config)
- protocol.start()
-
-
[docs]defserver_disconnect(self,session,reason=""):
- """
- Called by server to force a disconnect by sessid.
-
- Args:
- session (portalsession): Session to disconnect.
- reason (str, optional): Motivation for disconnect.
-
- """
- ifsession:
- session.disconnect(reason)
- ifsession.sessidinself:
- # in case sess.disconnect doesn't delete it
- delself[session.sessid]
- delsession
-
-
[docs]defserver_disconnect_all(self,reason=""):
- """
- Called by server when forcing a clean disconnect for everyone.
-
- Args:
- reason (str, optional): Motivation for disconnect.
-
- """
- forsessioninlist(self.values()):
- session.disconnect(reason)
- delsession
- self.clear()
-
-
[docs]defserver_logged_in(self,session,data):
- """
- The server tells us that the session has been authenticated.
- Update it. Called by the Server.
-
- Args:
- session (Session): Session logging in.
- data (dict): The session sync data.
-
- """
- session.load_sync_data(data)
- session.at_login()
-
-
[docs]defserver_session_sync(self,serversessions,clean=True):
- """
- Server wants to save data to the portal, maybe because it's
- about to shut down. We don't overwrite any sessions here, just
- update them in-place.
-
- Args:
- serversessions (dict): This is a dictionary
-
- `{sessid:{property:value},...}` describing
- the properties to sync on all sessions.
- clean (bool): If True, remove any Portal sessions that are
- not included in serversessions.
- """
- to_save=[sessidforsessidinserversessionsifsessidinself]
- # save protocols
- forsessidinto_save:
- self[sessid].load_sync_data(serversessions[sessid])
- ifclean:
- # disconnect out-of-sync missing protocols
- to_delete=[sessidforsessidinselfifsessidnotinto_save]
- forsessidinto_delete:
- self.server_disconnect(sessid)
-
-
[docs]defcount_loggedin(self,include_unloggedin=False):
- """
- Count loggedin connections, alternatively count all connections.
-
- Args:
- include_unloggedin (bool): Also count sessions that have
- not yet authenticated.
-
- Returns:
- count (int): Number of sessions.
-
- """
- returnlen(self.get_sessions(include_unloggedin=include_unloggedin))
-
-
[docs]defsessions_from_csessid(self,csessid):
- """
- Given a session id, retrieve the session (this is primarily
- intended to be called by web clients)
-
- Args:
- csessid (int): Session id.
-
- Returns:
- session (list): The matching session, if found.
-
- """
- return[
- sess
- forsessinself.get_sessions(include_unloggedin=True)
- ifhasattr(sess,"csessid")andsess.csessidandsess.csessid==csessid
- ]
-
-
[docs]defannounce_all(self,message):
- """
- Send message to all connected sessions.
-
- Args:
- message (str): Message to relay.
-
- Notes:
- This will create an on-the fly text-type
- send command.
-
- """
- forsessioninself.values():
- self.data_out(session,text=[[message],{}])
-
-
[docs]defdata_in(self,session,**kwargs):
- """
- Called by portal sessions for relaying data coming
- in from the protocol to the server.
-
- Args:
- session (PortalSession): Session receiving data.
-
- Keyword Args:
- kwargs (any): Other data from protocol.
-
- Notes:
- Data is serialized before passed on.
-
- """
- try:
- text=kwargs["text"]
- if(_MAX_CHAR_LIMIT>0)andlen(text)>_MAX_CHAR_LIMIT:
- ifsession:
- self.data_out(session,text=[[_ERROR_MAX_CHAR],{}])
- return
- exceptException:
- # if there is a problem to send, we continue
- pass
- ifsession:
- now=time.time()
-
- try:
- command_counter_reset=session.command_counter_reset
- exceptAttributeError:
- command_counter_reset=session.command_counter_reset=now
- session.command_counter=0
-
- # global command-rate limit
- ifmax(0,now-command_counter_reset)>1.0:
- # more than a second since resetting the counter. Refresh.
- session.command_counter_reset=now
- session.command_counter=0
-
- session.command_counter+=1
-
- ifsession.command_counter*_MIN_TIME_BETWEEN_COMMANDS>1.0:
- self.data_out(session,text=[[_ERROR_COMMAND_OVERFLOW],{}])
- return
-
- ifnotself.portal.amp_protocol:
- # this can happen if someone connects before AMP connection
- # was established (usually on first start)
- reactor.callLater(1.0,self.data_in,session,**kwargs)
- return
-
- # scrub data
- kwargs=self.clean_senddata(session,kwargs)
-
- # relay data to Server
- session.cmd_last=now
- self.portal.amp_protocol.send_MsgPortal2Server(session,**kwargs)
-
- # eventual local echo (text input only)
- if'text'inkwargsandsession.protocol_flags.get('LOCALECHO',False):
- self.data_out(session,text=kwargs['text'])
-
-
[docs]defdata_out(self,session,**kwargs):
- """
- Called by server for having the portal relay messages and data
- to the correct session protocol.
-
- Args:
- session (Session): Session sending data.
-
- Keyword Args:
- kwargs (any): Each key is a command instruction to the
- protocol on the form key = [[args],{kwargs}]. This will
- call a method send_<key> on the protocol. If no such
- method exixts, it sends the data to a method send_default.
-
- """
- # from evennia.server.profiling.timetrace import timetrace # DEBUG
- # text = timetrace(text, "portalsessionhandler.data_out") # DEBUG
-
- # distribute outgoing data to the correct session methods.
- ifsession:
- forcmdname,(cmdargs,cmdkwargs)inkwargs.items():
- funcname="send_%s"%cmdname.strip().lower()
- ifhasattr(session,funcname):
- # better to use hassattr here over try..except
- # - avoids hiding AttributeErrors in the call.
- try:
- getattr(session,funcname)(*cmdargs,**cmdkwargs)
- exceptException:
- log_trace()
- else:
- try:
- # note that send_default always takes cmdname
- # as arg too.
- session.send_default(cmdname,*cmdargs,**cmdkwargs)
- exceptException:
- log_trace()
-"""
-RSS parser for Evennia
-
-This connects an RSS feed to an in-game Evennia channel, sending messages
-to the channel whenever the feed updates.
-
-"""
-fromtwisted.internetimporttask,threads
-fromdjango.confimportsettings
-fromevennia.server.sessionimportSession
-fromevennia.utilsimportlogger
-
-RSS_ENABLED=settings.RSS_ENABLED
-# RETAG = re.compile(r'<[^>]*?>')
-
-ifRSS_ENABLED:
- try:
- importfeedparser
- exceptImportError:
- raiseImportError(
- "RSS requires python-feedparser to be installed. Install or set RSS_ENABLED=False."
- )
-
-
-
[docs]classRSSReader(Session):
- """
- A simple RSS reader using the feedparser module.
-
- """
-
-
[docs]def__init__(self,factory,url,rate):
- """
- Initialize the reader.
-
- Args:
- factory (RSSFactory): The protocol factory.
- url (str): The RSS url.
- rate (int): The seconds between RSS lookups.
-
- """
- self.url=url
- self.rate=rate
- self.factory=factory
- self.old_entries={}
-
-
[docs]defget_new(self):
- """
- Returns list of new items.
-
- """
- feed=feedparser.parse(self.url)
- new_entries=[]
- forentryinfeed["entries"]:
- idval=entry["id"]+entry.get("updated","")
- ifidvalnotinself.old_entries:
- self.old_entries[idval]=entry
- new_entries.append(entry)
- returnnew_entries
-
-
[docs]defdisconnect(self,reason=None):
- """
- Disconnect from feed.
-
- Args:
- reason (str, optional): Motivation for the disconnect.
-
- """
- ifself.factory.taskandself.factory.task.running:
- self.factory.task.stop()
- self.sessionhandler.disconnect(self)
-
- def_callback(self,new_entries,init):
- """
- Called when RSS returns.
-
- Args:
- new_entries (list): List of new RSS entries since last.
- init (bool): If this is a startup operation (at which
- point all entries are considered new).
-
- """
- ifnotinit:
- # for initialization we just ignore old entries
- forentryinreversed(new_entries):
- self.data_in(entry)
-
-
[docs]defdata_in(self,text=None,**kwargs):
- """
- Data RSS -> Evennia.
-
- Keyword Args:
- text (str): Incoming text
- kwargs (any): Options from protocol.
-
- """
- self.sessionhandler.data_in(self,bot_data_in=text,**kwargs)
[docs]defupdate(self,init=False):
- """
- Request the latest version of feed.
-
- Args:
- init (bool, optional): If this is an initialization call
- or not (during init, all entries are conidered new).
-
- Notes:
- This call is done in a separate thread to avoid blocking
- on slow connections.
-
- """
- return(
- threads.deferToThread(self.get_new)
- .addCallback(self._callback,init)
- .addErrback(self._errback)
- )
[docs]def__init__(self,sessionhandler,uid=None,url=None,rate=None):
- """
- Initialize the bot.
-
- Args:
- sessionhandler (PortalSessionHandler): The main sessionhandler object.
- uid (int): User id for the bot.
- url (str): The RSS URL.
- rate (int): How often for the RSS to request the latest RSS entries.
-
- """
- self.sessionhandler=sessionhandler
- self.url=url
- self.rate=rate
- self.uid=uid
- self.bot=RSSReader(self,url,rate)
- self.task=None
-
-
[docs]defstart(self):
- """
- Called by portalsessionhandler. Starts the bot.
-
- """
-
- deferrback(fail):
- logger.log_err(fail.value)
-
- # set up session and connect it to sessionhandler
- self.bot.init_session("rssbot",self.url,self.sessionhandler)
- self.bot.uid=self.uid
- self.bot.logged_in=True
- self.sessionhandler.connect(self.bot)
-
- # start repeater task
- self.bot.update(init=True)
- self.task=task.LoopingCall(self.bot.update)
- ifself.rate:
- self.task.start(self.rate,now=False).addErrback(errback)
-"""
-This module implements the ssh (Secure SHell) protocol for encrypted
-connections.
-
-This depends on a generic session module that implements the actual
-login procedure of the game, tracks sessions etc.
-
-Using standard ssh client,
-
-"""
-
-importos
-importre
-
-fromtwisted.cred.checkersimportcredentials
-fromtwisted.cred.portalimportPortal
-fromtwisted.conch.interfacesimportIConchUser
-
-_SSH_IMPORT_ERROR="""
-ERROR: Missing crypto library for SSH. Install it with
-
- pip install cryptography pyasn1 bcrypt
-
-(On older Twisted versions you may have to do 'pip install pycrypto pyasn1' instead).
-
-If you get a compilation error you must install a C compiler and the
-SSL dev headers (On Debian-derived systems this is the gcc and libssl-dev
-packages).
-"""
-
-try:
- fromtwisted.conch.ssh.keysimportKey
-exceptImportError:
- raiseImportError(_SSH_IMPORT_ERROR)
-
-fromtwisted.conch.ssh.userauthimportSSHUserAuthServer
-fromtwisted.conch.sshimportcommon
-fromtwisted.conch.insultsimportinsults
-fromtwisted.conch.manhole_sshimportTerminalRealm,_Glue,ConchFactory
-fromtwisted.conch.manholeimportManhole,recvline
-fromtwisted.internetimportdefer,protocol
-fromtwisted.conchimportinterfacesasiconch
-fromtwisted.pythonimportcomponents
-fromdjango.confimportsettings
-
-fromevennia.accounts.modelsimportAccountDB
-fromevennia.utilsimportansi
-fromevennia.utils.utilsimportto_str,class_from_module
-
-_RE_N=re.compile(r"\|n$")
-_RE_SCREENREADER_REGEX=re.compile(
- r"%s"%settings.SCREENREADER_REGEX_STRIP,re.DOTALL+re.MULTILINE
-)
-_GAME_DIR=settings.GAME_DIR
-_PRIVATE_KEY_FILE=os.path.join(_GAME_DIR,"server","ssh-private.key")
-_PUBLIC_KEY_FILE=os.path.join(_GAME_DIR,"server","ssh-public.key")
-_KEY_LENGTH=2048
-
-CTRL_C="\x03"
-CTRL_D="\x04"
-CTRL_BACKSLASH="\x1c"
-CTRL_L="\x0c"
-
-_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:
-{_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
-
- """
- 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
- here.
-
- """
-
- noisy=False
-
-
[docs]def__init__(self,starttuple):
- """
- For setting up the account. If account is not None then we'll
- login automatically.
-
- Args:
- starttuple (tuple): A (account, factory) tuple.
-
- """
- self.protocol_key="ssh"
- self.authenticated_account=starttuple[0]
- # obs must not be called self.factory, that gets overwritten!
- self.cfactory=starttuple[1]
-
-
[docs]defterminalSize(self,width,height):
- """
- Initialize the terminal and connect to the new session.
-
- Args:
- width (int): Width of terminal.
- height (int): Height of terminal.
-
- """
- # Clear the previous input line, redraw it at the new
- # cursor position
- self.terminal.eraseDisplay()
- self.terminal.cursorHome()
- self.width=width
- self.height=height
-
- # initialize the session
- client_address=self.getClientAddress()
- client_address=client_address.hostifclient_addresselseNone
- self.init_session("ssh",client_address,self.cfactory.sessionhandler)
-
- # since we might have authenticated already, we might set this here.
- ifself.authenticated_account:
- self.logged_in=True
- self.uid=self.authenticated_account.id
- self.sessionhandler.connect(self)
-
-
[docs]defconnectionMade(self):
- """
- This is called when the connection is first established.
-
- """
- recvline.HistoricRecvLine.connectionMade(self)
- self.keyHandlers[CTRL_C]=self.handle_INT
- self.keyHandlers[CTRL_D]=self.handle_EOF
- self.keyHandlers[CTRL_L]=self.handle_FF
- self.keyHandlers[CTRL_BACKSLASH]=self.handle_QUIT
-
- # initalize
-
-
[docs]defhandle_INT(self):
- """
- Handle ^C as an interrupt keystroke by resetting the current
- input variables to their initial state.
-
- """
- self.lineBuffer=[]
- self.lineBufferIndex=0
-
- self.terminal.nextLine()
- self.terminal.write("KeyboardInterrupt")
- self.terminal.nextLine()
-
-
[docs]defhandle_EOF(self):
- """
- Handles EOF generally used to exit.
-
- """
- ifself.lineBuffer:
- self.terminal.write("\a")
- else:
- self.handle_QUIT()
-
-
[docs]defhandle_FF(self):
- """
- Handle a 'form feed' byte - generally used to request a screen
- refresh/redraw.
-
- """
- self.terminal.eraseDisplay()
- self.terminal.cursorHome()
-
-
[docs]defhandle_QUIT(self):
- """
- Quit, end, and lose the connection.
-
- """
- self.terminal.loseConnection()
-
-
[docs]defconnectionLost(self,reason=None):
- """
- 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 loosing connection.
-
- """
- insults.TerminalProtocol.connectionLost(self,reason)
- self.sessionhandler.disconnect(self)
- self.terminal.loseConnection()
-
-
[docs]defgetClientAddress(self):
- """
- Get client address.
-
- Returns:
- address_and_port (tuple): The client's address and port in
- a tuple. For example `('127.0.0.1', 41917)`.
-
- """
- returnself.terminal.transport.getPeer()
-
-
[docs]deflineReceived(self,string):
- """
- Communication User -> Evennia. Any line return indicates a
- command for the purpose of the MUD. So we take the user input
- and pass it on to the game engine.
-
- Args:
- string (str): Input text.
-
- """
- self.sessionhandler.data_in(self,text=string)
-
-
[docs]defsendLine(self,string):
- """
- Communication Evennia -> User. Any string sent should
- already have been properly formatted and processed before
- reaching this point.
-
- Args:
- string (str): Output text.
-
- """
- forlineinstring.split("\n"):
- # the telnet-specific method for sending
- self.terminal.write(line)
- self.terminal.nextLine()
-
- # session-general method hooks
-
-
[docs]defat_login(self):
- """
- Called when this session gets authenticated by the server.
- """
- pass
-
-
[docs]defdisconnect(self,reason="Connection closed. Goodbye for now."):
- """
- Disconnect from server.
-
- Args:
- reason (str): Motivation for disconnect.
-
- """
- ifreason:
- self.data_out(text=((reason,),{}))
- self.connectionLost(reason)
-
-
[docs]defdata_out(self,**kwargs):
- """
- Data Evennia -> User
-
- Keyword Args:
- kwargs (any): Options to the protocol.
-
- """
- self.sessionhandler.data_out(self,**kwargs)
-
-
[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 (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
- 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!
-
- """
- # print "telnet.send_text", args,kwargs # DEBUG
- text=args[0]ifargselse""
- iftextisNone:
- return
- text=to_str(text)
-
- # handle arguments
- options=kwargs.get("options",{})
- flags=self.protocol_flags
- xterm256=options.get("xterm256",flags.get("XTERM256",True))
- useansi=options.get("ansi",flags.get("ANSI",True))
- raw=options.get("raw",flags.get("RAW",False))
- nocolor=options.get("nocolor",flags.get("NOCOLOR")ornot(xterm256oruseansi))
- # echo = options.get("echo", None) # DEBUG
- 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)
-
- 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=False,
- )
- self.sendLine(linetosend)
[docs]defauth_password(self,packet):
- """
- Password authentication.
-
- Used mostly for setting up the transport so we can query
- username and password later.
-
- Args:
- packet (Packet): Auth packet.
-
- """
- password=common.getNS(packet[1:])[0]
- c=credentials.UsernamePassword(self.user,password)
- c.transport=self.transport
- returnself.portal.login(c,None,IConchUser).addErrback(self._ebPassword)
-
-
-
[docs]classAccountDBPasswordChecker(object):
- """
- Checks the django db for the correct credentials for
- username/password otherwise it returns the account or None which is
- useful for the Realm.
-
- """
-
- noisy=False
- credentialInterfaces=(credentials.IUsernamePassword,)
-
-
[docs]classPassAvatarIdTerminalRealm(TerminalRealm):
- """
- Returns an avatar that passes the avatarId through to the
- protocol. This is probably not the best way to do it.
-
- """
-
- noisy=False
-
- def_getAvatar(self,avatarId):
- comp=components.Componentized()
- user=self.userFactory(comp,avatarId)
- sess=self.sessionFactory(comp)
-
- sess.transportFactory=self.transportFactory
- sess.chainedProtocolFactory=lambda:self.chainedProtocolFactory(avatarId)
-
- comp.setComponent(iconch.IConchUser,user)
- comp.setComponent(iconch.ISession,sess)
-
- returnuser
-
-
-
[docs]classTerminalSessionTransport_getPeer(object):
- """
- Taken from twisted's TerminalSessionTransport which doesn't
- provide getPeer to the transport. This one does.
-
- """
-
- noisy=False
-
-
[docs]defgetKeyPair(pubkeyfile,privkeyfile):
- """
- This function looks for RSA keypair files in the current directory. If they
- do not exist, the keypair is created.
- """
-
- ifnot(os.path.exists(pubkeyfile)andos.path.exists(privkeyfile)):
- # No keypair exists. Generate a new RSA keypair
- fromcryptography.hazmat.backendsimportdefault_backend
- fromcryptography.hazmat.primitives.asymmetricimportrsa
-
- rsa_key=Key(
- rsa.generate_private_key(
- public_exponent=65537,key_size=_KEY_LENGTH,backend=default_backend()
- )
- )
- public_key_string=rsa_key.public().toString(type="OPENSSH").decode()
- private_key_string=rsa_key.toString(type="OPENSSH").decode()
-
- # save keys for the future.
- withopen(privkeyfile,"wt")aspfile:
- pfile.write(private_key_string)
- print("Created SSH private key in '{}'".format(_PRIVATE_KEY_FILE))
- withopen(pubkeyfile,"wt")aspfile:
- pfile.write(public_key_string)
- print("Created SSH public key in '{}'".format(_PUBLIC_KEY_FILE))
- else:
- withopen(pubkeyfile)aspfile:
- public_key_string=pfile.read()
- withopen(privkeyfile)aspfile:
- private_key_string=pfile.read()
-
- returnKey.fromString(public_key_string),Key.fromString(private_key_string)
-"""
-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}]
-
- For more flexibility with certain clients, if `cmd_name` is capitalized,
- Evennia will leave its current capitalization (So CMD_nAmE would be sent
- as CMD.nAmE but cMD_Name would be Cmd.Name)
-
- 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:
- ifcmdname.istitle():
- # leave without capitalization
- gmcp_cmdname=".".join(wordforwordincmdname.split("_"))
- else:
- gmcp_cmdname=".".join(word.capitalize()forwordincmdname.split("_"))
- else:
- gmcp_cmdname="Core.%s"%(cmdnameifcmdname.istitle()elsecmdname.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
-"""
-Webclient based on websockets.
-
-This implements a webclient with WebSockets (http://en.wikipedia.org/wiki/WebSocket)
-by use of the autobahn-python package's implementation (https://github.com/crossbario/autobahn-python).
-It is used together with evennia/web/media/javascript/evennia_websocket_webclient.js.
-
-All data coming into the webclient is in the form of valid JSON on the form
-
-`["inputfunc_name", [args], {kwarg}]`
-
-which represents an "inputfunc" to be called on the Evennia side with *args, **kwargs.
-The most common inputfunc is "text", which takes just the text input
-from the command line and interprets it as an Evennia Command: `["text", ["look"], {}]`
-
-"""
-importre
-importjson
-importhtml
-fromdjango.confimportsettings
-fromevennia.utils.utilsimportmod_import,class_from_module
-fromevennia.utils.ansiimportparse_ansi
-fromevennia.utils.text2htmlimportparse_html
-fromautobahn.twisted.websocketimportWebSocketServerProtocol
-fromautobahn.exceptionimportDisconnected
-
-_RE_SCREENREADER_REGEX=re.compile(
- r"%s"%settings.SCREENREADER_REGEX_STRIP,re.DOTALL+re.MULTILINE
-)
-_CLIENT_SESSIONS=mod_import(settings.SESSION_ENGINE).SessionStore
-_UPSTREAM_IPS=settings.UPSTREAM_IPS
-
-# Status Code 1000: Normal Closure
-# called when the connection was closed through JavaScript
-CLOSE_NORMAL=WebSocketServerProtocol.CLOSE_STATUS_CODE_NORMAL
-
-# Status Code 1001: Going Away
-# called when the browser is navigating away from the page
-GOING_AWAY=WebSocketServerProtocol.CLOSE_STATUS_CODE_GOING_AWAY
-
-_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
- # webclient_authenticated_uid value of csession on disconnect
- nonce=0
-
-
[docs]defget_client_session(self):
- """
- Get the Client browser session (used for auto-login based on browser session)
-
- Returns:
- csession (ClientSession): This is a django-specific internal representation
- of the browser session.
-
- """
- try:
- # client will connect with wsurl?csessid&browserid
- webarg=self.http_request_uri.split("?",1)[1]
- exceptIndexError:
- # this may happen for custom webclients not caring for the
- # browser session.
- self.csessid=None
- returnNone
- exceptAttributeError:
- fromevennia.utilsimportlogger
-
- self.csessid=None
- logger.log_trace(str(self))
- returnNone
-
- self.csessid,*browserstr=webarg.split("&",1)
- ifbrowserstr:
- self.browserstr=str(browserstr[0])
-
- ifself.csessid:
- return_CLIENT_SESSIONS(session_key=self.csessid)
-
-
[docs]defonOpen(self):
- """
- This is called when the WebSocket connection is fully established.
-
- """
- client_address=self.transport.client
- client_address=client_address[0]ifclient_addresselseNone
-
- ifclient_addressin_UPSTREAM_IPSand"x-forwarded-for"inself.http_headers:
- addresses=[x.strip()forxinself.http_headers["x-forwarded-for"].split(",")]
- addresses.reverse()
-
- foraddrinaddresses:
- ifaddrnotin_UPSTREAM_IPS:
- client_address=addr
- break
-
- self.init_session("websocket",client_address,self.factory.sessionhandler)
-
- csession=self.get_client_session()# this sets self.csessid
- csessid=self.csessid
- uid=csessionandcsession.get("webclient_authenticated_uid",None)
- nonce=csessionandcsession.get("webclient_authenticated_nonce",0)
- ifuid:
- # the client session is already logged in.
- self.uid=uid
- self.nonce=nonce
- self.logged_in=True
-
- forold_sessioninself.sessionhandler.sessions_from_csessid(csessid):
- if(
- hasattr(old_session,"websocket_close_code")
- andold_session.websocket_close_code!=CLOSE_NORMAL
- ):
- # if we have old sessions with the same csession, they are remnants
- self.sessid=old_session.sessid
- self.sessionhandler.disconnect(old_session)
-
- browserstr=f":{self.browserstr}"ifself.browserstrelse""
- self.protocol_flags["CLIENTNAME"]=f"Evennia Webclient (websocket{browserstr})"
- self.protocol_flags["UTF-8"]=True
- self.protocol_flags["OOB"]=True
-
- # watch for dead links
- self.transport.setTcpKeepAlive(1)
- # actually do the connection
- self.sessionhandler.connect(self)
-
-
[docs]defdisconnect(self,reason=None):
- """
- Generic hook for the engine to call in order to
- disconnect this protocol.
-
- Args:
- reason (str or None): Motivation for the disconnection.
-
- """
- csession=self.get_client_session()
-
- ifcsession:
- # if the nonce is different, webclient_authenticated_uid has been
- # set *before* this disconnect (disconnect called after a new client
- # connects, which occurs in some 'fast' browsers like Google Chrome
- # and Mobile Safari)
- ifcsession.get("webclient_authenticated_nonce",0)==self.nonce:
- csession["webclient_authenticated_uid"]=None
- csession["webclient_authenticated_nonce"]=0
- csession.save()
- self.logged_in=False
-
- self.sessionhandler.disconnect(self)
- # autobahn-python:
- # 1000 for a normal close, 1001 if the browser window is closed,
- # 3000-4999 for app. specific,
- # in case anyone wants to expose this functionality later.
- #
- # sendClose() under autobahn/websocket/interfaces.py
- self.sendClose(CLOSE_NORMAL,reason)
-
-
[docs]defonClose(self,wasClean,code=None,reason=None):
- """
- This is executed when the connection is lost for whatever
- reason. it can also be called directly, from the disconnect
- method.
-
- Args:
- wasClean (bool): ``True`` if the WebSocket was closed cleanly.
- code (int or None): Close status as sent by the WebSocket peer.
- reason (str or None): Close reason as sent by the WebSocket peer.
-
- """
- ifcode==CLOSE_NORMALorcode==GOING_AWAY:
- self.disconnect(reason)
- else:
- self.websocket_close_code=code
-
-
[docs]defonMessage(self,payload,isBinary):
- """
- Callback fired when a complete WebSocket message was received.
-
- Args:
- payload (bytes): The WebSocket message received.
- isBinary (bool): Flag indicating whether payload is binary or
- UTF-8 encoded text.
-
- """
- cmdarray=json.loads(str(payload,"utf-8"))
- ifcmdarray:
- self.data_in(**{cmdarray[0]:[cmdarray[1],cmdarray[2]]})
-
-
[docs]defsendLine(self,line):
- """
- Send data to client.
-
- Args:
- line (str): Text to send.
-
- """
- try:
- returnself.sendMessage(line.encode())
- exceptDisconnected:
- # this can happen on an unclean close of certain browsers.
- # it means this link is actually already closed.
- self.disconnect(reason="Browser already closed.")
[docs]defdata_in(self,**kwargs):
- """
- Data User > Evennia.
-
- Args:
- text (str): Incoming text.
- kwargs (any): Options from protocol.
-
- Notes:
- At initilization, the client will send the special
- 'csessid' command to identify its browser session hash
- with the Evennia side.
-
- The websocket client will also pass 'websocket_close' command
- to report that the client has been closed and that the
- session should be disconnected.
-
- Both those commands are parsed and extracted already at
- this point.
-
- """
- if"websocket_close"inkwargs:
- self.disconnect()
- return
-
- self.sessionhandler.data_in(self,**kwargs)
-
-
[docs]defsend_text(self,*args,**kwargs):
- """
- Send text data. This will pre-process the text for
- color-replacement, conversion to html etc.
-
- Args:
- text (str): Text to send.
-
- Keyword Args:
- options (dict): Options-dict with the following keys understood:
- - raw (bool): No parsing at all (leave ansi-to-html markers unparsed).
- - nocolor (bool): Clean out all color.
- - screenreader (bool): Use Screenreader mode.
- - send_prompt (bool): Send a prompt with parsed html
-
- """
- ifargs:
- args=list(args)
- text=args[0]
- iftextisNone:
- return
- else:
- return
-
- flags=self.protocol_flags
-
- options=kwargs.pop("options",{})
- raw=options.get("raw",flags.get("RAW",False))
- client_raw=options.get("client_raw",False)
- nocolor=options.get("nocolor",flags.get("NOCOLOR",False))
- screenreader=options.get("screenreader",flags.get("SCREENREADER",False))
- prompt=options.get("send_prompt",False)
-
- ifscreenreader:
- # screenreader mode cleans up output
- text=parse_ansi(text,strip_ansi=True,xterm256=False,mxp=False)
- text=_RE_SCREENREADER_REGEX.sub("",text)
- cmd="prompt"ifpromptelse"text"
- ifraw:
- ifclient_raw:
- args[0]=text
- else:
- args[0]=html.escape(text)# escape html!
- else:
- args[0]=parse_html(text,strip_ansi=nocolor)
-
- # send to client on required form [cmdname, args, kwargs]
- self.sendLine(json.dumps([cmd,args,kwargs]))
[docs]defsend_default(self,cmdname,*args,**kwargs):
- """
- Data Evennia -> User.
-
- Args:
- cmdname (str): The first argument will always be the oob cmd name.
- *args (any): Remaining args will be arguments for `cmd`.
-
- Keyword Args:
- options (dict): These are ignored for oob commands. Use command
- arguments (which can hold dicts) to send instructions to the
- client instead.
-
- """
- ifnotcmdname=="options":
- self.sendLine(json.dumps([cmdname,args,kwargs]))
Source code for evennia.server.portal.webclient_ajax
-"""
-AJAX/COMET fallback webclient
-
-The AJAX/COMET web client consists of two components running on
-twisted and django. They are both a part of the Evennia website url
-tree (so the testing website might be located on
-http://localhost:4001/, whereas the webclient can be found on
-http://localhost:4001/webclient.)
-
-/webclient - this url is handled through django's template
- system and serves the html page for the client
- itself along with its javascript chat program.
-/webclientdata - this url is called by the ajax chat using
- POST requests (long-polling when necessary)
- The WebClient resource in this module will
- handle these requests and act as a gateway
- to sessions connected over the webclient.
-
-"""
-importjson
-importre
-importtime
-importhtml
-
-fromtwisted.webimportserver,resource
-fromtwisted.internet.taskimportLoopingCall
-fromdjango.utils.functionalimportPromise
-fromdjango.confimportsettings
-fromevennia.utils.ansiimportparse_ansi
-fromevennia.utilsimportutils
-fromevennia.utils.utilsimportto_bytes
-fromevennia.utils.text2htmlimportparse_html
-fromevennia.serverimportsession
-
-_CLIENT_SESSIONS=utils.mod_import(settings.SESSION_ENGINE).SessionStore
-_RE_SCREENREADER_REGEX=re.compile(
- r"%s"%settings.SCREENREADER_REGEX_STRIP,re.DOTALL+re.MULTILINE
-)
-_SERVERNAME=settings.SERVERNAME
-_KEEPALIVE=30# how often to check keepalive
-
-
-# defining a simple json encoder for returning
-# django data to the client. Might need to
-# extend this if one wants to send more
-# complex database objects too.
-
-
-
-
- def_responseFailed(self,failure,csessid,request):
- "callback if a request is lost/timed out"
- try:
- delself.requests[csessid]
- exceptKeyError:
- # nothing left to delete
- pass
-
- def_keepalive(self):
- """
- Callback for checking the connection is still alive.
- """
- now=time.time()
- to_remove=[]
- keep_alives=(
- (csessid,remove)
- forcsessid,(t,remove)inself.last_alive.items()
- ifnow-t>_KEEPALIVE
- )
- forcsessid,removeinkeep_alives:
- ifremove:
- # keepalive timeout. Line is dead.
- to_remove.append(csessid)
- else:
- # normal timeout - send keepalive
- self.last_alive[csessid]=(now,True)
- self.lineSend(csessid,["ajax_keepalive",[],{}])
- # remove timed-out sessions
- forcsessidinto_remove:
- sessions=self.sessionhandler.sessions_from_csessid(csessid)
- forsessinsessions:
- sess.disconnect()
- self.last_alive.pop(csessid,None)
- ifnotself.last_alive:
- # no more ajax clients. Stop the keepalive
- self.keep_alive.stop()
- self.keep_alive=None
-
-
[docs]defget_client_sessid(self,request):
- """
- Helper to get the client session id out of the request.
-
- Args:
- request (Request): Incoming request object.
- Returns:
- csessid (int): The client-session id.
-
- """
- returnhtml.escape(request.args[b"csessid"][0].decode("utf-8"))
-
-
[docs]defget_browserstr(self,request):
- """
- Get browser-string out of the request.
-
- Args:
- request (Request): Incoming request object.
- Returns:
- str: The browser name.
-
-
- """
- returnhtml.escape(request.args[b"browserstr"][0].decode("utf-8"))
-
-
[docs]defat_login(self):
- """
- Called when this session gets authenticated by the server.
- """
- pass
-
-
[docs]deflineSend(self,csessid,data):
- """
- This adds the data to the buffer and/or sends it to the client
- as soon as possible.
-
- Args:
- csessid (int): Session id.
- data (list): A send structure [cmdname, [args], {kwargs}].
-
- """
- request=self.requests.get(csessid)
- ifrequest:
- # we have a request waiting. Return immediately.
- request.write(jsonify(data))
- request.finish()
- delself.requests[csessid]
- else:
- # no waiting request. Store data in buffer
- dataentries=self.databuffer.get(csessid,[])
- dataentries.append(jsonify(data))
- self.databuffer[csessid]=dataentries
[docs]defmode_init(self,request):
- """
- This is called by render_POST when the client requests an init
- mode operation (at startup)
-
- Args:
- request (Request): Incoming request.
-
- """
- csessid=self.get_client_sessid(request)
- browserstr=self.get_browserstr(request)
-
- remote_addr=request.getClientIP()
-
- ifremote_addrinsettings.UPSTREAM_IPSandrequest.getHeader("x-forwarded-for"):
- addresses=[x.strip()forxinrequest.getHeader("x-forwarded-for").split(",")]
- addresses.reverse()
-
- foraddrinaddresses:
- ifaddrnotinsettings.UPSTREAM_IPS:
- remote_addr=addr
- break
-
- host_string="%s (%s:%s)"%(
- _SERVERNAME,
- request.getRequestHostname(),
- request.getHost().port,
- )
-
- sess=AjaxWebClientSession()
- sess.client=self
- sess.init_session("ajax/comet",remote_addr,self.sessionhandler)
-
- sess.csessid=csessid
- sess.browserstr=browserstr
- csession=_CLIENT_SESSIONS(session_key=sess.csessid)
- uid=csessionandcsession.get("webclient_authenticated_uid",False)
- ifuid:
- # the client session is already logged in
- sess.uid=uid
- sess.logged_in=True
-
- # watch for dead links
- self.last_alive[csessid]=(time.time(),False)
- ifnotself.keep_alive:
- # the keepalive is not running; start it.
- self.keep_alive=LoopingCall(self._keepalive)
- self.keep_alive.start(_KEEPALIVE,now=False)
-
- browserstr=f":{browserstr}"ifbrowserstrelse""
- sess.protocol_flags["CLIENTNAME"]=f"Evennia Webclient (ajax{browserstr})"
- sess.protocol_flags["UTF-8"]=True
- sess.protocol_flags["OOB"]=True
-
- # actually do the connection
- sess.sessionhandler.connect(sess)
-
- 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)
- returnb'""'
-
-
[docs]defmode_input(self,request):
- """
- This is called by render_POST when the client
- is sending data to the server.
-
- Args:
- request (Request): Incoming request.
-
- """
- csessid=self.get_client_sessid(request)
- self.last_alive[csessid]=(time.time(),False)
- cmdarray=json.loads(request.args.get(b"data")[0])
- forsessinself.sessionhandler.sessions_from_csessid(csessid):
- sess.data_in(**{cmdarray[0]:[cmdarray[1],cmdarray[2]]})
- returnb'""'
-
-
[docs]defmode_receive(self,request):
- """
- This is called by render_POST when the client is telling us
- that it is ready to receive data as soon as it is available.
- This is the basis of a long-polling (comet) mechanism: the
- server will wait to reply until data is available.
-
- Args:
- request (Request): Incoming request.
-
- """
- csessid=html.escape(request.args[b"csessid"][0].decode("utf-8"))
- self.last_alive[csessid]=(time.time(),False)
-
- dataentries=self.databuffer.get(csessid)
- ifdataentries:
- # we have data that could not be sent earlier (because client was not
- # ready to receive it). Return this buffered data immediately
- returndataentries.pop(0)
- else:
- # we have no data to send. End the old request and start
- # a new long-polling one
- request.notifyFinish().addErrback(self._responseFailed,csessid,request)
- ifcsessidinself.requests:
- self.requests[csessid].finish()# Clear any stale request.
- self.requests[csessid]=request
- returnserver.NOT_DONE_YET
-
-
[docs]defmode_close(self,request):
- """
- This is called by render_POST when the client is signalling
- that it is about to be closed.
-
- Args:
- request (Request): Incoming request.
-
- """
- csessid=self.get_client_sessid(request)
- try:
- sess=self.sessionhandler.sessions_from_csessid(csessid)[0]
- sess.sessionhandler.disconnect(sess)
- exceptIndexError:
- self.client_disconnect(csessid)
- returnb'""'
-
-
[docs]defrender_POST(self,request):
- """
- This function is what Twisted calls with POST requests coming
- in from the ajax client. The requests should be tagged with
- different modes depending on what needs to be done, such as
- initializing or sending/receving data through the request. It
- uses a long-polling mechanism to avoid sending data unless
- there is actual data available.
-
- Args:
- request (Request): Incoming request.
-
- """
- dmode=request.args.get(b"mode",[b"None"])[0].decode("utf-8")
-
- ifdmode=="init":
- # startup. Setup the server.
- returnself.mode_init(request)
- elifdmode=="input":
- # input from the client to the server
- returnself.mode_input(request)
- elifdmode=="receive":
- # the client is waiting to receive data.
- returnself.mode_receive(request)
- elifdmode=="close":
- # the client is closing
- returnself.mode_close(request)
- elifdmode=="keepalive":
- # A reply to our keepalive request - all is well
- returnself.mode_keepalive(request)
- else:
- # This should not happen if client sends valid data.
- returnb'""'
-
-
-#
-# A session type handling communication over the
-# web client interface.
-#
-
-
-
[docs]classAjaxWebClientSession(session.Session):
- """
- This represents a session running in an AjaxWebclient.
- """
-
-
[docs]defget_client_session(self):
- """
- Get the Client browser session (used for auto-login based on browser session)
-
- Returns:
- csession (ClientSession): This is a django-specific internal representation
- of the browser session.
-
- """
- ifself.csessid:
- return_CLIENT_SESSIONS(session_key=self.csessid)
[docs]defsend_default(self,cmdname,*args,**kwargs):
- """
- Data Evennia -> User.
-
- Args:
- cmdname (str): The first argument will always be the oob cmd name.
- *args (any): Remaining args will be arguments for `cmd`.
-
- Keyword Args:
- options (dict): These are ignored for oob commands. Use command
- arguments (which can hold dicts) to send instructions to the
- client instead.
-
- """
- ifnotcmdname=="options":
- self.client.lineSend(self.csessid,[cmdname,args,kwargs])
Source code for evennia.server.profiling.dummyrunner
-"""
-Dummy client runner
-
-This module implements a stand-alone launcher for stress-testing
-an Evennia game. It will launch any number of fake clients. These
-clients will log into the server and start doing random operations.
-Customizing and weighing these operations differently depends on
-which type of game is tested. The module contains a testing module
-for plain Evennia.
-
-Please note that you shouldn't run this on a production server!
-Launch the program without any arguments or options to see a
-full step-by-step setup help.
-
-Basically (for testing default Evennia):
-
- - Use an empty/testing database.
- - set PERMISSION_ACCOUNT_DEFAULT = "Builder"
- - start server, eventually with profiling active
- - launch this client runner
-
-If you want to customize the runner's client actions
-(because you changed the cmdset or needs to better
-match your use cases or add more actions), you can
-change which actions by adding a path to
-
- DUMMYRUNNER_ACTIONS_MODULE = <path.to.your.module>
-
-in your settings. See utils.dummyrunner_actions.py
-for instructions on how to define this module.
-
-"""
-
-
-importsys
-importtime
-importrandom
-fromargparseimportArgumentParser
-fromtwisted.conchimporttelnet
-fromtwisted.internetimportreactor,protocol
-fromtwisted.internet.taskimportLoopingCall
-
-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
-
-DUMMYRUNNER_SETTINGS=mod_import(settings.DUMMYRUNNER_SETTINGS_MODULE)
-ifnotDUMMYRUNNER_SETTINGS:
- raiseIOError(
- "Error: Dummyrunner could not find settings file at %s"
- %settings.DUMMYRUNNER_SETTINGS_MODULE
- )
-IDMAPPER_CACHE_MAXSIZE=settings.IDMAPPER_CACHE_MAXSIZE
-
-DATESTRING="%Y%m%d%H%M%S"
-CLIENTS=[]
-
-# Settings
-
-# number of clients to launch if no input is given on command line
-NCLIENTS=1
-# time between each 'tick', in seconds, if not set on command
-# line. All launched clients will be called upon to possibly do an
-# action with this frequency.
-TIMESTEP=DUMMYRUNNER_SETTINGS.TIMESTEP
-# chance of a client performing an action, per timestep. This helps to
-# spread out usage randomly, like it would be in reality.
-CHANCE_OF_ACTION=DUMMYRUNNER_SETTINGS.CHANCE_OF_ACTION
-# spread out the login action separately, having many accounts create accounts
-# and connect simultaneously is generally unlikely.
-CHANCE_OF_LOGIN=DUMMYRUNNER_SETTINGS.CHANCE_OF_LOGIN
-# Port to use, if not specified on command line
-TELNET_PORT=DUMMYRUNNER_SETTINGS.TELNET_PORTorsettings.TELNET_PORTS[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
-
-# 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=0
-
-
-INFO_STARTING="""
- Dummyrunner starting using {nclients} dummy account(s). If you don't see
- any connection messages, make sure that the Evennia server is
- running.
-
- 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="""
- Error: Evennia is not set up for dummyrunner. Before starting the
- server, make sure to include the following at *the end* of your
- settings file (remove when not using dummyrunner!):
-
- from evennia.server.profiling.settings_mixin import *
-
- This will change the settings in the following way:
- - change PERMISSION_ACCOUNT_DEFAULT to 'Developer' to allow clients
- 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
- add DUMMYRUNNER_MIXIN=True to your settings file to avoid this
- error completely.
-
- Warning: Don't run dummyrunner on a production database! It will
- create a lot of spammy objects and accounts!
- """
-
-
-ERROR_FEW_ACTIONS="""
- Dummyrunner settings error: The ACTIONS tuple is too short: it must
- contain at least login- and logout functions.
- """
-
-
-HELPTEXT="""
-DO NOT RUN THIS ON A PRODUCTION SERVER! USE A CLEAN/TESTING DATABASE!
-
-This stand-alone program launches dummy telnet clients against a
-running Evennia server. The idea is to mimic real accounts logging in
-and repeatedly doing resource-heavy commands so as to stress test the
-game. It uses the default command set to log in and issue commands, so
-if that was customized, some of the functionality will not be tested
-(it will not fail, the commands will just not be recognized). The
-running clients will create new objects and rooms all over the place
-as part of their running, so using a clean/testing database is
-strongly recommended.
-
-Setup:
- 1) setup a fresh/clean database (if using sqlite, just safe-copy
- away your real evennia.db3 file and create a new one with
- `evennia migrate`)
- 2) in server/conf/settings.py, add
-
- PERMISSION_ACCOUNT_DEFAULT="Builder"
-
- This is so that the dummy accounts can test building operations.
- You can also customize the dummyrunner by modifying a setting
- file specified by DUMMYRUNNER_SETTINGS_MODULE
-
- 3) Start Evennia like normal, optionally with profiling (--profile)
- 4) Run this dummy runner via the evennia launcher:
-
- evennia --dummyrunner <nr_of_clients>
-
- 5) Log on and determine if game remains responsive despite the
- heavier load. Note that if you activated profiling, there is a
- considerate additional overhead from the profiler too so you
- should usually not consider game responsivity when using the
- profiler at the same time.
- 6) If you use profiling, let the game run long enough to gather
- data, then stop the server cleanly using evennia stop or @shutdown.
- @shutdown. The profile appears as
- server/logs/server.prof/portal.prof (see Python's manual on
- cProfiler).
-
-Notes:
-
-The dummyrunner tends to create a lot of accounts all at once, which is
-a very heavy operation. This is not a realistic use-case - what you want
-to test is performance during run. A large
-number of clients here may lock up the client until all have been
-created. It may be better to connect multiple dummyrunners instead of
-starting one single one with a lot of accounts. Exactly what this number
-is depends on your computer power. So start with 10-20 clients and increase
-until you see the initial login slows things too much.
-
-"""
-
-
-
[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"
-
-
[docs]classDummyClient(telnet.StatefulTelnetProtocol):
- """
- Handles connection to a running Evennia server,
- mimicking a real account by sending commands on
- a timer.
-
- """
-
-
[docs]defcounter(self):
- """
- Produces a unique id, also between clients.
-
- Returns:
- counter (int): A unique counter.
-
- """
- returngidcounter()
-
-
[docs]deflogout(self):
- """
- Causes the client to log out of the server. Triggered by ctrl-c signal.
-
- """
- self._logging_out=True
- cmd=self._logout(self)[0]
- self.report(f"-> logout/disconnect ({self.istep} actions)",self.key)
- self.sendLine(bytes(cmd,'utf-8'))
-
-
[docs]defstep(self):
- """
- Perform a step. This is called repeatedly by the runner and
- causes the client to issue commands to the server. This holds
- all "intelligence" of the dummy client.
-
- """
- globalNLOGGING_IN,NLOGIN_SCREEN
-
- rand=random.random()
-
- ifnotself._cmdlist:
- # no commands ready. Load some.
-
- ifnotself._loggedin:
- ifrand<CHANCE_OF_LOGINorNLOGGING_IN<10:
- # lower rate of logins, but not below 1 / s
- # get the login commands
- self._cmdlist=list(makeiter(self._login(self)))
- NLOGGING_IN+=1# this is for book-keeping
- NLOGIN_SCREEN-=1
- self.report("-> create/login",self.key)
- self._loggedin=True
- else:
- # no login yet, so cmdlist not yet set
- return
- else:
- # we always pick a cumulatively random function
- crand=random.random()
- cfunc=[funcfor(cprob,func)inself._actionsifcprob>=crand][0]
- self._cmdlist=list(makeiter(cfunc(self)))
-
- # at this point we always have a list of commands
- ifrand<CHANCE_OF_ACTION:
- # send to the game
- 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}")
Source code for evennia.server.profiling.dummyrunner_settings
-"""
-Settings and actions for the dummyrunner
-
-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'. 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 below
-
-ACTIONS 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 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,
-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).
-
-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.
-
-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>"
-- 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.
-
-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).
-
-----
-
-"""
-importrandom
-importstring
-
-# Dummy runner settings
-
-# Time between each dummyrunner "tick", in seconds. Each dummy
-# will be called with this frequency.
-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.
-CHANCE_OF_ACTION=0.5
-
-# 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=0.01
-
-# Which telnet port to connect to. If set to None, uses the first
-# default telnet port of the running server.
-TELNET_PORT=None
-
-
-# Setup actions tuple
-
-# some convenient templates
-
-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"
-TOBJ_TEMPLATE="testing_button_%s"
-TOBJ_TYPECLASS="contrib.tutorial_examples.red_button.RedButton"
-
-
-# action function definitions (pick and choose from
-# these to build a client "usage profile"
-
-# login/logout
-
-
[docs]defc_login(client):
- "logins to the game"
- # we always use a new client name
- cname=DUMMY_NAME.format(gid=client.gid)
- cpwd=DUMMY_PWD.format(gid=client.gid)
- room_name=START_ROOM.format(gid=client.gid)
-
- # 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=(
- f"create {cname}{cpwd}",
- f"yes",# to confirm creation
- f"connect {cname}{cpwd}",
- f"dig {room_name}",
- f"teleport {room_name}",
- add_cmdset,
- )
- returncmds
[docs]defc_help(client):
- "reads help files"
- cmds=("help","dummyrunner_echo_response",)
- returncmds
-
-
-
[docs]defc_digs(client):
- "digs a new room, storing exit names on client"
- roomname=ROOM_TEMPLATE%client.counter()
- exitname1=EXIT_TEMPLATE%client.counter()
- exitname2=EXIT_TEMPLATE%client.counter()
- client.exits.extend([exitname1,exitname2])
- return("dig/tel %s = %s, %s"%(roomname,exitname1,exitname2),)
-
-
-
[docs]defc_creates_obj(client):
- "creates normal objects, storing their name on client"
- 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,
- )
- returncmds
-
-
-
[docs]defc_creates_button(client):
- "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)
- 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
-
-
-
[docs]defc_moves_n(client):
- "move through north exit if available"
- return("north",)
-
-
-
[docs]defc_moves_s(client):
- "move through south exit if available"
- return("south",)
-
-
-
[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}",)
-"""
-Script that saves memory and idmapper data over time.
-
-Data will be saved to game/logs/memoryusage.log. Note that
-the script will append to this file if it already exists.
-
-Call this module directly to plot the log (requires matplotlib and numpy).
-"""
-
-importos
-importsys
-importtime
-
-# TODO!
-# sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
-# os.environ['DJANGO_SETTINGS_MODULE'] = 'game.settings'
-importevennia
-fromevennia.utils.idmapperimportmodelsas_idmapper
-
-LOGFILE="logs/memoryusage.log"
-INTERVAL=30# log every 30 seconds
-
-
-
Source code for evennia.server.profiling.test_queries
-"""
-This is a little routine for viewing the sql queries that are executed by a given
-query as well as count them for optimization testing.
-
-"""
-
-importsys
-importos
-
-# sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
-# os.environ["DJANGO_SETTINGS_MODULE"] = "game.settings"
-fromdjango.dbimportconnection
-
-
-
[docs]defcount_queries(exec_string,setup_string):
- """
- Display queries done by exec_string. Use setup_string
- to setup the environment to test.
- """
-
- exec(setup_string)
-
- num_queries_old=len(connection.queries)
- exec(exec_string)
- nqueries=len(connection.queries)-num_queries_old
-
- forqueryinconnection.queries[-nqueriesifnquerieselse1:]:
- print(query["time"],query["sql"])
- print("Number of queries: %s"%nqueries)
[docs]deftest_c_creates_obj(self):
- objname="testing_obj_1"
- 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,
- ),
- )
- self.assertEqual(self.client.objs,[objname])
- self.clear_client_lists()
Source code for evennia.server.profiling.timetrace
-"""
-Trace a message through the messaging system
-"""
-
-importtime
-
-
-
[docs]deftimetrace(message,idstring,tracemessage="TEST_MESSAGE",final=False):
- """
- Trace a message with time stamps.
-
- Args:
- message (str): The actual message coming through
- idstring (str): An identifier string specifying where this trace is happening.
- tracemessage (str): The start of the message to tag.
- This message will get attached time stamp.
- final (bool): This is the final leg in the path - include total time in message
-
- """
- ifmessage.startswith(tracemessage):
- # the message is on the form TEST_MESSAGE tlast t0
- # where t0 is the initial starting time and last is the time
- # saved at the last stop.
- try:
- prefix,tlast,t0=message.split(None,2)
- tlast,t0=float(tlast),float(t0)
- except(IndexError,ValueError):
- t0=time.time()
- tlast=t0
- t1=t0
- else:
- t1=time.time()
- # print to log (important!)
- print("** timetrace (%s): dT=%fs, total=%fs."%(idstring,t1-tlast,t1-t0))
-
- iffinal:
- message=" **** %s (total %f) **** "%(tracemessage,t1-t0)
- else:
- message="%s%f%f"%(tracemessage,t1,t0)
- returnmessage
-"""
-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).
-
-"""
-importtime
-importsys
-importos
-importtraceback
-
-fromtwisted.webimportstatic
-fromtwisted.applicationimportinternet,service
-fromtwisted.internetimportreactor,defer
-fromtwisted.internet.taskimportLoopingCall
-fromtwisted.python.logimportILogObserver
-
-importdjango
-
-django.setup()
-
-importevennia
-importimportlib
-
-evennia._init()
-
-fromdjango.dbimportconnection
-fromdjango.db.utilsimportOperationalError
-fromdjango.confimportsettings
-
-fromevennia.accounts.modelsimportAccountDB
-fromevennia.scripts.modelsimportScriptDB
-fromevennia.server.modelsimportServerConfig
-
-fromevennia.utils.utilsimportget_evennia_version,mod_import,make_iter
-fromevennia.utilsimportlogger
-fromevennia.server.sessionhandlerimportSESSIONS
-
-fromdjango.utils.translationimportgettextas_
-
-_SA=object.__setattr__
-
-# a file with a flag telling the server to restart after shutdown or not.
-SERVER_RESTART=os.path.join(settings.GAME_DIR,"server","server.restart")
-
-# module containing hook methods called during start_stop
-SERVER_STARTSTOP_MODULE=mod_import(settings.AT_SERVER_STARTSTOP_MODULE)
-
-# modules containing plugin services
-SERVER_SERVICES_PLUGIN_MODULES=make_iter(settings.SERVER_SERVICES_PLUGIN_MODULES)
-
-
-# ------------------------------------------------------------
-# Evennia Server settings
-# ------------------------------------------------------------
-
-SERVERNAME=settings.SERVERNAME
-VERSION=get_evennia_version()
-
-AMP_ENABLED=True
-AMP_HOST=settings.AMP_HOST
-AMP_PORT=settings.AMP_PORT
-AMP_INTERFACE=settings.AMP_INTERFACE
-
-WEBSERVER_PORTS=settings.WEBSERVER_PORTS
-WEBSERVER_INTERFACES=settings.WEBSERVER_INTERFACES
-
-GUEST_ENABLED=settings.GUEST_ENABLED
-
-# server-channel mappings
-WEBSERVER_ENABLED=settings.WEBSERVER_ENABLEDandWEBSERVER_PORTSandWEBSERVER_INTERFACES
-IRC_ENABLED=settings.IRC_ENABLED
-RSS_ENABLED=settings.RSS_ENABLED
-GRAPEVINE_ENABLED=settings.GRAPEVINE_ENABLED
-WEBCLIENT_ENABLED=settings.WEBCLIENT_ENABLED
-GAME_INDEX_ENABLED=settings.GAME_INDEX_ENABLED
-
-INFO_DICT={
- "servername":SERVERNAME,
- "version":VERSION,
- "amp":"",
- "errors":"",
- "info":"",
- "webserver":"",
- "irc_rss":"",
-}
-
-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 function - this is called repeatedly by the server
-
-_MAINTENANCE_COUNT=0
-_FLUSH_CACHE=None
-_IDMAPPER_CACHE_MAXSIZE=settings.IDMAPPER_CACHE_MAXSIZE
-_GAMETIME_MODULE=None
-
-_IDLE_TIMEOUT=settings.IDLE_TIMEOUT
-_LAST_SERVER_TIME_SNAPSHOT=0
-
-
-def_server_maintenance():
- """
- This maintenance function handles repeated checks and updates that
- the server needs to do. It is called every minute.
- """
- globalEVENNIA,_MAINTENANCE_COUNT,_FLUSH_CACHE,_GAMETIME_MODULE
- global_LAST_SERVER_TIME_SNAPSHOT
-
- ifnot_FLUSH_CACHE:
- fromevennia.utils.idmapper.modelsimportconditional_flushas_FLUSH_CACHE
- ifnot_GAMETIME_MODULE:
- fromevennia.utilsimportgametimeas_GAMETIME_MODULE
-
- _MAINTENANCE_COUNT+=1
-
- now=time.time()
- if_MAINTENANCE_COUNT==1:
- # first call after a reload
- _GAMETIME_MODULE.SERVER_START_TIME=now
- _GAMETIME_MODULE.SERVER_RUNTIME=ServerConfig.objects.conf("runtime",default=0.0)
- _LAST_SERVER_TIME_SNAPSHOT=now
- else:
- # adjust the runtime not with 60s but with the actual elapsed time
- # in case this may varies slightly from 60s.
- _GAMETIME_MODULE.SERVER_RUNTIME+=now-_LAST_SERVER_TIME_SNAPSHOT
- _LAST_SERVER_TIME_SNAPSHOT=now
-
- # update game time and save it across reloads
- _GAMETIME_MODULE.SERVER_RUNTIME_LAST_UPDATED=now
- ServerConfig.objects.conf("runtime",_GAMETIME_MODULE.SERVER_RUNTIME)
-
- if_MAINTENANCE_COUNT%5==0:
- # check cache size every 5 minutes
- _FLUSH_CACHE(_IDMAPPER_CACHE_MAXSIZE)
- 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()
-
- # handle idle timeouts
- if_IDLE_TIMEOUT>0:
- reason=_("idle timeout exceeded")
- to_disconnect=[]
- forsessionin(
- sessforsessinSESSIONS.values()if(now-sess.cmd_last)>_IDLE_TIMEOUT
- ):
- ifnotsession.accountornotsession.account.access(
- session.account,"noidletimeout",default=False
- ):
- to_disconnect.append(session)
-
- forsessioninto_disconnect:
- SESSIONS.disconnect(session,reason=reason)
-
-
-# ------------------------------------------------------------
-# Evennia Main Server object
-# ------------------------------------------------------------
-
-
-
[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):
- """
- Setup the server.
-
- application - an instantiated Twisted application
-
- """
- sys.path.insert(1,".")
-
- # create a store of services
- self.services=service.MultiService()
- self.services.setServiceParent(application)
- self.amp_protocol=None# set by amp factory
- self.sessions=SESSIONS
- self.sessions.server=self
- self.process_id=os.getpid()
-
- # Database-specific startup optimizations.
- self.sqlite3_prep()
-
- self.start_time=time.time()
-
- # 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.
- # (see https://github.com/evennia/evennia/issues/1128)
- def_wrap_sigint_handler(*args):
- fromtwisted.internet.deferimportDeferred
-
- ifhasattr(self,"web_root"):
- d=self.web_root.empty_threadpool()
- d.addCallback(lambda_:self.shutdown("reload",_reactor_stopping=True))
- else:
- d=Deferred(lambda_:self.shutdown("reload",_reactor_stopping=True))
- d.addCallback(lambda_:reactor.stop())
- reactor.callLater(1,d.callback,None)
-
- reactor.sigInt=_wrap_sigint_handler
-
- # Server startup methods
-
-
[docs]defsqlite3_prep(self):
- """
- Optimize some SQLite stuff at startup since we
- can't save it to the database.
- """
- if(
- ".".join(str(i)foriindjango.VERSION)<"1.2"
- andsettings.DATABASES.get("default",{}).get("ENGINE")=="sqlite3"
- )or(
- hasattr(settings,"DATABASES")
- andsettings.DATABASES.get("default",{}).get("ENGINE",None)
- =="django.db.backends.sqlite3"
- ):
- cursor=connection.cursor()
- cursor.execute("PRAGMA cache_size=10000")
- cursor.execute("PRAGMA synchronous=OFF")
- cursor.execute("PRAGMA count_changes=OFF")
- cursor.execute("PRAGMA temp_store=2")
-
-
[docs]defupdate_defaults(self):
- """
- We make sure to store the most important object defaults here, so
- we can catch if they change and update them on-objects automatically.
- 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
-
- # setting names
- settings_names=(
- "CMDSET_CHARACTER",
- "CMDSET_ACCOUNT",
- "BASE_ACCOUNT_TYPECLASS",
- "BASE_OBJECT_TYPECLASS",
- "BASE_CHARACTER_TYPECLASS",
- "BASE_ROOM_TYPECLASS",
- "BASE_EXIT_TYPECLASS",
- "BASE_SCRIPT_TYPECLASS",
- "BASE_CHANNEL_TYPECLASS",
- )
- # get previous and current settings so they can be compared
- settings_compare=list(
- zip(
- [ServerConfig.objects.conf(name)fornameinsettings_names],
- [settings.__getattr__(name)fornameinsettings_names],
- )
- )
- mismatches=[
- ifori,tupinenumerate(settings_compare)iftup[0]andtup[1]andtup[0]!=tup[1]
- ]
- iflen(
- mismatches
- ):# can't use any() since mismatches may be [0] which reads as False for any()
- # we have a changed default. Import relevant objects and
- # run the update
- fromevennia.objects.modelsimportObjectDB
- fromevennia.comms.modelsimportChannelDB
-
- # from evennia.accounts.models import AccountDB
- fori,prev,currin(
- (i,tup[0],tup[1])fori,tupinenumerate(settings_compare)ifiinmismatches
- ):
- # update the database
- INFO_DICT["info"]=(
- " %s:\n '%s' changed to '%s'. Updating unchanged entries in database ..."
- %(settings_names[i],prev,curr)
- )
- ifi==0:
- ObjectDB.objects.filter(db_cmdset_storage__exact=prev).update(
- db_cmdset_storage=curr
- )
- ifi==1:
- AccountDB.objects.filter(db_cmdset_storage__exact=prev).update(
- db_cmdset_storage=curr
- )
- ifi==2:
- AccountDB.objects.filter(db_typeclass_path__exact=prev).update(
- db_typeclass_path=curr
- )
- ifiin(3,4,5,6):
- ObjectDB.objects.filter(db_typeclass_path__exact=prev).update(
- db_typeclass_path=curr
- )
- ifi==7:
- ScriptDB.objects.filter(db_typeclass_path__exact=prev).update(
- db_typeclass_path=curr
- )
- ifi==8:
- ChannelDB.objects.filter(db_typeclass_path__exact=prev).update(
- db_typeclass_path=curr
- )
- # store the new default and clean caches
- ServerConfig.objects.conf(settings_names[i],curr)
- ObjectDB.flush_instance_cache()
- AccountDB.flush_instance_cache()
- ScriptDB.flush_instance_cache()
- ChannelDB.flush_instance_cache()
- # if this is the first start we might not have a "previous"
- # setup saved. Store it now.
- [
- ServerConfig.objects.conf(settings_names[i],tup[1])
- fori,tupinenumerate(settings_compare)
- ifnottup[0]
- ]
-
-
[docs]defrun_initial_setup(self):
- """
- This is triggered by the amp protocol when the connection
- to the portal has been established.
- This attempts to run the initial_setup script of the server.
- It returns if this is not the first time the server starts.
- Once finished the last_initial_setup_step is set to 'done'
-
- """
- globalINFO_DICT
- initial_setup=importlib.import_module(settings.INITIAL_SETUP_MODULE)
- last_initial_setup_step=ServerConfig.objects.conf("last_initial_setup_step")
- try:
- ifnotlast_initial_setup_step:
- # None is only returned if the config does not exist,
- # i.e. this is an empty DB that needs populating.
- INFO_DICT["info"]=" Server started for the first time. Setting defaults."
- initial_setup.handle_setup()
- eliflast_initial_setup_stepnotin('done',-1):
- # last step crashed, so we weill resume from this step.
- # modules and setup will resume from this step, retrying
- # the last failed module. When all are finished, the step
- # is set to 'done' to show it does not need to be run again.
- INFO_DICT["info"]=" Resuming initial setup from step '{last}'.".format(
- last=last_initial_setup_step
- )
- initial_setup.handle_setup(last_initial_setup_step)
- exceptException:
- # stop server if this happens.
- print(traceback.format_exc())
- print("Error in initial setup. Stopping Server + Portal.")
- self.sessions.portal_shutdown()
[docs]defrun_init_hooks(self,mode):
- """
- Called by the amp client once receiving sync back from Portal
-
- Args:
- mode (str): One of shutdown, reload or reset
-
- """
- fromevennia.objects.modelsimportObjectDB
-
- # start server time and maintenance task
- self.maintenance_task=LoopingCall(_server_maintenance)
- self.maintenance_task.start(60,now=True)# call every minute
-
- # update eventual changed defaults
- self.update_defaults()
-
- [o.at_init()foroinObjectDB.get_all_cached_instances()]
- [p.at_init()forpinAccountDB.get_all_cached_instances()]
-
- # call correct server hook based on start file value
- ifmode=="reload":
- logger.log_msg("Server successfully reloaded.")
- self.at_server_reload_start()
- elifmode=="reset":
- # only run hook, don't purge sessions
- self.at_server_cold_start()
- logger.log_msg("Evennia Server successfully restarted in 'reset' mode.")
- elifmode=="shutdown":
- self.at_server_cold_start()
- # clear eventual lingering session storages
- ObjectDB.objects.clear_all_sessids()
- logger.log_msg("Evennia Server successfully started.")
-
- # always call this regardless of start type
- self.at_server_start()
-
-
[docs]@defer.inlineCallbacks
- defshutdown(self,mode="reload",_reactor_stopping=False):
- """
- Shuts down the server from inside it.
-
- 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
- # once; we don't need to run the shutdown procedure again.
- defer.returnValue(None)
-
- fromevennia.objects.modelsimportObjectDB
- fromevennia.server.modelsimportServerConfig
- fromevennia.utilsimportgametimeas_GAMETIME_MODULE
-
- ifmode=="reload":
- # call restart hooks
- ServerConfig.objects.conf("server_restart_mode","reload")
- yield[o.at_server_reload()foroinObjectDB.get_all_cached_instances()]
- yield[p.at_server_reload()forpinAccountDB.get_all_cached_instances()]
- yield[
- (s._pause_task(auto_pause=True),s.at_server_reload())
- forsinScriptDB.get_all_cached_instances()
- ifs.idands.is_active
- ]
- yieldself.sessions.all_sessions_portal_sync()
- self.at_server_reload_stop()
- # only save monitor state on reload, not on shutdown/reset
- fromevennia.scripts.monitorhandlerimportMONITOR_HANDLER
-
- MONITOR_HANDLER.save()
- else:
- ifmode=="reset":
- # like shutdown but don't unset the is_connected flag and don't disconnect sessions
- yield[o.at_server_shutdown()foroinObjectDB.get_all_cached_instances()]
- yield[p.at_server_shutdown()forpinAccountDB.get_all_cached_instances()]
- ifself.amp_protocol:
- yieldself.sessions.all_sessions_portal_sync()
- else:# shutdown
- yield[_SA(p,"is_connected",False)forpinAccountDB.get_all_cached_instances()]
- yield[o.at_server_shutdown()foroinObjectDB.get_all_cached_instances()]
- yield[
- (p.unpuppet_all(),p.at_server_shutdown())
- forpinAccountDB.get_all_cached_instances()
- ]
- yieldObjectDB.objects.clear_all_sessids()
- yield[
- (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()
-
- # tickerhandler state should always be saved.
- fromevennia.scripts.tickerhandlerimportTICKER_HANDLER
-
- TICKER_HANDLER.save()
-
- # always called, also for a reload
- self.at_server_stop()
-
- ifhasattr(self,"web_root"):# not set very first start
- yieldself.web_root.empty_threadpool()
-
- ifnot_reactor_stopping:
- # kill the server
- self.shutdown_complete=True
- reactor.callLater(1,reactor.stop)
-
- # we make sure the proper gametime is saved as late as possible
- ServerConfig.objects.conf("runtime",_GAMETIME_MODULE.runtime())
-
-
[docs]defget_info_dict(self):
- """
- Return the server info, for display.
-
- """
- returnINFO_DICT
-
- # server start/stop hooks
-
-
[docs]defat_server_start(self):
- """
- 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()
-
-
[docs]defat_server_stop(self):
- """
- 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()
-
-
[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()
-
-
[docs]defat_post_portal_sync(self,mode):
- """
- This is called just after the portal has finished syncing back data to the server
- after reconnecting.
-
- Args:
- mode (str): One of 'reload', 'reset' or 'shutdown'.
-
- """
-
- fromevennia.scripts.monitorhandlerimportMONITOR_HANDLER
-
- MONITOR_HANDLER.restore(mode=="reload")
-
- fromevennia.scripts.tickerhandlerimportTICKER_HANDLER
-
- TICKER_HANDLER.restore(mode=="reload")
-
- # Un-pause all scripts, stop non-persistent timers
- ScriptDB.objects.update_scripts_after_server_start()
-
- # start the task handler
- fromevennia.scripts.taskhandlerimportTASK_HANDLER
-
- TASK_HANDLER.load()
- TASK_HANDLER.create_delays()
-
- # create/update channels
- self.create_default_channels()
-
- # delete the temporary setting
- ServerConfig.objects.conf("server_restart_mode",delete=True)
-
-
[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()
-
-
[docs]defat_server_cold_start(self):
- """
- 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.
- fromevennia.objects.modelsimportObjectDB
-
- ObjectDB.objects.clear_all_sessids()
-
- # Remove non-persistent scripts
- fromevennia.scripts.modelsimportScriptDB
-
- forscriptinScriptDB.objects.filter(db_persistent=False):
- script._stop_task()
-
- ifGUEST_ENABLED:
- forguestinAccountDB.objects.all().filter(
- db_typeclass_path=settings.BASE_GUEST_TYPECLASS
- ):
- forcharacteringuest.db._playable_characters:
- ifcharacter:
- character.delete()
- guest.delete()
- ifSERVER_STARTSTOP_MODULE:
- SERVER_STARTSTOP_MODULE.at_server_cold_start()
-
-
[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()
-
-
-# ------------------------------------------------------------
-#
-# Start the Evennia game server and add all active services
-#
-# ------------------------------------------------------------
-
-
-# Tell the system the server is starting up; some things are not available yet
-try:
- ServerConfig.objects.conf("server_starting_mode",True)
-exceptOperationalError:
- print("Server server_starting_mode couldn't be set - database not set up.")
-
-
-# twistd requires us to define the variable 'application' so it knows
-# what to execute from.
-application=service.Application("Evennia")
-
-if"--nodaemon"notinsys.argv:
- # custom logging, but only if we are not running in interactive mode
- logfile=logger.WeeklyLogFile(
- os.path.basename(settings.SERVER_LOG_FILE),
- os.path.dirname(settings.SERVER_LOG_FILE),
- day_rotation=settings.SERVER_LOG_DAY_ROTATION,
- max_size=settings.SERVER_LOG_MAX_SIZE,
- )
- application.setComponent(ILogObserver,logger.ServerLogObserver(logfile).emit)
-
-# The main evennia server program. This sets up the database
-# and is where we store all the other services.
-EVENNIA=Evennia(application)
-
-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.
-
- ifacestr=""
- ifAMP_INTERFACE!="127.0.0.1":
- ifacestr="-%s"%AMP_INTERFACE
-
- INFO_DICT["amp"]="amp %s: %s"%(ifacestr,AMP_PORT)
-
- fromevennia.serverimportamp_client
-
- factory=amp_client.AMPClientFactory(EVENNIA)
- amp_service=internet.TCPClient(AMP_HOST,AMP_PORT,factory)
- amp_service.setName("ServerAMPClient")
- EVENNIA.services.addService(amp_service)
-
-ifWEBSERVER_ENABLED:
-
- # Start a django-compatible webserver.
-
- fromevennia.server.webserverimport(
- DjangoWebRoot,
- WSGIWebServer,
- Website,
- LockableThreadPool,
- PrivateStaticRoot,
- )
-
- # start a thread pool and define the root url (/) as a wsgi resource
- # recognized by Django
- threads=LockableThreadPool(
- minthreads=max(1,settings.WEBSERVER_THREADPOOL_LIMITS[0]),
- maxthreads=max(1,settings.WEBSERVER_THREADPOOL_LIMITS[1]),
- )
-
- web_root=DjangoWebRoot(threads)
- # point our media resources to url /media
- web_root.putChild(b"media",PrivateStaticRoot(settings.MEDIA_ROOT))
- # point our static resources to url /static
- web_root.putChild(b"static",PrivateStaticRoot(settings.STATIC_ROOT))
- EVENNIA.web_root=web_root
-
- ifWEB_PLUGINS_MODULE:
- # custom overloads
- web_root=WEB_PLUGINS_MODULE.at_webserver_root_creation(web_root)
-
- web_site=Website(web_root,logPath=settings.HTTP_LOG_FILE)
- web_site.is_portal=False
-
- INFO_DICT["webserver"]=""
- forproxyport,serverportinWEBSERVER_PORTS:
- # create the webserver (we only need the port for this)
- webserver=WSGIWebServer(threads,serverport,web_site,interface="127.0.0.1")
- webserver.setName("EvenniaWebServer%s"%serverport)
- EVENNIA.services.addService(webserver)
-
- INFO_DICT["webserver"]+="webserver: %s"%serverport
-
-ENABLED=[]
-ifIRC_ENABLED:
- # IRC channel connections
- ENABLED.append("irc")
-
-ifRSS_ENABLED:
- # RSS feed channel connections
- ENABLED.append("rss")
-
-ifGRAPEVINE_ENABLED:
- # Grapevine channel connections
- ENABLED.append("grapevine")
-
-ifGAME_INDEX_ENABLED:
- fromevennia.server.game_index_client.serviceimportEvenniaGameIndexService
-
- egi_service=EvenniaGameIndexService()
- EVENNIA.services.addService(egi_service)
-
-ifENABLED:
- INFO_DICT["irc_rss"]=", ".join(ENABLED)+" enabled."
-
-forplugin_moduleinSERVER_SERVICES_PLUGIN_MODULES:
- # external plugin protocols - load here
- plugin_module=mod_import(plugin_module)
- ifplugin_module:
- plugin_module.start_plugin_services(EVENNIA)
- else:
- print(f"Could not load plugin module {plugin_module}")
-
-# clear server startup mode
-try:
- ServerConfig.objects.conf("server_starting_mode",delete=True)
-exceptOperationalError:
- print("Server server_starting_mode couldn't unset - db not set up.")
-
-"""
-This defines a the Server's generic session object. This object represents
-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)
-"""
-importtime
-fromdjango.utilsimporttimezone
-fromdjango.confimportsettings
-fromevennia.comms.modelsimportChannelDB
-fromevennia.utilsimportlogger
-fromevennia.utils.utilsimportmake_iter,lazy_property,class_from_module
-fromevennia.commands.cmdsethandlerimportCmdSetHandler
-fromevennia.scripts.monitorhandlerimportMONITOR_HANDLER
-fromevennia.typeclasses.attributesimportAttributeHandler,InMemoryAttributeBackend,DbHolder
-
-_GA=object.__getattribute__
-_SA=object.__setattr__
-_ObjectDB=None
-_ANSI=None
-
-_BASE_SESSION_CLASS=class_from_module(settings.BASE_SESSION_CLASS)
-
-
-# -------------------------------------------------------------
-# Server Session
-# -------------------------------------------------------------
-
-
-
[docs]classServerSession(_BASE_SESSION_CLASS):
- """
- This class represents an account's session and is a template for
- individual protocols to communicate with Evennia.
-
- Each account gets a session assigned to them whenever they connect
- to the game server. All communication between game and account goes
- through their session.
-
- """
-
-
[docs]def__init__(self):
- """
- Initiate to avoid AttributeErrors down the line
-
- """
- self.puppet=None
- self.account=None
- self.cmdset_storage_string=""
- self.cmdset=CmdSetHandler(self,True)
[docs]defat_sync(self):
- """
- This is called whenever a session has been resynced with the
- portal. At this point all relevant attributes have already
- been set and self.account been assigned (if applicable).
-
- Since this is often called after a server restart we need to
- set up the session as it was.
-
- """
- global_ObjectDB
- ifnot_ObjectDB:
- fromevennia.objects.modelsimportObjectDBas_ObjectDB
-
- super(ServerSession,self).at_sync()
- ifnotself.logged_in:
- # assign the unloggedin-command set.
- self.cmdset_storage=settings.CMDSET_UNLOGGEDIN
-
- self.cmdset.update(init_mode=True)
-
- ifself.puid:
- # reconnect puppet (puid is only set if we are coming
- # back from a server reload). This does all the steps
- # done in the default @ic command but without any
- # hooks, echoes or access checks.
- obj=_ObjectDB.objects.get(id=self.puid)
- obj.sessions.add(self)
- obj.account=self.account
- self.puid=obj.id
- self.puppet=obj
- # obj.scripts.validate()
- obj.locks.cache_lock_bypass(obj)
-
-
[docs]defat_login(self,account):
- """
- Hook called by sessionhandler when the session becomes authenticated.
-
- Args:
- account (Account): The account associated with the session.
-
- """
- self.account=account
- self.uid=self.account.id
- self.uname=self.account.username
- self.logged_in=True
- self.conn_time=time.time()
- self.puid=None
- self.puppet=None
- self.cmdset_storage=settings.CMDSET_SESSION
-
- # Update account's last login time.
- self.account.last_login=timezone.now()
- self.account.save()
-
- # add the session-level cmdset
- self.cmdset=CmdSetHandler(self,True)
-
-
[docs]defat_disconnect(self,reason=None):
- """
- Hook called by sessionhandler when disconnecting this session.
-
- """
- ifself.logged_in:
- account=self.account
- ifself.puppet:
- account.unpuppet_object(self)
- uaccount=account
- uaccount.last_login=timezone.now()
- uaccount.save()
- # calling account hook
- account.at_disconnect(reason)
- self.logged_in=False
- ifnotself.sessionhandler.sessions_from_account(account):
- # no more sessions connected to this account
- account.is_connected=False
- # this may be used to e.g. delete account after disconnection etc
- account.at_post_disconnect()
- # remove any webclient settings monitors associated with this
- # session
- MONITOR_HANDLER.remove(account,"_saved_webclient_options",self.sessid)
-
-
[docs]defget_account(self):
- """
- Get the account associated with this session
-
- Returns:
- account (Account): The associated Account.
-
- """
- returnself.logged_inandself.account
-
-
[docs]defget_puppet(self):
- """
- Get the in-game character associated with this session.
-
- Returns:
- puppet (Object): The puppeted object, if any.
-
- """
- returnself.logged_inandself.puppet
-
- get_character=get_puppet
-
-
[docs]defget_puppet_or_account(self):
- """
- Get puppet or account.
-
- Returns:
- controller (Object or Account): The puppet if one exists,
- otherwise return the account.
-
- """
- ifself.logged_in:
- returnself.puppetifself.puppetelseself.account
- returnNone
-
-
[docs]deflog(self,message,channel=True):
- """
- Emits session info to the appropriate outputs and info channels.
-
- Args:
- message (str): The message to log.
- channel (bool, optional): Log to the CHANNEL_CONNECTINFO channel
- in addition to the server log.
-
- """
- cchan=channelandsettings.CHANNEL_CONNECTINFO
- ifcchan:
- try:
- cchan=ChannelDB.objects.get_channel(cchan["key"])
- cchan.msg("[%s]: %s"%(cchan.key,message))
- exceptException:
- logger.log_trace()
- logger.log_info(message)
-
-
[docs]defget_client_size(self):
- """
- Return eventual eventual width and height reported by the
- client. Note that this currently only deals with a single
- client window (windowID==0) as in a traditional telnet session.
-
- """
- flags=self.protocol_flags
- print("session flags:",flags)
- width=flags.get("SCREENWIDTH",{}).get(0,settings.CLIENT_DEFAULT_WIDTH)
- height=flags.get("SCREENHEIGHT",{}).get(0,settings.CLIENT_DEFAULT_HEIGHT)
- returnwidth,height
-
-
[docs]defupdate_session_counters(self,idle=False):
- """
- Hit this when the user enters a command in order to update
- idle timers and command counters.
-
- """
- # Idle time used for timeout calcs.
- self.cmd_last=time.time()
-
- # Store the timestamp of the user's last command.
- ifnotidle:
- # Increment the user's command counter.
- self.cmd_total+=1
- # Account-visible idle time, not used in idle timeout calcs.
- self.cmd_last_visible=self.cmd_last
-
-
[docs]defupdate_flags(self,**kwargs):
- """
- Update the protocol_flags and sync them with Portal.
-
- Keyword Args:
- protocol_flag (any): A key and value to set in the
- protocol_flags dictionary.
-
- Notes:
- Since protocols can vary, no checking is done
- as to the existene of the flag or not. The input
- data should have been validated before this call.
-
- """
- ifkwargs:
- self.protocol_flags.update(kwargs)
- self.sessionhandler.session_portal_sync(self)
-
-
[docs]defdata_out(self,**kwargs):
- """
- Sending data from Evennia->Client
-
- Keyword Args:
- text (str or tuple)
- any (str or tuple): Send-commands identified
- by their keys. Or "options", carrying options
- for the protocol(s).
-
- """
- self.sessionhandler.data_out(self,**kwargs)
-
-
[docs]defdata_in(self,**kwargs):
- """
- Receiving data from the client, sending it off to
- the respective inputfuncs.
-
- Keyword Args:
- 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)
-
-
[docs]defmsg(self,text=None,**kwargs):
- """
- Wrapper to mimic msg() functionality of Objects and Accounts.
-
- Args:
- text (str): String input.
-
- Keyword Args:
- any (str or tuple): Send-commands identified
- by their keys. Or "options", carrying options
- for the protocol(s).
-
- """
- # this can happen if this is triggered e.g. a command.msg
- # that auto-adds the session, we'd get a kwarg collision.
- kwargs.pop("session",None)
- kwargs.pop("from_obj",None)
- iftextisnotNone:
- self.data_out(text=text,**kwargs)
- else:
- self.data_out(**kwargs)
-
-
[docs]defexecute_cmd(self,raw_string,session=None,**kwargs):
- """
- Do something as this object. This method is normally never
- called directly, instead incoming command instructions are
- sent to the appropriate inputfunc already at the sessionhandler
- level. This method allows Python code to inject commands into
- this stream, and will lead to the text inputfunc be called.
-
- Args:
- raw_string (string): Raw command input
- session (Session): This is here to make API consistent with
- Account/Object.execute_cmd. If given, data is passed to
- that Session, otherwise use self.
- Keyword Args:
- Other keyword arguments will be added to the found command
- object instace as variables before it executes. This is
- unused by default Evennia but may be used to set flags and
- change operating paramaters for commands at run-time.
-
- """
- # inject instruction into input stream
- kwargs["text"]=((raw_string,),{})
- self.sessionhandler.data_in(sessionorself,**kwargs)
-
- def__eq__(self,other):
- """
- Handle session comparisons
-
- """
- try:
- returnself.address==other.address
- exceptAttributeError:
- returnFalse
-
- def__hash__(self):
- """
- Python 3 requires that any class which implements __eq__ must also
- implement __hash__ and that the corresponding hashes for equivalent
- instances are themselves equivalent.
-
- """
- returnhash(self.address)
-
- def__ne__(self,other):
- try:
- returnself.address!=other.address
- exceptAttributeError:
- returnTrue
-
- def__str__(self):
- """
- String representation of the user session class. We use
- this a lot in the server logs.
-
- """
- symbol=""
- ifself.logged_inandhasattr(self,"account")andself.account:
- symbol="(#%s)"%self.account.id
- try:
- ifhasattr(self.address,"__iter__"):
- address=":".join([str(part)forpartinself.address])
- else:
- address=self.address
- exceptException:
- address=self.address
- return"%s%s@%s"%(self.uname,symbol,address)
-
- def__repr__(self):
- return"%s"%str(self)
-
- # Dummy API hooks for use during non-loggedin operation
-
-
[docs]defat_cmdset_get(self,**kwargs):
- """
- A dummy hook all objects with cmdsets need to have
-
- """
-
- pass
-
- # Mock db/ndb properties for allowing easy storage on the session
- # (note that no databse is involved at all here. session.db.attr =
- # value just saves a normal property in memory, just like ndb).
-
-
[docs]defndb_get(self):
- """
- A non-persistent 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
- exceptAttributeError:
- self._ndb_holder=DbHolder(self,"nattrhandler",manager_name="nattributes")
- returnself._ndb_holder
-
- # @ndb.setter
-
[docs]defndb_set(self,value):
- """
- Stop accidentally replacing the db object
-
- Args:
- value (any): A value to store in the ndb.
-
- """
- string="Cannot assign directly to ndb object! "
- string+="Use ndb.attr=value instead."
- raiseException(string)
-
- ndb=property(ndb_get,ndb_set,ndb_del)
- db=property(ndb_get,ndb_set,ndb_del)
-
- # 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.
-
- """
- returnTrue
-"""
-This module defines a generic session class. All connection instances
-(both on Portal and Server side) should inherit from this class.
-
-"""
-fromdjango.confimportsettings
-
-importtime
-
-# ------------------------------------------------------------
-# Server Session
-# ------------------------------------------------------------
-
-
-
[docs]classSession:
- """
- This class represents a player's session and is a template for
- both portal- and server-side sessions.
-
- Each connection will see two session instances created:
-
- 1. A Portal session. This is customized for the respective connection
- protocols that Evennia supports, like Telnet, SSH etc. The Portal
- session must call init_session() as part of its initialization. The
- respective hook methods should be connected to the methods unique
- for the respective protocol so that there is a unified interface
- to Evennia.
- 2. A Server session. This is the same for all connected accounts,
- regardless of how they connect.
-
- The Portal and Server have their own respective sessionhandlers. These
- are synced whenever new connections happen or the Server restarts etc,
- which means much of the same information must be stored in both places
- e.g. the portal can re-sync with the server when the server reboots.
-
- """
-
-
[docs]definit_session(self,protocol_key,address,sessionhandler):
- """
- Initialize the Session. This should be called by the protocol when
- a new session is established.
-
- Args:
- protocol_key (str): By default, one of 'telnet', 'telnet/ssl', 'ssh',
- 'webclient/websocket' or 'webclient/ajax'.
- address (str): Client address.
- sessionhandler (SessionHandler): Reference to the
- main sessionhandler instance.
-
- """
- # This is currently 'telnet', 'ssh', 'ssl' or 'web'
- self.protocol_key=protocol_key
- # Protocol address tied to this session
- self.address=address
-
- # suid is used by some protocols, it's a hex key.
- self.suid=None
-
- # unique id for this session
- self.sessid=0# no sessid yet
- # client session id, if given by the client
- self.csessid=None
- # database id for the user connected to this session
- self.uid=None
- # user name, for easier tracking of sessions
- self.uname=None
- # if user has authenticated already or not
- self.logged_in=False
-
- # database id of puppeted object (if any)
- self.puid=None
-
- # session time statistics
- self.conn_time=time.time()
- self.cmd_last_visible=self.conn_time
- self.cmd_last=self.conn_time
- self.cmd_total=0
-
- self.protocol_flags={
- "ENCODING":"utf-8",
- "SCREENREADER":False,
- "INPUTDEBUG":False,
- "RAW":False,
- "NOCOLOR":False,
- "LOCALECHO":False,
- }
- self.server_data={}
-
- # map of input data to session methods
- self.datamap={}
-
- # a back-reference to the relevant sessionhandler this
- # session is stored in.
- self.sessionhandler=sessionhandler
-
-
[docs]defget_sync_data(self):
- """
- Get all data relevant to sync the session.
-
- Args:
- syncdata (dict): All syncdata values, based on
- the keys given by self._attrs_to_sync.
-
- """
- return{
- attr:getattr(self,attr)forattrinsettings.SESSION_SYNC_ATTRSifhasattr(self,attr)
- }
-
-
[docs]defload_sync_data(self,sessdata):
- """
- Takes a session dictionary, as created by get_sync_data, and
- loads it into the correct properties of the session.
-
- Args:
- sessdata (dict): Session data dictionary.
-
- """
- forpropname,valueinsessdata.items():
- if(
- propname=="protocol_flags"
- andisinstance(value,dict)
- andhasattr(self,"protocol_flags")
- andisinstance(self.protocol_flags,dict)
- ):
- # special handling to allow partial update of protocol flags
- self.protocol_flags.update(value)
- else:
- setattr(self,propname,value)
-
-
[docs]defat_sync(self):
- """
- Called after a session has been fully synced (including
- secondary operations such as setting self.account based
- on uid etc).
-
- """
- ifself.account:
- self.protocol_flags.update(
- self.account.attributes.get("_saved_protocol_flags",None)or{}
- )
-
- # access hooks
-
-
[docs]defdisconnect(self,reason=None):
- """
- generic hook called from the outside to disconnect this session
- should be connected to the protocols actual disconnect mechanism.
-
- Args:
- reason (str): Eventual text motivating the disconnect.
-
- """
- pass
-
-
[docs]defdata_out(self,**kwargs):
- """
- Generic hook for sending data out through the protocol. Server
- protocols can use this right away. Portal sessions
- should overload this to format/handle the outgoing data as needed.
-
- Keyword Args:
- kwargs (any): Other data to the protocol.
-
- """
- pass
-
-
[docs]defdata_in(self,**kwargs):
- """
- Hook for protocols to send incoming data to the engine.
-
- Keyword Args:
- kwargs (any): Other data from the protocol.
-
- """
- pass
-"""
-This module defines handlers for storing sessions when handles
-sessions of users connecting to the server.
-
-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.
-
-"""
-importtime
-
-fromdjango.confimportsettings
-fromevennia.commands.cmdhandlerimportCMD_LOGINSTART
-fromevennia.utils.loggerimportlog_trace
-fromevennia.utils.utilsimport(
- is_iter,
- make_iter,
- delay,
- callables_from_module,
- class_from_module,
-)
-fromevennia.server.portalimportamp
-fromevennia.server.signalsimportSIGNAL_ACCOUNT_POST_LOGIN,SIGNAL_ACCOUNT_POST_LOGOUT
-fromevennia.server.signalsimportSIGNAL_ACCOUNT_POST_FIRST_LOGIN,SIGNAL_ACCOUNT_POST_LAST_LOGOUT
-fromcodecsimportdecodeascodecs_decode
-fromdjango.utils.translationimportgettextas_
-
-_FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED=settings.FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED
-_BROADCAST_SERVER_RESTART_MESSAGES=settings.BROADCAST_SERVER_RESTART_MESSAGES
-
-# delayed imports
-_AccountDB=None
-_ServerSession=None
-_ServerConfig=None
-_ScriptDB=None
-_OOB_HANDLER=None
-
-_ERR_BAD_UTF8=_("Your client sent an incorrect UTF-8 sequence.")
-
-
-
[docs]defdelayed_import():
- """
- Helper method for delayed import of all needed entities.
-
- """
- global_ServerSession,_AccountDB,_ServerConfig,_ScriptDB
- ifnot_ServerSession:
- # we allow optional arbitrary serversession class for overloading
- _ServerSession=class_from_module(settings.SERVER_SESSION_CLASS)
- ifnot_AccountDB:
- fromevennia.accounts.modelsimportAccountDBas_AccountDB
- ifnot_ServerConfig:
- fromevennia.server.modelsimportServerConfigas_ServerConfig
- ifnot_ScriptDB:
- fromevennia.scripts.modelsimportScriptDBas_ScriptDB
- # including once to avoid warnings in Python syntax checkers
- 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"
-
-
-# -----------------------------------------------------------
-# SessionHandler base class
-# ------------------------------------------------------------
-
-
-
[docs]classSessionHandler(dict):
- """
- This handler holds a stack of sessions.
-
- """
-
- def__getitem__(self,key):
- """
- Clean out None-sessions automatically.
-
- """
- ifNoneinself:
- delself[None]
- returnsuper().__getitem__(key)
-
-
[docs]classServerSessionHandler(SessionHandler):
- """
- 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.
-
- """
-
- # AMP communication methods
-
-
[docs]def__init__(self,*args,**kwargs):
- """
- Init the handler.
-
- """
- super().__init__(*args,**kwargs)
- self.server=None# set at server initialization
- self.server_data={"servername":_SERVERNAME}
- # will be set on psync
- self.portal_start_time=0.0
-
- def_run_cmd_login(self,session):
- """
- Launch the CMD_LOGINSTART command. This is wrapped
- for delays.
-
- """
- ifnotsession.logged_in:
- self.data_in(session,text=[[CMD_LOGINSTART],{}])
-
-
[docs]defportal_connect(self,portalsessiondata):
- """
- Called by Portal when a new session has connected.
- Creates a new, unlogged-in game session.
-
- Args:
- portalsessiondata (dict): a dictionary of all property:value
- keys defining the session and which is marked to be
- synced.
-
- """
- delayed_import()
- global_ServerSession,_AccountDB,_ScriptDB
-
- sess=_ServerSession()
- sess.sessionhandler=self
- sess.load_sync_data(portalsessiondata)
- sess.at_sync()
- # validate all scripts
- # _ScriptDB.objects.validate()
- self[sess.sessid]=sess
-
- ifsess.logged_inandsess.uid:
- # Session is already logged in. This can happen in the
- # case of auto-authenticating protocols like SSH or
- # webclient's session sharing
- account=_AccountDB.objects.get_account_from_uid(sess.uid)
- ifaccount:
- # this will set account.is_connected too
- self.login(sess,account,force=True)
- return
- else:
- sess.logged_in=False
- sess.uid=None
-
- # show the first login command, may delay slightly to allow
- # the handshakes to finish.
- delay(_DELAY_CMD_LOGINSTART,self._run_cmd_login,sess)
-
-
[docs]defportal_session_sync(self,portalsessiondata):
- """
- Called by Portal when it wants to update a single session (e.g.
- because of all negotiation protocols have finally replied)
-
- Args:
- portalsessiondata (dict): a dictionary of all property:value
- keys defining the session and which is marked to be
- synced.
-
- """
- sessid=portalsessiondata.get("sessid")
- session=self.get(sessid)
- ifsession:
- # since some of the session properties may have had
- # a chance to change already before the portal gets here
- # the portal doesn't send all sessiondata but only
- # ones which should only be changed from portal (like
- # protocol_flags etc)
- session.load_sync_data(portalsessiondata)
-
-
[docs]defportal_sessions_sync(self,portalsessionsdata):
- """
- Syncing all session ids of the portal with the ones of the
- server. This is instantiated by the portal when reconnecting.
-
- Args:
- portalsessionsdata (dict): A dictionary
- `{sessid: {property:value},...}` defining each session and
- the properties in it which should be synced.
-
- """
- delayed_import()
- global_ServerSession,_AccountDB,_ServerConfig,_ScriptDB
-
- forsessinlist(self.values()):
- # we delete the old session to make sure to catch eventual
- # lingering references.
- delsess
-
- forsessid,sessdictinportalsessionsdata.items():
- sess=_ServerSession()
- sess.sessionhandler=self
- sess.load_sync_data(sessdict)
- ifsess.uid:
- sess.account=_AccountDB.objects.get_account_from_uid(sess.uid)
- self[sessid]=sess
- sess.at_sync()
-
- mode="reload"
-
- # tell the server hook we synced
- self.server.at_post_portal_sync(mode)
- # announce the reconnection
- if_BROADCAST_SERVER_RESTART_MESSAGES:
- self.announce_all(_(" ... Server restarted."))
-
-
[docs]defportal_disconnect(self,session):
- """
- Called from Portal when Portal session closed from the portal
- side. There is no message to report in this case.
-
- Args:
- session (Session): The Session to disconnect
-
- """
- # disconnect us without calling Portal since
- # Portal already knows.
- self.disconnect(session,reason="",sync_portal=False)
-
-
[docs]defportal_disconnect_all(self):
- """
- Called from Portal when Portal is closing down. All
- Sessions should die. The Portal should not be informed.
-
- """
- # set a watchdog to avoid self.disconnect from deleting
- # the session while we are looping over them
- self._disconnect_all=True
- forsessioninself.values():
- session.disconnect()
- delself._disconnect_all
-
- # server-side access methods
-
-
[docs]defstart_bot_session(self,protocol_path,configdict):
- """
- This method allows the server-side to force the Portal to
- create a new bot session.
-
- Args:
- protocol_path (str): The full python path to the bot's
- class.
- configdict (dict): This dict will be used to configure
- the bot (this depends on the bot protocol).
-
- Examples:
- start_bot_session("evennia.server.portal.irc.IRCClient",
- {"uid":1, "botname":"evbot", "channel":"#evennia",
- "network:"irc.freenode.net", "port": 6667})
-
- Notes:
- The new session will use the supplied account-bot uid to
- initiate an already logged-in connection. The Portal will
- treat this as a normal connection and henceforth so will
- the Server.
-
- """
- self.server.amp_protocol.send_AdminServer2Portal(
- DUMMYSESSION,operation=amp.SCONN,protocol_path=protocol_path,config=configdict
- )
-
-
[docs]defportal_restart_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=amp.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=amp.SRESET)
-
-
[docs]defportal_shutdown(self):
- """
- Called by server when it's time to shut down (the portal will shut us down and then shut
- itself down)
-
- """
- self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION,operation=amp.PSHUTD)
-
-
[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.
-
- Args:
- session (Session): The Session to authenticate.
- account (Account): The Account identified as associated with this Session.
- force (bool): Login also if the session thinks it's already logged in
- (this can happen for auto-authenticating protocols)
- testmode (bool, optional): This is used by unittesting for
- faking login without any AMP being actually active.
-
- """
- ifsession.logged_inandnotforce:
- # don't log in a session that is already logged in.
- return
-
- account.is_connected=True
-
- # sets up and assigns all properties on the session
- session.at_login(account)
-
- # account init
- account.at_init()
-
- # Check if this is the first time the *account* logs in
- ifaccount.db.FIRST_LOGIN:
- account.at_first_login()
- delaccount.db.FIRST_LOGIN
-
- account.at_pre_login()
-
- if_MULTISESSION_MODE==0:
- # disconnect all previous sessions.
- self.disconnect_duplicate_sessions(session)
-
- nsess=len(self.sessions_from_account(account))
- string="Logged in: {account}{address} ({nsessions} session(s) total)"
- string=string.format(account=account,address=session.address,nsessions=nsess)
- session.log(string)
- session.logged_in=True
- # sync the portal to the session
- ifnottestmode:
- self.server.amp_protocol.send_AdminServer2Portal(
- session,operation=amp.SLOGIN,sessiondata={"logged_in":True,"uid":session.uid}
- )
- account.at_post_login(session=session)
- ifnsess<2:
- SIGNAL_ACCOUNT_POST_FIRST_LOGIN.send(sender=account,session=session)
- SIGNAL_ACCOUNT_POST_LOGIN.send(sender=account,session=session)
-
-
[docs]defdisconnect(self,session,reason="",sync_portal=True):
- """
- Called from server side to remove session and inform portal
- of this fact.
-
- Args:
- session (Session): The Session to disconnect.
- reason (str, optional): A motivation for the disconnect.
- sync_portal (bool, optional): Sync the disconnect to
- Portal side. This should be done unless this was
- called by self.portal_disconnect().
-
- """
- session=self.get(session.sessid)
- ifnotsession:
- return
-
- ifhasattr(session,"account")andsession.account:
- # only log accounts logging off
- nsess=len(self.sessions_from_account(session.account))-1
- sreason=" ({})".format(reason)ifreasonelse""
- string="Logged out: {account}{address} ({nsessions} sessions(s) remaining){reason}"
- string=string.format(
- reason=sreason,account=session.account,address=session.address,nsessions=nsess
- )
- session.log(string)
-
- ifnsess==0:
- SIGNAL_ACCOUNT_POST_LAST_LOGOUT.send(sender=session.account,session=session)
-
- session.at_disconnect(reason)
- SIGNAL_ACCOUNT_POST_LOGOUT.send(sender=session.account,session=session)
- sessid=session.sessid
- ifsessidinselfandnothasattr(self,"_disconnect_all"):
- delself[sessid]
- ifsync_portal:
- # inform portal that session should be closed.
- self.server.amp_protocol.send_AdminServer2Portal(
- session,operation=amp.SDISCONN,reason=reason
- )
-
-
[docs]defall_sessions_portal_sync(self):
- """
- This is called by the server when it reboots. It syncs all session data
- to the portal. Returns a deferred!
-
- """
- sessdata=self.get_all_sync_data()
- returnself.server.amp_protocol.send_AdminServer2Portal(
- DUMMYSESSION,operation=amp.SSYNC,sessiondata=sessdata
- )
-
-
[docs]defsession_portal_sync(self,session):
- """
- This is called by the server when it wants to sync a single session
- with the Portal for whatever reason. Returns a deferred!
-
- """
- sessdata={session.sessid:session.get_sync_data()}
- returnself.server.amp_protocol.send_AdminServer2Portal(
- DUMMYSESSION,operation=amp.SSYNC,sessiondata=sessdata,clean=False
- )
-
-
[docs]defsession_portal_partial_sync(self,session_data):
- """
- Call to make a partial update of the session, such as only a particular property.
-
- Args:
- session_data (dict): Store `{sessid: {property:value}, ...}` defining one or
- more sessions in detail.
-
- """
- returnself.server.amp_protocol.send_AdminServer2Portal(
- DUMMYSESSION,operation=amp.SSYNC,sessiondata=session_data,clean=False
- )
-
-
[docs]defdisconnect_all_sessions(self,reason="You have been disconnected."):
- """
- Cleanly disconnect all of the connected sessions.
-
- Args:
- reason (str, optional): The reason for the disconnection.
-
- """
-
- forsessioninself:
- delsession
- # tell portal to disconnect all sessions
- self.server.amp_protocol.send_AdminServer2Portal(
- DUMMYSESSION,operation=amp.SDISCONNALL,reason=reason
- )
-
-
[docs]defdisconnect_duplicate_sessions(
- self,curr_session,reason=_("Logged in from elsewhere. Disconnecting.")
- ):
- """
- Disconnects any existing sessions with the same user.
-
- args:
- curr_session (Session): Disconnect all Sessions matching this one.
- reason (str, optional): A motivation for disconnecting.
-
- """
- uid=curr_session.uid
- # we can't compare sessions directly since this will compare addresses and
- # mean connecting from the same host would not catch duplicates
- sid=id(curr_session)
- doublet_sessions=[
- sessforsessinself.values()
- ifsess.logged_inandsess.uid==uidandid(sess)!=sid
- ]
-
- forsessionindoublet_sessions:
- self.disconnect(session,reason)
-
-
[docs]defvalidate_sessions(self):
- """
- Check all currently connected sessions (logged in and not) and
- see if any are dead or idle.
-
- """
- tcurr=time.time()
- reason=_("Idle timeout exceeded, disconnecting.")
- forsessionin(
- session
- forsessioninself.values()
- ifsession.logged_in
- and_IDLE_TIMEOUT>0
- and(tcurr-session.cmd_last)>_IDLE_TIMEOUT
- ):
- self.disconnect(session,reason=reason)
-
-
[docs]defaccount_count(self):
- """
- Get the number of connected accounts (not sessions since a
- account may have more than one session depending on settings).
- Only logged-in accounts are counted here.
-
- Returns:
- naccount (int): Number of connected accounts
-
- """
- returnlen(set(session.uidforsessioninself.values()ifsession.logged_in))
-
-
[docs]defall_connected_accounts(self):
- """
- Get a unique list of connected and logged-in Accounts.
-
- Returns:
- accounts (list): All conected Accounts (which may be fewer than the
- amount of Sessions due to multi-playing).
-
- """
- returnlist(
- set(
- session.account
- forsessioninself.values()
- ifsession.logged_inandsession.account
- )
- )
-
-
[docs]defsession_from_sessid(self,sessid):
- """
- Get session based on sessid, or None if not found
-
- Args:
- sessid (int or list): Session id(s).
-
- Return:
- sessions (Session or list): Session(s) found. This
- is a list if input was a list.
-
- """
- ifis_iter(sessid):
- return[self.get(sid)forsidinsessidifsidinself]
- returnself.get(sessid)
-
-
[docs]defsession_from_account(self,account,sessid):
- """
- Given an account and a session id, return the actual session
- object.
-
- Args:
- account (Account): The Account to get the Session from.
- sessid (int or list): Session id(s).
-
- Returns:
- sessions (Session or list): Session(s) found.
-
- """
- sessions=[
- self[sid]
- forsidinmake_iter(sessid)
- ifsidinselfandself[sid].logged_inandaccount.uid==self[sid].uid
- ]
- returnsessions[0]iflen(sessions)==1elsesessions
-
-
[docs]defsessions_from_account(self,account):
- """
- Given an account, return all matching sessions.
-
- Args:
- account (Account): Account to get sessions from.
-
- Returns:
- sessions (list): All Sessions associated with this account.
-
- """
- uid=account.uid
- return[sessionforsessioninself.values()ifsession.logged_inandsession.uid==uid]
-
-
[docs]defsessions_from_puppet(self,puppet):
- """
- Given a puppeted object, return all controlling sessions.
-
- Args:
- 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=puppet.sessid.get()
- returnsessions[0]iflen(sessions)==1elsesessions
[docs]defsessions_from_csessid(self,csessid):
- """
- Given a client identification hash (for session types that offer them)
- return all sessions with a matching hash.
-
- Args
- csessid (str): The session hash.
-
- Returns:
- sessions (list): The sessions with matching .csessid, if any.
-
- """
- ifcsessid:
- return[]
- return[
- sessionforsessioninself.values()ifsession.csessidandsession.csessid==csessid
- ]
-
-
[docs]defannounce_all(self,message):
- """
- Send message to all connected sessions
-
- Args:
- message (str): Message to send.
-
- """
- forsessioninself.values():
- self.data_out(session,text=message)
-
-
[docs]defdata_out(self,session,**kwargs):
- """
- Sending data Server -> Portal
-
- Args:
- session (Session): Session to relay to.
- text (str, optional): text data to return
-
- Notes:
- The outdata will be scrubbed for sending across
- the wire here.
- """
- # clean output for sending
- kwargs=self.clean_senddata(session,kwargs)
-
- # send across AMP
- self.server.amp_protocol.send_MsgServer2Portal(session,**kwargs)
-
-
[docs]defget_inputfuncs(self):
- """
- Get all registered inputfuncs (access function)
-
- Returns:
- inputfuncs (dict): A dict of {key:inputfunc,...}
- """
- return_INPUT_FUNCS
-
-
[docs]defdata_in(self,session,**kwargs):
- """
- We let the data take a "detour" to session.data_in
- so the user can override and see it all in one place.
- That method is responsible to in turn always call
- this class' `sessionhandler.call_inputfunc` with the
- (possibly processed) data.
-
- """
- ifsession:
- session.data_in(**kwargs)
-
-
[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)`.
-
- We also intercept OOB communication here.
-
- Args:
- sessions (Session): Session.
-
- Keyword Args:
- any (tuple): Incoming data from protocol, each
- on the form `commandname=((args), {kwargs})`.
-
- """
-
- # distribute incoming data to the correct receiving methods.
- ifsession:
- input_debug=session.protocol_flags.get("INPUTDEBUG",False)
- forcmdname,(cmdargs,cmdkwargs)inkwargs.items():
- cname=cmdname.strip().lower()
- try:
- cmdkwargs.pop("options",None)
- ifcnamein_INPUT_FUNCS:
- _INPUT_FUNCS[cname](session,*cmdargs,**cmdkwargs)
- else:
- _INPUT_FUNCS["default"](session,cname,*cmdargs,**cmdkwargs)
- exceptExceptionaserr:
- ifinput_debug:
- session.msg(err)
- log_trace()
-
-
-# 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
-
[docs]classThrottle:
- """
- Keeps a running count of failed actions per IP address.
-
- Available methods indicate whether or not the number of failures exceeds a
- particular threshold.
-
- 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 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.")
-
-
[docs]def__init__(self,**kwargs):
- """
- Allows setting of throttle parameters.
-
- Keyword Args:
- 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!
- """
- 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.
-
- Args:
- ip (str, optional): IP address of requestor
-
- Returns:
- storage (dict): When no IP is provided, returns a dict of all
- current IPs being tracked and the timestamps of their recent
- failures.
- timestamps (deque): When an IP is provided, returns a deque of
- timestamps of recent failures only for that IP.
-
- """
- ifip:
- cache_key=self.get_cache_key(str(ip))
- returnself.storage.get(cache_key,deque(maxlen=self.cache_size))
- else:
- keys_key=self.get_cache_key('keys')
- keys=self.storage.get_or_set(keys_key,set(),self.timeout)
- data=self.storage.get_many((self.get_cache_key(x)forxinkeys))
-
- found_keys=set(data.keys())
- iflen(keys)!=len(found_keys):
- self.storage.set(keys_key,found_keys,self.timeout)
-
- returndata
-
-
[docs]defupdate(self,ip,failmsg="Exceeded threshold."):
- """
- Store the time of the latest failure.
-
- Args:
- ip (str): IP address of requestor
- failmsg (str, optional): Message to display in logs upon activation
- of throttle.
-
- Returns:
- None
-
- """
- cache_key=self.get_cache_key(ip)
-
- # Get current status
- previously_throttled=self.check(ip)
-
- # Get previous failures, if any
- entries=self.storage.get(cache_key,[])
- entries.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 status
- currently_throttled=self.check(ip)
-
- # If this makes it engage, log a single activation event
- ifnotpreviously_throttledandcurrently_throttled:
- logger.log_sec(
- f"Throttle Activated: {failmsg} (IP: {ip}, "
- f"{self.limit} hits in {self.timeout} seconds.)"
- )
-
- self.record_ip(ip)
-
-
[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):
- """
- This will check the session's address against the
- storage dictionary to check they haven't spammed too many
- fails recently.
-
- Args:
- ip (str): IP address of requestor
-
- Returns:
- throttled (bool): True if throttling is active,
- 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.get(cache_key)
- iflatest_failsandlen(latest_fails)>=self.limit:
- # too many fails recently
- ifnow-latest_fails[-1]<self.timeout:
- # too soon - timeout in play
- self.touch(cache_key)
- returnTrue
- else:
- # timeout has passed. clear faillist
- self.remove(ip)
- returnFalse
- else:
- returnFalse
[docs]classEvenniaUsernameAvailabilityValidator:
- """
- Checks to make sure a given username is not taken or otherwise reserved.
- """
-
- def__call__(self,username):
- """
- Validates a username to make sure it is not in use or reserved.
-
- Args:
- username (str): Username to validate
-
- Returns:
- None (None): None if password successfully validated,
- raises ValidationError otherwise.
-
- """
-
- # Check guest list
- ifsettings.GUEST_LISTandusername.lower()in(
- guest.lower()forguestinsettings.GUEST_LIST
- ):
- raiseValidationError(
- _("Sorry, that username is reserved."),code="evennia_username_reserved"
- )
-
- # Check database
- exists=AccountDB.objects.filter(username__iexact=username).exists()
- ifexists:
- raiseValidationError(
- _("Sorry, that username is already taken."),code="evennia_username_taken"
- )
[docs]def__init__(
- self,
- regex=r"^[\w. @+\-',]+$",
- policy="Password should contain a mix of letters, "
- "spaces, digits and @/./+/-/_/'/, only.",
- ):
- """
- Constructs a standard Django password validator.
-
- Args:
- regex (str): Regex pattern of valid characters to allow.
- policy (str): Brief explanation of what the defined regex permits.
-
- """
- self.regex=regex
- self.policy=policy
-
-
[docs]defvalidate(self,password,user=None):
- """
- Validates a password string to make sure it meets predefined Evennia
- acceptable character policy.
-
- Args:
- password (str): Password to validate
- user (None): Unused argument but required by Django
-
- Returns:
- None (None): None if password successfully validated,
- raises ValidationError otherwise.
-
- """
- # Check complexity
- ifnotre.findall(self.regex,password):
- raiseValidationError(_(self.policy),code="evennia_password_policy")
-
-
[docs]defget_help_text(self):
- """
- Returns a user-facing explanation of the password policy defined
- by this validator.
-
- Returns:
- text (str): Explanation of password policy.
-
- """
- return_(
- "{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)
- )
-"""
-This implements resources for Twisted webservers using the WSGI
-interface of Django. This alleviates the need of running e.g. an
-Apache server to serve Evennia's web presence (although you could do
-that too if desired).
-
-The actual servers are started inside server.py as part of the Evennia
-application.
-
-(Lots of thanks to http://github.com/clemesha/twisted-wsgi-django for
-a great example/aid on how to do this.)
-
-
-"""
-importurllib.parse
-fromurllib.parseimportquoteasurlquote
-fromtwisted.webimportresource,http,server,static
-fromtwisted.internetimportreactor
-fromtwisted.applicationimportinternet
-fromtwisted.web.proxyimportReverseProxyResource
-fromtwisted.web.serverimportNOT_DONE_YET
-fromtwisted.pythonimportthreadpool
-fromtwisted.internetimportdefer
-
-fromtwisted.web.wsgiimportWSGIResource
-fromdjango.confimportsettings
-fromdjango.core.wsgiimportget_wsgi_application
-
-
-fromevennia.utilsimportlogger
-
-_UPSTREAM_IPS=settings.UPSTREAM_IPS
-_DEBUG=settings.DEBUG
-
-
-
[docs]classLockableThreadPool(threadpool.ThreadPool):
- """
- Threadpool that can be locked from accepting new requests.
- """
-
-
[docs]defcallInThread(self,func,*args,**kwargs):
- """
- called in the main reactor thread. Makes sure the pool
- is not locked before continuing.
- """
- ifself._accept_new:
- threadpool.ThreadPool.callInThread(self,func,*args,**kwargs)
[docs]defgetChild(self,path,request):
- """
- Create and return a proxy resource with the same proxy configuration
- as this one, except that its path also contains the segment given by
- path at the end.
-
- Args:
- path (str): Url path.
- request (Request object): Incoming request.
-
- Return:
- resource (EvenniaReverseProxyResource): A proxy resource.
-
- """
- request.notifyFinish().addErrback(
- lambdaf:0
- # lambda f: logger.log_trace("%s\nCaught errback in webserver.py" % f)
- )
- returnEvenniaReverseProxyResource(
- self.host,self.port,self.path+"/"+urlquote(path,safe=""),self.reactor
- )
-
-
[docs]defrender(self,request):
- """
- Render a request by forwarding it to the proxied server.
-
- Args:
- request (Request): Incoming request.
-
- Returns:
- not_done (char): Indicator to note request not yet finished.
-
- """
- # RFC 2616 tells us that we can omit the port if it's the default port,
- # but we have to provide it otherwise
- request.content.seek(0,0)
- qs=urllib.parse.urlparse(request.uri)[4]
- ifqs:
- rest=self.path+"?"+qs.decode()
- else:
- rest=self.path
- rest=rest.encode()
- clientFactory=self.proxyClientFactoryClass(
- request.method,
- rest,
- request.clientproto,
- request.getAllHeaders(),
- request.content.read(),
- request,
- )
- clientFactory.noisy=False
- self.reactor.connectTCP(self.host,self.port,clientFactory)
- # don't trigger traceback if connection is lost before request finish.
- request.notifyFinish().addErrback(lambdaf:0)
- # request.notifyFinish().addErrback(
- # lambda f:logger.log_trace("Caught errback in webserver.py: %s" % f)
- returnNOT_DONE_YET
-
-
-#
-# Website server resource
-#
-
-
-
[docs]classDjangoWebRoot(resource.Resource):
- """
- This creates a web root (/) that Django
- understands by tweaking the way
- child instances are recognized.
- """
-
-
[docs]def__init__(self,pool):
- """
- Setup the django+twisted resource.
-
- Args:
- pool (ThreadPool): The twisted threadpool.
-
- """
- self.pool=pool
- self._echo_log=True
- self._pending_requests={}
- super().__init__()
- self.wsgi_resource=WSGIResource(reactor,pool,get_wsgi_application())
-
-
[docs]defempty_threadpool(self):
- """
- Converts our _pending_requests list of deferreds into a DeferredList
-
- Returns:
- deflist (DeferredList): Contains all deferreds of pending requests.
-
- """
- self.pool.lock()
- ifself._pending_requestsandself._echo_log:
- self._echo_log=False# just to avoid multiple echoes
- msg="Webserver waiting for %i requests ... "
- logger.log_info(msg%len(self._pending_requests))
- returndefer.DeferredList(self._pending_requests,consumeErrors=True)
[docs]defgetChild(self,path,request):
- """
- To make things work we nudge the url tree to make this the
- root.
-
- Args:
- path (str): Url path.
- request (Request object): Incoming request.
-
- Notes:
- We make sure to save the request queue so
- that we can safely kill the threadpool
- on a server reload.
-
- """
- path0=request.prepath.pop(0)
- request.postpath.insert(0,path0)
-
- request.notifyFinish().addErrback(
- lambdaf:0
- # lambda f: logger.log_trace("%s\nCaught errback in webserver.py:" % f)
- )
-
- deferred=request.notifyFinish()
- self._pending_requests[deferred]=deferred
- deferred.addBoth(self._decrement_requests,deferred=deferred)
-
- returnself.wsgi_resource
-
-
-#
-# Site with deactivateable logging
-#
-
-
-
[docs]classWebsite(server.Site):
- """
- This class will only log http requests if settings.DEBUG is True.
- """
-
- noisy=False
-
-
[docs]deflogPrefix(self):
- "How to be named in logs"
- ifhasattr(self,"is_portal")andself.is_portal:
- return"Webserver-proxy"
- return"Webserver"
[docs]classWSGIWebServer(internet.TCPServer):
- """
- This is a WSGI webserver. It makes sure to start
- the threadpool after the service itself started,
- so as to register correctly with the twisted daemon.
-
- call with WSGIWebServer(threadpool, port, wsgi_resource)
-
- """
-
-
[docs]def__init__(self,pool,*args,**kwargs):
- """
- This just stores the threadpool.
-
- Args:
- pool (ThreadPool): The twisted threadpool.
- args, kwargs (any): Passed on to the TCPServer.
-
- """
- self.pool=pool
- super().__init__(*args,**kwargs)
-
-
[docs]defstartService(self):
- """
- Start the pool after the service starts.
-
- """
- super().startService()
- self.pool.start()
-
-
[docs]defstopService(self):
- """
- Safely stop the pool after the service stops.
-
- """
- super().stopService()
- self.pool.stop()
-
-
-
[docs]classPrivateStaticRoot(static.File):
- """
- This overrides the default static file resource so as to not make the
- directory listings public (that is, if you go to /media or /static you
- won't see an index of all static/media files on the server).
-
- """
-
-
-"""
-Attributes are arbitrary data stored on objects. Attributes supports
-both pure-string values and pickled arbitrary data.
-
-Attributes are also used to implement Nicks. This module also contains
-the Attribute- and NickHandlers as well as the `NAttributeHandler`,
-which is a non-db version of Attributes.
-
-
-"""
-importre
-importfnmatch
-
-fromcollectionsimportdefaultdict
-
-fromdjango.dbimportmodels
-fromdjango.confimportsettings
-fromdjango.utils.encodingimportsmart_str
-
-fromevennia.locks.lockhandlerimportLockHandler
-fromevennia.utils.idmapper.modelsimportSharedMemoryModel
-fromevennia.utils.dbserializeimportto_pickle,from_pickle
-fromevennia.utils.picklefieldimportPickledObjectField
-fromevennia.utils.utilsimportlazy_property,to_str,make_iter,is_iter
-
-_TYPECLASS_AGGRESSIVE_CACHE=settings.TYPECLASS_AGGRESSIVE_CACHE
-
-# -------------------------------------------------------------
-#
-# Attributes
-#
-# -------------------------------------------------------------
-
-
-
[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
- needs to store its open/closed/locked/unlocked state. These are done via
- attributes, rather than making different classes for each object type and
- storing them directly. The added benefit is that we can add/remove
- attributes on the fly as we like.
-
- The Attribute class defines the following properties:
- - key (str): Primary identifier.
- - lock_storage (str): Perm strings.
- - model (str): A string defining the model this is connected to. This
- is a natural_key, like "objects.objectdb"
- - date_created (datetime): When the attribute was created.
- - value (any): The data stored in the attribute, in pickled form
- using wrappers to be able to store/retrieve models.
- - strvalue (str): String-only data. This data is not pickled and
- is thus faster to search for in the database.
- - 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]classAttributeProperty:
- """
- Attribute property descriptor. Allows for specifying Attributes as Django-like 'fields'
- on the class level. Note that while one can set a lock on the Attribute,
- there is no way to *check* said lock when accessing via the property - use
- the full AttributeHandler if you need to do access checks.
-
- Example:
- ::
-
- class Character(DefaultCharacter):
- foo = AttributeProperty(default="Bar")
-
- """
- attrhandler_name="attributes"
-
-
[docs]def__init__(self,default=None,category=None,strattr=False,lockstring="",
- autocreate=False):
- """
- Initialize an Attribute as a property descriptor.
-
- Keyword Args:
- default (any): A default value if the attr is not set.
- category (str): The attribute's category. If unset, use class default.
- strattr (bool): If set, this Attribute *must* be a simple string, and will be
- stored more efficiently.
- lockstring (str): This is not itself useful with the property, but only if
- using the full AttributeHandler.get(accessing_obj=...) to access the
- Attribute.
- autocreate (bool): If an un-found Attr should lead to auto-creating the
- Attribute (with the default value). If `False`, the property will
- return the default value until it has been explicitly set. This means
- less database accesses, but also means the property will have no
- corresponding Attribute if wanting to access it directly via the
- AttributeHandler (it will also not show up in `examine`).
-
- """
- self._default=default
- self._category=category
- self._strattr=strattr
- self._lockstring=lockstring
- self._autocreate=autocreate
- self._key=""
-
- def__set_name__(self,cls,name):
- """
- Called when descriptor is first assigned to the class. It is called with
- the name of the field.
-
- """
- self._key=name
-
- def__get__(self,instance,owner):
- """
- Called when the attrkey is retrieved from the instance.
-
- """
- value=self._default
- try:
- value=(
- getattr(instance,self.attrhandler_name)
- .get(key=self._key,
- default=self._default,
- category=self._category,
- strattr=self._strattr,
- raise_exception=self._autocreate)
- )
- exceptAttributeError:
- ifself._autocreate:
- # attribute didn't exist and autocreate is set
- self.__set__(instance,self._default)
- else:
- raise
- finally:
- returnvalue
-
- def__set__(self,instance,value):
- """
- Called when assigning to the property (and when auto-creating an Attribute).
-
- """
- (
- getattr(instance,self.attrhandler_name)
- .add(self._key,
- value,
- category=self._category,
- lockstring=self._lockstring,
- strattr=self._strattr)
- )
-
- def__delete__(self,instance):
- """
- Called when running `del` on the field. Will remove/clear the Attribute.
-
- """
- (
- getattr(instance,self.attrhandler_name)
- .remove(key=self._key,
- category=self._category)
- )
-
-
-
[docs]classNAttributeProperty(AttributeProperty):
- """
- NAttribute property descriptor. Allows for specifying NAttributes as Django-like 'fields'
- on the class level.
-
- Example:
- ::
-
- class Character(DefaultCharacter):
- foo = NAttributeProperty(default="Bar")
-
- """
- attrhandler_name="nattributes"
-
-
-
[docs]classAttribute(IAttribute,SharedMemoryModel):
- """
- This attribute is stored via Django. Most Attributes will be using this class.
-
- """
-
- #
- # Attribute Database Model setup
- #
- # These database fields are all set using their corresponding properties,
- # named same as the field, but without the db_* prefix.
- db_key=models.CharField("key",max_length=255,db_index=True)
- db_value=PickledObjectField(
- "value",
- null=True,
- help_text="The data returned when the attribute is accessed. Must be "
- "written as a Python literal if editing through the admin "
- "interface. Attribute values which are not Python literals "
- "cannot be edited through the admin interface.",
- )
- db_strvalue=models.TextField(
- "strvalue",null=True,blank=True,help_text="String-specific storage for quick look-up"
- )
- db_category=models.CharField(
- "category",
- max_length=128,
- db_index=True,
- blank=True,
- null=True,
- help_text="Optional categorization of attribute.",
- )
- # Lock storage
- db_lock_storage=models.TextField(
- "locks",blank=True,help_text="Lockstrings for this object are stored here."
- )
- db_model=models.CharField(
- "model",
- max_length=32,
- db_index=True,
- blank=True,
- null=True,
- help_text="Which model of object this attribute is attached to (A "
- "natural key like 'objects.objectdb'). You should not change "
- "this value unless you know what you are doing.",
- )
- # subclass of Attribute (None or nick)
- db_attrtype=models.CharField(
- "attrtype",
- max_length=16,
- db_index=True,
- blank=True,
- null=True,
- help_text="Subclass of Attribute (None or nick)",
- )
- # time stamp
- db_date_created=models.DateTimeField("date_created",editable=False,auto_now_add=True)
-
- # Database manager
- # objects = managers.AttributeManager()
-
- classMeta:
- "Define Django meta options"
- verbose_name="Attribute"
-
- # Wrapper properties to easily set database fields. These are
- # @property decorators that allows to access these fields using
- # normal python operations (without having to remember to save()
- # etc). So e.g. a property 'attr' has a get/set/del decorator
- # defined that allows the user to do self.attr = value,
- # 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):
- 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)
-
- # value property (wraps db_value)
- @property
- defvalue(self):
- """
- Getter. Allows for `value = self.value`.
- We cannot cache here since it makes certain cases (such
- as storing a dbobj which is then deleted elsewhere) out-of-sync.
- The overhead of unpickling seems hard to avoid.
- """
- returnfrom_pickle(self.db_value,db_obj=self)
-
- @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)
- self.save(update_fields=["db_value"])
-
- @value.deleter
- defvalue(self):
- """Deleter. Allows for del attr.value. This removes the entire attribute."""
- self.delete()
-
-#
-# Handlers making use of the Attribute model
-#
-
-
-
[docs]classIAttributeBackend:
- """
- Abstract interface for the backends used by the Attribute Handler.
-
- All Backends must implement this base class.
- """
-
- _attrcreate="attrcreate"
- _attredit="attredit"
- _attrread="attrread"
- _attrclass=None
-
-
[docs]def__init__(self,handler,attrtype):
- self.handler=handler
- self.obj=handler.obj
- self._attrtype=attrtype
- self._objid=handler.obj.id
- self._cache={}
- # store category names fully cached
- self._catcache={}
- # full cache was run on all attributes
- self._cache_complete=False
-
-
[docs]defquery_all(self):
- """
- Fetch all Attributes from this object.
-
- 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={
- f"{to_str(attr.key).lower()}-{attr.category.lower()ifattr.categoryelseNone}":attr
- forattrinattrs
- }
- self._cache_complete=True
-
- 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)
-
- 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.
- Notes:
- When given a category only, a search for all objects
- of that cateogory is done and the category *name* is
- stored. This tells the system on subsequent calls that the
- list of cached attributes of this category is up-to-date
- and that the cache can be queried for category matches
- without missing any.
- The TYPECLASS_AGGRESSIVE_CACHE=False setting will turn off
- caching, causing each attribute access to trigger a
- database lookup.
-
- """
- key=key.strip().lower()ifkeyelseNone
- category=category.strip().lower()ifcategoryisnotNoneelseNone
- ifkey:
- returnself._get_cache_key(key,category)
- returnself._get_cache_category(category)
-
-
[docs]defget(self,key=None,category=None):
- """
- Frontend for .get_cache. Retrieves Attribute(s).
-
- 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 (IAttribute): The newly saved attribute
-
- """
- ifnot_TYPECLASS_AGGRESSIVE_CACHE:
- return
- ifnotkey:# don't allow an empty key in cache
- return
- cachekey="%s-%s"%(key,category)
- catkey="-%s"%category
- self._cache[cachekey]=attr_obj
- # mark that the category cache is no longer up-to-date
- self._catcache.pop(catkey,None)
- self._cache_complete=False
-
- def_delete_cache(self,key,category):
- """
- Remove attribute from cache
-
- Args:
- key (str): A cleaned key string
- category (str or None): A cleaned category name
-
- """
- catkey="-%s"%category
- ifkey:
- cachekey="%s-%s"%(key,category)
- self._cache.pop(cachekey,None)
- else:
- self._cache={
- key:attrobj
- forkey,attrobjinlist(self._cache.items())
- ifnotkey.endswith(catkey)
- }
- # mark that the category cache is no longer up-to-date
- self._catcache.pop(catkey,None)
- self._cache_complete=False
-
-
[docs]defreset_cache(self):
- """
- Reset cache from the outside.
- """
- self._cache_complete=False
- 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
- the object.
-
- Args:
- key (str or iterable): The Attribute key or keys to check for.
- If `None`, search by category.
- category (str or None): Limit the check to Attributes with this
- category (note, that `None` is the default category).
-
- Returns:
- has_attribute (bool or list): If the Attribute exists on
- this object or not. If `key` was given as an iterable then
- the return is a list of booleans.
-
- """
- ret=[]
- category=category.strip().lower()ifcategoryisnotNoneelseNone
- forkeystrinmake_iter(key):
- keystr=key.strip().lower()
- ret.extend(bool(attr)forattrinself.backend.get(keystr,category))
- returnret[0]iflen(ret)==1elseret
-
-
[docs]defget(
- self,
- key=None,
- default=None,
- category=None,
- return_obj=False,
- strattr=False,
- raise_exception=False,
- accessing_obj=None,
- default_access=True,
- return_list=False,
- ):
- """
- Get the Attribute.
-
- Args:
- key (str or list, optional): the attribute identifier or
- multiple attributes to get. if a list of keys, the
- method will return a list.
- default (any, optional): The value to return if an
- Attribute was not defined. If set, it will be returned in
- a one-item list.
- category (str, optional): the category within which to
- retrieve attribute(s).
- return_obj (bool, optional): If set, the return is not the value of the
- Attribute but the Attribute object itself.
- strattr (bool, optional): Return the `strvalue` field of
- the Attribute rather than the usual `value`, this is a
- string-only value for quick database searches.
- raise_exception (bool, optional): When an Attribute is not
- found, the return from this is usually `default`. If this
- is set, an exception is raised instead.
- accessing_obj (object, optional): If set, an `attrread`
- 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): 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
- categories. Each match will be the value of the found Attribute(s)
- unless `return_obj` is True, at which point it will be the
- attribute object itself or None. If `return_list` is True, this
- will always be a list, regardless of the number of elements.
-
- Raises:
- AttributeError: If `raise_exception` is set and no matching Attribute
- was found matching `key`.
-
- """
-
- ret=[]
- forkeystrinmake_iter(key):
- # it's okay to send a None key
- attr_objs=self.backend.get(keystr,category)
- ifattr_objs:
- ret.extend(attr_objs)
- elifraise_exception:
- raiseAttributeError
- elifreturn_obj:
- ret.append(None)
-
- ifaccessing_obj:
- # check 'attrread' locks
- ret=[
- attr
- forattrinret
- ifattr.access(accessing_obj,self._attrread,default=default_access)
- ]
- ifstrattr:
- ret=retifreturn_objelse[attr.strvalueforattrinretifattr]
- else:
- ret=retifreturn_objelse[attr.valueforattrinretifattr]
-
- ifreturn_list:
- returnretifretelse[default]ifdefaultisnotNoneelse[]
- returnret[0]ifretandlen(ret)==1elseretordefault
-
-
[docs]defadd(
- self,
- key,
- value,
- category=None,
- lockstring="",
- strattr=False,
- accessing_obj=None,
- default_access=True,
- ):
- """
- Add attribute to object, with optional `lockstring`.
-
- Args:
- key (str): An Attribute name to add.
- value (any or str): The value of the Attribute. If
- `strattr` keyword is set, this *must* be a string.
- category (str, optional): The category for the Attribute.
- The default `None` is the normal category used.
- lockstring (str, optional): A lock string limiting access
- to the attribute.
- strattr (bool, optional): Make this a string-only Attribute.
- This is only ever useful for optimization purposes.
- accessing_obj (object, optional): An entity to check for
- the `attrcreate` access-type. If not passing, this method
- will be exited.
- default_access (bool, optional): What access to grant if
- `accessing_obj` is given but no lock of the type
- `attrcreate` is defined on the Attribute in question.
-
- """
- ifaccessing_objandnotself.obj.access(
- accessing_obj,self._attrcreate,default=default_access
- ):
- # check create access
- return
-
- ifnotkey:
- return
-
- category=category.strip().lower()ifcategoryisnotNoneelseNone
- keystr=key.strip().lower()
- attr_obj=self.backend.get(key,category)
-
- ifattr_obj:
- # update an existing attribute object
- attr_obj=attr_obj[0]
- self.backend.update_attribute(attr_obj,value)
- else:
- # create a new Attribute (no OOB handlers can be notified)
- self.backend.create_attribute(keystr,category,lockstring,value,strattr)
-
-
[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): Each argument should be a tuples (can be 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)
-
- Keyword Args:
- strattr (bool): If `True`, value must be a string. This
- will save the value without pickling which is less
- flexible but faster to search (not often used except
- internally).
-
- 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 like
- normal .add does, and is mainly used internally. It does not
- use the normal self.add but apply the Attributes directly
- to the database.
-
- """
- self.backend.batch_add(*args,**kwargs)
-
-
[docs]defremove(
- self,
- key=None,
- category=None,
- raise_exception=False,
- accessing_obj=None,
- default_access=True,
- ):
- """
- Remove attribute or a list of attributes from object.
-
- Args:
- key (str or list, optional): An Attribute key to remove or a list of keys. If
- multiple keys, they must all be of the same `category`. If None and
- category is not given, remove all Attributes.
- category (str, optional): The category within which to
- remove the Attribute.
- raise_exception (bool, optional): If set, not finding the
- Attribute to delete will raise an exception instead of
- just quietly failing.
- accessing_obj (object, optional): An object to check
- against the `attredit` lock. If not given, the check will
- be skipped.
- default_access (bool, optional): The fallback access to
- grant if `accessing_obj` is given but there is no
- `attredit` lock set on the Attribute in question.
-
- Raises:
- AttributeError: If `raise_exception` is set and no matching Attribute
- was found matching `key`.
-
- Notes:
- If neither key nor category is given, this acts as clear().
-
- """
-
- ifkeyisNone:
- self.clear(
- category=category,accessing_obj=accessing_obj,default_access=default_access
- )
- return
-
- category=category.strip().lower()ifcategoryisnotNoneelseNone
-
- forkeystrinmake_iter(key):
- keystr=keystr.lower()
-
- attr_objs=self.backend.get(keystr,category)
- forattr_objinattr_objs:
- ifnot(
- accessing_obj
- andnotattr_obj.access(accessing_obj,self._attredit,default=default_access)
- ):
- self.backend.delete_attribute(attr_obj)
- ifnotattr_objsandraise_exception:
- raiseAttributeError
-
-
[docs]defclear(self,category=None,accessing_obj=None,default_access=True):
- """
- 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.
-
- """
- self.backend.clear_attributes(category,accessing_obj,default_access)
-
-
[docs]defall(self,accessing_obj=None,default_access=True):
- """
- Return all Attribute objects on this object, regardless of category.
-
- Args:
- accessing_obj (object, optional): Check the `attrread`
- lock on each attribute before returning them. If not
- given, this check is skipped.
- default_access (bool, optional): Use this permission as a
- fallback if `accessing_obj` is given but one or more
- Attributes has no lock of type `attrread` defined on them.
-
- Returns:
- Attributes (list): All the Attribute objects (note: Not
- their values!) in the handler.
-
- """
- attrs=self.backend.get_all_attributes()
-
- ifaccessing_obj:
- return[
- attr
- forattrinattrs
- ifattr.access(accessing_obj,self._attrread,default=default_access)
- ]
- else:
- returnattrs
-
-
-#
-# 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_OR=re.compile(r"(?<!\\)\|")
-_RE_NICK_RE_ARG=re.compile(r"arg([1-9][0-9]?)")
-_RE_NICK_ARG=re.compile(r"\\(\$)([1-9][0-9]?)")
-_RE_NICK_RAW_ARG=re.compile(r"(\$)([1-9][0-9]?)")
-_RE_NICK_SPACE=re.compile(r"\\ ")
-
-
-
[docs]definitialize_nick_templates(pattern,replacement,pattern_is_regex=False):
- """
- Initialize the nick templates for matching and remapping a string.
-
- Args:
- 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, template (str): Regex to match against strings and template
- with markers ``{arg1}, {arg2}``, etc for replacement using the standard
- `.format` method.
-
- Raises:
- 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 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.
-
- # 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))
-
- 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.")
-
- # 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"
-
- # 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):
- """
- Parse a text using a template and map it to another template
-
- Args:
- 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 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:
- matchdict={key:valueifvalueisnotNoneelse""
- forkey,valueinmatch.groupdict().items()}
- returnTrue,outtemplate.format(**matchdict)
- returnFalse,string
-
-
-
[docs]classNickHandler(AttributeHandler):
- """
- Handles the addition and removal of Nicks. Nicks are special
- versions of Attributes with an `_attrtype` hardcoded to `nick`.
- They also always use the `strvalue` fields for their data.
-
- """
-
- _attrtype="nick"
-
-
[docs]defhas(self,key,category="inputline"):
- """
- Args:
- key (str or iterable): The Nick key or keys to check for.
- category (str): Limit the check to Nicks with this
- category (note, that `None` is the default category).
-
- Returns:
- has_nick (bool or list): If the Nick exists on this object
- or not. If `key` was given as an iterable then the return
- is a list of booleans.
-
- """
- returnsuper().has(key,category=category)
-
-
[docs]defget(self,key=None,category="inputline",return_tuple=False,**kwargs):
- """
- Get the replacement value matching the given key and category
-
- Args:
- key (str or list, optional): the attribute identifier or
- multiple attributes to get. if a list of keys, the
- method will return a list.
- category (str, optional): the category within which to
- retrieve the nick. The "inputline" means replacing data
- sent by the user.
- return_tuple (bool, optional): return the full nick tuple rather
- than just the replacement. For non-template nicks this is just
- 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)
- else:
- retval=super().get(key=key,category=category,**kwargs)
- ifretval:
- return(
- retval[3]
- ifisinstance(retval,tuple)
- else[tup[3]fortupinmake_iter(retval)]
- )
- returnNone
-
-
[docs]defadd(self,pattern,replacement,category="inputline",pattern_is_regex=False,**kwargs):
- """
- Add a new nick, a mapping pattern -> replacement.
-
- Args:
- 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.
- 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."`
-
- """
- nick_regex,nick_template=initialize_nick_templates(
- pattern,replacement,pattern_is_regex=pattern_is_regex)
- super().add(pattern,(nick_regex,nick_template,pattern,replacement),
- category=category,**kwargs)
-
-
[docs]defremove(self,key,category="inputline",**kwargs):
- """
- Remove Nick with matching category.
-
- Args:
- key (str): A key for the nick to match for.
- category (str, optional): the category within which to
- removethe nick. The "inputline" means replacing data
- sent by the user.
- kwargs (any, optional): These are passed on to `AttributeHandler.get`.
-
- """
- super().remove(key,category=category,**kwargs)
-
-
[docs]defnickreplace(self,raw_string,categories=("inputline","channel"),include_account=True):
- """
- Apply nick replacement of entries in raw_string with nick replacement.
-
- Args:
- raw_string (str): The string in which to perform nick
- replacement.
- categories (tuple, optional): Replacement categories in
- which to perform the replacement, such as "inputline",
- "channel" etc.
- include_account (bool, optional): Also include replacement
- with nicks stored on the Account level.
- kwargs (any, optional): Not used.
-
- Returns:
- string (str): A string with matching keys replaced with
- their nick equivalents.
-
- """
- nicks={}
- forcategoryinmake_iter(categories):
- nicks.update(
- {
- nick.key:nick
- fornickinmake_iter(self.get(category=category,return_obj=True))
- ifnickandnick.key
- }
- )
- ifinclude_accountandself.obj.has_account:
- forcategoryinmake_iter(categories):
- nicks.update(
- {
- nick.key:nick
- fornickinmake_iter(
- self.obj.account.nicks.get(category=category,return_obj=True)
- )
- ifnickandnick.key
- }
- )
- forkey,nickinnicks.items():
- nick_regex,template,_,_=nick.value
- regex=self._regex_cache.get(nick_regex)
- ifnotregex:
- regex=re.compile(nick_regex,re.I+re.DOTALL+re.U)
- self._regex_cache[nick_regex]=regex
-
- is_match,raw_string=parse_nick_template(raw_string.strip(),regex,template)
- ifis_match:
- break
- returnraw_string
-"""
-This implements the common managers that are used by the
-abstract models in dbobjects.py (and which are thus shared by
-all Attributes and TypedObjects).
-
-"""
-importshlex
-fromdjango.db.modelsimportF,Q,Count,ExpressionWrapper,FloatField
-fromdjango.db.models.functionsimportCast
-fromevennia.utilsimportidmapper
-fromevennia.utils.utilsimportmake_iter,variable_from_module
-fromevennia.typeclasses.attributesimportAttribute
-fromevennia.typeclasses.tagsimportTag
-
-__all__=("TypedObjectManager",)
-_GA=object.__getattribute__
-_Tag=None
-
-
-# Managers
-
-
-
[docs]classTypedObjectManager(idmapper.manager.SharedMemoryManager):
- """
- Common ObjectManager for all dbobjects.
-
- """
-
- # common methods for all typed managers. These are used
- # in other methods. Returns querysets.
-
- # Attribute manager methods
-
[docs]defget_attribute(
- 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.
-
- Args:
- 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
- `strvalue`.
- strvalue (str, optional): The str-value to search for.
- Most Attributes will not have strvalue set. This is
- mutually exclusive to the `value` keyword and will take
- precedence if given.
- obj (Object, optional): On which object the Attribute to
- search for is.
- 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.
-
- Returns:
- list: The matching Attributes.
-
- """
- dbmodel=self.model.__dbclass__.__name__.lower()
- query=[("attribute__db_attrtype",attrtype),("attribute__db_model",dbmodel)]
- ifobj:
- query.append(("%s__id"%self.model.__dbclass__.__name__.lower(),obj.id))
- ifkey:
- query.append(("attribute__db_key",key))
- ifcategory:
- query.append(("attribute__db_category",category))
- ifstrvalue:
- query.append(("attribute__db_strvalue",strvalue))
- ifvalue:
- # no reason to make strvalue/value mutually exclusive at this level
- query.append(("attribute__db_value",value))
- returnAttribute.objects.filter(
- pk__in=self.model.db_attributes.through.objects.filter(**dict(query)).values_list(
- "attribute_id",flat=True
- )
- )
-
-
[docs]defget_nick(self,key=None,category=None,value=None,strvalue=None,obj=None):
- """
- Get a nick, in parallel to `get_attribute`.
-
- Args:
- key (str, optional): The nicks's key to search for
- category (str, optional): The category of the nicks(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 `strvalue`.
- strvalue (str, optional): The str-value to search for. Most Attributes
- will not have strvalue set. This is mutually exclusive to the `value`
- keyword and will take precedence if given.
- obj (Object, optional): On which object the Attribute to search for is.
-
- Returns:
- nicks (list): The matching Nicks.
-
- """
- returnself.get_attribute(
- key=key,category=category,value=value,strvalue=strvalue,obj=obj
- )
-
-
[docs]defget_by_attribute(
- self,key=None,category=None,value=None,strvalue=None,attrtype=None,**kwargs
- ):
- """
- Return objects having attributes with the given key, category,
- value, strvalue or combination of those criteria.
-
- Args:
- key (str, optional): The attribute's key to search for
- category (str, optional): The category of the attribute
- 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
- `strvalue`.
- strvalue (str, optional): The str-value to search for.
- Most Attributes will not have strvalue set. This is
- mutually exclusive to the `value` keyword and will take
- precedence if given.
- 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.
-
- Returns:
- obj (list): Objects having the matching Attributes.
-
- """
- dbmodel=self.model.__dbclass__.__name__.lower()
- query=[
- ("db_attributes__db_attrtype",attrtype),
- ("db_attributes__db_model",dbmodel),
- ]
- ifkey:
- query.append(("db_attributes__db_key",key))
- ifcategory:
- query.append(("db_attributes__db_category",category))
- ifstrvalue:
- query.append(("db_attributes__db_strvalue",strvalue))
- elifvalue:
- # strvalue and value are mutually exclusive
- query.append(("db_attributes__db_value",value))
- returnself.filter(**dict(query))
-
-
[docs]defget_by_nick(self,key=None,nick=None,category="inputline"):
- """
- Get object based on its key or nick.
-
- Args:
- key (str, optional): The attribute's key to search for
- nick (str, optional): The nickname to search for
- category (str, optional): The category of the nick
- to search for.
-
- Returns:
- obj (list): Objects having the matching Nicks.
-
- """
- returnself.get_by_attribute(key=key,category=category,strvalue=nick,attrtype="nick")
-
- # Tag manager methods
-
-
[docs]defget_tag(self,key=None,category=None,obj=None,tagtype=None,global_search=False):
- """
- Return Tag objects by key, by category, by object (it is
- stored on) or with a combination of those criteria.
-
- Args:
- key (str, optional): The Tag's key to search for
- category (str, optional): The Tag of the attribute(s)
- to search for.
- obj (Object, optional): On which object the Tag to
- search for is.
- 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
-
- Returns:
- tag (list): The matching Tags.
-
- """
- global_Tag
- ifnot_Tag:
- fromevennia.typeclasses.modelsimportTagas_Tag
- dbmodel=self.model.__dbclass__.__name__.lower()
- ifglobal_search:
- # search all tags using the Tag model
- query=[("db_tagtype",tagtype),("db_model",dbmodel)]
- ifobj:
- query.append(("id",obj.id))
- ifkey:
- query.append(("db_key",key))
- ifcategory:
- query.append(("db_category",category))
- return_Tag.objects.filter(**dict(query))
- else:
- # search only among tags stored on on this model
- query=[("tag__db_tagtype",tagtype),("tag__db_model",dbmodel)]
- ifobj:
- query.append(("%s__id"%self.model.__name__.lower(),obj.id))
- ifkey:
- query.append(("tag__db_key",key))
- ifcategory:
- query.append(("tag__db_category",category))
- returnTag.objects.filter(
- pk__in=self.model.db_tags.through.objects.filter(**dict(query)).values_list(
- "tag_id",flat=True
- )
- )
-
-
[docs]defget_permission(self,key=None,category=None,obj=None):
- """
- Get a permission from the database.
-
- Args:
- key (str, optional): The permission's identifier.
- category (str, optional): The permission's category.
- obj (object, optional): The object on which this Tag is set.
-
- Returns:
- permission (list): Permission objects.
-
- """
- returnself.get_tag(key=key,category=category,obj=obj,tagtype="permission")
-
-
[docs]defget_alias(self,key=None,category=None,obj=None):
- """
- Get an alias from the database.
-
- Args:
- key (str, optional): The permission's identifier.
- category (str, optional): The permission's category.
- obj (object, optional): The object on which this Tag is set.
-
- Returns:
- alias (list): Alias objects.
-
- """
- returnself.get_tag(key=key,category=category,obj=obj,tagtype="alias")
-
-
[docs]defget_by_tag(self,key=None,category=None,tagtype=None,**kwargs):
- """
- Return objects having tags with a given key or category or combination of the two.
- Also accepts multiple tags/category/tagtype
-
- Args:
- key (str or list, optional): Tag key or list of keys. Not case sensitive.
- category (str or list, optional): Tag category. Not case sensitive.
- If `key` is a list, a single category can either apply to all
- keys in that list or this must be a list matching the `key`
- list element by element. If no `key` is given, all objects with
- tags of this category are returned.
- tagtype (str, optional): 'type' of Tag, by default
- this is either `None` (a normal Tag), `alias` or
- `permission`. This always apply to all queried tags.
-
- Keyword Args:
- match (str): "all" (default) or "any"; determines whether the
- target object must be tagged with ALL of the provided
- tags/categories or ANY single one. ANY will perform a weighted
- sort, so objects with more tag matches will outrank those with
- fewer tag matches.
-
- Returns:
- objects (list): Objects with matching tag.
-
- Raises:
- IndexError: If `key` and `category` are both lists and `category` is shorter
- than `key`.
-
- """
- ifnot(keyorcategory):
- return[]
-
- global_Tag
- ifnot_Tag:
- fromevennia.typeclasses.modelsimportTagas_Tag
-
- anymatch="any"==kwargs.get("match","all").lower().strip()
-
- keys=make_iter(key)ifkeyelse[]
- categories=make_iter(category)ifcategoryelse[]
- n_keys=len(keys)
- n_categories=len(categories)
- unique_categories=sorted(set(categories))
- n_unique_categories=len(unique_categories)
-
- dbmodel=self.model.__dbclass__.__name__.lower()
- query=(
- self.filter(db_tags__db_tagtype__iexact=tagtype,db_tags__db_model__iexact=dbmodel)
- .distinct()
- .order_by("id")
- )
-
- ifn_keys>0:
- # keys and/or categories given
- ifn_categories==0:
- categories=[Nonefor_inrange(n_keys)]
- elifn_categories==1andn_keys>1:
- cat=categories[0]
- categories=[catfor_inrange(n_keys)]
- elif1<n_categories<n_keys:
- raiseIndexError(
- "get_by_tag needs a single category or a list of categories "
- "the same length as the list of tags."
- )
- clauses=Q()
- forikey,keyinenumerate(keys):
- # ANY mode; must match any one of the given tags/categories
- clauses|=Q(db_key__iexact=key,db_category__iexact=categories[ikey])
- else:
- # only one or more categories given
- clauses=Q()
- # ANY mode; must match any one of them
- forcategoryinunique_categories:
- clauses|=Q(db_category__iexact=category)
-
- tags=_Tag.objects.filter(clauses)
- query=query.filter(db_tags__in=tags).annotate(
- matches=Count("db_tags__pk",filter=Q(db_tags__in=tags),distinct=True)
- )
-
- ifanymatch:
- # ANY: Match any single tag, ordered by weight
- query=query.order_by("-matches")
- else:
- # Default ALL: Match all of the tags and optionally more
- n_req_tags=n_keysifn_keys>0elsen_unique_categories
- query=query.filter(matches__gte=n_req_tags)
-
- returnquery
-
-
[docs]defget_by_permission(self,key=None,category=None):
- """
- Return objects having permissions with a given key or category or
- combination of the two.
-
- Args:
- key (str, optional): Permissions key. Not case sensitive.
- category (str, optional): Permission category. Not case sensitive.
- Returns:
- objects (list): Objects with matching permission.
- """
- returnself.get_by_tag(key=key,category=category,tagtype="permission")
-
-
[docs]defget_by_alias(self,key=None,category=None):
- """
- Return objects having aliases with a given key or category or
- combination of the two.
-
- Args:
- key (str, optional): Alias key. Not case sensitive.
- category (str, optional): Alias category. Not case sensitive.
- Returns:
- objects (list): Objects with matching alias.
- """
- returnself.get_by_tag(key=key,category=category,tagtype="alias")
-
-
[docs]defcreate_tag(self,key=None,category=None,data=None,tagtype=None):
- """
- Create a new Tag of the base type associated with this
- object. This makes sure to create case-insensitive tags.
- If the exact same tag configuration (key+category+tagtype+dbmodel)
- exists on the model, a new tag will not be created, but an old
- one returned.
-
-
- Args:
- key (str, optional): Tag key. Not case sensitive.
- category (str, optional): Tag category. Not case sensitive.
- data (str, optional): Extra information about the tag.
- tagtype (str or None, optional): 'type' of Tag, by default
- this is either `None` (a normal Tag), `alias` or
- `permission`.
- Notes:
- The `data` field is not part of the uniqueness of the tag:
- Setting `data` on an existing tag will overwrite the old
- data field. It is intended only as a way to carry
- information about the tag (like a help text), not to carry
- any information about the tagged objects themselves.
-
- """
- data=str(data)ifdataisnotNoneelseNone
- # try to get old tag
-
- dbmodel=self.model.__dbclass__.__name__.lower()
- tag=self.get_tag(key=key,category=category,tagtype=tagtype,global_search=True)
- iftaganddataisnotNone:
- # get tag from list returned by get_tag
- tag=tag[0]
- # overload data on tag
- tag.db_data=data
- tag.save()
- elifnottag:
- # create a new tag
- global_Tag
- ifnot_Tag:
- fromevennia.typeclasses.modelsimportTagas_Tag
- tag=_Tag.objects.create(
- db_key=key.strip().lower()ifkeyisnotNoneelseNone,
- db_category=category.strip().lower()ifcategoryandkeyisnotNoneelseNone,
- db_data=data,
- db_model=dbmodel,
- db_tagtype=tagtype.strip().lower()iftagtypeisnotNoneelseNone,
- )
- tag.save()
- returnmake_iter(tag)[0]
-
-
[docs]defdbref(self,dbref,reqhash=True):
- """
- Determing if input is a valid dbref.
-
- Args:
- dbref (str or int): A possible dbref.
- reqhash (bool, optional): If the "#" is required for this
- to be considered a valid hash.
-
- Returns:
- dbref (int or None): The integer part of the dbref.
-
- Notes:
- Valid forms of dbref (database reference number) are
- either a string '#N' or an integer N.
-
- """
- ifreqhashandnot(isinstance(dbref,str)anddbref.startswith("#")):
- returnNone
- ifisinstance(dbref,str):
- dbref=dbref.lstrip("#")
- try:
- ifint(dbref)<0:
- returnNone
- exceptException:
- returnNone
- returndbref
-
-
[docs]defget_id(self,dbref):
- """
- Find object with given dbref.
-
- Args:
- dbref (str or int): The id to search for.
-
- Returns:
- object (TypedObject): The matched object.
-
- """
- dbref=self.dbref(dbref,reqhash=False)
- try:
- returnself.get(id=dbref)
- exceptself.model.DoesNotExist:
- pass
- returnNone
-
-
[docs]defdbref_search(self,dbref):
- """
- Alias to get_id.
-
- Args:
- dbref (str or int): The id to search for.
-
- Returns:
- Queryset: Queryset with 0 or 1 match.
-
- """
- dbref=self.dbref(dbref,reqhash=False)
- ifdbref:
- returnself.filter(id=dbref)
- returnself.none()
-
-
[docs]defget_dbref_range(self,min_dbref=None,max_dbref=None):
- """
- Get objects within a certain range of dbrefs.
-
- Args:
- min_dbref (int): Start of dbref range.
- max_dbref (int): End of dbref range (inclusive)
-
- Returns:
- objects (list): TypedObjects with dbrefs within
- the given dbref ranges.
-
- """
- retval=super().all()
- ifmin_dbrefisnotNone:
- retval=retval.filter(id__gte=self.dbref(min_dbref,reqhash=False))
- ifmax_dbrefisnotNone:
- retval=retval.filter(id__lte=self.dbref(max_dbref,reqhash=False))
- returnretval
-
-
[docs]defget_typeclass_totals(self,*args,**kwargs)->object:
- """
- Returns a queryset of typeclass composition statistics.
-
- Returns:
- qs (Queryset): A queryset of dicts containing the typeclass (name),
- the count of objects with that typeclass and a float representing
- the percentage of objects associated with the typeclass.
-
- """
- return(
- self.values("db_typeclass_path")
- .distinct()
- .annotate(
- # Get count of how many objects for each typeclass exist
- count=Count("db_typeclass_path")
- )
- .annotate(
- # Rename db_typeclass_path field to something more human
- typeclass=F("db_typeclass_path"),
- # Calculate this class' percentage of total composition
- percent=ExpressionWrapper(
- ((F("count")/float(self.count()))*100.0),output_field=FloatField(),
- ),
- )
- .values("typeclass","count","percent")
- )
-
-
[docs]defobject_totals(self):
- """
- Get info about database statistics.
-
- Returns:
- census (dict): A dictionary `{typeclass_path: number, ...}` with
- all the typeclasses active in-game as well as the number
- of such objects defined (i.e. the number of database
- object having that typeclass set on themselves).
-
- """
- stats=self.get_typeclass_totals().order_by("typeclass")
- return{x.get("typeclass"):x.get("count")forxinstats}
-
-
[docs]deftypeclass_search(self,typeclass,include_children=False,include_parents=False):
- """
- Searches through all objects returning those which has a
- certain typeclass. If location is set, limit search to objects
- in that location.
-
- Args:
- typeclass (str or class): A typeclass class or a python path to a typeclass.
- include_children (bool, optional): Return objects with
- given typeclass *and* all children inheriting from this
- typeclass. Mutuall exclusive to `include_parents`.
- include_parents (bool, optional): Return objects with
- given typeclass *and* all parents to this typeclass.
- Mutually exclusive to `include_children`.
-
- Returns:
- objects (list): The objects found with the given typeclasses.
-
- """
-
- ifcallable(typeclass):
- cls=typeclass.__class__
- typeclass="%s.%s"%(cls.__module__,cls.__name__)
- elifnotisinstance(typeclass,str)andhasattr(typeclass,"path"):
- typeclass=typeclass.path
-
- # query objects of exact typeclass
- query=Q(db_typeclass_path__exact=typeclass)
-
- ifinclude_children:
- # build requests for child typeclass objects
- clsmodule,clsname=typeclass.rsplit(".",1)
- cls=variable_from_module(clsmodule,clsname)
- subclasses=cls.__subclasses__()
- ifsubclasses:
- forchildin(childforchildinsubclassesifhasattr(child,"path")):
- query=query|Q(db_typeclass_path__exact=child.path)
- elifinclude_parents:
- # build requests for parent typeclass objects
- clsmodule,clsname=typeclass.rsplit(".",1)
- cls=variable_from_module(clsmodule,clsname)
- parents=cls.__mro__
- ifparents:
- forparentin(parentforparentinparentsifhasattr(parent,"path")):
- query=query|Q(db_typeclass_path__exact=parent.path)
- # actually query the database
- returnsuper().filter(query)
-
-
-classTypeclassManager(TypedObjectManager):
- """
- Manager for the typeclasses. The main purpose of this manager is
- to limit database queries to the given typeclass despite all
- typeclasses technically being defined in the same core database
- model.
-
- """
-
- # object-manager methods
- defsmart_search(self,query):
- """
- 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 following
- forms:
-
- [key|alias|#dbref...]
- [tag==<tagstr>[:category]...]
- [attr==<key>:<value>:category...]
-
- All three can be combined in the same query, separated by spaces.
-
- Returns:
- 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
- matching only works for matching primitives. For even more complex queries, such as
- 'in' operations or object field matching, use the full django query language.
-
- """
- # shlex splits by spaces unless escaped by quotes
- querysplit=shlex.split(query)
- queries,plustags,plusattrs,negtags,negattrs=[],[],[],[],[]
- foripart,partinenumerate(querysplit):
- key,rest=part,""
- if":"inpart:
- key,rest=part.split(":",1)
- # tags are on the form tag or tag:category
- ifkey.startswith("tag=="):
- plustags.append((key[5:],rest))
- continue
- elifkey.startswith("tag!="):
- negtags.append((key[5:],rest))
- continue
- # attrs are on the form attr:value or attr:value:category
- elifrest:
- value,category=rest,""
- if":"inrest:
- value,category=rest.split(":",1)
- ifkey.startswith("attr=="):
- plusattrs.append((key[7:],value,category))
- continue
- elifkey.startswith("attr!="):
- negattrs.append((key[7:],value,category))
- continue
- # if we get here, we are entering a key search criterion which
- # we assume is one word.
- queries.append(part)
- # build query from components
- query=" ".join(queries)
- # TODO
-
- defget(self,*args,**kwargs):
- """
- Overload the standard get. This will limit itself to only
- return the current typeclass.
-
- Args:
- args (any): These are passed on as arguments to the default
- django get method.
- Keyword Args:
- kwargs (any): These are passed on as normal arguments
- to the default django get method
- Returns:
- object (object): The object found.
-
- Raises:
- ObjectNotFound: The exact name of this exception depends
- on the model base used.
-
- """
- kwargs.update({"db_typeclass_path":self.model.path})
- returnsuper().get(**kwargs)
-
- deffilter(self,*args,**kwargs):
- """
- Overload of the standard filter function. This filter will
- limit itself to only the current typeclass.
-
- Args:
- args (any): These are passed on as arguments to the default
- django filter method.
- Keyword Args:
- kwargs (any): These are passed on as normal arguments
- to the default django filter method.
- Returns:
- objects (queryset): The objects found.
-
- """
- kwargs.update({"db_typeclass_path":self.model.path})
- returnsuper().filter(*args,**kwargs)
-
- defall(self):
- """
- Overload method to return all matches, filtering for typeclass.
-
- Returns:
- objects (queryset): The objects found.
-
- """
- returnsuper().all().filter(db_typeclass_path=self.model.path)
-
- deffirst(self):
- """
- Overload method to return first match, filtering for typeclass.
-
- Returns:
- object (object): The object found.
-
- Raises:
- ObjectNotFound: The exact name of this exception depends
- on the model base used.
-
- """
- returnsuper().filter(db_typeclass_path=self.model.path).first()
-
- deflast(self):
- """
- Overload method to return last match, filtering for typeclass.
-
- Returns:
- object (object): The object found.
-
- Raises:
- ObjectNotFound: The exact name of this exception depends
- on the model base used.
-
- """
- returnsuper().filter(db_typeclass_path=self.model.path).last()
-
- defcount(self):
- """
- Overload method to return number of matches, filtering for typeclass.
-
- Returns:
- integer : Number of objects found.
-
- """
- returnsuper().filter(db_typeclass_path=self.model.path).count()
-
- defannotate(self,*args,**kwargs):
- """
- Overload annotate method to filter on typeclass before annotating.
- Args:
- *args (any): Positional arguments passed along to queryset annotate method.
- **kwargs (any): Keyword arguments passed along to queryset annotate method.
-
- Returns:
- Annotated queryset.
- """
- return(
- super(TypeclassManager,self)
- .filter(db_typeclass_path=self.model.path)
- .annotate(*args,**kwargs)
- )
-
- defvalues(self,*args,**kwargs):
- """
- Overload values method to filter on typeclass first.
- Args:
- *args (any): Positional arguments passed along to values method.
- **kwargs (any): Keyword arguments passed along to values method.
-
- Returns:
- Queryset of values dictionaries, just filtered by typeclass first.
- """
- return(
- super(TypeclassManager,self)
- .filter(db_typeclass_path=self.model.path)
- .values(*args,**kwargs)
- )
-
- defvalues_list(self,*args,**kwargs):
- """
- Overload values method to filter on typeclass first.
- Args:
- *args (any): Positional arguments passed along to values_list method.
- **kwargs (any): Keyword arguments passed along to values_list method.
-
- Returns:
- Queryset of value_list tuples, just filtered by typeclass first.
- """
- return(
- super(TypeclassManager,self)
- .filter(db_typeclass_path=self.model.path)
- .values_list(*args,**kwargs)
- )
-
- def_get_subclasses(self,cls):
- """
- Recursively get all subclasses to a class.
-
- Args:
- cls (classoject): A class to get subclasses from.
- """
- all_subclasses=cls.__subclasses__()
- forsubclassinall_subclasses:
- all_subclasses.extend(self._get_subclasses(subclass))
- returnall_subclasses
-
- defget_family(self,*args,**kwargs):
- """
- Variation of get that not only returns the current typeclass
- but also all subclasses of that typeclass.
-
- Keyword Args:
- kwargs (any): These are passed on as normal arguments
- to the default django get method.
- Returns:
- objects (list): The objects found.
-
- Raises:
- ObjectNotFound: The exact name of this exception depends
- on the model base used.
-
- """
- paths=[self.model.path]+[
- "%s.%s"%(cls.__module__,cls.__name__)forclsinself._get_subclasses(self.model)
- ]
- kwargs.update({"db_typeclass_path__in":paths})
- returnsuper().get(*args,**kwargs)
-
- deffilter_family(self,*args,**kwargs):
- """
- Variation of filter that allows results both from typeclass
- and from subclasses of typeclass
-
- Args:
- args (any): These are passed on as arguments to the default
- django filter method.
- Keyword Args:
- kwargs (any): These are passed on as normal arguments
- to the default django filter method.
- Returns:
- objects (list): The objects found.
-
- """
- # query, including all subclasses
- paths=[self.model.path]+[
- "%s.%s"%(cls.__module__,cls.__name__)forclsinself._get_subclasses(self.model)
- ]
- kwargs.update({"db_typeclass_path__in":paths})
- returnsuper().filter(*args,**kwargs)
-
- defall_family(self):
- """
- Return all matches, allowing matches from all subclasses of
- the typeclass.
-
- Returns:
- objects (list): The objects found.
-
- """
- paths=[self.model.path]+[
- "%s.%s"%(cls.__module__,cls.__name__)forclsinself._get_subclasses(self.model)
- ]
- returnsuper().all().filter(db_typeclass_path__in=paths)
-
-"""
-This is the *abstract* django models for many of the database objects
-in Evennia. A django abstract (obs, not the same as a Python metaclass!) is
-a model which is not actually created in the database, but which only exists
-for other models to inherit from, to avoid code duplication. Any model can
-import and inherit from these classes.
-
-Attributes are database objects stored on other objects. The implementing
-class needs to supply a ForeignKey field attr_object pointing to the kind
-of object being mapped. Attributes storing iterables actually store special
-types of iterables named PackedList/PackedDict respectively. These make
-sure to save changes to them to database - this is criticial in order to
-allow for obj.db.mylist[2] = data. Also, all dbobjects are saved as
-dbrefs but are also aggressively cached.
-
-TypedObjects are objects 'decorated' with a typeclass - that is, the typeclass
-(which is a normal Python class implementing some special tricks with its
-get/set attribute methods, allows for the creation of all sorts of different
-objects all with the same database object underneath. Usually attributes are
-used to permanently store things not hard-coded as field on the database object.
-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.
-
-"""
-fromdjango.db.modelsimportsignals
-
-fromdjango.db.models.baseimportModelBase
-fromdjango.dbimportmodels
-fromdjango.contrib.contenttypes.modelsimportContentType
-fromdjango.core.exceptionsimportObjectDoesNotExist
-fromdjango.confimportsettings
-fromdjango.urlsimportreverse
-fromdjango.utils.encodingimportsmart_str
-fromdjango.utils.textimportslugify
-
-fromevennia.typeclasses.attributesimport(
- Attribute,
- AttributeHandler,
- ModelAttributeBackend,
- InMemoryAttributeBackend,
-)
-fromevennia.typeclasses.attributesimportDbHolder
-fromevennia.typeclasses.tagsimportTag,TagHandler,AliasHandler,PermissionHandler
-
-fromevennia.utils.idmapper.modelsimportSharedMemoryModel,SharedMemoryModelBase
-fromevennia.server.signalsimportSIGNAL_TYPED_OBJECT_POST_RENAME
-
-fromevennia.typeclassesimportmanagers
-fromevennia.locks.lockhandlerimportLockHandler
-fromevennia.utils.utilsimportis_iter,inherits_from,lazy_property,class_from_module
-fromevennia.utils.loggerimportlog_trace
-
-__all__=("TypedObject",)
-
-TICKER_HANDLER=None
-
-_PERMISSION_HIERARCHY=[p.lower()forpinsettings.PERMISSION_HIERARCHY]
-_TYPECLASS_AGGRESSIVE_CACHE=settings.TYPECLASS_AGGRESSIVE_CACHE
-_GA=object.__getattribute__
-_SA=object.__setattr__
-
-
-# signal receivers. Connected in __new__
-
-
-defcall_at_first_save(sender,instance,created,**kwargs):
- """
- Receives a signal just after the object is saved.
-
- """
- ifcreated:
- instance.at_first_save()
-
-
-defremove_attributes_on_delete(sender,instance,**kwargs):
- """
- Wipe object's Attributes when it's deleted
-
- """
- instance.db_attributes.all().delete()
-
-
-# ------------------------------------------------------------
-#
-# Typed Objects
-#
-# ------------------------------------------------------------
-
-
-#
-# Meta class for typeclasses
-#
-
-
-classTypeclassBase(SharedMemoryModelBase):
- """
- 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):
- """
- We must define our Typeclasses as proxies. We also store the
- path directly on the class, this is required by managers.
- """
-
- # storage of stats
- attrs["typename"]=name
- attrs["path"]="%s.%s"%(attrs["__module__"],name)
-
- def_get_dbmodel(bases):
- """Recursively get the dbmodel"""
- ifnothasattr(bases,"__iter__"):
- bases=[bases]
- forbaseinbases:
- try:
- ifbase._meta.proxyorbase._meta.abstract:
- forklsinbase._meta.parents:
- return_get_dbmodel(kls)
- exceptAttributeError:
- # this happens if trying to parse a non-typeclass mixin parent,
- # without a _meta
- continue
- else:
- returnbase
- returnNone
-
- dbmodel=_get_dbmodel(bases)
-
- ifnotdbmodel:
- raiseTypeError(f"{name} does not appear to inherit from a database model.")
-
- # typeclass proxy setup
- # first check explicit __applabel__ on the typeclass, then figure
- # it out from the dbmodel
- if"__applabel__"notinattrs:
- # find the app-label in one of the bases, usually the dbmodel
- attrs["__applabel__"]=dbmodel._meta.app_label
-
- if"Meta"notinattrs:
-
- classMeta:
- proxy=True
- app_label=attrs.get("__applabel__","typeclasses")
-
- attrs["Meta"]=Meta
- attrs["Meta"].proxy=True
-
- new_class=ModelBase.__new__(cls,name,bases,attrs)
-
- # django doesn't support inheriting proxy models so we hack support for
- # it here by injecting `proxy_for_model` to the actual dbmodel.
- # Unfortunately we cannot also set the correct model_name, because this
- # would block multiple-inheritance of typeclasses (Django doesn't allow
- # multiple bases of the same model).
- ifdbmodel:
- new_class._meta.proxy_for_model=dbmodel
- # Maybe Django will eventually handle this in the future:
- # new_class._meta.model_name = dbmodel._meta.model_name
-
- # attach signals
- signals.post_save.connect(call_at_first_save,sender=new_class)
- signals.pre_delete.connect(remove_attributes_on_delete,sender=new_class)
- returnnew_class
-
-
-#
-# Main TypedObject abstraction
-#
-
-
-
[docs]classTypedObject(SharedMemoryModel):
- """
- 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
-
- """
-
- #
- # TypedObject Database Model setup
- #
- #
- # These databse fields are all accessed and set using their corresponding
- # properties, named same as the field, but without the db_* prefix
- # (no separate save() call is needed)
-
- # Main identifier of the object, for searching. Is accessed with self.key
- # or self.name
- db_key=models.CharField("key",max_length=255,db_index=True)
- # This is the python path to the type class this object is tied to. The
- # typeclass is what defines what kind of Object this is)
- db_typeclass_path=models.CharField(
- "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.",
- db_index=True,
- )
- # Creation date. This is not changed once the object is created.
- db_date_created=models.DateTimeField("creation date",editable=False,auto_now_add=True)
- # Lock storage
- 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.",
- )
- # many2many relationships
- db_attributes=models.ManyToManyField(
- Attribute,
- 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.",
- )
-
- # Database manager
- objects=managers.TypedObjectManager()
-
- # quick on-object typeclass cache for speed
- _cached_typeclass=None
-
- # typeclass mechanism
-
-
[docs]defset_class_from_typeclass(self,typeclass_path=None):
- iftypeclass_path:
- try:
- self.__class__=class_from_module(
- typeclass_path,defaultpaths=settings.TYPECLASS_PATHS
- )
- exceptException:
- log_trace()
- try:
- self.__class__=class_from_module(self.__settingsclasspath__)
- exceptException:
- log_trace()
- try:
- self.__class__=class_from_module(self.__defaultclasspath__)
- exceptException:
- log_trace()
- self.__class__=self._meta.concrete_modelorself.__class__
- finally:
- self.db_typeclass_path=typeclass_path
- elifself.db_typeclass_path:
- try:
- self.__class__=class_from_module(self.db_typeclass_path)
- exceptException:
- log_trace()
- try:
- self.__class__=class_from_module(self.__defaultclasspath__)
- exceptException:
- log_trace()
- self.__dbclass__=self._meta.concrete_modelorself.__class__
- else:
- self.db_typeclass_path="%s.%s"%(self.__module__,self.__class__.__name__)
- # important to put this at the end since _meta is based on the set __class__
- try:
- self.__dbclass__=self._meta.concrete_modelorself.__class__
- exceptAttributeError:
- err_class=repr(self.__class__)
- self.__class__=class_from_module("evennia.objects.objects.DefaultObject")
- self.__dbclass__=class_from_module("evennia.objects.models.ObjectDB")
- self.db_typeclass_path="evennia.objects.objects.DefaultObject"
- log_trace(
- "Critical: Class %s of %s is not a valid typeclass!\nTemporarily falling back to %s."
- %(err_class,self,self.__class__)
- )
-
-
[docs]def__init__(self,*args,**kwargs):
- """
- The `__init__` method of typeclasses is the core operational
- code of the typeclass system, where it dynamically re-applies
- a class based on the db_typeclass_path database field rather
- than use the one in the model.
-
- Args:
- Passed through to parent.
-
- Keyword Args:
- Passed through to parent.
-
- Notes:
- The loading mechanism will attempt the following steps:
-
- 1. Attempt to load typeclass given on command line
- 2. Attempt to load typeclass stored in db_typeclass_path
- 3. Attempt to load `__settingsclasspath__`, which is by the
- default classes defined to be the respective user-set
- base typeclass settings, like `BASE_OBJECT_TYPECLASS`.
- 4. Attempt to load `__defaultclasspath__`, which is the
- base classes in the library, like DefaultObject etc.
- 5. If everything else fails, use the database model.
-
- Normal operation is to load successfully at either step 1
- or 2 depending on how the class was called. Tracebacks
- will be logged for every step the loader must take beyond
- 2.
-
- """
- typeclass_path=kwargs.pop("typeclass",None)
- super().__init__(*args,**kwargs)
- self.set_class_from_typeclass(typeclass_path=typeclass_path)
-
- # wrapper
- # Wrapper properties to easily set database fields. These are
- # @property decorators that allows to access these fields using
- # normal python operations (without having to remember to save()
- # etc). So e.g. a property 'attr' has a get/set/del decorator
- # defined that allows the user to do self.attr = value,
- # value = self.attr and del self.attr respectively (where self
- # is the object in question).
-
- # name property (alias to self.key)
- def__name_get(self):
- returnself.key
-
- def__name_set(self,value):
- self.key=value
-
- def__name_del(self):
- raiseException("Cannot delete name")
-
- name=property(__name_get,__name_set,__name_del)
-
- # key property (overrides's the idmapper's db_key for the at_rename hook)
- @property
- defkey(self):
- returnself.db_key
-
- @key.setter
- defkey(self,value):
- oldname=str(self.db_key)
- self.db_key=value
- self.save(update_fields=["db_key"])
- self.at_rename(oldname,value)
- SIGNAL_TYPED_OBJECT_POST_RENAME.send(sender=self,old_key=oldname,new_key=value)
-
- #
- #
- # TypedObject main class methods and properties
- #
- #
-
- def__eq__(self,other):
- try:
- returnself.__dbclass__==other.__dbclass__andself.dbid==other.dbid
- exceptAttributeError:
- returnFalse
-
- def__hash__(self):
- # this is required to maintain hashing
- returnsuper().__hash__()
-
- def__str__(self):
- returnsmart_str("%s"%self.db_key)
-
- def__repr__(self):
- return"%s"%self.db_key
-
- # @property
- def__dbid_get(self):
- """
- Caches and returns the unique id of the object.
- Use this instead of self.id, which is not cached.
- """
- returnself.id
-
- def__dbid_set(self,value):
- raiseException("dbid cannot be set!")
-
- def__dbid_del(self):
- raiseException("dbid cannot be deleted!")
-
- dbid=property(__dbid_get,__dbid_set,__dbid_del)
-
- # @property
- def__dbref_get(self):
- """
- Returns the object's dbref on the form #NN.
- """
- return"#%s"%self.id
-
- def__dbref_set(self):
- raiseException("dbref cannot be set!")
-
- def__dbref_del(self):
- raiseException("dbref cannot be deleted!")
-
- dbref=property(__dbref_get,__dbref_set,__dbref_del)
-
-
[docs]defat_idmapper_flush(self):
- """
- This is called when the idmapper cache is flushed and
- allows customized actions when this happens.
-
- Returns:
- do_flush (bool): If True, flush this object as normal. If
- False, don't flush and expect this object to handle
- the flushing on its own.
-
- Notes:
- The default implementation relies on being able to clear
- Django's Foreignkey cache on objects not affected by the
- flush (notably objects with an NAttribute stored). We rely
- on this cache being stored on the format "_<fieldname>_cache".
- If Django were to change this name internally, we need to
- update here (unlikely, but marking just in case).
-
- """
- ifself.nattributes.all():
- # we can't flush this object if we have non-persistent
- # attributes stored - those would get lost! Nevertheless
- # we try to flush as many references as we can.
- self.attributes.reset_cache()
- self.tags.reset_cache()
- # flush caches for all related fields
- forfieldinself._meta.fields:
- name="_%s_cache"%field.name
- iffield.is_relationandnameinself.__dict__:
- # a foreignkey - remove its cache
- delself.__dict__[name]
- returnFalse
- # a normal flush
- returnTrue
-
- #
- # Object manipulation methods
- #
-
-
[docs]@classmethod
- defsearch(cls,query,**kwargs):
- """
- Overridden by class children. This implements a common API.
-
- Args:
- query (str): A search query.
- **kwargs: Other search parameters.
-
- Returns:
- list: A list of 0, 1 or more matches, only of this typeclass.
-
- """
- ifcls.objects.dbref(query):
- return[cls.objects.get_id(query)]
- returnlist(cls.objects.filter(db_key__lower=query))
-
-
[docs]defis_typeclass(self,typeclass,exact=False):
- """
- Returns true if this object has this type OR has a typeclass
- which is an subclass of the given typeclass. This operates on
- the actually loaded typeclass (this is important since a
- failing typeclass may instead have its default currently
- loaded) typeclass - can be a class object or the python path
- to such an object to match against.
-
- Args:
- typeclass (str or class): A class or the full python path
- to the class to check.
- exact (bool, optional): Returns true only if the object's
- type is exactly this typeclass, ignoring parents.
-
- Returns:
- is_typeclass (bool): If this typeclass matches the given
- typeclass.
-
- """
- ifisinstance(typeclass,str):
- typeclass=[typeclass]+[
- "%s.%s"%(prefix,typeclass)forprefixinsettings.TYPECLASS_PATHS
- ]
- else:
- typeclass=[typeclass.path]
-
- selfpath=self.path
- ifexact:
- # check only exact match
- returnselfpathintypeclass
- else:
- # check parent chain
- returnany(
- hasattr(cls,"path")andcls.pathintypeclassforclsinself.__class__.mro()
- )
-
-
[docs]defswap_typeclass(
- self,
- new_typeclass,
- clean_attributes=False,
- run_start_hooks="all",
- no_default=True,
- clean_cmdsets=False,
- ):
- """
- This performs an in-situ swap of the typeclass. This means
- that in-game, this object will suddenly be something else.
- Account will not be affected. To 'move' an account to a different
- object entirely (while retaining this object's type), use
- self.account.swap_object().
-
- Note that this might be an error prone operation if the
- old/new typeclass was heavily customized - your code
- might expect one and not the other, so be careful to
- bug test your code if using this feature! Often its easiest
- to create a new object and just swap the account over to
- that one instead.
-
- Args:
- new_typeclass (str or classobj): Type to switch to.
- clean_attributes (bool or list, optional): Will delete all
- attributes stored on this object (but not any of the
- database fields such as name or location). You can't get
- attributes back, but this is often the safest bet to make
- sure nothing in the new typeclass clashes with the old
- one. If you supply a list, only those named attributes
- will be cleared.
- run_start_hooks (str or None, optional): This is either None,
- to not run any hooks, "all" to run all hooks defined by
- at_first_start, or a string with space-separated hook-names to run
- (for example 'at_object_creation'). This will
- always be called without arguments.
- no_default (bool, optiona): If set, the swapper will not
- allow for swapping to a default typeclass in case the
- given one fails for some reason. Instead the old one will
- be preserved.
- clean_cmdsets (bool, optional): Delete all cmdsets on the object.
-
- """
-
- ifnotcallable(new_typeclass):
- # this is an actual class object - build the path
- new_typeclass=class_from_module(new_typeclass,defaultpaths=settings.TYPECLASS_PATHS)
-
- # if we get to this point, the class is ok.
-
- ifinherits_from(self,"evennia.scripts.models.ScriptDB"):
- ifself.interval>0:
- raiseRuntimeError(
- "Cannot use swap_typeclass on time-dependent "
- "Script '%s'.\nStop and start a new Script of the "
- "right type instead."%self.key
- )
-
- self.typeclass_path=new_typeclass.path
- self.__class__=new_typeclass
-
- ifclean_attributes:
- # Clean out old attributes
- ifis_iter(clean_attributes):
- forattrinclean_attributes:
- self.attributes.remove(attr)
- fornattrinclean_attributes:
- ifhasattr(self.ndb,nattr):
- self.nattributes.remove(nattr)
- else:
- self.attributes.clear()
- self.nattributes.clear()
- ifclean_cmdsets:
- # purge all cmdsets
- self.cmdset.clear()
- self.cmdset.remove_default()
-
- ifrun_start_hooks=="all":
- # fake this call to mimic the first save
- self.at_first_save()
- elifrun_start_hooks:
- # a custom hook-name to call.
- forstart_hookinstr(run_start_hooks).split():
- getattr(self,run_start_hooks)()
-
- #
- # Lock / permission methods
- #
-
-
[docs]defaccess(
- self,accessing_obj,access_type="read",default=False,no_superuser_bypass=False,**kwargs
- ):
- """
- Determines if another object has permission to access this one.
-
- Args:
- accessing_obj (str): Object trying to access this one.
- access_type (str, optional): Type of access sought.
- default (bool, optional): What to return if no lock of
- access_type was found
- no_superuser_bypass (bool, optional): Turn off the
- superuser lock bypass (be careful with this one).
-
- Keyword Args:
- 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.
-
- """
- returnself.locks.check(
- accessing_obj,
- access_type=access_type,
- default=default,
- no_superuser_bypass=no_superuser_bypass,
- )
-
-
[docs]defcheck_permstring(self,permstring):
- """
- This explicitly checks if we hold particular permission
- without involving any locks.
-
- Args:
- permstring (str): The permission string to check against.
-
- Returns:
- result (bool): If the permstring is passed or not.
-
- """
- ifhasattr(self,"account"):
- if(
- self.account
- andself.account.is_superuser
- andnotself.account.attributes.get("_quell")
- ):
- returnTrue
- else:
- ifself.is_superuserandnotself.attributes.get("_quell"):
- returnTrue
-
- ifnotpermstring:
- returnFalse
- perm=permstring.lower()
- perms=[p.lower()forpinself.permissions.all()]
- ifperminperms:
- # simplest case - we have a direct match
- returnTrue
- ifpermin_PERMISSION_HIERARCHY:
- # check if we have a higher hierarchy position
- ppos=_PERMISSION_HIERARCHY.index(perm)
- returnany(
- True
- forhpos,hperminenumerate(_PERMISSION_HIERARCHY)
- ifhperminpermsandhpos>ppos
- )
- # we ignore pluralization (english only)
- ifperm.endswith("s"):
- returnself.check_permstring(perm[:-1])
-
- returnFalse
-
- #
- # Attribute storage
- #
-
- @property
- defdb(self):
- """
- Attribute handler wrapper. Allows for the syntax
-
- ```python
- obj.db.attrname = value
- # and
- value = obj.db.attrname
- # and
- del obj.db.attrname
- # and
- all_attr = obj.db.all()
- # (unless there is an attribute
- # named 'all', in which case that will be returned instead).
- ```
-
- """
- try:
- returnself._db_holder
- exceptAttributeError:
- self._db_holder=DbHolder(self,"attributes")
- returnself._db_holder
-
- @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
- defdb(self):
- "Stop accidental deletion."
- raiseException("Cannot delete the db object!")
-
- #
- # Non-persistent (ndb) storage
- #
-
- @property
- defndb(self):
- """
- 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
- exceptAttributeError:
- self._ndb_holder=DbHolder(self,"nattrhandler",manager_name="nattributes")
- returnself._ndb_holder
-
- @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)
-
- @ndb.deleter
- defndb(self):
- "Stop accidental deletion."
- raiseException("Cannot delete the ndb object!")
-
-
[docs]defget_display_name(self,looker,**kwargs):
- """
- Displays the name of the object in a viewer-aware manner.
-
- Args:
- looker (TypedObject, optional): The object or account that is looking
- at/getting inforamtion for this object. If not given, some
- 'safe' minimum level should be returned.
-
- Returns:
- name (str): A string containing the name of the object,
- including the DBREF if this user is privileged to control
- said object.
-
- Notes:
- This function could be extended to change how object names
- appear to users in character, but be wary. This function
- does not change an object's keys or aliases when
- searching, and is expected to produce something useful for
- builders.
-
- """
- ifself.access(looker,access_type="controls"):
- return"{}(#{})".format(self.name,self.id)
- returnself.name
-
-
[docs]defget_extra_info(self,looker,**kwargs):
- """
- Used when an object is in a list of ambiguous objects as an
- additional information tag.
-
- For instance, if you had potions which could have varying
- levels of liquid left in them, you might want to display how
- many drinks are left in each when selecting which to drop, but
- not in your normal inventory listing.
-
- Args:
- looker (TypedObject): The object or account that is looking
- at/getting information for this object.
-
- Returns:
- info (str): A string with disambiguating information,
- conventionally with a leading space.
-
- """
-
- ifself.location==looker:
- return" (carried)"
- return""
-
-
[docs]defat_rename(self,oldname,newname):
- """
- This Hook is called by @name on a successful rename.
-
- Args:
- oldname (str): The instance's original name.
- newname (str): The new name for the instance.
-
- """
- pass
-
- #
- # Web/Django methods
- #
-
-
[docs]defweb_get_admin_url(self):
- """
- Returns the URI path for the Django Admin page for this object.
-
- ex. Account#1 = '/admin/accounts/accountdb/1/change/'
-
- Returns:
- path (str): URI path to Django Admin page for object.
-
- """
- content_type=ContentType.objects.get_for_model(self.__class__)
- returnreverse(
- "admin:%s_%s_change"%(content_type.app_label,content_type.model),args=(self.id,)
- )
-
-
[docs]@classmethod
- defweb_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.
-
- """
- try:
- returnreverse("%s-create"%slugify(cls._meta.verbose_name))
- exceptException:
- return"#"
-
-
[docs]defweb_get_detail_url(self):
- """
- 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:
-
- ```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.
-
- 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.
-
- """
- try:
- returnreverse(
- "%s-detail"%slugify(self._meta.verbose_name),
- kwargs={"pk":self.pk,"slug":slugify(self.name)},
- )
- exceptException:
- return"#"
-
-
[docs]defweb_get_puppet_url(self):
- """
- 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.
- ::
-
- url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/puppet/$',
- CharPuppetView.as_view(), name='character-puppet')
-
- 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.
-
-
- """
- try:
- returnreverse(
- "%s-puppet"%slugify(self._meta.verbose_name),
- kwargs={"pk":self.pk,"slug":slugify(self.name)},
- )
- exceptException:
- return"#"
-
-
[docs]defweb_get_update_url(self):
- """
- 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:
-
- ```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
- a named view of 'character-update' would be referenced by this method.
- ::
-
- 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.
-
- 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)},
- )
- exceptException:
- return"#"
-
-
[docs]defweb_get_delete_url(self):
- """
- 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:
-
- ```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.
- ::
-
- 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.
-
- 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.
-
-
- """
- try:
- returnreverse(
- "%s-delete"%slugify(self._meta.verbose_name),
- kwargs={"pk":self.pk,"slug":slugify(self.name)},
- )
- exceptException:
- return"#"
-
- # Used by Django Sites/Admin
- get_absolute_url=web_get_detail_url
-"""
-Tags are entities that are attached to objects in the same way as
-Attributes. But contrary to Attributes, which are unique to an
-individual object, a single Tag can be attached to any number of
-objects at the same time.
-
-Tags are used for tagging, obviously, but the data structure is also
-used for storing Aliases and Permissions. This module contains the
-respective handlers.
-
-"""
-fromcollectionsimportdefaultdict
-
-fromdjango.confimportsettings
-fromdjango.dbimportmodels
-fromevennia.utils.utilsimportto_str,make_iter
-fromevennia.locks.lockfuncsimportpermasperm_lockfunc
-
-
-_TYPECLASS_AGGRESSIVE_CACHE=settings.TYPECLASS_AGGRESSIVE_CACHE
-
-# ------------------------------------------------------------
-#
-# Tags
-#
-# ------------------------------------------------------------
-
-
-
[docs]classTag(models.Model):
- """
- Tags are quick markers for objects in-game. An typeobject can have
- any number of tags, stored via its db_tags property. Tagging
- similar objects will make it easier to quickly locate the group
- later (such as when implementing zones). The main advantage of
- tagging as opposed to using tags is speed; a tag is very
- limited in what data it can hold, and the tag key+category is
- indexed for efficient lookup in the database. Tags are shared
- between objects - a new tag is only created if the key+category
- combination did not previously exist, making them unsuitable for
- storing object-related data (for this a regular Attribute should be
- used).
-
- The 'db_data' field is intended as a documentation field for the
- tag itself, such as to document what this tag+category stands for
- and display that in a web interface or similar.
-
- The main default use for Tags is to implement Aliases for objects.
- this uses the 'aliases' tag category, which is also checked by the
- default search functions of Evennia to allow quick searches by alias.
-
- """
-
- db_key=models.CharField(
- "key",max_length=255,null=True,help_text="tag identifier",db_index=True
- )
- db_category=models.CharField(
- "category",max_length=64,null=True,blank=True,help_text="tag category",db_index=True
- )
- db_data=models.TextField(
- "data",
- null=True,
- blank=True,
- help_text="optional data field with extra information. This is not searched for.",
- )
- # this is "objectdb" etc. Required behind the scenes
- db_model=models.CharField(
- "model",max_length=32,null=True,help_text="database model to Tag",db_index=True
- )
- # this is None, alias or permission
- db_tagtype=models.CharField(
- "tagtype",
- max_length=16,
- null=True,
- blank=True,
- help_text="overall type of Tag",
- db_index=True,
- )
-
- classMeta:
- "Define Django meta options"
- verbose_name="Tag"
- unique_together=(("db_key","db_category","db_tagtype","db_model"),)
- index_together=(("db_key","db_category","db_tagtype","db_model"),)
-
- def__lt__(self,other):
- returnstr(self)<str(other)
-
- def__str__(self):
- returnstr(
- "<Tag: %s%s>"
- %(self.db_key,"(category:%s)"%self.db_categoryifself.db_categoryelse"")
- )
-
-
-#
-# Handlers making use of the Tags model
-#
-
-
-
[docs]def__init__(self,obj):
- """
- Tags are stored internally in the TypedObject.db_tags m2m
- field with an tag.db_model based on the obj the taghandler is
- stored on and with a tagtype given by self.handlertype
-
- Args:
- obj (object): The object on which the handler is set.
-
- """
- self.obj=obj
- self._objid=obj.id
- self._model=obj.__dbclass__.__name__.lower()
- self._cache={}
- # store category names fully cached
- self._catcache={}
- # full cache was run on all tags
- self._cache_complete=False
-
- def_query_all(self):
- """
- Get all tags for this object.
-
- """
- query={
- "%s__id"%self._model:self._objid,
- "tag__db_model":self._model,
- "tag__db_tagtype":self._tagtype,
- }
- return[
- conn.tag
- forconningetattr(self.obj,self._m2m_fieldname).through.objects.filter(**query)
- ]
-
- def_fullcache(self):
- """
- Cache all tags of this object.
-
- """
- ifnot_TYPECLASS_AGGRESSIVE_CACHE:
- return
- tags=self._query_all()
- self._cache=dict(
- (
- "%s-%s"
- %(
- to_str(tag.db_key).lower(),
- tag.db_category.lower()iftag.db_categoryelseNone,
- ),
- tag,
- )
- fortagintags
- )
- self._cache_complete=True
-
- def_getcache(self,key=None,category=None):
- """
- Retrieve from cache or database (always caches)
-
- Args:
- key (str, optional): Tag key to query for
- category (str, optional): Tag category
-
- Returns:
- args (list): Returns a list of zero or more matches
- found from cache or database.
- Notes:
- When given a category only, a search for all objects
- of that category is done and a the category *name* is is
- stored. This tells the system on subsequent calls that the
- list of cached tags of this category is up-to-date
- and that the cache can be queried for category matches
- without missing any.
- The TYPECLASS_AGGRESSIVE_CACHE=False setting will turn off
- caching, causing each tag access to trigger a
- database lookup.
-
- """
- key=key.strip().lower()ifkeyelseNone
- category=category.strip().lower()ifcategoryelseNone
- ifkey:
- cachekey="%s-%s"%(key,category)
- tag=_TYPECLASS_AGGRESSIVE_CACHEandself._cache.get(cachekey,None)
- iftagand(nothasattr(tag,"pk")andtag.pkisNone):
- # clear out Tags deleted from elsewhere. We must search this anew.
- tag=None
- delself._cache[cachekey]
- iftag:
- return[tag]# return cached entity
- else:
- query={
- "%s__id"%self._model:self._objid,
- "tag__db_model":self._model,
- "tag__db_tagtype":self._tagtype,
- "tag__db_key__iexact":key.lower(),
- "tag__db_category__iexact":category.lower()ifcategoryelseNone,
- }
- conn=getattr(self.obj,self._m2m_fieldname).through.objects.filter(**query)
- ifconn:
- tag=conn[0].tag
- if_TYPECLASS_AGGRESSIVE_CACHE:
- self._cache[cachekey]=tag
- return[tag]
- 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[tagforkey,taginself._cache.items()ifkey.endswith(catkey)]
- else:
- # we have to query to make this category up-date in the cache
- query={
- "%s__id"%self._model:self._objid,
- "tag__db_model":self._model,
- "tag__db_tagtype":self._tagtype,
- "tag__db_category__iexact":category.lower()ifcategoryelseNone,
- }
- tags=[
- conn.tag
- forconningetattr(self.obj,self._m2m_fieldname).through.objects.filter(
- **query
- )
- ]
- if_TYPECLASS_AGGRESSIVE_CACHE:
- fortagintags:
- cachekey="%s-%s"%(tag.db_key,category)
- self._cache[cachekey]=tag
- # mark category cache as up-to-date
- self._catcache[catkey]=True
- returntags
- return[]
-
- def_setcache(self,key,category,tag_obj):
- """
- Update cache.
-
- Args:
- key (str): A cleaned key string
- category (str or None): A cleaned category name
- tag_obj (tag): The newly saved tag
-
- """
- ifnot_TYPECLASS_AGGRESSIVE_CACHE:
- return
- ifnotkey:# don't allow an empty key in cache
- return
- key,category=(key.strip().lower(),category.strip().lower()ifcategoryelsecategory)
- cachekey="%s-%s"%(key,category)
- catkey="-%s"%category
- self._cache[cachekey]=tag_obj
- # mark that the category cache is no longer up-to-date
- self._catcache.pop(catkey,None)
- self._cache_complete=False
-
- def_delcache(self,key,category):
- """
- Remove tag from cache
-
- Args:
- key (str): A cleaned key string
- category (str or None): A cleaned category name
-
- """
- key,category=(key.strip().lower(),category.strip().lower()ifcategoryelsecategory)
- catkey="-%s"%category
- ifkey:
- cachekey="%s-%s"%(key,category)
- self._cache.pop(cachekey,None)
- else:
- [self._cache.pop(key,None)forkeyinself._cacheifkey.endswith(catkey)]
- # mark that the category cache is no longer up-to-date
- self._catcache.pop(catkey,None)
- self._cache_complete=False
-
-
[docs]defreset_cache(self):
- """
- Reset the cache from the outside.
-
- """
- self._cache_complete=False
- self._cache={}
- self._catcache={}
-
-
[docs]defadd(self,key=None,category=None,data=None):
- """
- Add a new tag to the handler.
-
- Args:
- key (str or list): The name of the tag to add. If a list,
- add several Tags.
- category (str, optional): Category of Tag. `None` is the default category.
- data (str, optional): Info text about the tag(s) added.
- This can not be used to store object-unique info but only
- eventual info about the tag itself.
-
- Notes:
- If the tag + category combination matches an already
- existing Tag object, this will be re-used and no new Tag
- will be created.
-
- """
- ifnotkey:
- return
- ifnotself._cache_complete:
- self._fullcache()
- fortagstrinmake_iter(key):
- ifnottagstr:
- continue
- tagstr=str(tagstr).strip().lower()
- category=str(category).strip().lower()ifcategoryelsecategory
- data=str(data)ifdataisnotNoneelseNone
- # this will only create tag if no matches existed beforehand (it
- # will overload data on an existing tag since that is not
- # considered part of making the tag unique)
- tagobj=self.obj.__class__.objects.create_tag(
- key=tagstr,category=category,data=data,tagtype=self._tagtype
- )
- getattr(self.obj,self._m2m_fieldname).add(tagobj)
- self._setcache(tagstr,category,tagobj)
-
-
[docs]defhas(self,key=None,category=None,return_list=False):
- """
- Checks if the given Tag (or list of Tags) exists on the object.
-
- Args:
- key (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
- ifkey:
- fortag_strinmake_iter(key):
- 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.
-
- Args:
- key (str or list, optional): The tag or tags to retrieve.
- default (any, optional): The value to return in case of no match.
- category (str, optional): The Tag category to limit the
- request to. Note that `None` is the valid, default
- category. If no `key` is given, all tags of this category will be
- returned.
- return_tagobj (bool, optional): Return the Tag object itself
- instead of a string representation of the Tag.
- return_list (bool, optional): Always return a list, regardless
- of number of matches.
-
- Returns:
- tags (list): The matches, either string
- representations of the tags or the Tag objects themselves
- depending on `return_tagobj`. If 'default' is set, this
- will be a list with the default value as its only element.
-
- """
- ret=[]
- forkeystrinmake_iter(key):
- # note - the _getcache call removes case sensitivity for us
- ret.extend(
- [
- tagifreturn_tagobjelseto_str(tag.db_key)
- fortaginself._getcache(keystr,category)
- ]
- )
- ifreturn_list:
- returnretifretelse[default]ifdefaultisnotNoneelse[]
- returnret[0]iflen(ret)==1else(retifretelsedefault)
-
-
[docs]defremove(self,key=None,category=None):
- """
- Remove a tag from the handler based ond key and/or category.
-
- Args:
- key (str or list, optional): The tag or tags to retrieve.
- category (str, optional): The Tag category to limit the
- request to. Note that `None` is the valid, default
- category
- Notes:
- If neither key nor category is specified, this acts
- as .clear().
-
- """
- ifnotkey:
- # only category
- self.clear(category=category)
- return
-
- forkeyinmake_iter(key):
- ifnot(keyorkey.strip()):# we don't allow empty tags
- continue
- tagstr=key.strip().lower()
- category=category.strip().lower()ifcategoryelsecategory
-
- # This does not delete the tag object itself. Maybe it should do
- # that when no objects reference the tag anymore (but how to check)?
- # For now, tags are never deleted, only their connection to objects.
- tagobj=getattr(self.obj,self._m2m_fieldname).filter(
- db_key=tagstr,db_category=category,db_model=self._model,db_tagtype=self._tagtype
- )
- iftagobj:
- getattr(self.obj,self._m2m_fieldname).remove(tagobj[0])
- self._delcache(key,category)
-
-
[docs]defclear(self,category=None):
- """
- Remove all tags from the handler.
-
- Args:
- category (str, optional): The Tag category to limit the
- request to. Note that `None` is the valid, default
- category.
-
- """
- ifnotself._cache_complete:
- self._fullcache()
- query={
- "%s__id"%self._model:self._objid,
- "tag__db_model":self._model,
- "tag__db_tagtype":self._tagtype,
- }
- ifcategory:
- query["tag__db_category"]=category.strip().lower()
- getattr(self.obj,self._m2m_fieldname).through.objects.filter(**query).delete()
- self._cache={}
- self._catcache={}
- self._cache_complete=False
-
-
[docs]defall(self,return_key_and_category=False,return_objs=False):
- """
- Get all tags in this handler, regardless of category.
-
- Args:
- return_key_and_category (bool, optional): Return a list of
- tuples `[(key, category), ...]`.
- return_objs (bool, optional): Return tag objects.
-
- Returns:
- tags (list): A list of tag keys `[tagkey, tagkey, ...]` or
- a list of tuples `[(key, category), ...]` if
- `return_key_and_category` is set.
-
- """
- if_TYPECLASS_AGGRESSIVE_CACHE:
- ifnotself._cache_complete:
- self._fullcache()
- tags=sorted(self._cache.values())
- else:
- tags=sorted(self._query_all())
-
- ifreturn_key_and_category:
- # return tuple (key, category)
- return[(to_str(tag.db_key),tag.db_category)fortagintags]
- elifreturn_objs:
- returntags
- else:
- return[to_str(tag.db_key)fortagintags]
-
-
[docs]defbatch_add(self,*args):
- """
- 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.
-
- Notes:
- This will generate a mimimal number of self.add calls,
- based on the number of categories involved (including
- `None`) (data is not unique and may be overwritten by the content
- of a latter tuple with the same category).
-
- """
- keys=defaultdict(list)
- data={}
- fortupinargs:
- tup=make_iter(tup)
- nlen=len(tup)
- ifnlen==1:# just a key
- keys[None].append(tup[0])
- elifnlen==2:
- keys[tup[1]].append(tup[0])
- else:
- keys[tup[1]].append(tup[0])
- data[tup[1]]=tup[2]# overwrite previous
- forcategory,keyinkeys.items():
- self.add(key=key,category=category,data=data.get(category,None))
[docs]classAliasHandler(TagHandler):
- """
- A handler for the Alias Tag type.
-
- """
-
- _tagtype="alias"
-
-
-
[docs]classPermissionHandler(TagHandler):
- """
- A handler for the Permission Tag type.
-
- """
-
- _tagtype="permission"
-
-
[docs]defcheck(self,*permissions,require_all=False):
- """
- Straight-up check the provided permission against this handler. The check will pass if
-
- - any/all given permission exists on the handler (depending on if `require_all` is set).
- - If handler sits on puppeted object and this is a hierarachical perm, the puppeting
- Account's permission will also be included in the check, prioritizing the Account's perm
- (this avoids escalation exploits by puppeting a too-high prio character)
- - a permission is also considered to exist on the handler, if it is *lower* than
- a permission on the handler and this is a 'hierarchical' permission given
- in `settings.PERMISSION_HIERARCHY`. Example: If the 'Developer' hierarchical
- perm perm is set on the handler, and we check for the 'Builder' perm, the
- check will pass.
-
- Args:
- *permissions (str): Any number of permissions to check. By default,
- the permission is passed if any of these (or higher, if a
- hierarchical permission defined in settings.PERMISSION_HIERARCHY)
- exists in the handler. Permissions are not case-sensitive.
- require_all (bool): If set, *all* provided permissions much pass
- the check for the entire check to pass. By default only one
- needs to pass.
-
- Returns:
- bool: If the provided permission(s) pass the check on this handler.
-
- Example:
- ::
- can_enter = obj.permissions.check("Blacksmith", "Builder")
-
- Notes:
- This works the same way as the `perms` lockfunc and could be
- replicated with a lock check against the lockstring
-
- "locktype: perm(perm1) OR perm(perm2) OR ..."
-
- (using AND for the `require_all` condition).
-
- """
- ifrequire_all:
- returnall(perm_lockfunc(self.obj,None,perm)forperminpermissions)
- else:
- returnany(perm_lockfunc(self.obj,None,perm)forperminpermissions)
-"""
-ANSI - Gives colour to text.
-
-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)
-- 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."
-
-```
-
-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
-
-importre
-fromcollectionsimportOrderedDict
-
-fromdjango.confimportsettings
-
-fromevennia.utilsimportutils
-fromevennia.utilsimportlogger
-
-fromevennia.utils.utilsimportto_str
-
-MXP_ENABLED=settings.MXP_ENABLED
-
-
-# ANSI definitions
-
-ANSI_BEEP="\07"
-ANSI_ESCAPE="\033"
-ANSI_NORMAL="\033[0m"
-
-ANSI_UNDERLINE="\033[4m"
-ANSI_HILITE="\033[1m"
-ANSI_UNHILITE="\033[22m"
-ANSI_BLINK="\033[5m"
-ANSI_INVERSE="\033[7m"
-ANSI_INV_HILITE="\033[1;7m"
-ANSI_INV_BLINK="\033[7;5m"
-ANSI_BLINK_HILITE="\033[1;5m"
-ANSI_INV_BLINK_HILITE="\033[1;5;7m"
-
-# Foreground colors
-ANSI_BLACK="\033[30m"
-ANSI_RED="\033[31m"
-ANSI_GREEN="\033[32m"
-ANSI_YELLOW="\033[33m"
-ANSI_BLUE="\033[34m"
-ANSI_MAGENTA="\033[35m"
-ANSI_CYAN="\033[36m"
-ANSI_WHITE="\033[37m"
-
-# Background colors
-ANSI_BACK_BLACK="\033[40m"
-ANSI_BACK_RED="\033[41m"
-ANSI_BACK_GREEN="\033[42m"
-ANSI_BACK_YELLOW="\033[43m"
-ANSI_BACK_BLUE="\033[44m"
-ANSI_BACK_MAGENTA="\033[45m"
-ANSI_BACK_CYAN="\033[46m"
-ANSI_BACK_WHITE="\033[47m"
-
-# Formatting Characters
-ANSI_RETURN="\r\n"
-ANSI_TAB="\t"
-ANSI_SPACE=" "
-
-# Escapes
-ANSI_ESCAPES=("{{","\\\\","\|\|")
-
-_PARSE_CACHE=OrderedDict()
-_PARSE_CACHE_SIZE=10000
-
-_COLOR_NO_DEFAULT=settings.COLOR_NO_DEFAULT
-
-
-
[docs]classANSIParser(object):
- """
- A class that parses ANSI markup
- to ANSI command sequences
-
- We also allow to escape colour codes
- by prepending with an extra `|`.
-
- """
-
- # Mapping using {r {n etc
-
- ansi_map=[
- # alternative |-format
- (r"|n",ANSI_NORMAL),# reset
- (r"|/",ANSI_RETURN),# line break
- (r"|-",ANSI_TAB),# tab
- (r"|>",ANSI_SPACE*4),# indent (4 spaces)
- (r"|_",ANSI_SPACE),# space
- (r"|*",ANSI_INVERSE),# invert
- (r"|^",ANSI_BLINK),# blinking text (very annoying and not supported by all clients)
- (r"|u",ANSI_UNDERLINE),# underline
- (r"|r",ANSI_HILITE+ANSI_RED),
- (r"|g",ANSI_HILITE+ANSI_GREEN),
- (r"|y",ANSI_HILITE+ANSI_YELLOW),
- (r"|b",ANSI_HILITE+ANSI_BLUE),
- (r"|m",ANSI_HILITE+ANSI_MAGENTA),
- (r"|c",ANSI_HILITE+ANSI_CYAN),
- (r"|w",ANSI_HILITE+ANSI_WHITE),# pure white
- (r"|x",ANSI_HILITE+ANSI_BLACK),# dark grey
- (r"|R",ANSI_UNHILITE+ANSI_RED),
- (r"|G",ANSI_UNHILITE+ANSI_GREEN),
- (r"|Y",ANSI_UNHILITE+ANSI_YELLOW),
- (r"|B",ANSI_UNHILITE+ANSI_BLUE),
- (r"|M",ANSI_UNHILITE+ANSI_MAGENTA),
- (r"|C",ANSI_UNHILITE+ANSI_CYAN),
- (r"|W",ANSI_UNHILITE+ANSI_WHITE),# light grey
- (r"|X",ANSI_UNHILITE+ANSI_BLACK),# pure black
- # hilight-able colors
- (r"|h",ANSI_HILITE),
- (r"|H",ANSI_UNHILITE),
- (r"|!R",ANSI_RED),
- (r"|!G",ANSI_GREEN),
- (r"|!Y",ANSI_YELLOW),
- (r"|!B",ANSI_BLUE),
- (r"|!M",ANSI_MAGENTA),
- (r"|!C",ANSI_CYAN),
- (r"|!W",ANSI_WHITE),# light grey
- (r"|!X",ANSI_BLACK),# pure black
- # normal ANSI backgrounds
- (r"|[R",ANSI_BACK_RED),
- (r"|[G",ANSI_BACK_GREEN),
- (r"|[Y",ANSI_BACK_YELLOW),
- (r"|[B",ANSI_BACK_BLUE),
- (r"|[M",ANSI_BACK_MAGENTA),
- (r"|[C",ANSI_BACK_CYAN),
- (r"|[W",ANSI_BACK_WHITE),# light grey background
- (r"|[X",ANSI_BACK_BLACK),# pure black background
- ]
-
- ansi_xterm256_bright_bg_map=[
- # "bright" ANSI backgrounds using xterm256 since ANSI
- # standard does not support it (will
- # fallback to dark ANSI background colors if xterm256
- # is not supported by client)
- # |-style variations
- (r"|[r",r"|[500"),
- (r"|[g",r"|[050"),
- (r"|[y",r"|[550"),
- (r"|[b",r"|[005"),
- (r"|[m",r"|[505"),
- (r"|[c",r"|[055"),
- (r"|[w",r"|[555"),# white background
- (r"|[x",r"|[222"),
- ]# dark grey background
-
- # xterm256. These are replaced directly by
- # the sub_xterm256 method
-
- ifsettings.COLOR_NO_DEFAULT:
- ansi_map=settings.COLOR_ANSI_EXTRA_MAP
- xterm256_fg=settings.COLOR_XTERM256_EXTRA_FG
- xterm256_bg=settings.COLOR_XTERM256_EXTRA_BG
- xterm256_gfg=settings.COLOR_XTERM256_EXTRA_GFG
- xterm256_gbg=settings.COLOR_XTERM256_EXTRA_GBG
- ansi_xterm256_bright_bg_map=settings.COLOR_ANSI_XTERM256_BRIGHT_BG_EXTRA_MAP
- else:
- xterm256_fg=[r"\|([0-5])([0-5])([0-5])"]# |123 - foreground colour
- xterm256_bg=[r"\|\[([0-5])([0-5])([0-5])"]# |[123 - background colour
- xterm256_gfg=[r"\|=([a-z])"]# |=a - greyscale foreground
- xterm256_gbg=[r"\|\[=([a-z])"]# |[=a - greyscale background
- ansi_map+=settings.COLOR_ANSI_EXTRA_MAP
- xterm256_fg+=settings.COLOR_XTERM256_EXTRA_FG
- xterm256_bg+=settings.COLOR_XTERM256_EXTRA_BG
- xterm256_gfg+=settings.COLOR_XTERM256_EXTRA_GFG
- xterm256_gbg+=settings.COLOR_XTERM256_EXTRA_GBG
- ansi_xterm256_bright_bg_map+=settings.COLOR_ANSI_XTERM256_BRIGHT_BG_EXTRA_MAP
-
- mxp_re=r"\|lc(.*?)\|lt(.*?)\|le"
- mxp_url_re=r"\|lu(.*?)\|lt(.*?)\|le"
-
- # prepare regex matching
- brightbg_sub=re.compile(
- r"|".join([r"(?<!\|)%s"%re.escape(tup[0])fortupinansi_xterm256_bright_bg_map]),
- re.DOTALL,
- )
- xterm256_fg_sub=re.compile(r"|".join(xterm256_fg),re.DOTALL)
- xterm256_bg_sub=re.compile(r"|".join(xterm256_bg),re.DOTALL)
- xterm256_gfg_sub=re.compile(r"|".join(xterm256_gfg),re.DOTALL)
- xterm256_gbg_sub=re.compile(r"|".join(xterm256_gbg),re.DOTALL)
-
- # 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 sequences
- ansi_map_dict=dict(ansi_map)
- ansi_xterm256_bright_bg_map_dict=dict(ansi_xterm256_bright_bg_map)
-
- # prepare matching ansi codes overall
- ansi_re=r"\033\[[0-9;]+m"
- ansi_regex=re.compile(ansi_re)
-
- # escapes - these double-chars will be replaced with a single
- # instance of each
- ansi_escapes=re.compile(r"(%s)"%"|".join(ANSI_ESCAPES),re.DOTALL)
-
- # tabs/linebreaks |/ and |- should be able to be cleaned
- unsafe_tokens=re.compile(r"\|\/|\|-",re.DOTALL)
-
-
[docs]defsub_ansi(self,ansimatch):
- """
- Replacer used by `re.sub` to replace ANSI
- markers with correct ANSI sequences
-
- Args:
- ansimatch (re.matchobject): The match.
-
- Returns:
- processed (str): The processed match string.
-
- """
- returnself.ansi_map_dict.get(ansimatch.group(),"")
-
-
[docs]defsub_brightbg(self,ansimatch):
- """
- Replacer used by `re.sub` to replace ANSI
- bright background markers with Xterm256 replacement
-
- Args:
- ansimatch (re.matchobject): The match.
-
- Returns:
- processed (str): The processed match string.
-
- """
- returnself.ansi_xterm256_bright_bg_map_dict.get(ansimatch.group(),"")
-
-
[docs]defsub_xterm256(self,rgbmatch,use_xterm256=False,color_type="fg"):
- """
- This is a replacer method called by `re.sub` with the matched
- tag. It must return the correct ansi sequence.
-
- It checks `self.do_xterm256` to determine if conversion
- to standard ANSI should be done or not.
-
- Args:
- rgbmatch (re.matchobject): The match.
- use_xterm256 (bool, optional): Don't convert 256-colors to 16.
- color_type (str): One of 'fg', 'bg', 'gfg', 'gbg'.
-
- Returns:
- processed (str): The processed match string.
-
- """
- ifnotrgbmatch:
- return""
-
- # get tag, stripping the initial marker
- # rgbtag = rgbmatch.group()[1:]
-
- background=color_typein("bg","gbg")
- grayscale=color_typein("gfg","gbg")
-
- ifnotgrayscale:
- # 6x6x6 color-cube (xterm indexes 16-231)
- try:
- red,green,blue=[int(val)forvalinrgbmatch.groups()ifvalisnotNone]
- except(IndexError,ValueError):
- logger.log_trace()
- returnrgbmatch.group(0)
- else:
- # grayscale values (xterm indexes 0, 232-255, 15) for full spectrum
- try:
- letter=[valforvalinrgbmatch.groups()ifvalisnotNone][0]
- exceptIndexError:
- logger.log_trace()
- returnrgbmatch.group(0)
-
- ifletter=="a":
- colval=16# pure black @ index 16 (first color cube entry)
- elifletter=="z":
- colval=231# pure white @ index 231 (last color cube entry)
- else:
- # letter in range [b..y] (exactly 24 values!)
- colval=134+ord(letter)
-
- # ansi fallback logic expects r,g,b values in [0..5] range
- gray=(ord(letter)-97)/5.0
- red,green,blue=gray,gray,gray
-
- ifuse_xterm256:
-
- ifnotgrayscale:
- colval=16+(red*36)+(green*6)+blue
-
- return"\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) # noqa
-
- else:
- # xterm256 not supported, convert the rgb value to ansi instead
- ifred==green==blueandred<3:
- ifbackground:
- returnANSI_BACK_BLACK
- elifred>=1:
- returnANSI_HILITE+ANSI_BLACK
- else:
- returnANSI_NORMAL+ANSI_BLACK
- elifred==green==blue:
- ifbackground:
- returnANSI_BACK_WHITE
- elifred>=4:
- returnANSI_HILITE+ANSI_WHITE
- else:
- returnANSI_NORMAL+ANSI_WHITE
- elifred>greenandred>blue:
- ifbackground:
- returnANSI_BACK_RED
- elifred>=3:
- returnANSI_HILITE+ANSI_RED
- else:
- returnANSI_NORMAL+ANSI_RED
- elifred==greenandred>blue:
- ifbackground:
- returnANSI_BACK_YELLOW
- elifred>=3:
- returnANSI_HILITE+ANSI_YELLOW
- else:
- returnANSI_NORMAL+ANSI_YELLOW
- elifred==blueandred>green:
- ifbackground:
- returnANSI_BACK_MAGENTA
- elifred>=3:
- returnANSI_HILITE+ANSI_MAGENTA
- else:
- returnANSI_NORMAL+ANSI_MAGENTA
- elifgreen>blue:
- ifbackground:
- returnANSI_BACK_GREEN
- elifgreen>=3:
- returnANSI_HILITE+ANSI_GREEN
- else:
- returnANSI_NORMAL+ANSI_GREEN
- elifgreen==blue:
- ifbackground:
- returnANSI_BACK_CYAN
- elifgreen>=3:
- returnANSI_HILITE+ANSI_CYAN
- else:
- returnANSI_NORMAL+ANSI_CYAN
- else:# mostly blue
- ifbackground:
- returnANSI_BACK_BLUE
- elifblue>=3:
- returnANSI_HILITE+ANSI_BLUE
- else:
- returnANSI_NORMAL+ANSI_BLUE
-
-
[docs]defstrip_raw_codes(self,string):
- """
- Strips raw ANSI codes from a string.
-
- Args:
- string (str): The string to strip.
-
- Returns:
- string (str): The processed string.
-
- """
- returnself.ansi_regex.sub("",string)
-
-
[docs]defstrip_mxp(self,string):
- """
- Strips all MXP codes from a string.
-
- Args:
- string (str): The string to strip.
-
- Returns:
- string (str): The processed string.
-
- """
- string=self.mxp_sub.sub(r"\2",string)
- string=self.mxp_url_sub.sub(r"\1",string)# replace with url verbatim
- returnstring
-
-
[docs]defstrip_unsafe_tokens(self,string):
- """
- Strip explicitly ansi line breaks and tabs.
-
- """
- returnself.unsafe_tokens.sub('',string)
-
-
[docs]defparse_ansi(self,string,strip_ansi=False,xterm256=False,mxp=False):
- """
- Parses a string, subbing color codes according to the stored
- mapping.
-
- Args:
- string (str): The string to parse.
- strip_ansi (boolean, optional): Strip all found ansi markup.
- xterm256 (boolean, optional): If actually using xterm256 or if
- these values should be converted to 16-color ANSI.
- mxp (boolean, optional): Parse MXP commands in string.
-
- Returns:
- string (str): The parsed string.
-
- """
- ifhasattr(string,"_raw_string"):
- ifstrip_ansi:
- returnstring.clean()
- else:
- returnstring.raw()
-
- ifnotstring:
- return""
-
- # check cached parsings
- global_PARSE_CACHE
- cachekey="%s-%s-%s-%s"%(string,strip_ansi,xterm256,mxp)
- ifcachekeyin_PARSE_CACHE:
- return_PARSE_CACHE[cachekey]
-
- # pre-convert bright colors to xterm256 color tags
- string=self.brightbg_sub.sub(self.sub_brightbg,string)
-
- defdo_xterm256_fg(part):
- returnself.sub_xterm256(part,xterm256,"fg")
-
- defdo_xterm256_bg(part):
- returnself.sub_xterm256(part,xterm256,"bg")
-
- defdo_xterm256_gfg(part):
- returnself.sub_xterm256(part,xterm256,"gfg")
-
- defdo_xterm256_gbg(part):
- returnself.sub_xterm256(part,xterm256,"gbg")
-
- in_string=utils.to_str(string)
-
- # do string replacement
- parsed_string=[]
- parts=self.ansi_escapes.split(in_string)+[" "]
- forpart,sepinzip(parts[::2],parts[1::2]):
- pstring=self.xterm256_fg_sub.sub(do_xterm256_fg,part)
- pstring=self.xterm256_bg_sub.sub(do_xterm256_bg,pstring)
- pstring=self.xterm256_gfg_sub.sub(do_xterm256_gfg,pstring)
- pstring=self.xterm256_gbg_sub.sub(do_xterm256_gbg,pstring)
- pstring=self.ansi_sub.sub(self.sub_ansi,pstring)
- parsed_string.append("%s%s"%(pstring,sep[0].strip()))
- parsed_string="".join(parsed_string)
-
- ifnotmxp:
- parsed_string=self.strip_mxp(parsed_string)
-
- ifstrip_ansi:
- # remove all ansi codes (including those manually
- # inserted in string)
- returnself.strip_raw_codes(parsed_string)
-
- # cache and crop old cache
- _PARSE_CACHE[cachekey]=parsed_string
- iflen(_PARSE_CACHE)>_PARSE_CACHE_SIZE:
- _PARSE_CACHE.popitem(last=False)
-
- returnparsed_string
[docs]defparse_ansi(string,strip_ansi=False,parser=ANSI_PARSER,xterm256=False,mxp=False):
- """
- Parses a string, subbing color codes as needed.
-
- Args:
- string (str): The string to parse.
- strip_ansi (bool, optional): Strip all ANSI sequences.
- parser (ansi.AnsiParser, optional): A parser instance to use.
- xterm256 (bool, optional): Support xterm256 or not.
- mxp (bool, optional): Support MXP markup or not.
-
- Returns:
- string (str): The parsed string.
-
- """
- returnparser.parse_ansi(string,strip_ansi=strip_ansi,xterm256=xterm256,mxp=mxp)
-
-
-
[docs]defstrip_ansi(string,parser=ANSI_PARSER):
- """
- Strip all ansi from the string. This handles the Evennia-specific
- markup.
-
- Args:
- string (str): The string to strip.
- parser (ansi.AnsiParser, optional): The parser to use.
-
- Returns:
- string (str): The stripped string.
-
- """
- returnparser.parse_ansi(string,strip_ansi=True)
-
-
-
[docs]defstrip_raw_ansi(string,parser=ANSI_PARSER):
- """
- Remove raw ansi codes from string. This assumes pure
- ANSI-bytecodes in the string.
-
- Args:
- string (str): The string to parse.
- parser (bool, optional): The parser to use.
-
- Returns:
- string (str): the stripped string.
-
- """
- returnparser.strip_raw_codes(string)
-
-
-
[docs]defstrip_unsafe_tokens(string,parser=ANSI_PARSER):
- """
- Strip markup that can be used to create visual exploits
- (notably linebreaks and tags)
-
- """
- returnparser.strip_unsafe_tokens(string)
[docs]defraw(string):
- """
- Escapes a string into a form which won't be colorized by the ansi
- parser.
-
- Returns:
- string (str): The raw, escaped string.
-
- """
- returnstring.replace("{","{{").replace("|","||")
-
-
-# ------------------------------------------------------------
-#
-# ANSIString - ANSI-aware string class
-#
-# ------------------------------------------------------------
-
-
-def_spacing_preflight(func):
- """
- This wrapper function is used to do some preflight checks on
- functions used for padding ANSIStrings.
-
- """
-
- @functools.wraps(func)
- defwrapped(self,width=78,fillchar=None):
- iffillcharisNone:
- fillchar=" "
- if(len(fillchar)!=1)or(notisinstance(fillchar,str)):
- raiseTypeError("must be char, not %s"%type(fillchar))
- ifnotisinstance(width,int):
- raiseTypeError("integer argument expected, got %s"%type(width))
- _difference=width-len(self)
- if_difference<=0:
- returnself
- returnfunc(self,width,fillchar,_difference)
-
- returnwrapped
-
-
-def_query_super(func_name):
- """
- Have the string class handle this with the cleaned string instead
- of ANSIString.
-
- """
-
- defwrapped(self,*args,**kwargs):
- returngetattr(self.clean(),func_name)(*args,**kwargs)
-
- returnwrapped
-
-
-def_on_raw(func_name):
- """
- Like query_super, but makes the operation run on the raw string.
-
- """
-
- defwrapped(self,*args,**kwargs):
- args=list(args)
- try:
- string=args.pop(0)
- ifhasattr(string,"_raw_string"):
- args.insert(0,string.raw())
- else:
- args.insert(0,string)
- exceptIndexError:
- # just skip out if there are no more strings
- pass
- result=getattr(self._raw_string,func_name)(*args,**kwargs)
- ifisinstance(result,str):
- returnANSIString(result,decoded=True)
- returnresult
-
- returnwrapped
-
-
-def_transform(func_name):
- """
- Some string functions, like those manipulating capital letters,
- return a string the same length as the original. This function
- allows us to do the same, replacing all the non-coded characters
- with the resulting string.
-
- """
-
- defwrapped(self,*args,**kwargs):
- replacement_string=_query_super(func_name)(self,*args,**kwargs)
- to_string=[]
- char_counter=0
- forindexinrange(0,len(self._raw_string)):
- ifindexinself._code_indexes:
- to_string.append(self._raw_string[index])
- elifindexinself._char_indexes:
- to_string.append(replacement_string[char_counter])
- char_counter+=1
- returnANSIString(
- "".join(to_string),
- decoded=True,
- code_indexes=self._code_indexes,
- char_indexes=self._char_indexes,
- clean_string=replacement_string,
- )
-
- returnwrapped
-
-
-
[docs]classANSIMeta(type):
- """
- Many functions on ANSIString are just light wrappers around the string
- base class. We apply them here, as part of the classes construction.
-
- """
-
-
[docs]classANSIString(str,metaclass=ANSIMeta):
- """
- Unicode-like object that is aware of ANSI codes.
-
- This class can be used nearly identically to strings, in that it will
- report string length, handle slices, etc, much like a string object
- would. The methods should be used identically as string methods are.
-
- There is at least one exception to this (and there may be more, though
- they have not come up yet). When using ''.join() or u''.join() on an
- ANSIString, color information will get lost. You must use
- ANSIString('').join() to preserve color information.
-
- This implementation isn't perfectly clean, as it doesn't really have an
- understanding of what the codes mean in order to eliminate
- redundant characters-- though cleaning up the strings might end up being
- inefficient and slow without some C code when dealing with larger values.
- Such enhancements could be made as an enhancement to ANSI_PARSER
- if needed, however.
-
- If one is going to use ANSIString, one should generally avoid converting
- away from it until one is about to send information on the wire. This is
- because escape sequences in the string may otherwise already be decoded,
- and taken literally the second time around.
-
- """
-
- # A compiled Regex for the format mini-language:
- # https://docs.python.org/3/library/string.html#formatspec
- re_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+))?"
- r"(?P<type>b|c|d|e|E|f|F|g|G|n|o|s|x|X|%)?"
- )
-
- def__new__(cls,*args,**kwargs):
- """
- When creating a new ANSIString, you may use a custom parser that has
- the same attributes as the standard one, and you may declare the
- string to be handled as already decoded. It is important not to double
- decode strings, as escapes can only be respected once.
-
- Internally, ANSIString can also passes itself precached code/character
- indexes and clean strings to avoid doing extra work when combining
- ANSIStrings.
-
- """
- string=args[0]
- ifnotisinstance(string,str):
- string=to_str(string)
- parser=kwargs.get("parser",ANSI_PARSER)
- decoded=kwargs.get("decoded",False)orhasattr(string,"_raw_string")
- code_indexes=kwargs.pop("code_indexes",None)
- char_indexes=kwargs.pop("char_indexes",None)
- clean_string=kwargs.pop("clean_string",None)
- # All True, or All False, not just one.
- checks=[xisNoneforxin[code_indexes,char_indexes,clean_string]]
- ifnotlen(set(checks))==1:
- raiseValueError(
- "You must specify code_indexes, char_indexes, "
- "and clean_string together, or not at all."
- )
- ifnotall(checks):
- decoded=True
- ifnotdecoded:
- # Completely new ANSI String
- clean_string=parser.parse_ansi(string,strip_ansi=True,mxp=MXP_ENABLED)
- string=parser.parse_ansi(string,xterm256=True,mxp=MXP_ENABLED)
- elifclean_stringisnotNone:
- # We have an explicit clean string.
- pass
- elifhasattr(string,"_clean_string"):
- # It's already an ANSIString
- clean_string=string._clean_string
- code_indexes=string._code_indexes
- char_indexes=string._char_indexes
- string=string._raw_string
- else:
- # It's a string that has been pre-ansi decoded.
- clean_string=parser.strip_raw_codes(string)
-
- ifnotisinstance(string,str):
- string=string.decode("utf-8")
-
- ansi_string=super().__new__(ANSIString,to_str(clean_string))
- ansi_string._raw_string=string
- ansi_string._clean_string=clean_string
- ansi_string._code_indexes=code_indexes
- ansi_string._char_indexes=char_indexes
- returnansi_string
-
- def__str__(self):
- returnself._raw_string
-
- def__format__(self,format_spec):
- """
- This magic method covers ANSIString's behavior within a str.format() or f-string.
-
- 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
-
- 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.
- format_data=self.re_format.match(format_spec).groupdict()
- clean=self.clean()
- base_output=ANSIString(self.raw())
- align=format_data.get("align","<")
- fill=format_data.get("fill"," ")
-
- # Need to coerce width into an integer. We can be certain that it's numeric thanks to regex.
- width=format_data.get("width",None)
- ifwidthisNone:
- width=len(clean)
- else:
- width=int(width)
-
- ifalign=="<":
- base_output=self.ljust(width,fill)
- elifalign==">":
- base_output=self.rjust(width,fill)
- elifalign=="^":
- base_output=self.center(width,fill)
- elifalign=="=":
- pass
-
- # Return the raw string with ANSI markup, ready to be displayed.
- returnbase_output.raw()
-
- def__repr__(self):
- """
- Let's make the repr the command that would actually be used to
- construct this object, for convenience and reference.
-
- """
- return"ANSIString(%s, decoded=True)"%repr(self._raw_string)
-
-
[docs]def__init__(self,*_,**kwargs):
- """
- When the ANSIString is first initialized, a few internal variables
- have to be set.
-
- The first is the parser. It is possible to replace Evennia's standard
- ANSI parser with one of your own syntax if you wish, so long as it
- implements the same interface.
-
- The second is the _raw_string. This is the original "dumb" string
- with ansi escapes that ANSIString represents.
-
- The third thing to set is the _clean_string. This is a string that is
- devoid of all ANSI Escapes.
-
- Finally, _code_indexes and _char_indexes are defined. These are lookup
- tables for which characters in the raw string are related to ANSI
- escapes, and which are for the readable text.
-
- """
- self.parser=kwargs.pop("parser",ANSI_PARSER)
- super().__init__()
- ifself._code_indexesisNone:
- self._code_indexes,self._char_indexes=self._get_indexes()
-
- @staticmethod
- def_shifter(iterable,offset):
- """
- Takes a list of integers, and produces a new one incrementing all
- by a number.
-
- """
- ifnotoffset:
- returniterable
- return[i+offsetforiiniterable]
-
- @classmethod
- def_adder(cls,first,second):
- """
- Joins two ANSIStrings, preserving calculated info.
-
- """
-
- raw_string=first._raw_string+second._raw_string
- clean_string=first._clean_string+second._clean_string
- code_indexes=first._code_indexes[:]
- char_indexes=first._char_indexes[:]
- code_indexes.extend(cls._shifter(second._code_indexes,len(first._raw_string)))
- char_indexes.extend(cls._shifter(second._char_indexes,len(first._raw_string)))
- returnANSIString(
- raw_string,
- code_indexes=code_indexes,
- char_indexes=char_indexes,
- clean_string=clean_string,
- )
-
- def__add__(self,other):
- """
- We have to be careful when adding two strings not to reprocess things
- that don't need to be reprocessed, lest we end up with escapes being
- interpreted literally.
-
- """
- ifnotisinstance(other,str):
- returnNotImplemented
- ifnotisinstance(other,ANSIString):
- other=ANSIString(other)
- returnself._adder(self,other)
-
- def__radd__(self,other):
- """
- Likewise, if we're on the other end.
-
- """
- ifnotisinstance(other,str):
- returnNotImplemented
- ifnotisinstance(other,ANSIString):
- other=ANSIString(other)
- returnself._adder(other,self)
-
- def__getslice__(self,i,j):
- """
- This function is deprecated, so we just make it call the proper
- function.
-
- """
- returnself.__getitem__(slice(i,j))
-
- def_slice(self,slc):
- """
- This function takes a slice() object.
-
- Slices have to be handled specially. Not only are they able to specify
- a start and end with [x:y], but many forget that they can also specify
- an interval with [x:y:z]. As a result, not only do we have to track
- the ANSI Escapes that have played before the start of the slice, we
- must also replay any in these intervals, should they exist.
-
- Thankfully, slicing the _char_indexes table gives us the actual
- indexes that need slicing in the raw string. We can check between
- those indexes to figure out what escape characters need to be
- replayed.
-
- """
- char_indexes=self._char_indexes
- slice_indexes=char_indexes[slc]
- # If it's the end of the string, we need to append final color codes.
- ifnotslice_indexes:
- # if we find no characters it may be because we are just outside
- # of the interval, using an open-ended slice. We must replay all
- # of the escape characters until/after this point.
- ifchar_indexes:
- ifslc.startisNoneandslc.stopisNone:
- # a [:] slice of only escape characters
- returnANSIString(self._raw_string[slc])
- ifslc.startisNone:
- # this is a [:x] slice
- returnANSIString(self._raw_string[:char_indexes[0]])
- ifslc.stopisNone:
- # a [x:] slice
- returnANSIString(self._raw_string[char_indexes[-1]+1:])
- returnANSIString("")
- try:
- string=self[slc.startor0]._raw_string
- exceptIndexError:
- returnANSIString("")
- last_mark=slice_indexes[0]
- # Check between the slice intervals for escape sequences.
- i=None
- foriinslice_indexes[1:]:
- forindexinrange(last_mark,i):
- ifindexinself._code_indexes:
- string+=self._raw_string[index]
- last_mark=i
- try:
- string+=self._raw_string[i]
- exceptIndexError:
- # raw_string not long enough
- pass
- ifiisnotNone:
- append_tail=self._get_interleving(char_indexes.index(i)+1)
- else:
- append_tail=""
- returnANSIString(string+append_tail,decoded=True)
-
- def__getitem__(self,item):
- """
- Gateway for slices and getting specific indexes in the ANSIString. If
- this is a regexable ANSIString, it will get the data from the raw
- string instead, bypassing ANSIString's intelligent escape skipping,
- for reasons explained in the __new__ method's docstring.
-
- """
- ifisinstance(item,slice):
- # Slices must be handled specially.
- returnself._slice(item)
- try:
- self._char_indexes[item]
- exceptIndexError:
- raiseIndexError("ANSIString Index out of range")
- # Get character codes after the index as well.
- ifself._char_indexes[-1]==self._char_indexes[item]:
- append_tail=self._get_interleving(item+1)
- else:
- append_tail=""
- item=self._char_indexes[item]
-
- clean=self._raw_string[item]
- result=""
- # Get the character they're after, and replay all escape sequences
- # previous to it.
- forindexinrange(0,item+1):
- ifindexinself._code_indexes:
- result+=self._raw_string[index]
- returnANSIString(result+clean+append_tail,decoded=True)
-
-
[docs]defclean(self):
- """
- Return a string object *without* the ANSI escapes.
-
- Returns:
- clean_string (str): A unicode object with no ANSI escapes.
-
- """
- returnself._clean_string
-
-
[docs]defraw(self):
- """
- Return a string object with the ANSI escapes.
-
- Returns:
- raw (str): A unicode object *with* the raw ANSI escape sequences.
-
- """
- returnself._raw_string
-
-
[docs]defpartition(self,sep,reverse=False):
- """
- Splits once into three sections (with the separator being the middle section)
-
- We use the same techniques we used in split() to make sure each are
- colored.
-
- Args:
- sep (str): The separator to split the string on.
- reverse (boolean): Whether to split the string on the last
- occurrence of the separator rather than the first.
-
- Returns:
- ANSIString: The part of the string before the separator
- ANSIString: The separator itself
- ANSIString: The part of the string after the separator.
-
- """
- ifhasattr(sep,"_clean_string"):
- sep=sep.clean()
- ifreverse:
- parent_result=self._clean_string.rpartition(sep)
- else:
- parent_result=self._clean_string.partition(sep)
- current_index=0
- result=tuple()
- forsectioninparent_result:
- result+=(self[current_index:current_index+len(section)],)
- current_index+=len(section)
- returnresult
-
- def_get_indexes(self):
- """
- Two tables need to be made, one which contains the indexes of all
- readable characters, and one which contains the indexes of all ANSI
- escapes. It's important to remember that ANSI escapes require more
- that one character at a time, though no readable character needs more
- than one character, since the string base class abstracts that away
- from us. However, several readable characters can be placed in a row.
-
- We must use regexes here to figure out where all the escape sequences
- are hiding in the string. Then we use the ranges of their starts and
- ends to create a final, comprehensive list of all indexes which are
- dedicated to code, and all dedicated to text.
-
- It's possible that only one of these tables is actually needed, the
- other assumed to be what isn't in the first.
-
- """
-
- code_indexes=[]
- formatchinself.parser.ansi_regex.finditer(self._raw_string):
- code_indexes.extend(list(range(match.start(),match.end())))
- ifnotcode_indexes:
- # Plain string, no ANSI codes.
- returncode_indexes,list(range(0,len(self._raw_string)))
- # all indexes not occupied by ansi codes are normal characters
- char_indexes=[iforiinrange(len(self._raw_string))ifinotincode_indexes]
- returncode_indexes,char_indexes
-
- def_get_interleving(self,index):
- """
- Get the code characters from the given slice end to the next
- character.
-
- """
- try:
- index=self._char_indexes[index-1]
- exceptIndexError:
- return""
- s=""
- whileTrue:
- index+=1
- ifindexinself._char_indexes:
- break
- elifindexinself._code_indexes:
- s+=self._raw_string[index]
- else:
- break
- returns
-
- def__mul__(self,other):
- """
- Multiplication method. Implemented for performance reasons.
-
- """
- ifnotisinstance(other,int):
- returnNotImplemented
- raw_string=self._raw_string*other
- clean_string=self._clean_string*other
- code_indexes=self._code_indexes[:]
- char_indexes=self._char_indexes[:]
- foriinrange(other):
- code_indexes.extend(self._shifter(self._code_indexes,i*len(self._raw_string)))
- char_indexes.extend(self._shifter(self._char_indexes,i*len(self._raw_string)))
- returnANSIString(
- raw_string,
- code_indexes=code_indexes,
- char_indexes=char_indexes,
- clean_string=clean_string,
- )
-
- def__rmul__(self,other):
- returnself.__mul__(other)
-
-
[docs]defsplit(self,by=None,maxsplit=-1):
- """
- Splits a string based on a separator.
-
- Stolen from PyPy's pure Python string implementation, tweaked for
- ANSIString.
-
- PyPy is distributed under the MIT licence.
- http://opensource.org/licenses/MIT
-
- Args:
- by (str): A string to search for which will be used to split
- the string. For instance, ',' for 'Hello,world' would
- result in ['Hello', 'world']
- maxsplit (int): The maximum number of times to split the string.
- For example, a maxsplit of 2 with a by of ',' on the string
- 'Hello,world,test,string' would result in
- ['Hello', 'world', 'test,string']
- Returns:
- result (list of ANSIStrings): A list of ANSIStrings derived from
- this string.
-
- """
- drop_spaces=byisNone
- ifdrop_spaces:
- by=" "
-
- bylen=len(by)
- ifbylen==0:
- raiseValueError("empty separator")
-
- res=[]
- start=0
- whilemaxsplit!=0:
- next=self._clean_string.find(by,start)
- ifnext<0:
- break
- # Get character codes after the index as well.
- res.append(self[start:next])
- start=next+bylen
- maxsplit-=1# NB. if it's already < 0, it stays < 0
-
- res.append(self[start:len(self)])
- ifdrop_spaces:
- return[partforpartinresifpart!=""]
- returnres
-
-
[docs]defrsplit(self,by=None,maxsplit=-1):
- """
- Like split, but starts from the end of the string rather than the
- beginning.
-
- Stolen from PyPy's pure Python string implementation, tweaked for
- ANSIString.
-
- PyPy is distributed under the MIT licence.
- http://opensource.org/licenses/MIT
-
- Args:
- by (str): A string to search for which will be used to split
- the string. For instance, ',' for 'Hello,world' would
- result in ['Hello', 'world']
- maxsplit (int): The maximum number of times to split the string.
- For example, a maxsplit of 2 with a by of ',' on the string
- 'Hello,world,test,string' would result in
- ['Hello,world', 'test', 'string']
- Returns:
- result (list of ANSIStrings): A list of ANSIStrings derived from
- this string.
-
- """
- res=[]
- end=len(self)
- drop_spaces=byisNone
- ifdrop_spaces:
- by=" "
- bylen=len(by)
- ifbylen==0:
- raiseValueError("empty separator")
-
- whilemaxsplit!=0:
- next=self._clean_string.rfind(by,0,end)
- ifnext<0:
- break
- # Get character codes after the index as well.
- res.append(self[next+bylen:end])
- end=next
- maxsplit-=1# NB. if it's already < 0, it stays < 0
-
- res.append(self[:end])
- res.reverse()
- ifdrop_spaces:
- return[partforpartinresifpart!=""]
- returnres
-
-
[docs]defstrip(self,chars=None):
- """
- Strip from both ends, taking ANSI markers into account.
-
- Args:
- chars (str, optional): A string containing individual characters
- to strip off of both ends of the string. By default, any blank
- spaces are trimmed.
- Returns:
- result (ANSIString): A new ANSIString with the ends trimmed of the
- relevant characters.
-
- """
- clean=self._clean_string
- raw=self._raw_string
-
- # count continuous sequence of chars from left and right
- nlen=len(clean)
- nlstripped=nlen-len(clean.lstrip(chars))
- nrstripped=nlen-len(clean.rstrip(chars))
-
- # within the stripped regions, only retain parts of the raw
- # string *not* matching the clean string (these are ansi/mxp tags)
- lstripped=""
- ic,ir1=0,0
- whilenlstripped:
- ific>=nlstripped:
- break
- elifraw[ir1]!=clean[ic]:
- lstripped+=raw[ir1]
- else:
- ic+=1
- ir1+=1
- rstripped=""
- ic,ir2=nlen-1,len(raw)-1
- whilenrstripped:
- ifnlen-ic>nrstripped:
- break
- elifraw[ir2]!=clean[ic]:
- rstripped+=raw[ir2]
- else:
- ic-=1
- ir2-=1
- rstripped=rstripped[::-1]
- returnANSIString(lstripped+raw[ir1:ir2+1]+rstripped)
-
-
[docs]deflstrip(self,chars=None):
- """
- Strip from the left, taking ANSI markers into account.
-
- Args:
- chars (str, optional): A string containing individual characters
- to strip off of the left end of the string. By default, any
- blank spaces are trimmed.
- Returns:
- result (ANSIString): A new ANSIString with the left end trimmed of
- the relevant characters.
-
- """
- clean=self._clean_string
- raw=self._raw_string
-
- # count continuous sequence of chars from left and right
- nlen=len(clean)
- nlstripped=nlen-len(clean.lstrip(chars))
- # within the stripped regions, only retain parts of the raw
- # string *not* matching the clean string (these are ansi/mxp tags)
- lstripped=""
- ic,ir1=0,0
- whilenlstripped:
- ific>=nlstripped:
- break
- elifraw[ir1]!=clean[ic]:
- lstripped+=raw[ir1]
- else:
- ic+=1
- ir1+=1
- returnANSIString(lstripped+raw[ir1:])
-
-
[docs]defrstrip(self,chars=None):
- """
- Strip from the right, taking ANSI markers into account.
-
- Args:
- chars (str, optional): A string containing individual characters
- to strip off of the right end of the string. By default, any
- blank spaces are trimmed.
- Returns:
- result (ANSIString): A new ANSIString with the right end trimmed of
- the relevant characters.
-
- """
- clean=self._clean_string
- raw=self._raw_string
- nlen=len(clean)
- nrstripped=nlen-len(clean.rstrip(chars))
- rstripped=""
- ic,ir2=nlen-1,len(raw)-1
- whilenrstripped:
- ifnlen-ic>nrstripped:
- break
- elifraw[ir2]!=clean[ic]:
- rstripped+=raw[ir2]
- else:
- ic-=1
- ir2-=1
- rstripped=rstripped[::-1]
- returnANSIString(raw[:ir2+1]+rstripped)
-
-
[docs]defjoin(self,iterable):
- """
- Joins together strings in an iterable, using this string between each
- one.
-
- NOTE: This should always be used for joining strings when ANSIStrings
- are involved. Otherwise color information will be discarded by python,
- due to details in the C implementation of strings.
-
- Args:
- iterable (list of strings): A list of strings to join together
-
- Returns:
- ANSIString: A single string with all of the iterable's
- contents concatenated, with this string between each.
-
- Examples:
- ::
-
- >>> ANSIString(', ').join(['up', 'right', 'left', 'down'])
- ANSIString('up, right, left, down')
-
- """
- result=ANSIString("")
- last_item=None
- foriteminiterable:
- iflast_itemisnotNone:
- result+=self._raw_string
- ifnotisinstance(item,ANSIString):
- item=ANSIString(item)
- result+=item
- last_item=item
- returnresult
-
- def_filler(self,char,amount):
- """
- Generate a line of characters in a more efficient way than just adding
- ANSIStrings.
-
- """
- ifnotisinstance(char,ANSIString):
- line=char*amount
- returnANSIString(
- char*amount,
- code_indexes=[],
- char_indexes=list(range(0,len(line))),
- clean_string=char,
- )
- try:
- start=char._code_indexes[0]
- exceptIndexError:
- start=None
- end=char._char_indexes[0]
- prefix=char._raw_string[start:end]
- postfix=char._raw_string[end+1:]
- line=char._clean_string*amount
- code_indexes=[iforiinrange(0,len(prefix))]
- length=len(prefix)+len(line)
- code_indexes.extend([iforiinrange(length,length+len(postfix))])
- char_indexes=self._shifter(list(range(0,len(line))),len(prefix))
- raw_string=prefix+line+postfix
- returnANSIString(
- raw_string,clean_string=line,char_indexes=char_indexes,code_indexes=code_indexes
- )
-
- # The following methods should not be called with the '_difference' argument explicitly. This is
- # data provided by the wrapper _spacing_preflight.
-
[docs]@_spacing_preflight
- defcenter(self,width,fillchar,_difference):
- """
- Center some text with some spaces padding both sides.
-
- Args:
- width (int): The target width of the output string.
- fillchar (str): A single character string to pad the output string
- with.
- Returns:
- result (ANSIString): A string padded on both ends with fillchar.
-
- """
- remainder=_difference%2
- _difference//=2
- spacing=self._filler(fillchar,_difference)
- result=spacing+self+spacing+self._filler(fillchar,remainder)
- returnresult
-
-
[docs]@_spacing_preflight
- defljust(self,width,fillchar,_difference):
- """
- Left justify some text.
-
- Args:
- width (int): The target width of the output string.
- fillchar (str): A single character string to pad the output string
- with.
- Returns:
- result (ANSIString): A string padded on the right with fillchar.
-
- """
- returnself+self._filler(fillchar,_difference)
-
-
[docs]@_spacing_preflight
- defrjust(self,width,fillchar,_difference):
- """
- Right justify some text.
-
- Args:
- width (int): The target width of the output string.
- fillchar (str): A single character string to pad the output string
- with.
- Returns:
- result (ANSIString): A string padded on the left with fillchar.
-
- """
- returnself._filler(fillchar,_difference)+self
-"""
-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.
-
-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
-the batch-code processor. There is no in-game character that moves and
-that can be affected by what is being built - the database is
-populated on the fly. The drawback is safety and entry threshold - the
-code is executed as would any server code, without mud-specific
-permission-checks, and you have full access to modifying objects
-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.
-
-# Batch-command processor file syntax
-
-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.
-
-::
-
- 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 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.
-
-## Example of batch.ev file:
-
-::
-
- # batch file
- # all lines starting with # are comments; they also indicate
- # that a command definition is over.
-
- create box
-
- # this comment ends the @create command.
-
- set box/desc = A large box.
-
- Inside are some scattered piles of clothing.
-
-
- It seems the bottom of the box is a bit loose.
-
- # Again, this comment indicates the @set command is over. Note how
- # the description could be freely added. Excess whitespace on a line
- # is ignored. An empty line in the command definition is parsed as a \n
- # (so two empty lines becomes a new paragraph).
-
- 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.
- #IMPORT examples.batch
-
- 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)
-
-An example batch file is `contrib/examples/batch_example.ev`.
-
-# Batch-code processor file syntax
-
-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
-is loaded as a file and executed directly, so changes to the file will
-apply immediately without a server @reload.
-
-Optionally, one can add some special commented tokens to split the
-execution of the code for the benefit of the batchprocessor's
-interactive- and debug-modes. This allows to conveniently step through
-the code and re-run sections of it easily during development.
-
-Code blocks are marked by commented tokens alone on a line:
-
-- `#HEADER` - This denotes code that should be pasted at the top of all
- other code. Multiple HEADER statements - regardless of where
- it exists in the file - is the same as one big block.
- Observe that changes to variables made in one block is not
- preserved between blocks!
-- `#CODE` - This designates a code block that will be executed like a
- stand-alone piece of code together with any HEADER(s)
- defined. It is mainly used as a way to mark stop points for
- the interactive mode of the batchprocessor. If no CODE block
- is defined in the module, the entire module (including HEADERS)
- is assumed to be a CODE block.
-- `#INSERT path.filename` - This imports another batch_code.py file and
- runs it in the given position. The inserted file will retain
- its own HEADERs which will not be mixed with the headers of
- 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
- when running a CODE block multiple times during testing.
- (avoids creating a slew of same-named db objects)
-
-## Example batch.py file
-
-::
-
- #HEADER
-
- from django.conf import settings
- from evennia.utils import create
- from types import basetypes
-
- GOLD = 10
-
- #CODE
-
- obj = create.create_object(basetypes.Object)
- obj2 = create.create_object(basetypes.Object)
- obj.location = caller.location
- obj.db.gold = GOLD
- caller.msg("The object was created!")
-
- if DEBUG:
- obj.delete()
- obj2.delete()
-
- #INSERT another_batch_file
-
- #CODE
-
- script = create.create_script()
-
-"""
-importre
-importcodecs
-importtraceback
-importsys
-fromdjango.confimportsettings
-fromevennia.utilsimportutils
-
-_ENCODINGS=settings.ENCODINGS
-_RE_INSERT=re.compile(r"^\#INSERT (.*)$",re.MULTILINE)
-_RE_CLEANBLOCK=re.compile(r"^\#.*?$|^\s*$",re.MULTILINE)
-_RE_CMD_SPLIT=re.compile(r"^\#.*?$",re.MULTILINE)
-_RE_CODE_OR_HEADER=re.compile(
- r"((?:\A|^)#CODE|(?:/A|^)#HEADER|\A)(.*?)$(.*?)(?=^#CODE.*?$|^#HEADER.*?$|\Z)",
- re.MULTILINE+re.DOTALL,
-)
-
-
-# -------------------------------------------------------------
-# Helper function
-# -------------------------------------------------------------
-
-
-
[docs]defread_batchfile(pythonpath,file_ending=".py"):
- """
- This reads the contents of a batch-file. Filename is considered
- to be a python path to a batch file relative the directory
- specified in `settings.py`.
-
- file_ending specify which batchfile ending should be assumed (.ev
- or .py). The ending should not be included in the python path.
-
- Args:
- pythonpath (str): A dot-python path to a file.
- file_ending (str): The file ending of this file (.ev or .py)
-
- Returns:
- text (str): The text content of the batch file.
-
- Raises:
- IOError: If problems reading file.
-
- """
-
- # find all possible absolute paths
- abspaths=utils.pypath_to_realpath(pythonpath,file_ending,settings.BASE_BATCHPROCESS_PATHS)
- ifnotabspaths:
- raiseIOError("Absolute batchcmd paths could not be found.")
- text=None
- decoderr=[]
- forabspathinabspaths:
- # try different paths, until we get a match
- # we read the file directly into string.
- forfile_encodingin_ENCODINGS:
- # try different encodings, in order
- try:
- withcodecs.open(abspath,"r",encoding=file_encoding)asfobj:
- text=fobj.read()
- except(ValueError,UnicodeDecodeError)ase:
- # this means an encoding error; try another encoding
- decoderr.append(str(e))
- continue
- break
- ifnottextanddecoderr:
- raiseUnicodeDecodeError("\n".join(decoderr),bytearray(),0,0,"")
-
- returntext
[docs]classBatchCommandProcessor(object):
- """
- This class implements a batch-command processor.
-
- """
-
-
[docs]defparse_file(self,pythonpath):
- """
- This parses the lines of a batch-command-file.
-
- 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.
-
- """
-
- text="".join(read_batchfile(pythonpath,file_ending=".ev"))
-
- defreplace_insert(match):
- """Map replace entries"""
- try:
- path=match.group(1)
- return"\n#\n".join(self.parse_file(path))
- exceptIOError:
- raiseIOError("#INSERT {} failed.".format(path))
-
- text=_RE_INSERT.sub(replace_insert,text)
- commands=_RE_CMD_SPLIT.split(text)
- commands=[c.strip("\r\n")forcincommands]
- commands=[cforcincommandsifc]
-
- returncommands
[docs]classBatchCodeProcessor(object):
- """
- This implements a batch-code processor
-
- """
-
-
[docs]defparse_file(self,pythonpath):
- """
- This parses the lines of a batch-code file
-
- Args:
- pythonpath (str): The dot-python path to the file.
-
- Returns:
- 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.
- 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
- this file are processed with their headers *separately* before
- being inserted at the point of the #INSERT.
- 5. Code after the last block is considered part of the last header/code
- block
-
-
- """
-
- text="".join(read_batchfile(pythonpath,file_ending=".py"))
-
- defreplace_insert(match):
- """Run parse_file on the import before sub:ing it into this file"""
- path=match.group(1)
- try:
- return"# batchcode insert (%s):"%path+"\n".join(self.parse_file(path))
- exceptIOErroraserr:
- raiseIOError("#INSERT {} failed.".format(path))
-
- # process and then insert code from all #INSERTS
- text=_RE_INSERT.sub(replace_insert,text)
-
- headers=[]
- codes=[]
- forimatch,matchinenumerate(list(_RE_CODE_OR_HEADER.finditer(text))):
- mtype=match.group(1).strip()
- # we need to handle things differently at the start of the file
- ifmtype:
- istart,iend=match.span(3)
- else:
- istart,iend=match.start(2),match.end(3)
- code=text[istart:iend]
- ifmtype=="#HEADER":
- headers.append(code)
- else:# either #CODE or matching from start of file
- codes.append(code)
-
- # join all headers together to one
- header="# batchcode header:\n%s\n\n"%"\n\n".join(headers)ifheaderselse""
- # add header to each code block
- codes=["%s# batchcode code:\n%s"%(header,code)forcodeincodes]
- returncodes
-
-
[docs]defcode_exec(self,code,extra_environ=None,debug=False):
- """
- Execute a single code block, including imports and appending
- global vars.
-
- Args:
- code (str): Code to run.
- extra_environ (dict): Environment variables to run with code.
- debug (bool, optional): Set the DEBUG variable in the execution
- namespace.
-
- Returns:
- err (str or None): An error code or None (ok).
-
- """
- # define the execution environment
- environdict={"settings_module":settings,"DEBUG":debug}
- forkey,valueinextra_environ.items():
- environdict[key]=value
-
- # initializing the django settings at the top of code
- code=(
- "# batchcode evennia initialization: \n"
- "try: settings_module.configure()\n"
- "except RuntimeError: pass\n"
- "finally: del settings_module\n\n%s"%code
- )
-
- # execute the block
- try:
- exec(code,environdict)
- exceptException:
- etype,value,tb=sys.exc_info()
-
- fname=tb_filename(tb)
- fortbintb_iter(tb):
- iffname!=tb_filename(tb):
- break
- lineno=tb.tb_lineno-1
- err=""
- foriline,lineinenumerate(code.split("\n")):
- ifiline==lineno:
- err+="\n|w%02i|n: %s"%(iline+1,line)
- eliflineno-5<iline<lineno+5:
- err+="\n%02i: %s"%(iline+1,line)
-
- err+="\n".join(traceback.format_exception(etype,value,tb))
- returnerr
- returnNone
-"""
-Containers
-
-Containers are storage classes usually initialized from a setting. They
-represent Singletons and acts as a convenient place to find resources (
-available as properties on the singleton)
-
-evennia.GLOBAL_SCRIPTS
-evennia.OPTION_CLASSES
-
-"""
-
-
-frompickleimportdumps
-fromdjango.confimportsettings
-fromevennia.utils.utilsimportclass_from_module,callables_from_module
-fromevennia.utilsimportlogger
-
-
-SCRIPTDB=None
-
-
-
[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
- considered a read-only affair.
-
- The container is initialized by a list of modules containing callables.
-
- """
-
- storage_modules=[]
-
-
[docs]def__init__(self):
- """
- Read data from module.
-
- """
- self.loaded_data=None
-
-
[docs]defload_data(self):
- """
- Delayed import to avoid eventual circular imports from inside
- the storage modules.
-
- """
- ifself.loaded_dataisNone:
- self.loaded_data={}
- formoduleinself.storage_modules:
- self.loaded_data.update(callables_from_module(module))
[docs]defget(self,key,default=None):
- """
- Retrive data by key (in case of not knowing it beforehand).
-
- Args:
- key (str): The name of the script.
- default (any, optional): Value to return if key is not found.
-
- Returns:
- any (any): The data loaded on this container.
-
- """
- self.load_data()
- returnself.loaded_data.get(key,default)
-
-
[docs]defall(self):
- """
- Get all stored data
-
- Returns:
- scripts (list): All global script objects stored on the container.
-
- """
- self.load_data()
- returnlist(self.loaded_data.values())
-
-
-
[docs]classOptionContainer(Container):
- """
- Loads and stores the final list of OPTION CLASSES.
-
- Can access these as properties or dictionary-contents.
- """
-
- storage_modules=settings.OPTION_CLASS_MODULES
-
-
-
[docs]classGlobalScriptContainer(Container):
- """
- Simple Handler object loaded by the Evennia API to contain and manage a
- game's Global Scripts. This will list global Scripts created on their own
- but will also auto-(re)create scripts defined in `settings.GLOBAL_SCRIPTS`.
-
- Example:
- import evennia
- evennia.GLOBAL_SCRIPTS.scriptname
-
- Note:
- This does not use much of the BaseContainer since it's not loading
- callables from settings but a custom dict of tuples.
-
- """
-
-
[docs]def__init__(self):
- """
- Note: We must delay loading of typeclasses since this module may get
- initialized before Scripts are actually initialized.
-
- """
- self.typeclass_storage=None
- self.loaded_data={
- key:{}ifdataisNoneelsedataforkey,datainsettings.GLOBAL_SCRIPTS.items()
- }
-
- def_get_scripts(self,key=None,default=None):
- globalSCRIPTDB
- ifnotSCRIPTDB:
- fromevennia.scripts.modelsimportScriptDBasSCRIPTDB
- ifkey:
- try:
- returnSCRIPTDB.objects.get(db_key__exact=key,db_obj__isnull=True)
- exceptSCRIPTDB.DoesNotExist:
- returndefault
- else:
- returnSCRIPTDB.objects.filter(db_obj__isnull=True)
-
- def_load_script(self,key):
- self.load_data()
-
- typeclass=self.typeclass_storage[key]
- script=typeclass.objects.filter(
- db_key=key,db_account__isnull=True,db_obj__isnull=True).first()
-
- kwargs={**self.loaded_data[key]}
- kwargs['key']=key
- kwargs['persistent']=kwargs.get('persistent',True)
-
- compare_hash=str(dumps(kwargs,protocol=4))
-
- ifscript:
- script_hash=script.attributes.get("global_script_settings",category="settings_hash")
- ifscript_hashisNone:
- # legacy - store the hash anew and assume no change
- script.attributes.add("global_script_settings",compare_hash,
- category="settings_hash")
- elifscript_hash!=compare_hash:
- # wipe the old version and create anew
- logger.log_info(f"GLOBAL_SCRIPTS: Settings changed for {key} ({typeclass}).")
- script.stop()
- script.delete()
- script=None
-
- ifnotscript:
- logger.log_info(f"GLOBAL_SCRIPTS: (Re)creating {key} ({typeclass}).")
-
- script,errors=typeclass.create(**kwargs)
- iferrors:
- logger.log_err("\n".join(errors))
- returnNone
-
- # store a hash representation of the setup
- script.attributes.add("_global_script_settings",
- compare_hash,category="settings_hash")
- script.start()
-
- returnscript
-
-
[docs]defstart(self):
- """
- Called last in evennia.__init__ to initialize the container late
- (after script typeclasses have finished loading).
-
- We include all global scripts in the handler and
- make sure to auto-load time-based scripts.
-
- """
- # populate self.typeclass_storage
- self.load_data()
-
- # start registered scripts
- forkeyinself.loaded_data:
- self._load_script(key)
-
-
[docs]defload_data(self):
- """
- This delayed import avoids trying to load Scripts before they are
- initialized.
-
- """
- ifself.typeclass_storageisNone:
- self.typeclass_storage={}
- forkey,datainself.loaded_data.items():
- try:
- typeclass=data.get("typeclass",settings.BASE_SCRIPT_TYPECLASS)
- self.typeclass_storage[key]=class_from_module(typeclass)
- exceptException:
- logger.log_trace(
- f"GlobalScriptContainer could not start import global script {key}.")
-
-
[docs]defget(self,key,default=None):
- """
- Retrive data by key (in case of not knowing it beforehand). Any
- scripts that are in settings.GLOBAL_SCRIPTS that are not found
- will be recreated on-demand.
-
- Args:
- key (str): The name of the script.
- default (any, optional): Value to return if key is not found
- at all on this container (i.e it cannot be loaded at all).
-
- Returns:
- any (any): The data loaded on this container.
- """
- res=self._get_scripts(key)
- ifnotres:
- ifkeyinself.loaded_data:
- # recreate if we have the info
- returnself._load_script(key)ordefault
- returndefault
- returnres
-
-
[docs]defall(self):
- """
- Get all global scripts. Note that this will not auto-start
- scripts defined in settings.
-
- Returns:
- scripts (list): All global script objects stored on the container.
-
- """
- self.typeclass_storage=None
- self.load_data()
- forkeyinself.loaded_data:
- self._load_script(key)
- returnself._get_scripts(None)
-
-
-# Create all singletons
-
-GLOBAL_SCRIPTS=GlobalScriptContainer()
-OPTION_CLASSES=OptionContainer()
-
-"""
-This module handles serialization of arbitrary python structural data,
-intended primarily to be stored in the database. It also supports
-storing Django model instances (which plain pickle cannot do).
-
-This serialization is used internally by the server, notably for
-storing data in Attributes and for piping data to process pools.
-
-The purpose of dbserialize is to handle all forms of data. For
-well-structured non-arbitrary exchange, such as communicating with a
-rich web client, a simpler JSON serialization makes more sense.
-
-This module also implements the `SaverList`, `SaverDict` and `SaverSet`
-classes. These are iterables that track their position in a nested
-structure and makes sure to send updates up to their root. This is
-used by Attributes - without it, one would not be able to update mutables
-in-situ, e.g `obj.db.mynestedlist[3][5] = 3` would never be saved and
-be out of sync with the database.
-
-"""
-fromfunctoolsimportupdate_wrapper
-fromcollectionsimportdeque,OrderedDict,defaultdict
-fromcollections.abcimportMutableSequence,MutableSet,MutableMapping
-
-try:
- frompickleimportdumps,loads
-exceptImportError:
- frompickleimportdumps,loads
-fromdjango.core.exceptionsimportObjectDoesNotExist
-fromdjango.contrib.contenttypes.modelsimportContentType
-fromdjango.utils.safestringimportSafeString
-fromevennia.utils.utilsimportuses_database,is_iter,to_bytes
-fromevennia.utilsimportlogger
-
-__all__=("to_pickle","from_pickle","do_pickle","do_unpickle","dbserialize","dbunserialize")
-
-PICKLE_PROTOCOL=2
-
-
-# message to send if editing an already deleted Attribute in a savermutable
-_ERROR_DELETED_ATTR=(
- "{cls_name}{obj} has had its root Attribute deleted. "
- "It must be cast to a {non_saver_name} before it can be modified further."
-)
-
-
-def_get_mysql_db_version():
- """
- This is a helper method for specifically getting the version
- string of a MySQL database.
-
- Returns:
- mysql_version (str): The currently used mysql database
- version.
-
- """
- fromdjango.dbimportconnection
-
- conn=connection.cursor()
- conn.execute("SELECT VERSION()")
- version=conn.fetchone()
- returnversionandstr(version[0])or""
-
-
-# initialization and helpers
-
-
-_GA=object.__getattribute__
-_SA=object.__setattr__
-_FROM_MODEL_MAP=None
-_TO_MODEL_MAP=None
-_IGNORE_DATETIME_MODELS=None
-_SESSION_HANDLER=None
-
-
-def_IS_PACKED_DBOBJ(o):
- returnisinstance(o,tuple)andlen(o)==4ando[0]=="__packed_dbobj__"
-
-
-def_IS_PACKED_SESSION(o):
- returnisinstance(o,tuple)andlen(o)==3ando[0]=="__packed_session__"
-
-
-ifuses_database("mysql")and_get_mysql_db_version()<"5.6.4":
- # mysql <5.6.4 don't support millisecond precision
- _DATESTRING="%Y:%m:%d-%H:%M:%S:000000"
-else:
- _DATESTRING="%Y:%m:%d-%H:%M:%S:%f"
-
-
-def_TO_DATESTRING(obj):
- """
- Creates datestring hash.
-
- Args:
- obj (Object): Database object.
-
- Returns:
- datestring (str): A datestring hash.
-
- """
- try:
- return_GA(obj,"db_date_created").strftime(_DATESTRING)
- exceptAttributeError:
- # this can happen if object is not yet saved - no datestring is then set
- try:
- obj.save()
- exceptAttributeError:
- # we have received a None object, for example due to an erroneous save.
- returnNone
- return_GA(obj,"db_date_created").strftime(_DATESTRING)
-
-
-def_init_globals():
- """Lazy importing to avoid circular import issues"""
- global_FROM_MODEL_MAP,_TO_MODEL_MAP,_SESSION_HANDLER,_IGNORE_DATETIME_MODELS
- ifnot_FROM_MODEL_MAP:
- _FROM_MODEL_MAP=defaultdict(str)
- _FROM_MODEL_MAP.update(dict((c.model,c.natural_key())forcinContentType.objects.all()))
- ifnot_TO_MODEL_MAP:
- fromdjango.confimportsettings
-
- _TO_MODEL_MAP=defaultdict(str)
- _TO_MODEL_MAP.update(
- dict((c.natural_key(),c.model_class())forcinContentType.objects.all())
- )
- _IGNORE_DATETIME_MODELS=[]
- forsrc_key,dst_keyinsettings.ATTRIBUTE_STORED_MODEL_RENAME:
- _TO_MODEL_MAP[src_key]=_TO_MODEL_MAP.get(dst_key,None)
- _IGNORE_DATETIME_MODELS.append(src_key)
- ifnot_SESSION_HANDLER:
- fromevennia.server.sessionhandlerimportSESSION_HANDLERas_SESSION_HANDLER
-
-
-#
-# SaverList, SaverDict, SaverSet - Attribute-specific helper classes and functions
-#
-
-
-def_save(method):
- """method decorator that saves data to Attribute"""
-
- defsave_wrapper(self,*args,**kwargs):
- self.__doc__=method.__doc__
- ret=method(self,*args,**kwargs)
- self._save_tree()
- returnret
-
- returnupdate_wrapper(save_wrapper,method)
-
-
-class_SaverMutable(object):
- """
- Parent class for properly handling of nested mutables in
- an Attribute. If not used something like
- obj.db.mylist[1][2] = "test" (allocation to a nested list)
- will not save the updated value to the database.
- """
-
- def__init__(self,*args,**kwargs):
- """store all properties for tracking the tree"""
- self._parent=kwargs.pop("_parent",None)
- self._db_obj=kwargs.pop("_db_obj",None)
- self._data=None
-
- def__bool__(self):
- """Make sure to evaluate as False if empty"""
- returnbool(self._data)
-
- def_save_tree(self):
- """recursively traverse back up the tree, save when we reach the root"""
- ifself._parent:
- self._parent._save_tree()
- elifself._db_obj:
- ifnotself._db_obj.pk:
- cls_name=self.__class__.__name__
- try:
- non_saver_name=cls_name.split("_Saver",1)[1].lower()
- exceptIndexError:
- non_saver_name=cls_name
- raiseValueError(
- _ERROR_DELETED_ATTR.format(
- cls_name=cls_name,obj=self,non_saver_name=non_saver_name
- )
- )
- self._db_obj.value=self
- else:
- logger.log_err("_SaverMutable %s has no root Attribute to save to."%self)
-
- def_convert_mutables(self,data):
- """converts mutables to Saver* variants and assigns ._parent property"""
-
- defprocess_tree(item,parent):
- """recursively populate the tree, storing parents"""
- dtype=type(item)
- ifdtypein(str,int,float,bool,tuple):
- returnitem
- elifdtype==list:
- dat=_SaverList(_parent=parent)
- dat._data.extend(process_tree(val,dat)forvalinitem)
- returndat
- elifdtype==dict:
- dat=_SaverDict(_parent=parent)
- dat._data.update((key,process_tree(val,dat))forkey,valinitem.items())
- returndat
- elifdtype==set:
- dat=_SaverSet(_parent=parent)
- dat._data.update(process_tree(val,dat)forvalinitem)
- returndat
- returnitem
-
- returnprocess_tree(data,self)
-
- def__repr__(self):
- returnself._data.__repr__()
-
- def__len__(self):
- returnself._data.__len__()
-
- def__iter__(self):
- returnself._data.__iter__()
-
- def__getitem__(self,key):
- returnself._data.__getitem__(key)
-
- def__eq__(self,other):
- returnself._data==other
-
- def__ne__(self,other):
- returnself._data!=other
-
- def__lt__(self,other):
- returnself._data<other
-
- def__gt__(self,other):
- returnself._data>other
-
- @_save
- def__setitem__(self,key,value):
- self._data.__setitem__(key,self._convert_mutables(value))
-
- @_save
- def__delitem__(self,key):
- self._data.__delitem__(key)
-
-
-class_SaverList(_SaverMutable,MutableSequence):
- """
- A list that saves itself to an Attribute when updated.
- """
-
- def__init__(self,*args,**kwargs):
- super().__init__(*args,**kwargs)
- self._data=list()
-
- @_save
- def__iadd__(self,otherlist):
- self._data=self._data.__add__(otherlist)
- returnself._data
-
- def__add__(self,otherlist):
- returnlist(self._data)+otherlist
-
- @_save
- definsert(self,index,value):
- self._data.insert(index,self._convert_mutables(value))
-
- def__eq__(self,other):
- try:
- returnlist(self._data)==list(other)
- exceptTypeError:
- returnFalse
-
- def__ne__(self,other):
- try:
- returnlist(self._data)!=list(other)
- exceptTypeError:
- returnTrue
-
- defindex(self,value,*args):
- returnself._data.index(value,*args)
-
- @_save
- defsort(self,*,key=None,reverse=False):
- self._data.sort(key=key,reverse=reverse)
-
- defcopy(self):
- returnself._data.copy()
-
-
-class_SaverDict(_SaverMutable,MutableMapping):
- """
- A dict that stores changes to an Attribute when updated
- """
-
- def__init__(self,*args,**kwargs):
- super().__init__(*args,**kwargs)
- self._data=dict()
-
- defhas_key(self,key):
- returnkeyinself._data
-
- @_save
- defupdate(self,*args,**kwargs):
- self._data.update(*args,**kwargs)
-
-
-class_SaverSet(_SaverMutable,MutableSet):
- """
- A set that saves to an Attribute when updated
- """
-
- def__init__(self,*args,**kwargs):
- super().__init__(*args,**kwargs)
- self._data=set()
-
- def__contains__(self,value):
- returnself._data.__contains__(value)
-
- @_save
- defadd(self,value):
- self._data.add(self._convert_mutables(value))
-
- @_save
- defdiscard(self,value):
- self._data.discard(value)
-
-
-class_SaverOrderedDict(_SaverMutable,MutableMapping):
- """
- An ordereddict that can be saved and operated on.
- """
-
- def__init__(self,*args,**kwargs):
- super().__init__(*args,**kwargs)
- self._data=OrderedDict()
-
- defhas_key(self,key):
- returnkeyinself._data
-
-
-class_SaverDeque(_SaverMutable):
- """
- A deque that can be saved and operated on.
- """
-
- def__init__(self,*args,**kwargs):
- super().__init__(*args,**kwargs)
- self._data=deque()
-
- @_save
- defappend(self,*args,**kwargs):
- self._data.append(*args,**kwargs)
-
- @_save
- defappendleft(self,*args,**kwargs):
- self._data.appendleft(*args,**kwargs)
-
- @_save
- defclear(self):
- self._data.clear()
-
- @_save
- defextendleft(self,*args,**kwargs):
- self._data.extendleft(*args,**kwargs)
-
- # maxlen property
- def_getmaxlen(self):
- returnself._data.maxlen
-
- def_setmaxlen(self,value):
- self._data.maxlen=value
-
- def_delmaxlen(self):
- delself._data.maxlen
-
- maxlen=property(_getmaxlen,_setmaxlen,_delmaxlen)
-
- @_save
- defpop(self,*args,**kwargs):
- returnself._data.pop(*args,**kwargs)
-
- @_save
- defpopleft(self,*args,**kwargs):
- returnself._data.popleft(*args,**kwargs)
-
- @_save
- defreverse(self):
- self._data.reverse()
-
- @_save
- defrotate(self,*args):
- self._data.rotate(*args)
-
- @_save
- defremove(self,*args):
- self._data.remove(*args)
-
-
-_DESERIALIZE_MAPPING={
- _SaverList.__name__:list,
- _SaverDict.__name__:dict,
- _SaverSet.__name__:set,
- _SaverOrderedDict.__name__:OrderedDict,
- _SaverDeque.__name__:deque,
-}
-
-
-defdeserialize(obj):
- """
- Make sure to *fully* decouple a structure from the database, by turning all _Saver*-mutables
- inside it back into their normal Python forms.
-
- """
-
- def_iter(obj):
- typ=type(obj)
- tname=typ.__name__
- iftnamein("_SaverDict","dict"):
- return{_iter(key):_iter(val)forkey,valinobj.items()}
- eliftnamein_DESERIALIZE_MAPPING:
- return_DESERIALIZE_MAPPING[tname](_iter(val)forvalinobj)
- elifis_iter(obj):
- returntyp(_iter(val)forvalinobj)
- returnobj
-
- return_iter(obj)
-
-
-#
-# serialization helpers
-
-
-defpack_dbobj(item):
- """
- Check and convert django database objects to an internal representation.
-
- Args:
- item (any): A database entity to pack
-
- Returns:
- packed (any or tuple): Either returns the original input item
- or the packing tuple `("__packed_dbobj__", key, creation_time, id)`.
-
- """
- _init_globals()
- obj=item
- natural_key=_FROM_MODEL_MAP[
- hasattr(obj,"id")
- andhasattr(obj,"db_date_created")
- andhasattr(obj,"__dbclass__")
- andobj.__dbclass__.__name__.lower()
- ]
- # build the internal representation as a tuple
- # ("__packed_dbobj__", key, creation_time, id)
- return(
- natural_key
- and("__packed_dbobj__",natural_key,_TO_DATESTRING(obj),_GA(obj,"id"))
- oritem
- )
-
-
-defunpack_dbobj(item):
- """
- Check and convert internal representations back to Django database
- models.
-
- Args:
- item (packed_dbobj): The fact that item is a packed dbobj
- should be checked before this call.
-
- Returns:
- unpacked (any): Either the original input or converts the
- internal store back to a database representation (its
- typeclass is returned if applicable).
-
- """
- _init_globals()
- try:
- obj=item[3]and_TO_MODEL_MAP[item[1]].objects.get(id=item[3])
- exceptObjectDoesNotExist:
- returnNone
- exceptTypeError:
- ifhasattr(item,"pk"):
- # this happens if item is already an obj
- returnitem
- returnNone
- ifitem[1]in_IGNORE_DATETIME_MODELS:
- # if we are replacing models we ignore the datatime
- returnobj
- else:
- # even if we got back a match, check the sanity of the date (some
- # databases may 're-use' the id)
- return_TO_DATESTRING(obj)==item[2]andobjorNone
-
-
-defpack_session(item):
- """
- Handle the safe serializion of Sessions objects (these contain
- hidden references to database objects (accounts, puppets) so they
- can't be safely serialized).
-
- Args:
- item (Session)): This item must have all properties of a session
- before entering this call.
-
- Returns:
- packed (tuple or None): A session-packed tuple on the form
- `(__packed_session__, sessid, conn_time)`. If this sessid
- does not match a session in the Session handler, None is returned.
-
- """
- _init_globals()
- session=_SESSION_HANDLER.get(item.sessid)
- ifsessionandsession.conn_time==item.conn_time:
- # we require connection times to be identical for the Session
- # to be accepted as actually being a session (sessids gets
- # reused all the time).
- return(
- item.conn_time
- anditem.sessid
- and("__packed_session__",_GA(item,"sessid"),_GA(item,"conn_time"))
- )
- returnNone
-
-
-defunpack_session(item):
- """
- Check and convert internal representations back to Sessions.
-
- Args:
- item (packed_session): The fact that item is a packed session
- should be checked before this call.
-
- Returns:
- unpacked (any): Either the original input or converts the
- internal store back to a Session. If Session no longer
- exists, None will be returned.
- """
- _init_globals()
- session=_SESSION_HANDLER.get(item[1])
- ifsessionandsession.conn_time==item[2]:
- # we require connection times to be identical for the Session
- # to be accepted as the same as the one stored (sessids gets
- # reused all the time).
- returnsession
- returnNone
-
-
-#
-# Access methods
-
-
-
[docs]defto_pickle(data):
- """
- This prepares data on arbitrary form to be pickled. It handles any
- nested structure and returns data on a form that is safe to pickle
- (including having converted any database models to their internal
- representation). We also convert any Saver*-type objects back to
- their normal representations, they are not pickle-safe.
-
- Args:
- data (any): Data to pickle.
-
- Returns:
- data (any): Pickled data.
-
- """
-
- defprocess_item(item):
- """Recursive processor and identification of data"""
- dtype=type(item)
- ifdtypein(str,int,float,bool,bytes,SafeString):
- returnitem
- elifdtype==tuple:
- returntuple(process_item(val)forvalinitem)
- elifdtypein(list,_SaverList):
- return[process_item(val)forvalinitem]
- elifdtypein(dict,_SaverDict):
- returndict((process_item(key),process_item(val))forkey,valinitem.items())
- elifdtypein(set,_SaverSet):
- returnset(process_item(val)forvalinitem)
- elifdtypein(OrderedDict,_SaverOrderedDict):
- returnOrderedDict((process_item(key),process_item(val))forkey,valinitem.items())
- elifdtypein(deque,_SaverDeque):
- returndeque(process_item(val)forvalinitem)
-
- elifhasattr(item,"__iter__"):
- # we try to conserve the iterable class, if not convert to list
- try:
- returnitem.__class__([process_item(val)forvalinitem])
- except(AttributeError,TypeError):
- return[process_item(val)forvalinitem]
- elifhasattr(item,"sessid")andhasattr(item,"conn_time"):
- returnpack_session(item)
- try:
- returnpack_dbobj(item)
- exceptTypeError:
- returnitem
- exceptException:
- logger.log_err(f"The object {item} of type {type(item)} could not be stored.")
- raise
-
- returnprocess_item(data)
-
-
-# @transaction.autocommit
-
[docs]deffrom_pickle(data,db_obj=None):
- """
- This should be fed a just de-pickled data object. It will be converted back
- to a form that may contain database objects again. Note that if a database
- object was removed (or changed in-place) in the database, None will be
- returned.
-
- Args:
- data (any): Pickled data to unpickle.
- db_obj (Atribute, any): This is the model instance (normally
- an Attribute) that _Saver*-type iterables (_SaverList etc)
- 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.
-
- Returns:
- data (any): Unpickled data.
-
- """
-
- defprocess_item(item):
- """Recursive processor and identification of data"""
- dtype=type(item)
- ifdtypein(str,int,float,bool,bytes,SafeString):
- returnitem
- elif_IS_PACKED_DBOBJ(item):
- # this must be checked before tuple
- returnunpack_dbobj(item)
- elif_IS_PACKED_SESSION(item):
- returnunpack_session(item)
- elifdtype==tuple:
- returntuple(process_item(val)forvalinitem)
- elifdtype==dict:
- returndict((process_item(key),process_item(val))forkey,valinitem.items())
- elifdtype==set:
- returnset(process_item(val)forvalinitem)
- elifdtype==OrderedDict:
- returnOrderedDict((process_item(key),process_item(val))forkey,valinitem.items())
- elifdtype==deque:
- returndeque(process_item(val)forvalinitem)
- elifhasattr(item,"__iter__"):
- try:
- # we try to conserve the iterable class if
- # it accepts an iterator
- returnitem.__class__(process_item(val)forvalinitem)
- except(AttributeError,TypeError):
- return[process_item(val)forvalinitem]
- returnitem
-
- defprocess_tree(item,parent):
- """Recursive processor, building a parent-tree from iterable data"""
- dtype=type(item)
- ifdtypein(str,int,float,bool,bytes,SafeString):
- returnitem
- elif_IS_PACKED_DBOBJ(item):
- # this must be checked before tuple
- returnunpack_dbobj(item)
- elifdtype==tuple:
- returntuple(process_tree(val,item)forvalinitem)
- elifdtype==list:
- dat=_SaverList(_parent=parent)
- dat._data.extend(process_tree(val,dat)forvalinitem)
- returndat
- elifdtype==dict:
- dat=_SaverDict(_parent=parent)
- dat._data.update(
- (process_item(key),process_tree(val,dat))forkey,valinitem.items()
- )
- returndat
- elifdtype==set:
- dat=_SaverSet(_parent=parent)
- dat._data.update(set(process_tree(val,dat)forvalinitem))
- returndat
- elifdtype==OrderedDict:
- dat=_SaverOrderedDict(_parent=parent)
- dat._data.update(
- (process_item(key),process_tree(val,dat))forkey,valinitem.items()
- )
- returndat
- elifdtype==deque:
- dat=_SaverDeque(_parent=parent)
- dat._data.extend(process_item(val)forvalinitem)
- returndat
- elifhasattr(item,"__iter__"):
- try:
- # we try to conserve the iterable class if it
- # accepts an iterator
- returnitem.__class__(process_tree(val,parent)forvalinitem)
- except(AttributeError,TypeError):
- dat=_SaverList(_parent=parent)
- dat._data.extend(process_tree(val,dat)forvalinitem)
- returndat
- returnitem
-
- ifdb_obj:
- # convert lists, dicts and sets to their Saved* counterparts. It
- # is only relevant if the "root" is an iterable of the right type.
- dtype=type(data)
- ifdtype==list:
- dat=_SaverList(_db_obj=db_obj)
- dat._data.extend(process_tree(val,dat)forvalindata)
- returndat
- elifdtype==dict:
- dat=_SaverDict(_db_obj=db_obj)
- dat._data.update(
- (process_item(key),process_tree(val,dat))forkey,valindata.items()
- )
- returndat
- elifdtype==set:
- dat=_SaverSet(_db_obj=db_obj)
- dat._data.update(process_tree(val,dat)forvalindata)
- returndat
- elifdtype==OrderedDict:
- dat=_SaverOrderedDict(_db_obj=db_obj)
- dat._data.update(
- (process_item(key),process_tree(val,dat))forkey,valindata.items()
- )
- returndat
- elifdtype==deque:
- dat=_SaverDeque(_db_obj=db_obj)
- dat._data.extend(process_item(val)forvalindata)
- returndat
- returnprocess_item(data)
-
-
-
[docs]defdo_pickle(data):
- """Perform pickle to string"""
- try:
- returndumps(data,protocol=PICKLE_PROTOCOL)
- exceptException:
- logger.log_err(f"Could not pickle data for storage: {data}")
- raise
-
-
-
[docs]defdo_unpickle(data):
- """Retrieve pickle from pickled string"""
- try:
- returnloads(to_bytes(data))
- exceptException:
- logger.log_err(f"Could not unpickle data from storage: {data}")
- raise
-
-
-
[docs]defdbserialize(data):
- """Serialize to pickled form in one step"""
- returndo_pickle(to_pickle(data))
-
-
-
[docs]defdbunserialize(data,db_obj=None):
- """Un-serialize in one step. See from_pickle for help db_obj."""
- returnfrom_pickle(do_unpickle(data),db_obj=db_obj)
-"""
-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.
-
-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:
-
-```python
-from evennia.utils.eveditor import EvEditor
-
-# set up an editor to edit the caller's 'desc' Attribute
-def _loadfunc(caller):
- return caller.db.desc
-
-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.
-
-"""
-importre
-
-fromdjango.confimportsettings
-fromevenniaimportCmdSet
-fromevennia.utilsimportis_iter,fill,dedent,logger,justify,to_str,utils
-fromevennia.utils.ansiimportraw
-fromevennia.commandsimportcmdhandler
-fromdjango.utils.translationimportgettextas_
-
-# we use cmdhandler instead of evennia.syscmdkeys to
-# avoid some cases of loading before evennia init'd
-_CMD_NOMATCH=cmdhandler.CMD_NOMATCH
-_CMD_NOINPUT=cmdhandler.CMD_NOINPUT
-
-_RE_GROUP=re.compile(r"\".*?\"|\'.*?\'|\S*")
-_COMMAND_DEFAULT_CLASS=utils.class_from_module(settings.COMMAND_DEFAULT_CLASS)
-# use NAWS in the future?
-_DEFAULT_WIDTH=settings.CLIENT_DEFAULT_WIDTH
-
-# -------------------------------------------------------------
-#
-# texts
-#
-# -------------------------------------------------------------
-
-_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>
- ::: - escape - enter ':' as the only character on the line.
- :h - this help.
-
- :w - save the buffer (don't quit)
- :wq - save buffer and quit
- :q - quit (will be asked to save if buffer was changed)
- :q! - quit without saving, no questions asked
-
- :u - (undo) step backwards in undo history
- :uu - (redo) step forward in undo history
- :UU - reset all changes back to initial state
-
- :dd <l> - delete last line or line(s) <l>
- :dw <l> <w> - delete word or regex <w> in entire buffer or on line <l>
- :DD - clear entire buffer
-
- :y <l> - yank (copy) line(s) <l> to the copy buffer
- :x <l> - cut line(s) <l> and store it in the copy buffer
- :p <l> - put (paste) previously copied line(s) directly after <l>
- :i <l> <txt> - insert new text <txt> at line <l>. Old line will move down
- :r <l> <txt> - replace line <l> with text <txt>
- :I <l> <txt> - insert text at the beginning of line <l>
- :A <l> <txt> - append text after the end of line <l>
-
- :s <l> <w> <txt> - search/replace word or regex <w> in buffer or on line <l>
-
- :j <l> <w> - justify buffer or line <l>. <w> is f, c, l or r. Default f (full)
- :f <l> - flood-fill entire buffer or line <l>: Equivalent to :j left
- :fi <l> - indent entire buffer or line <l>
- :fd <l> - de-indent entire buffer or line <l>
-
- :echo - turn echoing of the input on/off (helpful for some clients)
-""")
-
-_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=_("""
- :! - 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}
-
-|rBuffer load function error. Could not load initial data.|n
-""")
-
-_ERROR_SAVEFUNC=_("""
-{error}
-
-|rSave function returned an error. Buffer not 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.")
-
-_ERROR_QUITFUNC=_("""
-{error}
-
-|rQuit function gave an error. Skipping.|n
-""")
-
-_ERROR_PERSISTENT_SAVING=_("""
-{error}
-
-|rThe editor state could not be saved for persistent mode. Switching
-to non-persistent mode (which means the editor session won't survive
-an eventual server reload - so save often!)|n
-""")
-
-_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.")
-
-# -------------------------------------------------------------
-#
-# Handle yes/no quit question
-#
-# -------------------------------------------------------------
-
-
-
[docs]classCmdSaveYesNo(_COMMAND_DEFAULT_CLASS):
- """
- Save the editor state on quit. This catches
- nomatches (defaults to Yes), and avoid saves only if
- command was given specifically as "no" or "n".
- """
-
- key=_CMD_NOMATCH
- aliases=_CMD_NOINPUT
- locks="cmd:all()"
- help_cateogory="LineEditor"
-
-
[docs]deffunc(self):
- """
- Implement the yes/no choice.
-
- """
- # this is only called from inside the lineeditor
- # so caller.ndb._lineditor must be set.
-
- self.caller.cmdset.remove(SaveYesNoCmdSet)
- ifself.raw_string.strip().lower()in("no","n"):
- # answered no
- self.caller.msg(self.caller.ndb._eveditor.quit())
- else:
- # answered yes (default)
- self.caller.ndb._eveditor.save_buffer()
- self.caller.ndb._eveditor.quit()
[docs]defparse(self):
- """
- Handles pre-parsing. Editor commands are on the form
-
- ::
-
- :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.
-
- """
-
- editor=self.caller.ndb._eveditor
- ifnoteditor:
- # this will completely replace the editor
- _load_editor(self.caller)
- editor=self.caller.ndb._eveditor
- self.editor=editor
-
- linebuffer=self.editor.get_buffer().split("\n")
-
- nlines=len(linebuffer)
-
- # The regular expression will split the line by whitespaces,
- # stripping extra whitespaces, except if the text is
- # surrounded by single- or double quotes, in which case they
- # will be kept together and extra whitespace preserved. You
- # can input quotes on the line by alternating single and
- # double quotes.
- arglist=[partforpartin_RE_GROUP.findall(self.args)ifpart]
- temp=[]
- forarginarglist:
- # we want to clean the quotes, but only one type,
- # in case we are nesting.
- ifarg.startswith('"'):
- arg.strip('"')
- elifarg.startswith("'"):
- arg.strip("'")
- temp.append(arg)
- arglist=temp
-
- # A dumb split, without grouping quotes
- words=self.args.split()
-
- # current line number
- cline=nlines-1
-
- # the first argument could also be a range of line numbers, on the
- # form <lstart>:<lend>. Either of the ends could be missing, to
- # mean start/end of buffer respectively.
-
- lstart,lend=cline,cline+1
- linerange=False
- ifarglistandarglist[0].count(":")==1:
- part1,part2=arglist[0].split(":")
- ifpart1andpart1.isdigit():
- lstart=min(max(0,int(part1))-1,nlines)
- linerange=True
- ifpart2andpart2.isdigit():
- lend=min(lstart+1,int(part2))+1
- linerange=True
- elifarglistandarglist[0].isdigit():
- lstart=min(max(0,int(arglist[0])-1),nlines)
- lend=lstart+1
- linerange=True
- iflinerange:
- arglist=arglist[1:]
-
- # nicer output formatting of the line range.
- lstr=(
- "line %i"%(lstart+1)
- ifnotlinerangeorlstart+1==lend
- else"lines %i-%i"%(lstart+1,lend)
- )
-
- # arg1 and arg2 is whatever arguments. Line numbers or -ranges are
- # never included here.
- args=" ".join(arglist)
- arg1,arg2="",""
- iflen(arglist)>1:
- arg1,arg2=arglist[0]," ".join(arglist[1:])
- else:
- arg1=" ".join(arglist)
-
- # store for use in func()
-
- self.linebuffer=linebuffer
- self.nlines=nlines
- self.arglist=arglist
- self.cline=cline
- self.lstart=lstart
- self.lend=lend
- self.linerange=linerange
- self.lstr=lstr
- self.words=words
- self.args=args
- self.arg1=arg1
- self.arg2=arg2
-
-
-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))
- unsaved=caller.attributes.get("_eveditor_unsaved",False)
- indent=caller.attributes.get("_eveditor_indent",0)
- ifsaved_options:
- eveditor=EvEditor(caller,**saved_options[0])
- ifsaved_buffer:
- # we have to re-save the buffer data so we can handle subsequent restarts
- caller.attributes.add("_eveditor_buffer_temp",(saved_buffer,saved_undo))
- setattr(eveditor,"_buffer",saved_buffer)
- setattr(eveditor,"_undo_buffer",saved_undo)
- setattr(eveditor,"_undo_pos",len(saved_undo)-1)
- setattr(eveditor,"_unsaved",unsaved)
- setattr(eveditor,"_indent",indent)
- forkey,valueinsaved_options[1].items():
- setattr(eveditor,key,value)
- else:
- # something went wrong. Cleanup.
- caller.cmdset.remove(EvEditorCmdSet)
-
-
-
[docs]classCmdLineInput(CmdEditorBase):
- """
- No command match - Inputs line of text into buffer.
-
- """
-
- key=_CMD_NOMATCH
- aliases=_CMD_NOINPUT
-
-
[docs]deffunc(self):
- """
- Adds the line without any formatting changes.
-
- If the editor handles code, it might add automatic
- indentation.
- """
- caller=self.caller
- editor=caller.ndb._eveditor
- buf=editor.get_buffer()
-
- # add a line of text to buffer
- line=self.raw_string.strip("\r\n")
- ifeditor._codefuncandeditor._indent>=0:
- # if automatic indentation is active, add spaces
- line=editor.deduce_indent(line,buf)
- buf=lineifnotbufelsebuf+"\n%s"%line
- self.editor.update_buffer(buf)
- ifself.editor._echo_mode:
- # need to do it here or we will be off one line
- cline=len(self.editor.get_buffer().split("\n"))
- ifeditor._codefunc:
- # display the current level of identation
- indent=editor._indent
- ifindent<0:
- indent="off"
-
- self.caller.msg("|b%02i|||n (|g%s|n) %s"%(cline,indent,raw(line)))
- else:
- self.caller.msg("|b%02i|||n %s"%(cline,raw(self.args)))
[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
- itself.
-
- """
-
-
[docs]def__init__(
- self,
- caller,
- loadfunc=None,
- savefunc=None,
- quitfunc=None,
- key="",
- persistent=False,
- codefunc=False,
- ):
- """
- Launches a full in-game line editor, mimicking the functionality of VIM.
-
- Args:
- caller (Object): Who is using the editor.
- loadfunc (callable, optional): This will be called as
- `loadfunc(caller)` when the editor is first started. Its
- return will be used as the editor's starting buffer.
- savefunc (callable, optional): This will be called as
- `savefunc(caller, buffer)` when the save-command is given and
- is used to actually determine where/how result is saved.
- It should return `True` if save was successful and also
- handle any feedback to the user.
- quitfunc (callable, optional): This will optionally be
- called as `quitfunc(caller)` when the editor is
- exited. If defined, it should handle all wanted feedback
- to the user.
- quitfunc_args (tuple, optional): Optional tuple of arguments to
- supply to `quitfunc`.
- key (str, optional): An optional key for naming this
- session and make it unique from other editing sessions.
- persistent (bool, optional): Make the editor survive a reboot. Note
- that if this is set, all callables must be possible to pickle
- codefunc (bool, optional): If given, will run the editor in code mode.
- This will be called as `codefunc(caller, buf)`.
-
- Notes:
- In persistent mode, all the input callables (savefunc etc)
- must be possible to be *pickled*, this excludes e.g.
- callables that are class methods or functions defined
- dynamically or as part of another function. In
- non-persistent mode no such restrictions exist.
-
-
-
- """
- self._key=key
- self._caller=caller
- self._caller.ndb._eveditor=self
- self._buffer=""
- self._unsaved=False
- self._persistent=persistent
- self._indent=0
-
- ifloadfunc:
- self._loadfunc=loadfunc
- else:
- self._loadfunc=lambdacaller:self._buffer
- self.load_buffer()
- ifsavefunc:
- self._savefunc=savefunc
- else:
- self._savefunc=lambdacaller,buffer:caller.msg(_ERROR_NO_SAVEFUNC)
- ifquitfunc:
- self._quitfunc=quitfunc
- else:
- self._quitfunc=lambdacaller:caller.msg(_DEFAULT_NO_QUITFUNC)
- self._codefunc=codefunc
-
- # store the original version
- self._pristine_buffer=self._buffer
- self._sep="-"
-
- # undo operation buffer
- self._undo_buffer=[self._buffer]
- self._undo_pos=0
- self._undo_max=20
-
- # copy buffer
- self._copy_buffer=[]
-
- ifpersistent:
- # save in tuple {kwargs, other options}
- try:
- caller.attributes.add(
- "_eveditor_saved",
- (
- dict(
- loadfunc=loadfunc,
- savefunc=savefunc,
- quitfunc=quitfunc,
- codefunc=codefunc,
- key=key,
- persistent=persistent,
- ),
- dict(_pristine_buffer=self._pristine_buffer,_sep=self._sep),
- ),
- )
- caller.attributes.add("_eveditor_buffer_temp",(self._buffer,self._undo_buffer))
- caller.attributes.add("_eveditor_unsaved",False)
- caller.attributes.add("_eveditor_indent",0)
- exceptExceptionaserr:
- caller.msg(_ERROR_PERSISTENT_SAVING.format(error=err))
- logger.log_trace(_TRACE_PERSISTENT_SAVING)
- persistent=False
-
- # Create the commands we need
- caller.cmdset.add(EvEditorCmdSet,persistent=persistent)
-
- # echo inserted text back to caller
- self._echo_mode=True
-
- # show the buffer ui
- self.display_buffer()
-
-
[docs]defload_buffer(self):
- """
- Load the buffer using the load function hook.
-
- """
- try:
- self._buffer=self._loadfunc(self._caller)
- ifnotisinstance(self._buffer,str):
- self._caller.msg(f"|rBuffer is of type |w{type(self._buffer)})|r. "
- "Continuing, it is converted to a string "
- "(and will be saved as such)!|n")
- self._buffer=to_str(self._buffer)
- exceptExceptionase:
- fromevennia.utilsimportlogger
-
- logger.log_trace()
- self._caller.msg(_ERROR_LOADFUNC.format(error=e))
-
-
[docs]defget_buffer(self):
- """
- Return:
- buffer (str): The current buffer.
-
- """
- returnself._buffer
-
-
[docs]defupdate_buffer(self,buf):
- """
- This should be called when the buffer has been changed
- somehow. It will handle unsaved flag and undo updating.
-
- Args:
- buf (str): The text to update the buffer with.
-
- """
- ifis_iter(buf):
- buf="\n".join(buf)
-
- ifbuf!=self._buffer:
- self._buffer=buf
- self.update_undo()
- self._unsaved=True
- ifself._persistent:
- self._caller.attributes.add(
- "_eveditor_buffer_temp",(self._buffer,self._undo_buffer)
- )
- self._caller.attributes.add("_eveditor_unsaved",True)
- self._caller.attributes.add("_eveditor_indent",self._indent)
[docs]defsave_buffer(self):
- """
- Saves the content of the buffer.
-
- """
- ifself._unsavedorself._codefunc:
- # always save code - this allows us to tie execution to
- # saving if we want.
- try:
- ifself._savefunc(self._caller,self._buffer):
- # Save codes should return a true value to indicate
- # save worked. The saving function is responsible for
- # any status messages.
- self._unsaved=False
- exceptExceptionase:
- self._caller.msg(_ERROR_SAVEFUNC.format(error=e))
- else:
- self._caller.msg(_MSG_SAVE_NO_CHANGE)
-
-
[docs]defupdate_undo(self,step=None):
- """
- This updates the undo position.
-
- Args:
- step (int, optional): The amount of steps
- to progress the undo position to. This
- may be a negative value for undo and
- a positive value for redo.
-
- """
- ifstepandstep<0:
- # undo
- ifself._undo_pos<=0:
- self._caller.msg(_MSG_NO_UNDO)
- else:
- self._undo_pos=max(0,self._undo_pos+step)
- self._buffer=self._undo_buffer[self._undo_pos]
- self._caller.msg(_MSG_UNDO)
- elifstepandstep>0:
- # redo
- ifself._undo_pos>=len(self._undo_buffer)-1orself._undo_pos+1>=self._undo_max:
- self._caller.msg(_MSG_NO_REDO)
- else:
- self._undo_pos=min(
- self._undo_pos+step,min(len(self._undo_buffer),self._undo_max)-1
- )
- self._buffer=self._undo_buffer[self._undo_pos]
- self._caller.msg(_MSG_REDO)
- ifnotself._undo_bufferor(
- self._undo_bufferandself._buffer!=self._undo_buffer[self._undo_pos]
- ):
- # save undo state
- self._undo_buffer=self._undo_buffer[:self._undo_pos+1]+[self._buffer]
- self._undo_pos=len(self._undo_buffer)-1
-
-
[docs]defdisplay_buffer(self,buf=None,offset=0,linenums=True,options={"raw":False}):
- """
- This displays the line editor buffer, or selected parts of it.
-
- Args:
- buf (str, optional): The buffer or part of buffer to display.
- offset (int, optional): If `buf` is set and is not the full buffer,
- `offset` should define the actual starting line number, to
- get the linenum display right.
- linenums (bool, optional): Show line numbers in buffer.
- options: raw (bool, optional): Tell protocol to not parse
- formatting information.
-
- """
- ifbufisNone:
- buf=self._buffer
- ifis_iter(buf):
- buf="\n".join(buf)
-
- lines=buf.split("\n")
- nlines=len(lines)
- nwords=len(buf.split())
- nchars=len(buf)
-
- sep=self._sep
- header=(
- "|n"
- +sep*10
- +_("Line Editor [{name}]").format(name=self._key)
- +sep*(_DEFAULT_WIDTH-24-len(self._key))
- )
- footer=(
- "|n"
- +sep*10
- +"[l:%02i w:%03i c:%04i]"%(nlines,nwords,nchars)
- +sep*12
- +_("(:h for help)")
- +sep*(_DEFAULT_WIDTH-54)
- )
- iflinenums:
- main="\n".join(
- "|b%02i|||n %s"%(iline+1+offset,raw(line))
- foriline,lineinenumerate(lines)
- )
- else:
- main="\n".join([raw(line)forlineinlines])
- string="%s\n%s\n%s"%(header,main,footer)
- self._caller.msg(string,options=options)
-
-
[docs]defdisplay_help(self):
- """
- Shows the help entry for the editor.
-
- """
- string=self._sep*_DEFAULT_WIDTH+_HELP_TEXT
- ifself._codefunc:
- string+=_HELP_CODE
- string+=_HELP_LEGEND+self._sep*_DEFAULT_WIDTH
- self._caller.msg(string)
-
-
[docs]defdeduce_indent(self,line,buffer):
- """
- Try to deduce the level of indentation of the given line.
-
- """
- keywords={
- "elif ":["if "],
- "else:":["if ","try"],
- "except":["try:"],
- "finally:":["try:"],
- }
- opening_tags=("if ","try:","for ","while ")
-
- # If the line begins by one of the given keywords
- indent=self._indent
- ifany(line.startswith(kw)forkwinkeywords.keys()):
- # Get the keyword and matching begin tags
- keyword=[kwforkwinkeywordsifline.startswith(kw)][0]
- begin_tags=keywords[keyword]
- forolineinreversed(buffer.splitlines()):
- ifany(oline.lstrip(" ").startswith(tag)fortaginbegin_tags):
- # This line begins with a begin tag, takes the identation
- indent=(len(oline)-len(oline.lstrip(" ")))/4
- break
-
- self._indent=indent+1
- ifself._persistent:
- self._caller.attributes.add("_eveditor_indent",self._indent)
- elifany(line.startswith(kw)forkwinopening_tags):
- self._indent=indent+1
- ifself._persistent:
- self._caller.attributes.add("_eveditor_indent",self._indent)
-
- line=" "*4*indent+line
- returnline
-# coding=utf-8
-"""
-EvForm - a way to create advanced ASCII forms
-
-This is intended for creating advanced ASCII game forms, such as a
-large pretty character sheet or info document.
-
-The system works on the basis of a readin template that is given in a
-separate Python file imported into the handler. This file contains
-some optional settings and a string mapping out the form. The template
-has markers in it to denounce fields to fill. The markers map the
-absolute size of the field and will be filled with an `evtable.EvCell`
-object when displaying the form.
-
-Example of input file `testform.py`:
-
-```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 |
-| | |
--------------------------------------------------
-'''
-```
-
-The first line of the `FORM` string is ignored. The forms and table
-markers must mark out complete, unbroken rectangles, each containing
-one embedded single-character identifier (so the smallest element
-possible is a 3-character wide form). The identifier can be any
-character except for the `FORM_CHAR` and `TABLE_CHAR` and some of the
-common ASCII-art elements, like space, `_` `|` `*` etc (see
-`INVALID_FORMCHARS` in this module). Form Rectangles can have any size,
-but must be separated from each other by at least one other
-character's width.
-
-
-Use as follows:
-
-```python
- from evennia import EvForm, EvTable
-
- # create a new form from the template
- form = EvForm("path/to/testform.py")
-
- (MudForm can also take a dictionary holding
- the required keys FORMCHAR, TABLECHAR and FORM)
-
- # add data to each tagged form cell
- form.map(cells={1: "Tom the Bouncer",
- 2: "Griatch",
- 3: "A sturdy fellow",
- 4: 12,
- 5: 10,
- 6: 5,
- 7: 18,
- 8: 10,
- 9: 3})
- # create the EvTables
- tableA = EvTable("HP","MV","MP",
- table=[["**"], ["*****"], ["***"]],
- border="incols")
- tableB = EvTable("Skill", "Value", "Exp",
- table=[["Shooting", "Herbalism", "Smithing"],
- [12,14,9],["550/1200", "990/1400", "205/900"]],
- border="incols")
- # add the tables to the proper ids in the form
- form.map(tables={"A": tableA,
- "B": tableB})
-
- print(form)
-```
-
-This produces the following result:
-
-::
-
- .------------------------------------------------.
- | |
- | Name: Tom the Player: Griatch |
- | Bouncer |
- | |
- >----------------------------------------------<
- | |
- | Desc: A sturdy STR: 12 DEX: 10 |
- | fellow INT: 5 STA: 18 |
- | LUC: 10 MAG: 3 |
- | |
- >----------------------------------------------<
- | | |
- | HP|MV|MP | Skill |Value |Exp |
- | ~~+~~+~~ | ~~~~~~~~~~~+~~~~~~~~~~~+~~~~~~~~~~~ |
- | **|**|** | Shooting |12 |550/1200 |
- | |**|* | Herbalism |14 |990/1400 |
- | |* | | Smithing |9 |205/900 |
- | | |
- ------------------------------------------------
-
-The marked forms have been replaced with EvCells of text and with
-EvTables. The form can be updated by simply re-applying `form.map()`
-with the updated data.
-
-When working with the template ASCII file, you can use `form.reload()`
-to re-read the template and re-apply all existing mappings.
-
-Each component is restrained to the width and height specified by the
-template, so it will resize to fit (or crop text if the area is too
-small for it). If you try to fit a table into an area it cannot fit
-into (when including its borders and at least one line of text), the
-form will raise an error.
-
-----
-
-"""
-
-importre
-importcopy
-fromevennia.utils.evtableimportEvCell,EvTable
-fromevennia.utils.utilsimportall_from_module,to_str,is_iter
-fromevennia.utils.ansiimportANSIString
-
-# non-valid form-identifying characters (which can thus be
-# used as separators between forms without being detected
-# as an identifier). These should be listed in regex form.
-
-INVALID_FORMCHARS=r"\s\/\|\\\*\_\-\#\<\>\~\^\:\;\.\,"
-# if there is an ansi-escape (||) we have to replace this with ||| to make sure
-# to properly escape down the line
-_ANSI_ESCAPE=re.compile(r"\|\|")
-
-
-def_to_rect(lines):
- """
- Forces all lines to be as long as the longest
-
- Args:
- lines (list): list of `ANSIString`s
-
- Returns:
- (list): list of `ANSIString`s of
- same length as the longest input line
-
- """
- maxl=max(len(line)forlineinlines)
- return[line+" "*(maxl-len(line))forlineinlines]
-
-
-def_to_ansi(obj,regexable=False):
- "convert to ANSIString"
- ifisinstance(obj,ANSIString):
- returnobj
- elifisinstance(obj,str):
- # since ansi will be parsed twice (here and in the normal ansi send), we have to
- # escape the |-structure twice. TODO: This is tied to the default color-tag syntax
- # which is not ideal for those wanting to replace/extend it ...
- obj=_ANSI_ESCAPE.sub(r"||||",obj)
- ifisinstance(obj,dict):
- returndict((key,_to_ansi(value,regexable=regexable))forkey,valueinobj.items())
- elifis_iter(obj):
- return[_to_ansi(o)foroinobj]
- else:
- returnANSIString(obj,regexable=regexable)
-
-
-
[docs]classEvForm:
- """
- This object is instantiated with a text file and parses
- it for rectangular form fields. It can then be fed a
- mapping so as to populate the fields with fixed-width
- EvCell or Tables.
-
- """
-
-
[docs]def__init__(self,filename=None,cells=None,tables=None,form=None,**kwargs):
- """
- Initiate the form
-
- Keyword Args:
- filename (str): Path to template file.
- 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).
-
- """
- self.filename=filename
- self.input_form_dict=form
-
- self.cells_mapping=(
- dict((to_str(key),value)forkey,valueincells.items())ifcellselse{}
- )
- self.tables_mapping=(
- dict((to_str(key),value)forkey,valueintables.items())iftableselse{}
- )
-
- self.cellchar="x"
- self.tablechar="c"
-
- self.raw_form=[]
- self.form=[]
-
- # clean kwargs (these cannot be overridden)
- kwargs.pop("enforce_size",None)
- kwargs.pop("width",None)
- kwargs.pop("height",None)
- # table/cell options
- self.options=kwargs
-
- self.reload()
-
- def_parse_rectangles(self,cellchar,tablechar,form,**kwargs):
- """
- Parse a form for rectangular formfields identified by formchar
- enclosing an identifier.
-
- """
-
- # update options given at creation with new input - this
- # allows e.g. self.map() to add custom settings for individual
- # cells/tables
- custom_options=copy.copy(self.options)
- custom_options.update(kwargs)
-
- nform=len(form)
-
- mapping={}
- cell_coords={}
- table_coords={}
-
- # Locate the identifier tags and the horizontal end coords for all forms
- re_cellchar=re.compile(
- r"%s+([^%s%s]+)%s+"%(cellchar,INVALID_FORMCHARS,cellchar,cellchar)
- )
- re_tablechar=re.compile(
- r"%s+([^%s%s|+])%s+"%(tablechar,INVALID_FORMCHARS,tablechar,tablechar)
- )
- foriy,lineinenumerate(_to_ansi(form,regexable=True)):
- # find cells
- ix0=0
- whileTrue:
- match=re_cellchar.search(line,ix0)
- ifmatch:
- # get the width of the rectangle directly from the match
- cell_coords[match.group(1)]=[iy,match.start(),match.end()]
- ix0=match.end()
- else:
- break
- # find tables
- ix0=0
- whileTrue:
- match=re_tablechar.search(line,ix0)
- ifmatch:
- # get the width of the rectangle directly from the match
- table_coords[match.group(1)]=[iy,match.start(),match.end()]
- ix0=match.end()
- else:
- break
-
- # get rectangles and assign EvCells
- forkey,(iy,leftix,rightix)incell_coords.items():
- # scan up to find top of rectangle
- dy_up=0
- ifiy>0:
- foriinrange(1,iy):
- ifall(form[iy-i][ix]==cellcharforixinrange(leftix,rightix)):
- dy_up+=1
- else:
- break
- # find bottom edge of rectangle
- dy_down=0
- ifiy<nform-1:
- foriinrange(1,nform-iy-1):
- ifall(form[iy+i][ix]==cellcharforixinrange(leftix,rightix)):
- dy_down+=1
- else:
- break
-
- # we have our rectangle. Calculate size of EvCell.
- iyup=iy-dy_up
- iydown=iy+dy_down
- width=rightix-leftix
- height=abs(iyup-iydown)+1
-
- # we have all the coordinates we need. Create EvCell.
- data=self.cells_mapping.get(key,"")
- # if key == "1":
-
- options={
- "pad_left":0,
- "pad_right":0,
- "pad_top":0,
- "pad_bottom":0,
- "align":"l",
- "valign":"t",
- "enforce_size":True,
- }
- options.update(custom_options)
- # if key=="4":
-
- mapping[key]=(
- iyup,
- leftix,
- width,
- height,
- EvCell(data,width=width,height=height,**options),
- )
-
- # get rectangles and assign Tables
- forkey,(iy,leftix,rightix)intable_coords.items():
-
- # scan up to find top of rectangle
- dy_up=0
- ifiy>0:
- foriinrange(1,iy):
- ifall(form[iy-i][ix]==tablecharforixinrange(leftix,rightix)):
- dy_up+=1
- else:
- break
- # find bottom edge of rectangle
- dy_down=0
- ifiy<nform-1:
- foriinrange(1,nform-iy-1):
- ifall(form[iy+i][ix]==tablecharforixinrange(leftix,rightix)):
- dy_down+=1
- else:
- break
-
- # we have our rectangle. Calculate size of Table.
- iyup=iy-dy_up
- iydown=iy+dy_down
- width=rightix-leftix
- height=abs(iyup-iydown)+1
-
- # we have all the coordinates we need. Create Table.
- table=self.tables_mapping.get(key,None)
-
- options={
- "pad_left":0,
- "pad_right":0,
- "pad_top":0,
- "pad_bottom":0,
- "align":"l",
- "valign":"t",
- "enforce_size":True,
- }
- options.update(custom_options)
-
- iftable:
- table.reformat(width=width,height=height,**options)
- else:
- table=EvTable(width=width,height=height,**options)
- mapping[key]=(iyup,leftix,width,height,table)
-
- returnmapping
-
- def_populate_form(self,raw_form,mapping):
- """
- Insert cell contents into form at given locations
-
- """
- form=copy.copy(raw_form)
- forkey,(iy0,ix0,width,height,cell_or_table)inmapping.items():
- # rect is a list of <height> lines, each <width> wide
- rect=cell_or_table.get()
- foril,rectlineinenumerate(rect):
- formline=form[iy0+il]
- # insert new content, replacing old
- form[iy0+il]=formline[:ix0]+rectline+formline[ix0+width:]
- returnform
-
-
[docs]defmap(self,cells=None,tables=None,**kwargs):
- """
- Add mapping for form.
-
- Args:
- cells (dict): A dictionary of {identifier:celltext}
- tables (dict): A dictionary of {identifier:table}
-
- Notes:
- kwargs will be forwarded to tables/cells. See
- `evtable.EvCell` and `evtable.EvTable` for info.
-
- """
- # clean kwargs (these cannot be overridden)
- kwargs.pop("enforce_size",None)
- kwargs.pop("width",None)
- kwargs.pop("height",None)
-
- new_cells=dict((to_str(key),value)forkey,valueincells.items())ifcellselse{}
- new_tables=dict((to_str(key),value)forkey,valueintables.items())iftableselse{}
-
- self.cells_mapping.update(new_cells)
- self.tables_mapping.update(new_tables)
- self.reload()
-
-
[docs]defreload(self,filename=None,form=None,**kwargs):
- """
- Creates the form from a stored file name.
-
- Args:
- filename (str): The file to read from.
- form (dict): A mapping for the form.
-
- Notes:
- Kwargs are passed through to Cel creation.
-
- """
- # clean kwargs (these cannot be overridden)
- kwargs.pop("enforce_size",None)
- kwargs.pop("width",None)
- kwargs.pop("height",None)
-
- ifformorself.input_form_dict:
- datadict=formifformelseself.input_form_dict
- self.input_form_dict=datadict
- eliffilenameorself.filename:
- filename=filenameiffilenameelseself.filename
- datadict=all_from_module(filename)
- self.filename=filename
- else:
- datadict={}
-
- cellchar=to_str(datadict.get("FORMCHAR","x"))
- self.cellchar=to_str(cellchar[0]iflen(cellchar)>1elsecellchar)
- tablechar=datadict.get("TABLECHAR","c")
- self.tablechar=tablechar[0]iflen(tablechar)>1elsetablechar
-
- # split into a list of list of lines. Form can be indexed with form[iy][ix]
- raw_form=_to_ansi(datadict.get("FORM","").split("\n"))
- self.raw_form=_to_rect(raw_form)
-
- # strip first line
- self.raw_form=self.raw_form[1:]ifself.raw_formelseself.raw_form
-
- self.options.update(kwargs)
-
- # parse and replace
- self.mapping=self._parse_rectangles(
- self.cellchar,self.tablechar,self.raw_form,**kwargs
- )
- self.form=self._populate_form(self.raw_form,self.mapping)
-
- def__str__(self):
- "Prints the form"
- returnstr(ANSIString("\n").join([lineforlineinself.form]))
-"""
-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
-
- EvMenu(caller, menu_module_path,
- 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
-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.
-
-The `persistent` keyword will make the menu survive a server reboot.
-It is `False` by default. Note that if using persistent mode, every
-node and callback in the menu must be possible to be *pickled*, this
-excludes e.g. callables that are class methods or functions defined
-dynamically or as part of another function. In non-persistent mode
-no such restrictions exist.
-
-The menu is defined in a module (this can be the same module as the
-command definition too) with function definitions:
-
-```python
-
- def node1(caller):
- # (this is the start node if called like above)
- # code
- return text, options
-
- def node_with_other_name(caller, input_string):
- # code
- return text, options
-
- def another_node(caller, input_string, **kwargs):
- # code
- return text, options
-```
-
-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
-both one or two arguments interchangeably. It also accepts nodes
-that takes `**kwargs`.
-
-The menu tree itself is available on the caller as
-`caller.ndb._evmenu`. This makes it a convenient place to store
-temporary state variables between nodes, since this NAttribute is
-deleted when the menu is exited.
-
-The return values must be given in the above order, but each can be
-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.
-- `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:
-
- - `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.
- - `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.
- - `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 the empty string (only), the current node is re-run.
-
-If `key` is not given, the option will automatically be identified by
-its number 1..N.
-
-Example:
-
-```python
-
- # in menu_module.py
-
- def node1(caller):
- text = ("This is a node text",
- "This is help text for this node")
- options = ({"key": "testing",
- "desc": "Select this to go to node 2",
- "goto": ("node2", {"foo": "bar"}),
- "exec": "callback1"},
- {"desc": "Go to node 3.",
- "goto": "node3"})
- return text, options
-
- def callback1(caller):
- # this is called when choosing the "testing" option in node1
- # (before going to node2). If it returned a string, say 'node3',
- # then the next node would be node3 instead of node2 as specified
- # by the normal 'goto' option key above.
- caller.msg("Callback called!")
-
- def node2(caller, **kwargs):
- text = '''
- This is node 2. It only allows you to go back
- to the original node1. This extra indent will
- be stripped. We don't include a help text but
- here are the variables passed to us: {}
- '''.format(kwargs)
- options = {"goto": "node1"}
- return text, options
-
- def node3(caller):
- 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
- ______________________________________
-
- testing: Select this to go to node 2
- 2: Go to node 3
-
-Where you can both enter "testing" and "1" to select the first option.
-If the client supports MXP, they may also mouse-click on "testing" to
-do the same. When making this selection, a function "callback1" in the
-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`.
-
-
-## Menu generation from template string
-
-In evmenu.py is a helper function `parse_menu_template` that parses a
-template-string and outputs a menu-tree dictionary suitable to pass into
-EvMenu:
-::
-
- menutree = evmenu.parse_menu_template(caller, menu_template, goto_callables)
- EvMenu(caller, menutree)
-
-For maximum flexibility you can inject normally-created nodes in the menu tree
-before passing it to EvMenu. If that's not needed, you can also create a menu
-in one step with:
-
-```python
-
- evmenu.template2menu(caller, menu_template, goto_callables)
-
-```
-
-The `goto_callables` is a mapping `{"funcname": callable, ...}`, where each
-callable must be a module-global function on the form
-`funcname(caller, raw_string, **kwargs)` (like any goto-callable). The
-`menu_template` is a multi-line string on the following form:
-::
-
- ## node start
-
- This is the text of the start node.
- The text area can have multiple lines, line breaks etc.
-
- Each option below is one of these forms
- key: desc -> gotostr_or_func
- key: gotostr_or_func
- >: gotostr_or_func
- > glob/regex: gotostr_or_func
-
- ## options
-
- # comments are only allowed from beginning of line.
- # Indenting is not necessary, but good for readability
-
- 1: Option number 1 -> node1
- 2: Option number 2 -> node2
- next: This steps next -> go_back()
- # the -> can be ignored if there is no desc
- back: go_back(from_node=start)
- abort: abort
-
- ## node node1
-
- Text for Node1. Enter a message!
- <return> to go back.
-
- ## options
-
- # Starting the option-line with >
- # allows to perform different actions depending on
- # what is inserted.
-
- # this catches everything starting with foo
- > foo*: handle_foo_message()
-
- # regex are also allowed (this catches number inputs)
- > [0-9]+?: handle_numbers()
-
- # this catches the empty return
- >: start
-
- # this catches everything else
- > *: handle_message(from_node=node1)
-
- ## node node2
-
- Text for Node2. Just go back.
-
- ## options
-
- >: start
-
- ## node abort
-
- This exits the menu since there is no `## options` section.
-
-Each menu node is defined by a `# node <name>` containing the text of the node,
-followed by `## options` Also `## NODE` and `## OPTIONS` work. No python code
-logics is allowed in the template, this code is not evaluated but parsed. More
-advanced dynamic usage requires a full node-function (which can be added to the
-generated dict, as said).
-
-Adding `(..)` to a goto treats it as a callable and it must then be included in
-the `goto_callable` mapping. Only named keywords (or no args at all) are
-allowed, these will be added to the `**kwargs` going into the callable. Quoting
-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.
-
-----
-
-"""
-
-importre
-importinspect
-
-fromastimportliteral_eval
-fromfnmatchimportfnmatch
-
-frominspectimportisfunction,getargspec
-fromdjango.confimportsettings
-fromevenniaimportCommand,CmdSet
-fromevennia.utilsimportlogger
-fromevennia.utils.evtableimportEvTable
-fromevennia.utils.ansiimportstrip_ansi
-fromevennia.utils.utilsimportmod_import,make_iter,pad,to_str,m_len,is_iter,dedent,crop
-fromevennia.commandsimportcmdhandler
-
-# i18n
-fromdjango.utils.translationimportgettextas_
-
-# read from protocol NAWS later?
-_MAX_TEXT_WIDTH=settings.CLIENT_DEFAULT_WIDTH
-
-# we use cmdhandler instead of evennia.syscmdkeys to
-# avoid some cases of loading before evennia init'd
-_CMD_NOMATCH=cmdhandler.CMD_NOMATCH
-_CMD_NOINPUT=cmdhandler.CMD_NOINPUT
-
-# Return messages
-
-
-_ERR_NOT_IMPLEMENTED=_(
- "Menu node '{nodename}' is either not implemented or caused an error. "
- "Make another choice or try 'q' to abort."
-)
-_ERR_GENERAL=_("Error in menu node '{nodename}'.")
-_ERR_NO_OPTION_DESC=_("No description.")
-_HELP_FULL=_("Commands: <menu option>, help, quit")
-_HELP_NO_QUIT=_("Commands: <menu option>, help")
-_HELP_NO_OPTIONS=_("Commands: help, quit")
-_HELP_NO_OPTIONS_NO_QUIT=_("Commands: help")
-_HELP_NO_OPTION_MATCH=_("Choose an option or try 'help'.")
-
-_ERROR_PERSISTENT_SAVING="""
-{error}
-
-|rThe menu state could not be saved for persistent mode. Switching
-to non-persistent mode (which means the menu session won't survive
-an eventual server reload).|n
-"""
-
-_TRACE_PERSISTENT_SAVING=(
- "EvMenu 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."
-)
-
-
-
[docs]classEvMenuError(RuntimeError):
- """
- Error raised by menu when facing internal errors.
-
- """
-
- pass
-
-
-
[docs]classEvMenuGotoAbortMessage(RuntimeError):
- """
- This can be raised by a goto-callable to abort the goto flow. The message
- stored with the executable will be sent to the caller who will remain on
- the current node. This can be used to pass single-line returns without
- re-running the entire node with text and options.
-
- Example:
- raise EvMenuGotoMessage("That makes no sense.")
-
- """
-
-
-# -------------------------------------------------------------
-#
-# Menu command and command set
-#
-# -------------------------------------------------------------
-
-
-
[docs]defget_help(self):
- return"Menu commands are explained within the menu."
-
-
[docs]deffunc(self):
- """
- Implement all menu commands.
- """
-
- def_restore(caller):
- # check if there is a saved menu available.
- # this will re-start a completely new evmenu call.
- saved_options=caller.attributes.get("_menutree_saved")
- ifsaved_options:
- startnode_tuple=caller.attributes.get("_menutree_saved_startnode")
- try:
- startnode,startnode_input=startnode_tuple
- exceptValueError:# old form of startnode store
- startnode,startnode_input=startnode_tuple,""
- ifstartnode:
- saved_options[2]["startnode"]=startnode
- saved_options[2]["startnode_input"]=startnode_input
- MenuClass=saved_options[0]
- # this will create a completely new menu call
- MenuClass(caller,*saved_options[1],**saved_options[2])
- returnTrue
- returnNone
-
- caller=self.caller
- # we store Session on the menu since this can be hard to
- # get in multisession environments if caller is an Account.
- menu=caller.ndb._evmenu
- ifnotmenu:
- if_restore(caller):
- return
- orig_caller=caller
- caller=caller.accountifhasattr(caller,"account")elseNone
- menu=caller.ndb._evmenuifcallerelseNone
- ifnotmenu:
- ifcallerand_restore(caller):
- return
- caller=self.session
- menu=caller.ndb._evmenu
- ifnotmenu:
- # can't restore from a session
- err="Menu object not found as %s.ndb._evmenu!"%orig_caller
- orig_caller.msg(
- err
- )# don't give the session as a kwarg here, direct to original
- raiseEvMenuError(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).
- caller.ndb._evmenu._session=self.session
- # we have a menu, use it.
- menu.parse_input(self.raw_string)
-
-
-
[docs]classEvMenuCmdSet(CmdSet):
- """
- The Menu cmdset replaces the current cmdset.
-
- """
-
- key="menu_cmdset"
- priority=1
- mergetype="Replace"
- no_objs=True
- no_exits=True
- no_channels=False
-
-
[docs]defat_cmdset_creation(self):
- """
- Called when creating the set.
- """
- self.add(CmdEvMenuNode())
-
-
-# ------------------------------------------------------------
-#
-# Menu main class
-#
-# -------------------------------------------------------------
-
-
-
[docs]classEvMenu:
- """
- This object represents an operational menu. It is initialized from
- a menufile.py instruction.
-
- """
-
- # convenient helpers for easy overloading
- node_border_char="_"
-
-
[docs]def__init__(
- self,
- caller,
- menudata,
- startnode="start",
- cmdset_mergetype="Replace",
- cmdset_priority=1,
- auto_quit=True,
- auto_look=True,
- auto_help=True,
- cmd_on_exit="look",
- persistent=False,
- startnode_input="",
- session=None,
- debug=False,
- **kwargs,
- ):
- """
- Initialize the menu tree and start the caller onto the first node.
-
- Args:
- caller (Object, Account or Session): The user of the menu.
- menudata (str, module or dict): The full or relative path to the module
- holding the menu tree data. All global functions in this module
- whose name doesn't start with '_ ' will be parsed as menu nodes.
- Also the module itself is accepted as input. Finally, a dictionary
- menu tree can be given directly. This must then be a mapping
- `{"nodekey":callable,...}` where `callable` must be called as
- and return the data expected of a menu node. This allows for
- dynamic menu creation.
- startnode (str, optional): The starting node name in the menufile.
- cmdset_mergetype (str, optional): 'Replace' (default) means the menu
- commands will be exclusive - no other normal commands will
- be usable while the user is in the menu. 'Union' means the
- menu commands will be integrated with the existing commands
- (it will merge with `merge_priority`), if so, make sure that
- the menu's command names don't collide with existing commands
- in an unexpected way. Also the CMD_NOMATCH and CMD_NOINPUT will
- be overloaded by the menu cmdset. Other cmdser mergetypes
- has little purpose for the menu.
- cmdset_priority (int, optional): The merge priority for the
- menu command set. The default (1) is usually enough for most
- types of menus.
- auto_quit (bool, optional): Allow user to use "q", "quit" or
- "exit" to leave the menu at any point. Recommended during
- development!
- auto_look (bool, optional): Automatically make "looK" or "l" to
- re-show the last node. Turning this off means you have to handle
- re-showing nodes yourself, but may be useful if you need to
- use "l" for some other purpose.
- auto_help (bool, optional): Automatically make "help" or "h" show
- the current help entry for the node. If turned off, eventual
- help must be handled manually, but it may be useful if you
- need 'h' for some other purpose, for example.
- cmd_on_exit (callable, str or None, optional): When exiting the menu
- (either by reaching a node with no options or by using the
- in-built quit command (activated with `allow_quit`), this
- callback function or command string will be executed.
- The callback function takes two parameters, the caller then the
- EvMenu object. This is called after cleanup is complete.
- Set to None to not call any command.
- persistent (bool, optional): Make the Menu persistent (i.e. it will
- survive a reload. This will make the Menu cmdset persistent. Use
- with caution - if your menu is buggy you may end up in a state
- you can't get out of! Also note that persistent mode requires
- that all formatters, menu nodes and callables are possible to
- *pickle*. When the server is reloaded, the latest node shown will be completely
- re-run with the same input arguments - so be careful if you are counting
- up some persistent counter or similar - the counter may be run twice if
- reload happens on the node that does that. Note that if `debug` is True,
- this setting is ignored and assumed to be False.
- startnode_input (str or (str, dict), optional): Send an input text to `startnode` as if
- a user input text from a fictional previous node. If including the dict, this will
- be passed as **kwargs to that node. When the server reloads,
- the latest visited node will be re-run as `node(caller, raw_string, **kwargs)`.
- session (Session, optional): This is useful when calling EvMenu from an account
- in multisession mode > 2. Note that this session only really relevant
- for the very first display of the first node - after that, EvMenu itself
- will keep the session updated from the command input. So a persistent
- menu will *not* be using this same session anymore after a reload.
- debug (bool, optional): If set, the 'menudebug' command will be made available
- 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.
- **kwargs: All kwargs will become initialization variables on `caller.ndb._menutree`,
- to be available at run.
-
- Raises:
- EvMenuError: If the start/end node is not found in menu tree.
-
- Notes:
- While running, the menu is stored on the caller as `caller.ndb._evmenu`. Also
- the current Session (from the Command, so this is still valid in multisession
- environments) is available through `caller.ndb._evmenu._session`. The `_evmenu`
- property is a good one for storing intermediary data on between nodes since it
- will be automatically deleted when the menu closes.
-
- In persistent mode, all nodes, formatters and callbacks in the menu must be
- possible to be *pickled*, this excludes e.g. callables that are class methods
- or functions defined dynamically or as part of another function. In
- non-persistent mode no such restrictions exist.
-
- """
- self._startnode=startnode
- self._menutree=self._parse_menudata(menudata)
- self._persistent=persistentifnotdebugelseFalse
- self._quitting=False
-
- ifstartnodenotinself._menutree:
- raiseEvMenuError("Start node '%s' not in menu tree!"%startnode)
-
- # public variables made available to the command
-
- self.caller=caller
-
- # track EvMenu kwargs
- self.auto_quit=auto_quit
- self.auto_look=auto_look
- self.auto_help=auto_help
- self.debug_mode=debug
- self._session=session
- ifisinstance(cmd_on_exit,str):
- # At this point menu._session will have been replaced by the
- # menu command to the actual session calling.
- self.cmd_on_exit=lambdacaller,menu:caller.execute_cmd(
- cmd_on_exit,session=menu._session
- )
- elifcallable(cmd_on_exit):
- self.cmd_on_exit=cmd_on_exit
- else:
- self.cmd_on_exit=None
- # current menu state
- self.default=None
- self.nodetext=None
- self.helptext=None
- self.options=None
- self.nodename=None
- self.node_kwargs={}
-
- # used for testing
- self.test_options={}
- self.test_nodetext=""
-
- # assign kwargs as initialization vars on ourselves.
- reserved_clash=set(
- (
- "_startnode",
- "_menutree",
- "_session",
- "_persistent",
- "cmd_on_exit",
- "default",
- "nodetext",
- "helptext",
- "options",
- "cmdset_mergetype",
- "auto_quit",
- )
- ).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."
- )
- forkey,valinkwargs.items():
- setattr(self,key,val)
-
- ifself.caller.ndb._evmenu:
- # an evmenu already exists - we try to close it cleanly. Note that this will
- # not fire the previous menu's end node.
- try:
- self.caller.ndb._evmenu.close_menu()
- exceptException:
- pass
-
- # store ourself on the object
- self.caller.ndb._evmenu=self
-
- # DEPRECATED - for backwards-compatibility
- self.caller.ndb._menutree=self
-
- ifpersistent:
- # save the menu to the database
- calldict={
- "startnode":startnode,
- "cmdset_mergetype":cmdset_mergetype,
- "cmdset_priority":cmdset_priority,
- "auto_quit":auto_quit,
- "auto_look":auto_look,
- "auto_help":auto_help,
- "cmd_on_exit":cmd_on_exit,
- "persistent":persistent,
- }
- calldict.update(kwargs)
- try:
- caller.attributes.add("_menutree_saved",(self.__class__,(menudata,),calldict))
- caller.attributes.add("_menutree_saved_startnode",(startnode,startnode_input))
- exceptExceptionaserr:
- self.msg(_ERROR_PERSISTENT_SAVING.format(error=err))
- logger.log_trace(_TRACE_PERSISTENT_SAVING)
- persistent=False
-
- # set up the menu command on the caller
- menu_cmdset=EvMenuCmdSet()
- menu_cmdset.mergetype=str(cmdset_mergetype).lower().capitalize()or"Replace"
- menu_cmdset.priority=int(cmdset_priority)
- self.caller.cmdset.add(menu_cmdset,persistent=persistent)
-
- reserved_startnode_kwargs=set(("nodename","raw_string"))
- startnode_kwargs={}
- ifisinstance(startnode_input,(tuple,list))andlen(startnode_input)>1:
- startnode_input,startnode_kwargs=startnode_input[:2]
- ifnotisinstance(startnode_kwargs,dict):
- raiseEvMenuError("startnode_input must be either a str or a tuple (str, dict).")
- clashing_kwargs=reserved_startnode_kwargs.intersection(set(startnode_kwargs.keys()))
- ifclashing_kwargs:
- raiseRuntimeError(
- f"Evmenu startnode_inputs includes kwargs {tuple(clashing_kwargs)} that "
- "clashes with EvMenu's internal usage."
- )
-
- # start the menu
- self.goto(self._startnode,startnode_input,**startnode_kwargs)
-
- def_parse_menudata(self,menudata):
- """
- Parse a menufile for node functions and store in dictionary
- map. Alternatively, accept a pre-made mapping dictionary of
- node functions.
-
- Args:
- menudata (str, module or dict): The python.path to the menufile,
- or the python module itself. If a dict, this should be a
- mapping nodename:callable, where the callable must match
- the criteria for a menu node.
-
- Returns:
- menutree (dict): A {nodekey: func}
-
- """
- ifisinstance(menudata,dict):
- # This is assumed to be a pre-loaded menu tree.
- returnmenudata
- else:
- # a python path of a module
- module=mod_import(menudata)
- returndict(
- (key,func)
- forkey,funcinmodule.__dict__.items()
- ifisfunction(func)andnotkey.startswith("_")
- )
-
- def_format_node(self,nodetext,optionlist):
- """
- Format the node text + option section
-
- Args:
- nodetext (str): The node text
- optionlist (list): List of (key, desc) pairs.
-
- Returns:
- string (str): The options section, including
- all needed spaces.
-
- Notes:
- This will adjust the columns of the options, first to use
- a maxiumum of 4 rows (expanding in columns), then gradually
- growing to make use of the screen space.
-
- """
-
- # handle the node text
- nodetext=self.nodetext_formatter(nodetext)
-
- # handle the options
- optionstext=self.options_formatter(optionlist)
-
- # format the entire node
- returnself.node_formatter(nodetext,optionstext)
-
- def_safe_call(self,callback,raw_string,**kwargs):
- """
- 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.
-
- """
- try:
- try:
- nargs=len(getargspec(callback).args)
- exceptTypeError:
- raiseEvMenuError("Callable {} doesn't accept any arguments!".format(callback))
- supports_kwargs=bool(getargspec(callback).keywords)
- ifnargs<=0:
- raiseEvMenuError("Callable {} doesn't accept any arguments!".format(callback))
-
- ifsupports_kwargs:
- ifnargs>1:
- ret=callback(self.caller,raw_string,**kwargs)
- # callback accepting raw_string, **kwargs
- else:
- # callback accepting **kwargs
- ret=callback(self.caller,**kwargs)
- elifnargs>1:
- # callback accepting raw_string
- ret=callback(self.caller,raw_string)
- else:
- # normal callback, only the caller as arg
- ret=callback(self.caller)
- exceptEvMenuError:
- errmsg=_ERR_GENERAL.format(nodename=callback)
- self.msg(errmsg)
- logger.log_trace()
- raise
-
- returnret
-
- def_execute_node(self,nodename,raw_string,**kwargs):
- """
- Execute a node.
-
- Args:
- nodename (str): Name of node.
- raw_string (str): The raw default string entered on the
- previous node (only used if the node accepts it as an
- argument)
- kwargs (any, optional): Optional kwargs for the node.
-
- Returns:
- nodetext, options (tuple): The node text (a string or a
- tuple and the options tuple, if any.
-
- """
- try:
- node=self._menutree[nodename]
- exceptKeyError:
- self.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename))
- raiseEvMenuError
- try:
- kwargs["_current_nodename"]=nodename
- ret=self._safe_call(node,raw_string,**kwargs)
- ifisinstance(ret,(tuple,list))andlen(ret)>1:
- nodetext,options=ret[:2]
- else:
- nodetext,options=ret,None
- exceptKeyError:
- self.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename))
- logger.log_trace()
- raiseEvMenuError
- exceptException:
- self.msg(_ERR_GENERAL.format(nodename=nodename))
- logger.log_trace()
- raise
-
- # store options to make them easier to test
- self.test_options=options
- self.test_nodetext=nodetext
-
- returnnodetext,options
-
-
[docs]defmsg(self,txt):
- """
- This is a central point for sending return texts to the caller. It
- allows for a central point to add custom messaging when creating custom
- EvMenu overrides.
-
- Args:
- txt (str): The text to send.
-
- Notes:
- By default this will send to the same session provided to EvMenu
- (if `session` kwarg was provided to `EvMenu.__init__`). It will
- also send it with a `type=menu` for the benefit of OOB/webclient.
-
- """
- self.caller.msg(text=(txt,{"type":"menu"}),session=self._session)
-
-
[docs]defrun_exec(self,nodename,raw_string,**kwargs):
- """
- NOTE: This is deprecated. Use `goto` directly instead.
-
- Run a function or node as a callback (with the 'exec' option key).
-
- Args:
- nodename (callable or str): A callable to run as
- `callable(caller, raw_string)`, or the Name of an existing
- node to run as a callable. This may or may not return
- a string.
- raw_string (str): The raw default string entered on the
- previous node (only used if the node accepts it as an
- argument)
- kwargs (any): These are optional kwargs passed into goto
-
- Returns:
- new_goto (str or None): A replacement goto location string or
- None (no replacement).
- Notes:
- Relying on exec callbacks to set the goto location is
- very powerful but will easily lead to spaghetti structure and
- hard-to-trace paths through the menu logic. So be careful with
- relying on this.
-
- """
- try:
- ifcallable(nodename):
- # this is a direct callable - execute it directly
- ret=self._safe_call(nodename,raw_string,**kwargs)
- ifisinstance(ret,(tuple,list)):
- ifnotlen(ret)>1ornotisinstance(ret[1],dict):
- raiseEvMenuError(
- "exec callable must return either None, str or (str, dict)"
- )
- ret,kwargs=ret[:2]
- else:
- # nodename is a string; lookup as node and run as node in-place (don't goto it)
- # execute the node
- ret=self._execute_node(nodename,raw_string,**kwargs)
- ifisinstance(ret,(tuple,list)):
- ifnotlen(ret)>1andret[1]andnotisinstance(ret[1],dict):
- raiseEvMenuError("exec node must return either None, str or (str, dict)")
- ret,kwargs=ret[:2]
- exceptEvMenuErroraserr:
- errmsg="Error in exec '%s' (input: '%s'): %s"%(nodename,raw_string.rstrip(),err)
- self.msg("|r%s|n"%errmsg)
- logger.log_trace(errmsg)
- return
-
- ifisinstance(ret,str):
- # only return a value if a string (a goto target), ignore all other returns
- ifnotret:
- # an empty string - rerun the same node
- returnself.nodename
- returnret,kwargs
- returnNone
-
-
[docs]defextract_goto_exec(self,nodename,option_dict):
- """
- Helper: Get callables and their eventual kwargs.
-
- Args:
- nodename (str): The current node name (used for error reporting).
- option_dict (dict): The seleted option's dict.
-
- Returns:
- goto (str, callable or None): The goto directive in the option.
- goto_kwargs (dict): Kwargs for `goto` if the former is callable, otherwise empty.
- execute (callable or None): Executable given by the `exec` directive.
- exec_kwargs (dict): Kwargs for `execute` if it's callable, otherwise empty.
-
- """
- goto_kwargs,exec_kwargs={},{}
- goto,execute=option_dict.get("goto",None),option_dict.get("exec",None)
- ifgotoandisinstance(goto,(tuple,list)):
- iflen(goto)>1:
- goto,goto_kwargs=goto[:2]# ignore any extra arguments
- ifnothasattr(goto_kwargs,"__getitem__"):
- # not a dict-like structure
- raiseEvMenuError(
- "EvMenu node {}: goto kwargs is not a dict: {}".format(
- nodename,goto_kwargs
- )
- )
- else:
- goto=goto[0]
- ifexecuteandisinstance(execute,(tuple,list)):
- iflen(execute)>1:
- execute,exec_kwargs=execute[:2]# ignore any extra arguments
- ifnothasattr(exec_kwargs,"__getitem__"):
- # not a dict-like structure
- raiseEvMenuError(
- "EvMenu node {}: exec kwargs is not a dict: {}".format(
- nodename,goto_kwargs
- )
- )
- else:
- execute=execute[0]
- returngoto,goto_kwargs,execute,exec_kwargs
-
-
[docs]defgoto(self,nodename,raw_string,**kwargs):
- """
- Run a node by name, optionally dynamically generating that name first.
-
- Args:
- nodename (str or callable): Name of node or a callable
- to be called as `function(caller, raw_string, **kwargs)` or
- `function(caller, **kwargs)` to return the actual goto string or
- 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.
-
- """
-
- ifcallable(nodename):
- # run the "goto" callable, if possible
- inp_nodename=nodename
- nodename=self._safe_call(nodename,raw_string,**kwargs)
- ifisinstance(nodename,(tuple,list)):
- ifnotlen(nodename)>1ornotisinstance(nodename[1],dict):
- raiseEvMenuError(
- "{}: goto callable must return str or (str, dict)".format(inp_nodename)
- )
- nodename,kwargs=nodename[:2]
- ifnotnodename:
- # no nodename return. Re-run current node
- nodename=self.nodename
- try:
- # execute the found node, make use of the returns.
- nodetext,options=self._execute_node(nodename,raw_string,**kwargs)
- exceptEvMenuError:
- return
-
- ifself._persistent:
- self.caller.attributes.add(
- "_menutree_saved_startnode",(nodename,(raw_string,kwargs))
- )
-
- # validation of the node return values
- helptext=""
- ifis_iter(nodetext):
- iflen(nodetext)>1:
- nodetext,helptext=nodetext[:2]
- else:
- nodetext=nodetext[0]
- nodetext=""ifnodetextisNoneelsestr(nodetext)
- options=[options]ifisinstance(options,dict)elseoptions
-
- # this will be displayed in the given order
- display_options=[]
- # this is used for lookup
- self.options={}
- self.default=None
- ifoptions:
- forinum,dicinenumerate(options):
- # fix up the option dicts
- keys=make_iter(dic.get("key"))
- desc=dic.get("desc",dic.get("text",None))
- if"_default"inkeys:
- keys=[keyforkeyinkeysifkey!="_default"]
- goto,goto_kwargs,execute,exec_kwargs=self.extract_goto_exec(nodename,dic)
- self.default=(goto,goto_kwargs,execute,exec_kwargs)
- else:
- # use the key (only) if set, otherwise use the running number
- keys=list(make_iter(dic.get("key",str(inum+1).strip())))
- goto,goto_kwargs,execute,exec_kwargs=self.extract_goto_exec(nodename,dic)
- ifkeys:
- display_options.append((keys[0],desc))
- forkeyinkeys:
- ifgotoorexecute:
- self.options[strip_ansi(key).strip().lower()]=(
- goto,
- goto_kwargs,
- execute,
- exec_kwargs,
- )
-
- self.nodetext=self._format_node(nodetext,display_options)
- self.node_kwargs=kwargs
- self.nodename=nodename
-
- # handle the helptext
- ifhelptext:
- self.helptext=self.helptext_formatter(helptext)
- elifoptions:
- self.helptext=_HELP_FULLifself.auto_quitelse_HELP_NO_QUIT
- else:
- self.helptext=_HELP_NO_OPTIONSifself.auto_quitelse_HELP_NO_OPTIONS_NO_QUIT
-
- self.display_nodetext()
- ifnotoptions:
- self.close_menu()
-
-
[docs]defrun_exec_then_goto(self,runexec,goto,raw_string,runexec_kwargs=None,goto_kwargs=None):
- """
- Call 'exec' callback and goto (which may also be a callable) in sequence.
-
- Args:
- runexec (callable or str): Callback to run before goto. If
- the callback returns a string, this is used to replace
- the `goto` string/callable before being passed into the goto handler.
- goto (str): The target node to go to next (may be replaced
- by `runexec`)..
- raw_string (str): The original user input.
- runexec_kwargs (dict, optional): Optional kwargs for runexec.
- goto_kwargs (dict, optional): Optional kwargs for goto.
-
- """
- ifrunexec:
- # replace goto only if callback returns
- goto,goto_kwargs=self.run_exec(
- runexec,raw_string,**(runexec_kwargsifrunexec_kwargselse{})
- )or(goto,goto_kwargs)
- ifgoto:
- self.goto(goto,raw_string,**(goto_kwargsifgoto_kwargselse{}))
-
-
[docs]defclose_menu(self):
- """
- Shutdown menu; occurs when reaching the end node or using the quit command.
- """
- ifnotself._quitting:
- # avoid multiple calls from different sources
- self._quitting=True
- self.caller.cmdset.remove(EvMenuCmdSet)
- delself.caller.ndb._evmenu
- ifself._persistent:
- self.caller.attributes.remove("_menutree_saved")
- self.caller.attributes.remove("_menutree_saved_startnode")
- ifself.cmd_on_exitisnotNone:
- self.cmd_on_exit(self.caller,self)
- # special for template-generated menues
- delself.caller.db._evmenu_template_contents
-
-
[docs]defprint_debug_info(self,arg):
- """
- Messages the caller with the current menu state, for debug purposes.
-
- Args:
- arg (str): Arg to debug instruction, either nothing, 'full' or the name
- of a property to inspect.
-
- """
- all_props=inspect.getmembers(self)
- all_methods=[nameforname,_ininspect.getmembers(self,predicate=inspect.ismethod)]
- all_builtins=[nameforname,_ininspect.getmembers(self,predicate=inspect.isbuiltin)]
- props={
- prop:value
- forprop,valueinall_props
- ifpropnotinall_methodsandpropnotinall_builtinsandnotprop.endswith("__")
- }
-
- local={
- key:var
- forkey,varinlocals().items()
- ifkeynotinall_propsandnotkey.endswith("__")
- }
-
- ifarg:
- ifarginprops:
- debugtxt=" |y* {}:|n\n{}".format(arg,props[arg])
- elifarginlocal:
- debugtxt=" |y* {}:|n\n{}".format(arg,local[arg])
- elifarg=="full":
- debugtxt=(
- "|yMENU DEBUG full ... |n\n"
- +"\n".join(
- "|y *|n {}: {}".format(key,val)forkey,valinsorted(props.items())
- )
- +"\n |yLOCAL VARS:|n\n"
- +"\n".join(
- "|y *|n {}: {}".format(key,val)forkey,valinsorted(local.items())
- )
- +"\n |y... END MENU DEBUG|n"
- )
- else:
- debugtxt="|yUsage: menudebug full|<name of property>|n"
- else:
- debugtxt=(
- "|yMENU DEBUG properties ... |n\n"
- +"\n".join(
- "|y *|n {}: {}".format(key,crop(to_str(val,force_string=True),width=50))
- forkey,valinsorted(props.items())
- )
- +"\n |yLOCAL VARS:|n\n"
- +"\n".join(
- "|y *|n {}: {}".format(key,crop(to_str(val,force_string=True),width=50))
- forkey,valinsorted(local.items())
- )
- +"\n |y... END MENU DEBUG|n"
- )
- self.msg(debugtxt)
-
-
[docs]defparse_input(self,raw_string):
- """
- Parses the incoming string from the menu user.
-
- Args:
- raw_string (str): The incoming, unmodified string
- from the user.
- Notes:
- This method is expected to parse input and use the result
- to relay execution to the relevant methods of the menu. It
- should also report errors directly to the user.
-
- """
- cmd=strip_ansi(raw_string.strip().lower())
-
- try:
- ifself.optionsandcmdinself.options:
- # this will take precedence over the default commands
- # below
- goto,goto_kwargs,execfunc,exec_kwargs=self.options[cmd]
- self.run_exec_then_goto(execfunc,goto,raw_string,exec_kwargs,goto_kwargs)
- elifself.auto_lookandcmdin("look","l"):
- self.display_nodetext()
- elifself.auto_helpandcmdin("help","h"):
- self.display_helptext()
- elifself.auto_quitandcmdin("quit","q","exit"):
- self.close_menu()
- elifself.debug_modeandcmd.startswith("menudebug"):
- self.print_debug_info(cmd[9:].strip())
- elifself.default:
- goto,goto_kwargs,execfunc,exec_kwargs=self.default
- self.run_exec_then_goto(execfunc,goto,raw_string,exec_kwargs,goto_kwargs)
- else:
- self.msg(_HELP_NO_OPTION_MATCH)
- exceptEvMenuGotoAbortMessageaserr:
- # custom interrupt from inside a goto callable - print the message and
- # stay on the current node.
- self.msg(str(err))
[docs]defnodetext_formatter(self,nodetext):
- """
- Format the node text itself.
-
- Args:
- nodetext (str): The full node text (the text describing the node).
-
- Returns:
- nodetext (str): The formatted node text.
-
- """
- returndedent(nodetext.strip("\n"),baseline_index=0).rstrip()
-
-
[docs]defhelptext_formatter(self,helptext):
- """
- Format the node's help text
-
- Args:
- helptext (str): The unformatted help text for the node.
-
- Returns:
- helptext (str): The formatted help text.
-
- """
- returndedent(helptext.strip("\n"),baseline_index=0).rstrip()
-
-
[docs]defoptions_formatter(self,optionlist):
- """
- Formats the option block.
-
- Args:
- optionlist (list): List of (key, description) tuples for every
- option related to this node.
- caller (Object, Account or None, optional): The caller of the node.
-
- Returns:
- options (str): The formatted option display.
-
- """
- ifnotoptionlist:
- return""
-
- # column separation distance
- colsep=4
-
- nlist=len(optionlist)
-
- # get the widest option line in the table.
- table_width_max=-1
- table=[]
- forkey,descinoptionlist:
- ifkeyordesc:
- desc_string=": %s"%descifdescelse""
- table_width_max=max(
- table_width_max,
- max(m_len(p)forpinkey.split("\n"))
- +max(m_len(p)forpindesc_string.split("\n"))
- +colsep,
- )
- raw_key=strip_ansi(key)
- ifraw_key!=key:
- # already decorations in key definition
- table.append(" |lc%s|lt%s|le%s"%(raw_key,key,desc_string))
- else:
- # add a default white color to key
- table.append(" |lc%s|lt|w%s|n|le%s"%(raw_key,raw_key,desc_string))
- ncols=_MAX_TEXT_WIDTH//table_width_max# number of ncols
-
- ifncols<0:
- # no visible option at all
- return""
-
- ncols=ncols+1ifncols==0elsencols
- # get the amount of rows needed (start with 4 rows)
- nrows=4
- whilenrows*ncols<nlist:
- nrows+=1
- ncols=nlist//nrows# number of full columns
- nlastcol=nlist%nrows# number of elements in last column
-
- # get the final column count
- ncols=ncols+1ifnlastcol>0elsencols
- ifncols>1:
- # only extend if longer than one column
- table.extend([" "foriinrange(nrows-nlastcol)])
-
- # build the actual table grid
- table=[table[icol*nrows:(icol*nrows)+nrows]foricolinrange(0,ncols)]
-
- # adjust the width of each column
- foricolinrange(len(table)):
- col_width=(
- max(max(m_len(p)forpinpart.split("\n"))forpartintable[icol])+colsep
- )
- table[icol]=[pad(part,width=col_width+colsep,align="l")forpartintable[icol]]
-
- # format the table into columns
- returnstr(EvTable(table=table,border="none"))
-
-
[docs]defnode_formatter(self,nodetext,optionstext):
- """
- Formats the entirety of the node.
-
- Args:
- nodetext (str): The node text as returned by `self.nodetext_formatter`.
- optionstext (str): The options display as returned by `self.options_formatter`.
- caller (Object, Account or None, optional): The caller of the node.
-
- Returns:
- node (str): The formatted node to display.
-
- """
- sep=self.node_border_char
-
- ifself._session:
- screen_width=self._session.protocol_flags.get("SCREENWIDTH",{0:_MAX_TEXT_WIDTH})[0]
- else:
- screen_width=_MAX_TEXT_WIDTH
-
- nodetext_width_max=max(m_len(line)forlineinnodetext.split("\n"))
- options_width_max=max(m_len(line)forlineinoptionstext.split("\n"))
- total_width=min(screen_width,max(options_width_max,nodetext_width_max))
- separator1=sep*total_width+"\n\n"ifnodetext_width_maxelse""
- separator2="\n"+sep*total_width+"\n\n"iftotal_widthelse""
- returnseparator1+"|n"+nodetext+"|n"+separator2+"|n"+optionstext
-
-
-# -----------------------------------------------------------
-#
-# List node (decorator turning a node into a list with
-# look/edit/add functionality for the elements)
-#
-# -----------------------------------------------------------
-
-
-
[docs]deflist_node(option_generator,select=None,pagesize=10):
- """
- Decorator for making an EvMenu node into a multi-page list node. Will add new options,
- prepending those options added in the node.
-
- Args:
- 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 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:
-
- ```python
- def select(caller, selection, available_choices=None, **kwargs):
- '''
- This will be called by all auto-generated options except any 'extra_options'
- you return from the node (those you need to handle normally).
-
- 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.
- '''
-
- # (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. These are the ordered list of named options (descs) visible
- on the current node page.
-
- """
-
- defdecorator(func):
- def_select_parser(caller,raw_string,**kwargs):
- """
- Parse the select action
-
- """
- available_choices=kwargs.pop("available_choices",[])
-
- try:
- index=int(raw_string.strip())-1
- selection=available_choices[index]
- exceptException:
- caller.msg(_("|rInvalid choice.|n"))
- else:
- ifcallable(select):
- try:
- ifbool(getargspec(select).keywords):
- returnselect(
- caller,selection,available_choices=available_choices,**kwargs)
- else:
- returnselect(caller,selection,**kwargs)
- exceptException:
- logger.log_trace("Error in EvMenu.list_node decorator:\n "
- f"select-callable: {select}\n with args: ({caller}"
- f"{selection}, {available_choices}, {kwargs}) raised "
- "exception.")
- elifselect:
- # we assume a string was given, we inject the result into the kwargs
- # to pass on to the next node
- kwargs["selection"]=selection
- returnstr(select)
- # this means the previous node will be re-run with these same kwargs
- returnNone
-
- def_list_node(caller,raw_string,**kwargs):
-
- option_list=(
- option_generator(caller)ifcallable(option_generator)elseoption_generator
- )
-
- npages=0
- page_index=0
- page=[]
- options=[]
-
- ifoption_list:
- nall_options=len(option_list)
- pages=[
- option_list[ind:ind+pagesize]forindinrange(0,nall_options,pagesize)
- ]
- npages=len(pages)
-
- page_index=max(0,min(npages-1,kwargs.get("optionpage_index",0)))
- page=pages[page_index]
-
- text=""
- extra_text=None
-
- # dynamic, multi-page option list. Each selection leads to the `select`
- # callback being called with a result from the available choices
- options.extend(
- [
- {"desc":opt,"goto":(_select_parser,{"available_choices":page,**kwargs})}
- foroptinpage
- ]
- )
-
- ifnpages>1:
- # if the goto callable returns None, the same node is rerun, and
- # kwargs not used by the callable are passed on to the node. This
- # allows us to call ourselves over and over, using different kwargs.
- options.append(
- {
- "key":(_("|Wcurrent|n"),"c"),
- "desc":"|W({}/{})|n".format(page_index+1,npages),
- "goto":(lambdacaller:None,{"optionpage_index":page_index}),
- }
- )
- ifpage_index>0:
- options.append(
- {
- "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"),
- "goto":(lambdacaller:None,{"optionpage_index":page_index+1}),
- }
- )
-
- # add data from the decorated node
-
- decorated_options=[]
- supports_kwargs=bool(getargspec(func).keywords)
- try:
- ifsupports_kwargs:
- text,decorated_options=func(caller,raw_string,**kwargs)
- else:
- text,decorated_options=func(caller,raw_string)
- exceptTypeError:
- try:
- ifsupports_kwargs:
- text,decorated_options=func(caller,**kwargs)
- else:
- text,decorated_options=func(caller)
- exceptException:
- raise
- exceptException:
- logger.log_trace()
- else:
- ifisinstance(decorated_options,dict):
- decorated_options=[decorated_options]
- else:
- decorated_options=make_iter(decorated_options)
-
- extra_options=[]
- ifisinstance(decorated_options,dict):
- decorated_options=[decorated_options]
- foreoptindecorated_options:
- cback=("goto"ineoptand"goto")or("exec"ineoptand"exec")orNone
- ifcback:
- signature=eopt[cback]
- ifcallable(signature):
- # callable with no kwargs defined
- eopt[cback]=(signature,{"available_choices":page})
- elifis_iter(signature):
- iflen(signature)>1andisinstance(signature[1],dict):
- signature[1]["available_choices"]=page
- eopt[cback]=signature
- elifsignature:
- # a callable alone in a tuple (i.e. no previous kwargs)
- eopt[cback]=(signature[0],{"available_choices":page})
- else:
- # malformed input.
- logger.log_err(
- "EvMenu @list_node decorator found "
- "malformed option to decorate: {}".format(eopt)
- )
- extra_options.append(eopt)
-
- options.extend(extra_options)
- text=text+"\n\n"+extra_textifextra_textelsetext
-
- returntext,options
-
- return_list_node
-
- returndecorator
[docs]classCmdGetInput(Command):
- """
- Enter your data and press return.
- """
-
- key=_CMD_NOMATCH
- aliases=_CMD_NOINPUT
-
-
[docs]deffunc(self):
- """This is called when user enters anything."""
- caller=self.caller
- try:
- getinput=caller.ndb._getinput
- ifnotgetinputandhasattr(caller,"account"):
- getinput=caller.account.ndb._getinput
- ifgetinput:
- caller=caller.account
- callback=getinput._callback
-
- caller.ndb._getinput._session=self.session
- prompt=caller.ndb._getinput._prompt
- args=caller.ndb._getinput._args
- kwargs=caller.ndb._getinput._kwargs
- result=self.raw_string.rstrip()# we strip the ending line break caused by sending
-
- ok=notcallback(caller,prompt,result,*args,**kwargs)
- ifok:
- # only clear the state if the callback does not return
- # anything
- delcaller.ndb._getinput
- caller.cmdset.remove(InputCmdSet)
- exceptException:
- # make sure to clean up cmdset if something goes wrong
- caller.msg("|rError in get_input. Choice not confirmed (report to admin)|n")
- logger.log_trace("Error in get_input")
- caller.cmdset.remove(InputCmdSet)
[docs]defget_input(caller,prompt,callback,session=None,*args,**kwargs):
- """
- 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.
- 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.
- 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 (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.
-
- 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.
-
- """
- ifnotcallable(callback):
- raiseRuntimeError("get_input: input callback is not callable.")
- caller.ndb._getinput=_Prompt()
- caller.ndb._getinput._callback=callback
- caller.ndb._getinput._prompt=prompt
- caller.ndb._getinput._session=session
- caller.ndb._getinput._args=args
- caller.ndb._getinput._kwargs=kwargs
- caller.cmdset.add(InputCmdSet,persistent=False)
- 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
-#
-# -------------------------------------------------------------
-
-_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
-)
-
-_HELP_NO_OPTION_MATCH=_("Choose an option or try 'help'.")
-
-_OPTION_INPUT_MARKER=">"
-_OPTION_ALIAS_MARKER=";"
-_OPTION_SEP_MARKER=":"
-_OPTION_CALL_MARKER="->"
-_OPTION_COMMENT_START="#"
-
-
-# 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):
- """
- 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
- callable registered with the template generator. This involves parsing the
- func-name and running literal-eval on its kwargs.
-
- """
- match=_RE_CALLABLE.match(goto)
- ifmatch:
- gotofunc=match.group("funcname")
- gotokwargs=match.group("kwargs")or""
- ifgotofuncingoto_callables:
- forkwargingotokwargs.split(","):
- ifkwargand"="inkwarg:
- key,value=[part.strip()forpartinkwarg.split("=",1)]
- 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.")
- try:
- key=literal_eval(key)
- exceptValueError:
- pass
- try:
- value=literal_eval(value)
- exceptValueError:
- pass
- kwargs[key]=value
-
- goto=goto_callables[gotofunc](caller,raw_string,**kwargs)
- ifgotoisNone:
- returngoto,{"generated_nodename":current_nodename}
- returngoto,{"generated_nodename":goto}
-
-
-def_generated_goto_func(caller,raw_string,**kwargs):
- """
- This rerouter handles normal direct goto func call matches.
-
- key : ... -> goto_callable(**kwargs)
-
- """
- 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)
-
-
-def_generated_input_goto_func(caller,raw_string,**kwargs):
- """
- This goto-func acts as a rerouter for >-type line parsing (by acting as the
- _default option). The patterns discovered in the menu maps to different
- *actual* goto-funcs. We map to those here.
-
- >pattern: ... -> goto_callable
-
- """
- gotomap=kwargs["evmenu_gotomap"]
- goto_callables=kwargs["evmenu_goto_callables"]
- current_nodename=kwargs["evmenu_current_nodename"]
- raw_string=raw_string.strip("\n")# strip is necessary to catch empty return
-
- # start with glob patterns
- forpattern,gotoingotomap.items():
- iffnmatch(raw_string.lower(),pattern):
- return_process_callable(caller,goto,goto_callables,raw_string,
- current_nodename,kwargs)
- # no glob pattern match; try regex
- forpattern,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)
- # no match, show error
- raiseEvMenuGotoAbortMessage(_HELP_NO_OPTION_MATCH)
-
-
-def_generated_node(caller,raw_string,**kwargs):
- """
- Every node in the templated menu will be this node, but with dynamically
- changing text/options. It must be a global function like this because
- otherwise we could not make the templated-menu persistent.
-
- """
- text,options=caller.db._evmenu_template_contents[kwargs["_current_nodename"]]
- returntext,options
-
-
-
[docs]defparse_menu_template(caller,menu_template,goto_callables=None):
- """
- Parse menu-template string. The main function of the EvMenu templating system.
-
- Args:
- caller (Object or Account): Entity using the menu.
- menu_template (str): Menu described using the templating format.
- goto_callables (dict, optional): Mapping between call-names and callables
- on the form `callable(caller, raw_string, **kwargs)`. These are what is
- available to use in the `menu_template` string.
-
- Returns:
- dict: A `{"node": nodefunc}` menutree suitable to pass into EvMenu.
-
- """
- def_validate_kwarg(goto,kwarg):
- """
- Validate goto-callable kwarg is on correct form.
- """
- 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.")
- key,_=[part.strip()forpartinkwarg.split("=",1)]
- 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.")
-
- def_parse_options(nodename,optiontxt,goto_callables):
- """
- Parse option section into option dict.
-
- """
- options=[]
- optiontxt=optiontxt[0].strip()ifoptiontxtelse""
- optionlist=[optline.strip()foroptlineinoptiontxt.split("\n")]
- inputparsemap={}
-
- forinum,optlineinenumerate(optionlist):
- ifoptline.startswith(_OPTION_COMMENT_START)or_OPTION_SEP_MARKERnotinoptline:
- # skip comments or invalid syntax
- continue
- key=""
- desc=""
- pattern=None
-
- key,goto=[part.strip()forpartinoptline.split(_OPTION_SEP_MARKER,1)]
-
- # desc -> goto
- if_OPTION_CALL_MARKERingoto:
- desc,goto=[part.strip()forpartingoto.split(_OPTION_CALL_MARKER,1)]
-
- # validate callable
- match=_RE_CALLABLE.match(goto)
- ifmatch:
- kwargs=match.group("kwargs")
- ifkwargs:
- forkwarginkwargs.split(','):
- _validate_kwarg(goto,kwarg)
-
- # parse key [;aliases|pattern]
- key=[part.strip()forpartinkey.split(_OPTION_ALIAS_MARKER)]
- ifnotkey:
- # fall back to this being the Nth option
- key=[f"{inum+1}"]
- main_key=key[0]
-
- 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()
- inputparsemap[pattern]=goto
- else:
- # a regular goto string/callable target
- option={
- "key":key,
- "goto":(
- _generated_goto_func,
- {
- "evmenu_goto":goto,
- "evmenu_current_nodename":nodename,
- "evmenu_goto_callables":goto_callables,
- },
- ),
- }
- ifdesc:
- option["desc"]=desc
- options.append(option)
-
- ifinputparsemap:
- # if this exists we must create a _default entry too
- options.append(
- {
- "key":"_default",
- "goto":(
- _generated_input_goto_func,
- {
- "evmenu_gotomap":inputparsemap,
- "evmenu_current_nodename":nodename,
- "evmenu_goto_callables":goto_callables,
- },
- ),
- }
- )
-
- returnoptions
-
- def_parse(caller,menu_template,goto_callables):
- """
- Parse the menu string format into a node tree.
-
- """
- nodetree={}
- splits=_RE_NODE.split(menu_template)
- splits=splits[1:]ifsplitselse[]
-
- # from evennia import set_trace;set_trace(term_size=(140,120))
- content_map={}
- fornode_indinrange(0,len(splits),2):
- nodename,nodetxt=splits[node_ind],splits[node_ind+1]
- text,*optiontxt=_RE_OPTIONS_SEP.split(nodetxt,maxsplit=2)
- options=_parse_options(nodename,optiontxt,goto_callables)
- content_map[nodename]=(text,options)
- nodetree[nodename]=_generated_node
- caller.db._evmenu_template_contents=content_map
-
- returnnodetree
-
- return_parse(caller,menu_template,goto_callables)
-
-
-
[docs]deftemplate2menu(
- caller,
- menu_template,
- goto_callables=None,
- startnode="start",
- persistent=False,
- **kwargs,
-):
- """
- Helper function to generate and start an EvMenu based on a menu template
- string. This will internall call `parse_menu_template` and run a default
- EvMenu with its results.
-
- Args:
- caller (Object or Account): The entity using the menu.
- menu_template (str): The menu-template string describing the content
- and structure of the menu. It can also be the python-path to, or a module
- containing a `MENU_TEMPLATE` global variable with the template.
- goto_callables (dict, optional): Mapping of callable-names to
- module-global objects to reference by name in the menu-template.
- Must be on the form `callable(caller, raw_string, **kwargs)`.
- startnode (str, optional): The name of the startnode, if not 'start'.
- persistent (bool, optional): If the generated menu should be persistent.
- **kwargs: All kwargs will be passed into EvMenu.
-
- Returns:
- EvMenu: The generated EvMenu.
-
- """
- goto_callables=goto_callablesor{}
- menu_tree=parse_menu_template(caller,menu_template,goto_callables)
- returnEvMenu(
- caller,
- menu_tree,
- persistent=persistent,
- **kwargs,
- )
-# -*- coding: utf-8 -*-
-"""
-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).
-
-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 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)
-```
-
-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.
-
-----
-
-"""
-fromdjango.confimportsettings
-fromdjango.db.models.queryimportQuerySet
-fromdjango.core.paginatorimportPaginator
-fromevennia.commands.commandimportCommand
-fromevennia.commands.cmdsetimportCmdSet
-fromevennia.commandsimportcmdhandler
-fromevennia.utils.ansiimportANSIString
-fromevennia.utils.utilsimportmake_iter,inherits_from,justify,dedent
-fromdjango.utils.translationimportgettextas_
-
-_CMD_NOMATCH=cmdhandler.CMD_NOMATCH
-_CMD_NOINPUT=cmdhandler.CMD_NOINPUT
-
-# we need to use NAWS for this
-_SCREEN_WIDTH=settings.CLIENT_DEFAULT_WIDTH
-_SCREEN_HEIGHT=settings.CLIENT_DEFAULT_HEIGHT
-
-_EVTABLE=None
-
-_LBR=ANSIString("\n")
-
-# text
-
-_DISPLAY="""{text}
-(|wPage|n [{pageno}/{pagemax}] |wn|next|n || |wp|nrevious || |wt|nop || |we|nnd || |wq|nuit)"""
-
-
-
[docs]classCmdMore(Command):
- """
- Manipulate the text paging. Catch no-input with aliases.
- """
-
- key=_CMD_NOINPUT
- aliases=["quit","q","abort","a","next","n","previous","p","top","t","end","e"]
- auto_help=False
-
-
[docs]deffunc(self):
- """
- Implement the command
- """
- more=self.caller.ndb._more
- ifnotmoreandhasattr(self.caller,"account"):
- more=self.caller.account.ndb._more
- ifnotmore:
- self.caller.msg("Error in loading the pager. Contact an admin.")
- return
-
- cmd=self.cmdstring
-
- ifcmdin("abort","a","q"):
- more.page_quit()
- elifcmdin("previous","p"):
- more.page_back()
- elifcmdin("top","t","look","l"):
- more.page_top()
- elifcmdin("end","e"):
- more.page_end()
- else:
- # return or n, next
- more.page_next()
-
-
-
[docs]classCmdMoreExit(Command):
- """
- Any non-more command will exit the pager.
-
- """
- key=_CMD_NOMATCH
-
-
[docs]deffunc(self):
- """
- Exit pager and re-fire the failed command.
-
- """
- more=self.caller.ndb._more
- more.page_quit()
-
- # re-fire the command (in new cmdset)
- self.caller.execute_cmd(self.raw_string)
-
-
-
[docs]classCmdSetMore(CmdSet):
- """
- Stores the more command
- """
-
- key="more_commands"
- priority=110
- mergetype="Replace"
-
-
[docs]classEvMore(object):
- """
- The main pager object
- """
-
-
[docs]def__init__(
- self,
- caller,
- inp,
- always_page=False,
- session=None,
- justify=False,
- justify_kwargs=None,
- exit_on_lastpage=False,
- exit_cmd=None,
- page_formatter=str,
- **kwargs,
- ):
-
- """
- 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` 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 `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 `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
- 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.
-
- Examples:
-
- ```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
- self._always_page=always_page
-
- ifnotsession:
- # if not supplied, use the first session to
- # determine screen size
- sessions=caller.sessions.get()
- ifnotsessions:
- return
- session=sessions[0]
- self._session=session
-
- self._justify=justify
- self._justify_kwargs=justify_kwargs
- self.exit_on_lastpage=exit_on_lastpage
- self.exit_cmd=exit_cmd
- self._exit_msg=_("|xExited pager.|n")
- self._kwargs=kwargs
-
- self._data=None
-
- self._pages=[]
- self._npos=0
-
- self._npages=1
- self._paginator=self.paginator_index
- self._page_formatter=str
-
- # set up individual pages for different sessions
- height=max(4,session.protocol_flags.get("SCREENHEIGHT",{0:_SCREEN_HEIGHT})[0]-4)
- self.width=session.protocol_flags.get("SCREENWIDTH",{0:_SCREEN_WIDTH})[0]
- # always limit number of chars to 10 000 per page
- self.height=min(10000//max(1,self.width),height)
-
- # does initial parsing of input
- self.init_pages(inp)
-
- # kick things into gear
- self.start()
-
- # EvMore functional methods
-
-
[docs]defdisplay(self,show_footer=True):
- """
- Pretty-print the page.
- """
- pos=0
- text="[no content]"
- ifself._npages>0:
- pos=self._npos
- text=self.page_formatter(self.paginator(pos))
- ifshow_footer:
- page=_DISPLAY.format(text=text,pageno=pos+1,pagemax=self._npages)
- else:
- page=text
- # check to make sure our session is still valid
- sessions=self._caller.sessions.get()
- ifnotsessions:
- self.page_quit()
- return
- # this must be an 'is', not == check
- ifnotany(sesforsesinsessionsifself._sessionisses):
- self._session=sessions[0]
- self._caller.msg(text=page,session=self._session,**self._kwargs)
-
-
[docs]defpage_top(self):
- """
- Display the top page
- """
- self._npos=0
- self.display()
[docs]defpage_next(self):
- """
- Scroll the text to the next page. Quit if already at the end
- of the page.
- """
- ifself._npos>=self._npages-1:
- # exit if we are already at the end
- self.page_quit()
- else:
- self._npos+=1
- ifself.exit_on_lastpageandself._npos>=(self._npages-1):
- self.display(show_footer=False)
- self.page_quit(quiet=True)
- else:
- self.display()
-
-
[docs]defpage_back(self):
- """
- Scroll the text back up, at the most to the top.
- """
- self._npos=max(0,self._npos-1)
- self.display()
[docs]defstart(self):
- """
- Starts the pagination
- """
- ifself._npages<=1andnotself._always_page:
- # no need for paging; just pass-through.
- self.display(show_footer=False)
- else:
- # go into paging mode
- # first pass on the msg kwargs
- self._caller.ndb._more=self
- self._caller.cmdset.add(CmdSetMore)
-
- # goto top of the text
- self.page_top()
-
- # default paginators - responsible for extracting a specific page number
-
-
[docs]defpaginator_index(self,pageno):
- """Paginate to specific, known index"""
- returnself._data[pageno]
-
-
[docs]defpaginator_slice(self,pageno):
- """
- 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]defpaginator_django(self,pageno):
- """
- Paginate using the django queryset Paginator API. Note that his is indexed from 1.
- """
- returnself._data.page(pageno+1)
-
- # default helpers to set up particular input types
-
-
[docs]definit_evtable(self,table):
- """The input is an EvTable."""
- iftable.height:
- # enforced height of each paged table, plus space for evmore extras
- self.height=table.height-4
-
- # convert table to string
- text=str(table)
- self._justify=False
- self._justify_kwargs=None# enforce
- self.init_str(text)
-
-
[docs]definit_queryset(self,qs):
- """The input is a queryset"""
- nsize=qs.count()# we assume each will be a line
- self._npages=nsize//self.height+(0ifnsize%self.height==0else1)
- self._data=qs
-
-
[docs]definit_django_paginator(self,pages):
- """
- The input is a django Paginator object.
- """
- self._npages=pages.num_pages
- self._data=pages
-
-
[docs]definit_iterable(self,inp):
- """The input is something other than a string - convert to iterable of strings"""
- inp=make_iter(inp)
- nsize=len(inp)
- self._npages=nsize//self.height+(0ifnsize%self.height==0else1)
- self._data=inp
-
-
[docs]definit_f_str(self,text):
- """
- 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")
- self._npages=len(self._data)
-
-
[docs]definit_str(self,text):
- """The input is a string"""
-
- ifself._justify:
- # we must break very long lines into multiple ones. Note that this
- # will also remove spurious whitespace.
- justify_kwargs=self._justify_kwargsor{}
- width=self._justify_kwargs.get("width",self.width)
- justify_kwargs["width"]=width
- justify_kwargs["align"]=self._justify_kwargs.get("align","l")
- justify_kwargs["indent"]=self._justify_kwargs.get("indent",0)
-
- lines=[]
- forlineintext.split("\n"):
- iflen(line)>width:
- lines.extend(justify(line,**justify_kwargs).split("\n"))
- else:
- lines.append(line)
- else:
- # no justification. Simple division by line
- lines=text.split("\n")
-
- self._data=[
- _LBR.join(lines[i:i+self.height])foriinrange(0,len(lines),self.height)
- ]
- self._npages=len(self._data)
-
- # Hooks for customizing input handling and formatting (override in a child class)
-
-
[docs]definit_pages(self,inp):
- """
- Initialize the pagination. By default, will analyze input type to determine
- how pagination automatically.
-
- Args:
- inp (any): Incoming data to be paginated. By default, handles pagination of
- 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.
- - 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.
-
- By default, helper methods are called that perform these actions
- depending on supported inputs.
-
- """
- ifinherits_from(inp,"evennia.utils.evtable.EvTable"):
- # an EvTable
- self.init_evtable(inp)
- self._paginator=self.paginator_index
- elifisinstance(inp,QuerySet):
- # a queryset
- self.init_queryset(inp)
- self._paginator=self.paginator_slice
- elifisinstance(inp,Paginator):
- self.init_django_paginator(inp)
- self._paginator=self.paginator_django
- elifnotisinstance(inp,str):
- # anything else not a str
- self.init_iterable(inp)
- self._paginator=self.paginator_slice
- elif"\f"ininp:
- # string with \f line-break markers in it
- self.init_f_str(inp)
- self._paginator=self.paginator_index
- else:
- # a string
- self.init_str(inp)
- self._paginator=self.paginator_index
-
-
[docs]defpaginator(self,pageno):
- """
- Paginator. The data operated upon is in `self._data`.
-
- Args:
- pageno (int): The page number to view, from 0...N-1
- Returns:
- str: The page to display (without any decorations, those are added
- by EvMore).
-
- """
- returnself._paginator(pageno)
-
-
[docs]defpage_formatter(self,page):
- """
- Page formatter. Every page passes through this method. Override
- it to customize behvaior per-page. A common use is to generate a new
- EvTable for every page (this is more efficient than to generate one huge
- EvTable across many pages and feed it into EvMore all at once).
-
- Args:
- page (any): A piece of data representing one page to display. This must
-
- Returns:
- str: A ready-formatted page to display. Extra footer with help about
- switching to the next/prev page will be added automatically
-
- """
- returnself._page_formatter(page)
-"""
-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:
-
-```python
- from evennia.utils import evtable
-
- 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
-```
-
-Result:
-
-::
-
- +----------------------+----------+---+--------------------------+
- | Heading1 | Heading2 | | |
- +~~~~~~~~~~~~~~~~~~~~~~+~~~~~~~~~~+~~~+~~~~~~~~~~~~~~~~~~~~~~~~~~+
- | 1 | 4 | 7 | This is long data |
- +----------------------+----------+---+--------------------------+
- | 2 | 5 | 8 | This is even longer data |
- +----------------------+----------+---+--------------------------+
- | 3 | 6 | 9 | |
- +----------------------+----------+---+--------------------------+
- | This is a single row | | | |
- +----------------------+----------+---+--------------------------+
-
-As seen, the table will automatically expand with empty cells to make
-the table symmetric. Tables can be restricted to a given width:
-
-```python
- table.reformat(width=50, align="l")
-```
-
-(We could just have added these keywords to the table creation call)
-
-This yields the following result:
-
-::
-
- +-----------+------------+-----------+-----------+
- | Heading1 | Heading2 | | |
- +~~~~~~~~~~~+~~~~~~~~~~~~+~~~~~~~~~~~+~~~~~~~~~~~+
- | 1 | 4 | 7 | This is |
- | | | | long data |
- +-----------+------------+-----------+-----------+
- | | | | This is |
- | 2 | 5 | 8 | even |
- | | | | longer |
- | | | | data |
- +-----------+------------+-----------+-----------+
- | 3 | 6 | 9 | |
- +-----------+------------+-----------+-----------+
- | This is a | | | |
- | single | | | |
- | row | | | |
- +-----------+------------+-----------+-----------+
-
-
-Table-columns can be individually formatted. Note that if an
-individual column is set with a specific width, table auto-balancing
-will not affect this column (this may lead to the full table being too
-wide, 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):
-
-```python
-
-table.reformat_column(3, width=30, align="r")
-print table
-```
-
-::
-
- +-----------+-------+-----+-----------------------------+---------+
- | Heading1 | Headi | | | |
- | | ng2 | | | |
- +~~~~~~~~~~~+~~~~~~~+~~~~~+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+~~~~~~~~~+
- | 1 | 4 | 7 | This is long data | Test1 |
- +-----------+-------+-----+-----------------------------+---------+
- | 2 | 5 | 8 | This is even longer data | Test3 |
- +-----------+-------+-----+-----------------------------+---------+
- | 3 | 6 | 9 | | Test4 |
- +-----------+-------+-----+-----------------------------+---------+
- | This is a | | | | |
- | single | | | | |
- | row | | | | |
- +-----------+-------+-----+-----------------------------+---------+
-
-When adding new rows/columns their data can have its own alignments
-(left/center/right, top/center/bottom).
-
-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` 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.
-
-----
-
-"""
-
-fromdjango.confimportsettings
-fromtextwrapimportTextWrapper
-fromcopyimportdeepcopy,copy
-fromevennia.utils.utilsimportis_iter,display_lenasd_len
-fromevennia.utils.ansiimportANSIString
-
-_DEFAULT_WIDTH=settings.CLIENT_DEFAULT_WIDTH
-
-
-def_to_ansi(obj):
- """
- convert to ANSIString.
-
- Args:
- obj (str): Convert incoming text to
- be ANSI aware ANSIStrings.
- """
- ifis_iter(obj):
- return[_to_ansi(o)foroinobj]
- else:
- returnANSIString(obj)
-
-
-_whitespace="\t\n\x0b\x0c\r "
-
-
-
[docs]classANSITextWrapper(TextWrapper):
- """
- This is a wrapper work class for handling strings with ANSI tags
- in it. It overloads the standard library `TextWrapper` class and
- is used internally in `EvTable` and has no public methods.
-
- """
-
- def_munge_whitespace(self,text):
- """_munge_whitespace(text : string) -> string
-
- Munge whitespace in text: expand tabs and convert all other
- whitespace characters to spaces. Eg. " foo\tbar\n\nbaz"
- becomes " foo bar baz".
- """
- returntext
-
- # TODO: Ignore expand_tabs/replace_whitespace until ANSIString handles them.
- # - don't remove this code. /Griatch
- # if self.expand_tabs:
- # text = text.expandtabs()
- # if self.replace_whitespace:
- # if isinstance(text, str):
- # text = text.translate(self.whitespace_trans)
- # return text
-
- def_split(self,text):
- """_split(text : string) -> [string]
-
- Split the text to wrap into indivisible chunks. Chunks are
- not quite the same as words; see _wrap_chunks() for full
- details. As an example, the text
- Look, goof-ball -- use the -b option!
- breaks into the following chunks:
- 'Look,', ' ', 'goof-', 'ball', ' ', '--', ' ',
- 'use', ' ', 'the', ' ', '-b', ' ', 'option!'
- if break_on_hyphens is True, or in:
- 'Look,', ' ', 'goof-ball', ' ', '--', ' ',
- 'use', ' ', 'the', ' ', '-b', ' ', option!'
- otherwise.
- """
- # NOTE-PYTHON3: The following code only roughly approximates what this
- # function used to do. Regex splitting on ANSIStrings is
- # dropping ANSI codes, so we're using ANSIString.split
- # for the time being.
- #
- # A less hackier solution would be appreciated.
- chunks=_to_ansi(text).split()
-
- chunks=[chunk+" "forchunkinchunksifchunk]# remove empty chunks
-
- iflen(chunks)>1:
- chunks[-1]=chunks[-1][0:-1]
-
- returnchunks
-
- def_wrap_chunks(self,chunks):
- """_wrap_chunks(chunks : [string]) -> [string]
-
- Wrap a sequence of text chunks and return a list of lines of
- length 'self.width' or less. (If 'break_long_words' is false,
- some lines may be longer than this.) Chunks correspond roughly
- to words and the whitespace between them: each chunk is
- indivisible (modulo 'break_long_words'), but a line break can
- come between any two chunks. Chunks should not have internal
- whitespace; ie. a chunk is either all whitespace or a "word".
- Whitespace chunks will be removed from the beginning and end of
- lines, but apart from that whitespace is preserved.
- """
- lines=[]
- ifself.width<=0:
- raiseValueError("invalid width %r (must be > 0)"%self.width)
-
- # Arrange in reverse order so items can be efficiently popped
- # from a stack of chucks.
- chunks.reverse()
-
- whilechunks:
-
- # Start the list of chunks that will make up the current line.
- # cur_len is just the length of all the chunks in cur_line.
- cur_line=[]
- cur_len=0
-
- # Figure out which static string will prefix this line.
- iflines:
- indent=self.subsequent_indent
- else:
- indent=self.initial_indent
-
- # Maximum width for this line.
- width=self.width-d_len(indent)
-
- # First chunk on line is whitespace -- drop it, unless this
- # is the very beginning of the text (ie. no lines started yet).
- ifself.drop_whitespaceandchunks[-1].strip()==""andlines:
- delchunks[-1]
-
- whilechunks:
- ln=d_len(chunks[-1])
-
- # Can at least squeeze this chunk onto the current line.
- ifcur_len+ln<=width:
- cur_line.append(chunks.pop())
- cur_len+=ln
-
- # Nope, this line is full.
- else:
- break
-
- # The current line is full, and the next chunk is too big to
- # fit on *any* line (not just this one).
- ifchunksandd_len(chunks[-1])>width:
- self._handle_long_word(chunks,cur_line,cur_len,width)
-
- # If the last chunk on this line is all whitespace, drop it.
- ifself.drop_whitespaceandcur_lineandcur_line[-1].strip()=="":
- delcur_line[-1]
-
- # Convert current line back to a string and store it in list
- # of all lines (return value).
- ifcur_line:
- ln=""
- forwincur_line:# ANSI fix
- ln+=w#
- lines.append(indent+ln)
- returnlines
[docs]defwrap(text,width=_DEFAULT_WIDTH,**kwargs):
- """
- Wrap a single paragraph of text, returning a list of wrapped lines.
-
- Reformat the single paragraph in 'text' so it fits in lines of no
- more than 'width' columns, and return a list of wrapped lines. By
- default, tabs in 'text' are expanded with string.expandtabs(), and
- all other whitespace characters (including newline) are converted to
-
- Args:
- text (str): Text to wrap.
- width (int, optional): Width to wrap `text` to.
-
- Keyword Args:
- See TextWrapper class for available keyword args to customize
- wrapping behaviour.
-
- """
- w=ANSITextWrapper(width=width,**kwargs)
- returnw.wrap(text)
-
-
-
[docs]deffill(text,width=_DEFAULT_WIDTH,**kwargs):
- """Fill a single paragraph of text, returning a new string.
-
- Reformat the single paragraph in 'text' to fit in lines of no more
- than 'width' columns, and return a new string containing the entire
- wrapped paragraph. As with wrap(), tabs are expanded and other
- whitespace characters converted to space.
-
- Args:
- text (str): Text to fill.
- width (int, optional): Width of fill area.
-
- Keyword Args:
- See TextWrapper class for available keyword args to customize
- filling behaviour.
-
- """
- w=ANSITextWrapper(width=width,**kwargs)
- returnw.fill(text)
-
-
-# EvCell class (see further down for the EvTable itself)
-
-
-
[docs]classEvCell:
- """
- Holds a single data cell for the table. A cell has a certain width
- and height and contains one or more lines of data. It can shrink
- and resize as needed.
-
- """
-
-
[docs]def__init__(self,data,**kwargs):
- """
- Args:
- data (str): The un-padded data of the entry.
-
- Keyword Args:
- width (int): Desired width of cell. It will pad
- to this size.
- height (int): Desired height of cell. it will pad
- to this size.
- pad_width (int): General padding width. This can be overruled
- by individual settings below.
- pad_left (int): Number of extra pad characters on the left.
- pad_right (int): Number of extra pad characters on the right.
- pad_top (int): Number of extra pad lines top (will pad with `vpad_char`).
- pad_bottom (int): Number of extra pad lines bottom (will pad with `vpad_char`).
- pad_char (str)- pad character to use for padding. This is overruled
- by individual settings below (default `" "`).
- hpad_char (str): Pad character to use both for extra horizontal
- padding (default `" "`).
- vpad_char (str): Pad character to use for extra vertical padding
- and for vertical fill (default `" "`).
- fill_char (str): Character used to filling (expanding cells to
- desired size). This can be overruled by individual settings below.
- hfill_char (str): Character used for horizontal fill (default `" "`).
- vfill_char (str): Character used for vertical fill (default `" "`).
- align (str): Should be one of "l", "r" or "c" for left-, right- or center
- horizontal alignment respectively. Default is left-aligned.
- valign (str): Should be one of "t", "b" or "c" for top-, bottom and center
- vertical alignment respectively. Default is centered.
- border_width (int): General border width. This is overruled
- by individual settings below.
- border_left (int): Left border width.
- border_right (int): Right border width.
- border_top (int): Top border width.
- border_bottom (int): Bottom border width.
- border_char (str): This will use a single border char for all borders.
- overruled by individual settings below.
- border_left_char (str): Char used for left border.
- border_right_char (str): Char used for right border.
- border_top_char (str): Char used for top border.
- border_bottom_char (str): Char user for bottom border.
- corner_char (str): Character used when two borders cross. (default is "").
- This is overruled by individual settings below.
- corner_top_left_char (str): Char used for "nw" corner.
- corner_top_right_char (str): Char used for "ne" corner.
- corner_bottom_left_char (str): Char used for "sw" corner.
- corner_bottom_right_char (str): Char used for "se" corner.
- crop_string (str): String to use when cropping sideways, default is `'[...]'`.
- crop (bool): Crop contentof cell rather than expand vertically, default=`False`.
- enforce_size (bool): If true, the width/height of the cell is
- strictly enforced and extra text will be cropped rather than the
- cell growing vertically.
-
- Raises:
- Exception: for impossible cell size requirements where the
- border width or height cannot fit, or the content is too
- small.
-
- """
-
- self.formatted=None
- padwidth=kwargs.get("pad_width",None)
- padwidth=int(padwidth)ifpadwidthisnotNoneelseNone
- self.pad_left=int(kwargs.get("pad_left",padwidthifpadwidthisnotNoneelse1))
- self.pad_right=int(kwargs.get("pad_right",padwidthifpadwidthisnotNoneelse1))
- self.pad_top=int(kwargs.get("pad_top",padwidthifpadwidthisnotNoneelse0))
- self.pad_bottom=int(kwargs.get("pad_bottom",padwidthifpadwidthisnotNoneelse0))
-
- self.enforce_size=kwargs.get("enforce_size",False)
-
- # avoid multi-char pad_chars messing up counting
- pad_char=kwargs.get("pad_char"," ")
- pad_char=pad_char[0]ifpad_charelse" "
- hpad_char=kwargs.get("hpad_char",pad_char)
- self.hpad_char=hpad_char[0]ifhpad_charelsepad_char
- vpad_char=kwargs.get("vpad_char",pad_char)
- self.vpad_char=vpad_char[0]ifvpad_charelsepad_char
-
- fill_char=kwargs.get("fill_char"," ")
- fill_char=fill_char[0]iffill_charelse" "
- hfill_char=kwargs.get("hfill_char",fill_char)
- self.hfill_char=hfill_char[0]ifhfill_charelse" "
- vfill_char=kwargs.get("vfill_char",fill_char)
- self.vfill_char=vfill_char[0]ifvfill_charelse" "
-
- self.crop_string=kwargs.get("crop_string","[...]")
-
- # borders and corners
- borderwidth=kwargs.get("border_width",0)
- self.border_left=kwargs.get("border_left",borderwidth)
- self.border_right=kwargs.get("border_right",borderwidth)
- self.border_top=kwargs.get("border_top",borderwidth)
- self.border_bottom=kwargs.get("border_bottom",borderwidth)
-
- borderchar=kwargs.get("border_char",None)
- self.border_left_char=kwargs.get("border_left_char",bordercharifbordercharelse"|")
- self.border_right_char=kwargs.get(
- "border_right_char",bordercharifbordercharelseself.border_left_char
- )
- self.border_top_char=kwargs.get("border_top_char",bordercharifbordercharelse"-")
- self.border_bottom_char=kwargs.get(
- "border_bottom_char",bordercharifbordercharelseself.border_top_char
- )
-
- corner_char=kwargs.get("corner_char","+")
- self.corner_top_left_char=kwargs.get("corner_top_left_char",corner_char)
- self.corner_top_right_char=kwargs.get("corner_top_right_char",corner_char)
- self.corner_bottom_left_char=kwargs.get("corner_bottom_left_char",corner_char)
- self.corner_bottom_right_char=kwargs.get("corner_bottom_right_char",corner_char)
-
- # alignments
- self.align=kwargs.get("align","l")
- self.valign=kwargs.get("valign","c")
-
- self.data=self._split_lines(_to_ansi(data))
- self.raw_width=max(d_len(line)forlineinself.data)
- self.raw_height=len(self.data)
-
- # this is extra trimming required for cels in the middle of a table only
- self.trim_horizontal=0
- self.trim_vertical=0
-
- # width/height is given without left/right or top/bottom padding
- if"width"inkwargs:
- width=kwargs.pop("width")
- self.width=(
- width-self.pad_left-self.pad_right-self.border_left-self.border_right
- )
- ifself.width<=0<self.raw_width:
- raiseException("Cell width too small - no space for data.")
- else:
- self.width=self.raw_width
- if"height"inkwargs:
- height=kwargs.pop("height")
- self.height=(
- height-self.pad_top-self.pad_bottom-self.border_top-self.border_bottom
- )
- ifself.height<=0<self.raw_height:
- raiseException("Cell height too small - no space for data.")
- else:
- self.height=self.raw_height
-
- # prepare data
- # self.formatted = self._reformat()
-
- def_crop(self,text,width):
- """
- Apply cropping of text.
-
- Args:
- text (str): The text to crop.
- width (int): The width to crop `text` to.
-
- """
- ifd_len(text)>width:
- crop_string=self.crop_string
- returntext[:width-d_len(crop_string)]+crop_string
- returntext
-
- def_reformat(self):
- """
- Apply all EvCells' formatting operations.
-
- """
- data=self._border(self._pad(self._valign(self._align(self._fit_width(self.data)))))
- returndata
-
- def_split_lines(self,text):
- """
- Simply split by linebreaks
-
- Args:
- text (str): text to split.
-
- Returns:
- split (list): split text.
- """
- returntext.split("\n")
-
- def_fit_width(self,data):
- """
- Split too-long lines to fit the desired width of the Cell.
-
- Args:
- data (str): Text to adjust to the cell's width.
-
- Returns:
- adjusted data (str): The adjusted text.
-
- Notes:
- This also updates `raw_width`.
-
-
- """
- width=self.width
- adjusted_data=[]
- forlineindata:
- if0<width<d_len(line):
- # replace_whitespace=False, expand_tabs=False is a
- # fix for ANSIString not supporting expand_tabs/translate
- adjusted_data.extend(
- [
- ANSIString(part+ANSIString("|n"))
- forpartinwrap(line,width=width,drop_whitespace=False)
- ]
- )
- else:
- adjusted_data.append(line)
- ifself.enforce_size:
- # don't allow too high cells
- excess=len(adjusted_data)-self.height
- ifexcess>0:
- # too many lines. Crop and mark last line with crop_string
- crop_string=self.crop_string
- adjusted_data=adjusted_data[:-excess]
- crop_string_length=len(crop_string)
- iflen(adjusted_data[-1])>crop_string_length:
- adjusted_data[-1]=adjusted_data[-1][:-crop_string_length]+crop_string
- else:
- adjusted_data[-1]+=crop_string
- elifexcess<0:
- # too few lines. Fill to height.
- adjusted_data.extend([""for_inrange(excess)])
-
- returnadjusted_data
-
- def_center(self,text,width,pad_char):
- """
- Horizontally center text on line of certain width, using padding.
-
- Args:
- text (str): The text to center.
- width (int): How wide the area is (in characters) where `text`
- should be centered.
- pad_char (str): Which padding character to use.
-
- Returns:
- text (str): Centered text.
-
- """
- excess=width-d_len(text)
- ifexcess<=0:
- returntext
- ifexcess%2:
- # uneven padding
- narrowside=(excess//2)*pad_char
- widerside=narrowside+pad_char
- ifwidth%2:
- returnnarrowside+text+widerside
- else:
- returnwiderside+text+narrowside
- else:
- # even padding - same on both sides
- side=(excess//2)*pad_char
- returnside+text+side
-
- def_align(self,data):
- """
- Align list of rows of cell. Whitespace characters will be stripped
- if there is only one whitespace character - otherwise, it's assumed
- the caller may be trying some manual formatting in the text.
-
- Args:
- data (str): Text to align.
-
- Returns:
- text (str): Aligned result.
-
- """
- align=self.align
- hfill_char=self.hfill_char
- width=self.width
- ifalign=="l":
- lines=[
- (
- line.lstrip(" ")+" "
- ifline.startswith(" ")andnotline.startswith(" ")
- elseline
- )
- +hfill_char*(width-d_len(line))
- forlineindata
- ]
- returnlines
- elifalign=="r":
- return[
- hfill_char*(width-d_len(line))
- +(
- " "+line.rstrip(" ")
- ifline.endswith(" ")andnotline.endswith(" ")
- elseline
- )
- forlineindata
- ]
- else:# center, 'c'
- return[self._center(line,self.width,self.hfill_char)forlineindata]
-
- def_valign(self,data):
- """
- Align cell vertically
-
- Args:
- data (str): Text to align.
-
- Returns:
- text (str): Vertically aligned text.
-
- """
- valign=self.valign
- height=self.height
- cheight=len(data)
- excess=height-cheight
- padline=self.vfill_char*self.width
-
- ifexcess<=0:
- returndata
- # only care if we need to add new lines
- ifvalign=="t":
- returndata+[padlinefor_inrange(excess)]
- elifvalign=="b":
- return[padlinefor_inrange(excess)]+data
- else:# center
- narrowside=[padlinefor_inrange(excess//2)]
- widerside=narrowside+[padline]
- ifexcess%2:
- # uneven padding
- ifheight%2:
- returnwiderside+data+narrowside
- else:
- returnnarrowside+data+widerside
- else:
- # even padding, same on both sides
- returnnarrowside+data+narrowside
-
- def_pad(self,data):
- """
- Pad data with extra characters on all sides.
-
- Args:
- data (str): Text to pad.
-
- Returns:
- text (str): Padded text.
-
- """
- left=self.hpad_char*self.pad_left
- right=self.hpad_char*self.pad_right
- vfill=(self.width+self.pad_left+self.pad_right)*self.vpad_char
- top=[vfillfor_inrange(self.pad_top)]
- bottom=[vfillfor_inrange(self.pad_bottom)]
- returntop+[left+line+rightforlineindata]+bottom
-
- def_border(self,data):
- """
- Add borders to the cell.
-
- Args:
- data (str): Text to surround with borders.
-
- Return:
- text (str): Text with borders.
-
- """
-
- left=self.border_left_char*self.border_left+ANSIString("|n")
- right=ANSIString("|n")+self.border_right_char*self.border_right
-
- cwidth=(
- self.width
- +self.pad_left
- +self.pad_right
- +max(0,self.border_left-1)
- +max(0,self.border_right-1)
- )
-
- vfill=self.corner_top_left_charifleftelse""
- vfill+=cwidth*self.border_top_char
- vfill+=self.corner_top_right_charifrightelse""
- top=[vfillfor_inrange(self.border_top)]
-
- vfill=self.corner_bottom_left_charifleftelse""
- vfill+=cwidth*self.border_bottom_char
- vfill+=self.corner_bottom_right_charifrightelse""
- bottom=[vfillfor_inrange(self.border_bottom)]
-
- returntop+[left+line+rightforlineindata]+bottom
-
-
[docs]defget_min_height(self):
- """
- Get the minimum possible height of cell, including at least
- one line for data.
-
- Returns:
- min_height (int): The mininum height of cell.
-
- """
- returnself.pad_top+self.pad_bottom+self.border_bottom+self.border_top+1
-
-
[docs]defget_min_width(self):
- """
- Get the minimum possible width of cell, including at least one
- character-width for data.
-
- Returns:
- min_width (int): The minimum width of cell.
-
- """
- returnself.pad_left+self.pad_right+self.border_left+self.border_right+1
-
-
[docs]defget_height(self):
- """
- Get natural height of cell, including padding.
-
- Returns:
- natural_height (int): Height of cell.
-
- """
- returnlen(self.formatted)# if self.formatted else 0
-
-
[docs]defget_width(self):
- """
- Get natural width of cell, including padding.
-
- Returns:
- natural_width (int): Width of cell.
-
- """
- returnd_len(self.formatted[0])# if self.formatted else 0
-
-
[docs]defreplace_data(self,data,**kwargs):
- """
- Replace cell data. This causes a full reformat of the cell.
-
- Args:
- data (str): Cell data.
-
- Notes:
- The available keyword arguments are the same as for
- `EvCell.__init__`.
-
- """
- self.data=self._split_lines(_to_ansi(data))
- self.raw_width=max(d_len(line)forlineinself.data)
- self.raw_height=len(self.data)
- self.reformat(**kwargs)
-
-
[docs]defreformat(self,**kwargs):
- """
- Reformat the EvCell with new options
-
- Keyword Args:
- The available keyword arguments are the same as for `EvCell.__init__`.
-
- Raises:
- Exception: If the cells cannot shrink enough to accomodate
- the options or the data given.
-
- """
- # keywords that require manipulation
- padwidth=kwargs.get("pad_width",None)
- padwidth=int(padwidth)ifpadwidthisnotNoneelseNone
- self.pad_left=int(
- kwargs.pop("pad_left",padwidthifpadwidthisnotNoneelseself.pad_left)
- )
- self.pad_right=int(
- kwargs.pop("pad_right",padwidthifpadwidthisnotNoneelseself.pad_right)
- )
- self.pad_top=int(
- kwargs.pop("pad_top",padwidthifpadwidthisnotNoneelseself.pad_top)
- )
- self.pad_bottom=int(
- kwargs.pop("pad_bottom",padwidthifpadwidthisnotNoneelseself.pad_bottom)
- )
-
- self.enforce_size=kwargs.get("enforce_size",False)
-
- pad_char=kwargs.pop("pad_char",None)
- hpad_char=kwargs.pop("hpad_char",pad_char)
- self.hpad_char=hpad_char[0]ifhpad_charelseself.hpad_char
- vpad_char=kwargs.pop("vpad_char",pad_char)
- self.vpad_char=vpad_char[0]ifvpad_charelseself.vpad_char
-
- fillchar=kwargs.pop("fill_char",None)
- hfill_char=kwargs.pop("hfill_char",fillchar)
- self.hfill_char=hfill_char[0]ifhfill_charelseself.hfill_char
- vfill_char=kwargs.pop("vfill_char",fillchar)
- self.vfill_char=vfill_char[0]ifvfill_charelseself.vfill_char
-
- borderwidth=kwargs.get("border_width",None)
- self.border_left=kwargs.pop(
- "border_left",borderwidthifborderwidthisnotNoneelseself.border_left
- )
- self.border_right=kwargs.pop(
- "border_right",borderwidthifborderwidthisnotNoneelseself.border_right
- )
- self.border_top=kwargs.pop(
- "border_top",borderwidthifborderwidthisnotNoneelseself.border_top
- )
- self.border_bottom=kwargs.pop(
- "border_bottom",borderwidthifborderwidthisnotNoneelseself.border_bottom
- )
-
- borderchar=kwargs.get("border_char",None)
- self.border_left_char=kwargs.pop(
- "border_left_char",bordercharifbordercharelseself.border_left_char
- )
- self.border_right_char=kwargs.pop(
- "border_right_char",bordercharifbordercharelseself.border_right_char
- )
- self.border_top_char=kwargs.pop(
- "border_topchar",bordercharifbordercharelseself.border_top_char
- )
- self.border_bottom_char=kwargs.pop(
- "border_bottom_char",bordercharifbordercharelseself.border_bottom_char
- )
-
- corner_char=kwargs.get("corner_char",None)
- self.corner_top_left_char=kwargs.pop(
- "corner_top_left",corner_charifcorner_charisnotNoneelseself.corner_top_left_char
- )
- self.corner_top_right_char=kwargs.pop(
- "corner_top_right",
- corner_charifcorner_charisnotNoneelseself.corner_top_right_char,
- )
- self.corner_bottom_left_char=kwargs.pop(
- "corner_bottom_left",
- corner_charifcorner_charisnotNoneelseself.corner_bottom_left_char,
- )
- self.corner_bottom_right_char=kwargs.pop(
- "corner_bottom_right",
- corner_charifcorner_charisnotNoneelseself.corner_bottom_right_char,
- )
-
- # this is used by the table to adjust size of cells with borders in the middle
- # of the table
- self.trim_horizontal=kwargs.pop("trim_horizontal",self.trim_horizontal)
- self.trim_vertical=kwargs.pop("trim_vertical",self.trim_vertical)
-
- # fill all other properties
- forkey,valueinkwargs.items():
- setattr(self,key,value)
-
- # Handle sizes
- if"width"inkwargs:
- width=kwargs.pop("width")
- self.width=(
- width
- -self.pad_left
- -self.pad_right
- -self.border_left
- -self.border_right
- +self.trim_horizontal
- )
- # if self.width <= 0 and self.raw_width > 0:
- ifself.width<=0<self.raw_width:
- raiseException("Cell width too small, no room for data.")
- if"height"inkwargs:
- height=kwargs.pop("height")
- self.height=(
- height
- -self.pad_top
- -self.pad_bottom
- -self.border_top
- -self.border_bottom
- +self.trim_vertical
- )
- ifself.height<=0<self.raw_height:
- raiseException("Cell height too small, no room for data.")
-
- # reformat (to new sizes, padding, header and borders)
- self.formatted=self._reformat()
-
-
[docs]defget(self):
- """
- Get data, padded and aligned in the form of a list of lines.
-
- """
- self.formatted=self._reformat()
- returnself.formatted
[docs]classEvColumn(object):
- """
- This class holds a list of Cells to represent a column of a table.
- It holds operations and settings that affect *all* cells in the
- column.
-
- Columns are not intended to be used stand-alone; they should be
- incorporated into an EvTable (like EvCells)
-
- """
-
-
[docs]def__init__(self,*args,**kwargs):
- """
- Args:
- Text for each row in the column
-
- Keyword Args:
- All `EvCell.__init_` keywords are available, these
- settings will be persistently applied to every Cell in the
- column.
-
- """
- self.options=kwargs# column-specific options
- self.column=[EvCell(data,**kwargs)fordatainargs]
-
- def_balance(self,**kwargs):
- """
- Make sure to adjust the width of all cells so we form a
- coherent and lined-up column. Will enforce column-specific
- options to cells.
-
- Keyword Args:
- Extra keywords to modify the column setting. Same keywords
- as in `EvCell.__init__`.
-
- """
- col=self.column
- # fixed options for the column will override those requested in the call!
- # this is particularly relevant to things like width/height, to avoid
- # fixed-widths columns from being auto-balanced
- kwargs.update(self.options)
- # use fixed width or adjust to the largest cell
- if"width"notinkwargs:
- [
- cell.reformat()forcellincol
- ]# this is necessary to get initial widths of all cells
- kwargs["width"]=max(cell.get_width()forcellincol)ifcolelse0
- [cell.reformat(**kwargs)forcellincol]
-
-
[docs]defadd_rows(self,*args,**kwargs):
- """
- Add new cells to column. They will be inserted as
- a series of rows. It will inherit the options
- of the rest of the column's cells (use update to change
- options).
-
- Args:
- Texts for the new cells
- ypos (int, optional): Index position in table before which to insert the
- new column. Uses Python indexing, so to insert at the top,
- use `ypos=0`. If not given, data will be inserted at the end
- of the column.
-
- Keyword Args:
- Available keywods as per `EvCell.__init__`.
-
- """
- # column-level options override those in kwargs
- options={**kwargs,**self.options}
-
- ypos=kwargs.get("ypos",None)
- ifyposisNoneorypos>len(self.column):
- # add to the end
- self.column.extend([EvCell(data,**options)fordatainargs])
- else:
- # insert cells before given index
- ypos=min(len(self.column)-1,max(0,int(ypos)))
- new_cells=[EvCell(data,**options)fordatainargs]
- self.column=self.column[:ypos]+new_cells+self.column[ypos:]
- # self._balance(**kwargs)
-
-
[docs]defreformat(self,**kwargs):
- """
- Change the options for the column.
-
- Keyword Args:
- Keywords as per `EvCell.__init__`.
-
- """
- self._balance(**kwargs)
-
-
[docs]defreformat_cell(self,index,**kwargs):
- """
- reformat cell at given index, keeping column options if
- necessary.
-
- Args:
- index (int): Index location of the cell in the column,
- starting from 0 for the first row to Nrows-1.
-
- Keyword Args:
- Keywords as per `EvCell.__init__`.
-
- """
- # column-level options take precedence here
- kwargs.update(self.options)
- self.column[index].reformat(**kwargs)
[docs]classEvTable(object):
- """
- The table class holds a list of EvColumns, each consisting of EvCells so
- that the result is a 2D matrix.
- """
-
-
[docs]def__init__(self,*args,**kwargs):
- """
- Args:
- Header texts for the table.
-
- Keyword Args:
- table (list of lists or list of `EvColumns`, optional):
- This is used to build the table in a quick way. If not
- given, the table will start out empty and `add_` methods
- need to be used to add rows/columns.
- header (bool, optional): `True`/`False` - turn off the
- header texts (`*args`) being treated as a header (such as
- not adding extra underlining)
- pad_width (int, optional): How much empty space to pad your cells with
- (default is 1)
- border (str, optional)): The border style to use. This is one of
- - `None` - No border drawing at all.
- - "table" - only a border around the whole table.
- - "tablecols" - table and column borders. (default)
- - "header" - only border under header.
- - "cols" - only vertical borders.
- - "incols" - vertical borders, no outer edges.
- - "rows" - only borders between rows.
- - "cells" - border around all cells.
- border_width (int, optional): Width of table borders, if border is active.
- Note that widths wider than 1 may give artifacts in the corners. Default is 1.
- corner_char (str, optional): Character to use in corners when border is active.
- Default is `+`.
- corner_top_left_char (str, optional): Character used for "nw" corner of table.
- Defaults to `corner_char`.
- corner_top_right_char (str, optional): Character used for "ne" corner of table.
- Defaults to `corner_char`.
- corner_bottom_left_char (str, optional): Character used for "sw" corner of table.
- Defaults to `corner_char`.
- corner_bottom_right_char (str, optional): Character used for "se" corner of table.
- Defaults to `corner_char`.
- pretty_corners (bool, optional): Use custom characters to
- make the table corners look "rounded". Uses UTF-8
- characters. Defaults to `False` for maximum compatibility with various displays
- that may occationally have issues with UTF-8 characters.
- header_line_char (str, optional): Character to use for underlining
- the header row (default is '~'). Requires `border` to not be `None`.
- width (int, optional): Fixed width of table. If not set,
- width is set by the total width of each column. This will
- 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`.
- 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).
- This keyword has no meaning if `width` is set.
-
- Raises:
- Exception: If given erroneous input or width settings for the data.
-
- Notes:
- Beyond those table-specific keywords, the non-overlapping keywords
- of `EvCell.__init__` are also available. These will be passed down
- to every cell in the table.
-
- """
- # at this point table is a 2D grid - a list of columns
- # x is the column position, y the row
- table=kwargs.pop("table",[])
-
- # header is a list of texts. We merge it to the table's top
- header=[_to_ansi(head)forheadinargs]
- self.header=header!=[]
- ifself.header:
- iftable:
- excess=len(header)-len(table)
- ifexcess>0:
- # header bigger than table
- table.extend([]for_inrange(excess))
- elifexcess<0:
- # too short header
- header.extend(_to_ansi([""for_inrange(abs(excess))]))
- forix,headinginenumerate(header):
- table[ix].insert(0,heading)
- else:
- table=[[heading]forheadinginheader]
- # even though we inserted the header, we can still turn off
- # header border underling etc. We only allow this if a header
- # was actually set
- self.header=kwargs.pop("header",self.header)ifself.headerelseFalse
- hchar=kwargs.pop("header_line_char","~")
- self.header_line_char=hchar[0]ifhcharelse"~"
-
- border=kwargs.pop("border","tablecols")
- ifborderisNone:
- border="none"
- ifbordernotin(
- "none",
- "table",
- "tablecols",
- "header",
- "incols",
- "cols",
- "rows",
- "cells",
- ):
- raiseException("Unsupported border type: '%s'"%border)
- self.border=border
-
- # border settings are passed into Cell as well (so kwargs.get and not pop)
- self.border_width=kwargs.get("border_width",1)
- self.corner_char=kwargs.get("corner_char","+")
- pcorners=kwargs.pop("pretty_corners",False)
- self.corner_top_left_char=_to_ansi(
- kwargs.pop("corner_top_left_char","."ifpcornerselseself.corner_char)
- )
- self.corner_top_right_char=_to_ansi(
- kwargs.pop("corner_top_right_char","."ifpcornerselseself.corner_char)
- )
- self.corner_bottom_left_char=_to_ansi(
- kwargs.pop("corner_bottom_left_char"," "ifpcornerselseself.corner_char)
- )
- self.corner_bottom_right_char=_to_ansi(
- kwargs.pop("corner_bottom_right_char"," "ifpcornerselseself.corner_char)
- )
-
- self.width=kwargs.pop("width",None)
- self.height=kwargs.pop("height",None)
- self.evenwidth=kwargs.pop("evenwidth",False)
- self.maxwidth=kwargs.pop("maxwidth",None)
- ifself.maxwidthandself.widthandself.maxwidth<self.width:
- raiseException("table maxwidth < table width!")
- # size in cell cols/rows
- self.ncols=len(table)
- self.nrows=max(len(col)forcolintable)iftableelse0
- # size in characters (gets set when _balance is called)
- self.nwidth=0
- self.nheight=0
- # save options
- self.options=kwargs
-
- # use the temporary table to generate the table on the fly, as a list of EvColumns
- self.table=[EvColumn(*col,**kwargs)forcolintable]
-
- # this is the actual working table
- self.worktable=None
-
- # balance the table
- # self._balance()
-
- def_cellborders(self,ix,iy,nx,ny,**kwargs):
- """
- Adds borders to the table by adjusting the input kwarg to
- instruct cells to build a border in the right positions.
-
- Args:
- ix (int): x index positions in table.
- iy (int): y index positions in table.
- nx (int): x size of table.
- ny (int): y size of table.
-
- Keyword Args:
- Keywords as per `EvTable.__init__`.
-
- Returns:
- table (str): string with the correct borders.
-
- Notes:
- A copy of the kwarg is returned to the cell. This is method
- is called by self._borders.
-
- """
-
- ret=kwargs.copy()
-
- # handle the various border modes
- border=self.border
- header=self.header
-
- bwidth=self.border_width
- headchar=self.header_line_char
-
- defcorners(ret):
- """Handle corners of table"""
- ifix==0andiy==0:
- ret["corner_top_left_char"]=self.corner_top_left_char
- ifix==nxandiy==0:
- ret["corner_top_right_char"]=self.corner_top_right_char
- ifix==0andiy==ny:
- ret["corner_bottom_left_char"]=self.corner_bottom_left_char
- ifix==nxandiy==ny:
- ret["corner_bottom_right_char"]=self.corner_bottom_right_char
- returnret
-
- defleft_edge(ret):
- """add vertical border along left table edge"""
- ifix==0:
- ret["border_left"]=bwidth
- # ret["trim_horizontal"] = bwidth
- returnret
-
- deftop_edge(ret):
- """add border along top table edge"""
- ifiy==0:
- ret["border_top"]=bwidth
- # ret["trim_vertical"] = bwidth
- returnret
-
- defright_edge(ret):
- """add vertical border along right table edge"""
- ifix==nx:# and 0 < iy < ny:
- ret["border_right"]=bwidth
- # ret["trim_horizontal"] = 0
- returnret
-
- defbottom_edge(ret):
- """add border along bottom table edge"""
- ifiy==ny:
- ret["border_bottom"]=bwidth
- # ret["trim_vertical"] = bwidth
- returnret
-
- defcols(ret):
- """Adding vertical borders inside the table"""
- if0<=ix<nx:
- ret["border_right"]=bwidth
- returnret
-
- defrows(ret):
- """Adding horizontal borders inside the table"""
- if0<=iy<ny:
- ret["border_bottom"]=bwidth
- returnret
-
- defhead(ret):
- """Add header underline"""
- ifiy==0:
- # put different bottom line for header
- ret["border_bottom"]=bwidth
- ret["border_bottom_char"]=headchar
- returnret
-
- # use the helper functions to define various
- # table "styles"
-
- ifborderin("table","tablecols","cells"):
- ret=bottom_edge(right_edge(top_edge(left_edge(corners(ret)))))
- ifborderin("cols","tablecols","cells"):
- ret=cols(right_edge(left_edge(ret)))
- ifborderin"incols":
- ret=cols(ret)
- ifborderin("rows","cells"):
- ret=rows(bottom_edge(top_edge(ret)))
- ifheaderandbordernotin("none",None):
- ret=head(ret)
-
- returnret
-
- def_borders(self):
- """
- Add borders to table. This is called from self._balance.
- """
- nx,ny=self.ncols-1,self.nrows-1
- options=self.options
- forix,colinenumerate(self.worktable):
- foriy,cellinenumerate(col):
- col.reformat_cell(iy,**self._cellborders(ix,iy,nx,ny,**options))
-
- def_balance(self):
- """
- Balance the table. This means to make sure
- all cells on the same row have the same height,
- that all columns have the same number of rows
- and that the table fits within the given width.
- """
-
- # we make all modifications on a working copy of the
- # actual table. This allows us to add columns/rows
- # and re-balance over and over without issue.
- self.worktable=deepcopy(self.table)
- # self._borders()
- # return
- options=copy(self.options)
-
- # balance number of rows to make a rectangular table
- # column by column
- ncols=len(self.worktable)
- nrows=[len(col)forcolinself.worktable]
- nrowmax=max(nrows)ifnrowselse0
- foricol,nrowinenumerate(nrows):
- self.worktable[icol].reformat(**options)
- ifnrow<nrowmax:
- # add more rows to too-short columns
- empty_rows=[""for_inrange(nrowmax-nrow)]
- self.worktable[icol].add_rows(*empty_rows)
- self.ncols=ncols
- self.nrows=nrowmax
-
- # add borders - these add to the width/height, so we must do this before calculating
- # width/height
- self._borders()
-
- # equalize widths within each column
- cwidths=[max(cell.get_width()forcellincol)forcolinself.worktable]
-
- ifself.widthorself.maxwidthandself.maxwidth<sum(cwidths):
- # we set a table width. Horizontal cells will be evenly distributed and
- # expand vertically as needed (unless self.height is set, see below)
-
- # use fixed width, or set to maxwidth
- width=self.widthifself.widthelseself.maxwidth
-
- ifncols:
- # get minimum possible cell widths for each row
- cwidths_min=[max(cell.get_min_width()forcellincol)forcolinself.worktable]
- cwmin=sum(cwidths_min)
-
- # get which cols have separately set widths - these should be locked
- # note that we need to remove cwidths_min for each lock to avoid counting
- # it twice (in cwmin and in locked_cols)
- locked_cols={
- icol:col.options["width"]-cwidths_min[icol]
- foricol,colinenumerate(self.worktable)
- if"width"incol.options
- }
- locked_width=sum(locked_cols.values())
-
- excess=width-cwmin-locked_width
-
- iflen(locked_cols)>=ncolsandexcess:
- # we can't adjust the width at all - all columns are locked
- raiseException(
- "Cannot balance table to width %s - "
- "all columns have a set, fixed width summing to %s!"
- %(self.width,sum(cwidths))
- )
-
- ifexcess<0:
- # the locked cols makes it impossible
- raiseException(
- "Cannot shrink table width to %s. "
- "Minimum size (and/or fixed-width columns) "
- "sets minimum at %s."%(self.width,cwmin+locked_width)
- )
-
- ifself.evenwidth:
- # make each column of equal width
- # use cwidths as a work-array to track weights
- cwidths=copy(cwidths_min)
- correction=0
- whilecorrection<excess:
- # flood-fill the minimum table starting with the smallest columns
- ci=cwidths.index(min(cwidths))
- ifciinlocked_cols:
- # locked column, make sure it's not picked again
- cwidths[ci]+=9999
- cwidths_min[ci]=locked_cols[ci]
- else:
- cwidths_min[ci]+=1
- correction+=1
- cwidths=cwidths_min
- else:
- # make each column expand more proportional to their data size
- # we use cwidth as a work-array to track weights
- correction=0
- whilecorrection<excess:
- # fill wider columns first
- ci=cwidths.index(max(cwidths))
- ifciinlocked_cols:
- # locked column, make sure it's not picked again
- cwidths[ci]-=9999
- cwidths_min[ci]=locked_cols[ci]
- else:
- cwidths_min[ci]+=1
- correction+=1
- # give a just changed col less prio next run
- cwidths[ci]-=3
- cwidths=cwidths_min
-
- # reformat worktable (for width align)
- forix,colinenumerate(self.worktable):
- try:
- col.reformat(width=cwidths[ix],**options)
- exceptException:
- raise
-
- # 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)
- ]
-
- ifself.height:
- # if we are fixing the table height, it means cells must crop text instead of resizing.
- ifnrowmax:
-
- # get minimum possible cell heights for each column
- cheights_min=[
- max(cell.get_min_height()forcellin(col[iy]forcolinself.worktable))
- foriyinrange(nrowmax)
- ]
- chmin=sum(cheights_min)
-
- # get which cols have separately set heights - these should be locked
- # note that we need to remove cheights_min for each lock to avoid counting
- # it twice (in chmin and in locked_cols)
- locked_cols={
- icol:col.options["height"]-cheights_min[icol]
- foricol,colinenumerate(self.worktable)
- if"height"incol.options
- }
- locked_height=sum(locked_cols.values())
-
- excess=self.height-chmin-locked_height
-
- ifchmin>self.height:
- # we cannot shrink any more
- raiseException(
- "Cannot shrink table height to %s. Minimum "
- "size (and/or fixed-height rows) sets minimum at %s."
- %(self.height,chmin+locked_height)
- )
-
- # Add all the excess at the end of the table
- # Note: Older solutions tried to balance individual
- # rows' vsize. This could lead to empty rows that
- # looked like a bug. This solution instead
- # adds empty rows at the end which is less sophisticated
- # but much more visually consistent.
- cheights_min[-1]+=excess
- cheights=cheights_min
-
- # we must tell cells to crop instead of expanding
- options["enforce_size"]=True
-
- # reformat table (for vertical align)
- forix,colinenumerate(self.worktable):
- foriy,cellinenumerate(col):
- try:
- col.reformat_cell(iy,height=cheights[iy],**options)
- exceptExceptionase:
- msg="ix=%s, iy=%s, height=%s: %s"%(ix,iy,cheights[iy],e.message)
- raiseException("Error in vertical align:\n%s"%msg)
-
- # calculate actual table width/height in characters
- self.cwidth=sum(cwidths)
- self.cheight=sum(cheights)
-
- def_generate_lines(self):
- """
- Generates lines across all columns
- (each cell may contain multiple lines)
- This will also balance the table.
- """
- self._balance()
- foriyinrange(self.nrows):
- cell_row=[col[iy]forcolinself.worktable]
- # this produces a list of lists, each of equal length
- cell_data=[cell.get()forcellincell_row]
- cell_height=min(len(lines)forlinesincell_data)
- forilineinrange(cell_height):
- yieldANSIString("").join(_to_ansi(celldata[iline]forcelldataincell_data))
-
-
[docs]defadd_header(self,*args,**kwargs):
- """
- Add header to table. This is a number of texts to be put at
- the top of the table. They will replace an existing header.
-
- Args:
- args (str): These strings will be used as the header texts.
-
- Keyword Args:
- Same keywords as per `EvTable.__init__`. Will be applied
- to the new header's cells.
-
- """
- self.header=True
- self.add_row(ypos=0,*args,**kwargs)
-
-
[docs]defadd_column(self,*args,**kwargs):
- """
- Add a column to table. If there are more rows in new column
- than there are rows in the current table, the table will
- expand with empty rows in the other columns. If too few, the
- new column with get new empty rows. All filling rows are added
- to the end.
-
- Args:
- args (`EvColumn` or multiple strings): Either a single EvColumn instance or
- a number of data string arguments to be used to create a new column.
- header (str, optional): The header text for the column
- xpos (int, optional): Index position in table *before* which
- to input new column. If not given, column will be added to the end
- of the table. Uses Python indexing (so first column is `xpos=0`)
-
- Keyword Args:
- Other keywords as per `Cell.__init__`.
-
- """
- # this will replace default options with new ones without changing default
- options=dict(list(self.options.items())+list(kwargs.items()))
-
- xpos=kwargs.get("xpos",None)
- column=EvColumn(*args,**options)
- wtable=self.ncols
- htable=self.nrows
-
- header=kwargs.get("header",None)
- ifheader:
- column.add_rows(str(header),ypos=0,**options)
- self.header=True
- elifself.header:
- # we have a header already. Offset
- column.add_rows("",ypos=0,**options)
-
- # Calculate whether the new column needs to expand to the
- # current table size, or if the table needs to expand to
- # the column size.
- # This needs to happen after the header rows have already been
- # added to the column in order for the size calculations to match.
- excess=len(column)-htable
- ifexcess>0:
- # we need to add new rows to table
- forcolinself.table:
- empty_rows=[""for_inrange(excess)]
- col.add_rows(*empty_rows,**options)
- self.nrows+=excess
- elifexcess<0:
- # we need to add new rows to new column
- empty_rows=[""for_inrange(abs(excess))]
- column.add_rows(*empty_rows,**options)
- self.nrows-=excess
-
- ifxposisNoneorxpos>wtable-1:
- # add to the end
- self.table.append(column)
- else:
- # insert column
- xpos=min(wtable-1,max(0,int(xpos)))
- self.table.insert(xpos,column)
- self.ncols+=1
- # self._balance()
-
-
[docs]defadd_row(self,*args,**kwargs):
- """
- Add a row to table (not a header). If there are more cells in
- the given row than there are cells in the current table the
- table will be expanded with empty columns to match. These will
- be added to the end of the table. In the same way, adding a
- line with too few cells will lead to the last ones getting
- padded.
-
- Args:
- args (str): Any number of string argumnets to use as the
- data in the row (one cell per argument).
- ypos (int, optional): Index position in table before which to
- input new row. If not given, will be added to the end of the table.
- Uses Python indexing (so first row is `ypos=0`)
-
- Keyword Args:
- Other keywords are as per `EvCell.__init__`.
-
- """
- # this will replace default options with new ones without changing default
- row=list(args)
- options=dict(list(self.options.items())+list(kwargs.items()))
-
- ypos=kwargs.get("ypos",None)
- wtable=self.ncols
- htable=self.nrows
- excess=len(row)-wtable
-
- ifexcess>0:
- # we need to add new empty columns to table
- empty_rows=[""for_inrange(htable)]
- self.table.extend([EvColumn(*empty_rows,**options)for_inrange(excess)])
- self.ncols+=excess
- elifexcess<0:
- # we need to add more cells to row
- row.extend([""for_inrange(abs(excess))])
- self.ncols-=excess
-
- ifyposisNoneorypos>htable-1:
- # add new row to the end
- foricol,colinenumerate(self.table):
- col.add_rows(row[icol],**options)
- else:
- # insert row elsewhere
- ypos=min(htable-1,max(0,int(ypos)))
- foricol,colinenumerate(self.table):
- col.add_rows(row[icol],ypos=ypos,**options)
- self.nrows+=1
- # self._balance()
-
-
[docs]defreformat(self,**kwargs):
- """
- Force a re-shape of the entire table.
-
- Keyword Args:
- Table options as per `EvTable.__init__`.
-
- """
- self.width=kwargs.pop("width",self.width)
- self.height=kwargs.pop("height",self.height)
- forkey,valueinkwargs.items():
- setattr(self,key,value)
-
- hchar=kwargs.pop("header_line_char",self.header_line_char)
-
- # border settings are also passed on into EvCells (so kwargs.get, not kwargs.pop)
- self.header_line_char=hchar[0]ifhcharelseself.header_line_char
- self.border_width=kwargs.get("border_width",self.border_width)
- self.corner_char=kwargs.get("corner_char",self.corner_char)
- self.header_line_char=kwargs.get("header_line_char",self.header_line_char)
-
- self.corner_top_left_char=_to_ansi(kwargs.pop("corner_top_left_char",self.corner_char))
- self.corner_top_right_char=_to_ansi(kwargs.pop("corner_top_right_char",self.corner_char))
- self.corner_bottom_left_char=_to_ansi(
- kwargs.pop("corner_bottom_left_char",self.corner_char)
- )
- self.corner_bottom_right_char=_to_ansi(
- kwargs.pop("corner_bottom_right_char",self.corner_char)
- )
-
- self.options.update(kwargs)
-
-
[docs]defreformat_column(self,index,**kwargs):
- """
- Sends custom options to a specific column in the table.
-
- Args:
- index (int): Which column to reformat. The column index is
- given from 0 to Ncolumns-1.
-
- Keyword Args:
- Column options as per `EvCell.__init__`.
-
- Raises:
- Exception: if an invalid index is found.
-
- """
- ifindex>len(self.table):
- raiseException("Not a valid column index")
- # we update the columns' options which means eventual width/height
- # will be 'locked in' and withstand auto-balancing width/height from the table later
- self.table[index].options.update(kwargs)
- self.table[index].reformat(**kwargs)
-
-
[docs]defget(self):
- """
- Return lines of table as a list.
-
- Returns:
- table_lines (list): The lines of the table, in order.
-
- """
- return[lineforlineinself._generate_lines()]
-
- def__str__(self):
- """print table (this also balances it)"""
- # h = "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
- returnstr(str(ANSIString("\n").join([lineforlineinself._generate_lines()])))
-
-
-def_test():
- """Test"""
- table=EvTable(
- "|yHeading1|n",
- "|gHeading2|n",
- table=[[1,2,3],[4,5,6],[7,8,9]],
- border="cells",
- align="l",
- )
- table.add_column("|rThis is long data|n","|bThis is even longer data|n")
- table.add_row("This is a single row")
- print(str(table))
- table.reformat(width=50)
- print(str(table))
- table.reformat_column(3,width=30,align="r")
- print(str(table))
- returntable
-
-
-def_test2():
- table=EvTable("|yHeading1|n","|B|[GHeading2|n","Heading3")
- foriinrange(100):
- table.add_row(
- "This is col 0, row %i"%i,
- "|gThis is col 1, row |w%i|n|g.|n"%i,
- "This is col 2, row %i"%i,
- )
- returntable
-
-"""
-The gametime module handles the global passage of time in the mud.
-
-It also supplies some useful methods to convert between
-in-mud time and real-world time as well allows to get the
-total runtime of the server and the current uptime.
-
-"""
-
-importtime
-fromdatetimeimportdatetime,timedelta
-
-fromdjango.db.utilsimportOperationalError
-fromdjango.confimportsettings
-fromevenniaimportDefaultScript
-fromevennia.server.modelsimportServerConfig
-fromevennia.utils.createimportcreate_script
-
-# Speed-up factor of the in-game time compared
-# to real time.
-
-TIMEFACTOR=settings.TIME_FACTOR
-IGNORE_DOWNTIMES=settings.TIME_IGNORE_DOWNTIMES
-
-
-# Only set if gametime_reset was called at some point.
-try:
- GAME_TIME_OFFSET=ServerConfig.objects.conf("gametime_offset",default=0)
-exceptOperationalError:
- # the db is not initialized
- print("Gametime offset could not load - db not set up.")
- GAME_TIME_OFFSET=0
-
-# Common real-life time measure, in seconds.
-# You should not change this.
-
-# these are kept updated by the server maintenance loop
-SERVER_START_TIME=0.0
-SERVER_RUNTIME_LAST_UPDATED=0.0
-SERVER_RUNTIME=0.0
-
-# note that these should not be accessed directly since they may
-# need further processing. Access from server_epoch() and game_epoch().
-_SERVER_EPOCH=None
-_GAME_EPOCH=None
-
-# Helper Script dealing in gametime (created by `schedule` function
-# below).
-
-
-
[docs]defruntime():
- """
- Get the total runtime of the server since first start (minus
- downtimes)
-
- Args:
- format (bool, optional): Format into a time representation.
-
- Returns:
- time (float or tuple): The runtime or the same time split up
- into time units.
-
- """
- returnSERVER_RUNTIME+time.time()-SERVER_RUNTIME_LAST_UPDATED
-
-
-
[docs]defserver_epoch():
- """
- Get the server epoch. We may need to calculate this on the fly.
-
- """
- global_SERVER_EPOCH
- ifnot_SERVER_EPOCH:
- _SERVER_EPOCH=(
- ServerConfig.objects.conf("server_epoch",default=None)ortime.time()-runtime()
- )
- return_SERVER_EPOCH
-
-
-
[docs]defuptime():
- """
- Get the current uptime of the server since last reload
-
- Args:
- format (bool, optional): Format into time representation.
-
- Returns:
- time (float or tuple): The uptime or the same time split up
- into time units.
-
- """
- returntime.time()-SERVER_START_TIME
-
-
-
[docs]defportal_uptime():
- """
- Get the current uptime of the portal.
-
- Returns:
- time (float): The uptime of the portal.
- """
- fromevennia.server.sessionhandlerimportSESSIONS
-
- returntime.time()-SESSIONS.portal_start_time
-
-
-
[docs]defgame_epoch():
- """
- Get the game epoch.
-
- """
- game_epoch=settings.TIME_GAME_EPOCH
- returngame_epochifgame_epochisnotNoneelseserver_epoch()
-
-
-
[docs]defgametime(absolute=False):
- """
- Get the total gametime of the server since first start (minus downtimes)
-
- Args:
- absolute (bool, optional): Get the absolute game time, including
- the epoch. This could be converted to an absolute in-game
- date.
-
- Returns:
- time (float): The gametime as a virtual timestamp.
-
- Notes:
- If one is using a standard calendar, one could convert the unformatted
- return to a date using Python's standard `datetime` module like this:
- `datetime.datetime.fromtimestamp(gametime(absolute=True))`
-
- """
- epoch=game_epoch()ifabsoluteelse0
- ifIGNORE_DOWNTIMES:
- gtime=epoch+(time.time()-server_epoch())*TIMEFACTOR
- else:
- gtime=epoch+(runtime()-GAME_TIME_OFFSET)*TIMEFACTOR
- returngtime
-
-
-
[docs]defreal_seconds_until(sec=None,min=None,hour=None,day=None,month=None,year=None):
- """
- Return the real seconds until game time.
-
- Args:
- sec (int or None): number of absolute seconds.
- min (int or None): number of absolute minutes.
- hour (int or None): number of absolute hours.
- day (int or None): number of absolute days.
- month (int or None): number of absolute months.
- year (int or None): number of absolute years.
-
- Returns:
- The number of real seconds before the given game time is up.
-
- Example:
- real_seconds_until(hour=5, min=10, sec=0)
-
- If the game time is 5:00, TIME_FACTOR is set to 2 and you ask
- the number of seconds until it's 5:10, then this function should
- return 300 (5 minutes).
-
-
- """
- current=datetime.fromtimestamp(gametime(absolute=True))
- s_sec=secifsecisnotNoneelsecurrent.second
- s_min=minifminisnotNoneelsecurrent.minute
- s_hour=hourifhourisnotNoneelsecurrent.hour
- s_day=dayifdayisnotNoneelsecurrent.day
- s_month=monthifmonthisnotNoneelsecurrent.month
- s_year=yearifyearisnotNoneelsecurrent.year
- projected=datetime(s_year,s_month,s_day,s_hour,s_min,s_sec)
-
- ifprojected<=current:
- # We increase one unit of time depending on parameters
- ifmonthisnotNone:
- projected=projected.replace(year=s_year+1)
- elifdayisnotNone:
- try:
- projected=projected.replace(month=s_month+1)
- exceptValueError:
- projected=projected.replace(month=1)
- elifhourisnotNone:
- projected+=timedelta(days=1)
- elifminisnotNone:
- projected+=timedelta(seconds=3600)
- else:
- projected+=timedelta(seconds=60)
-
- # Get the number of gametime seconds between these two dates
- seconds=(projected-current).total_seconds()
- returnseconds/TIMEFACTOR
-
-
-
[docs]defschedule(
- callback,repeat=False,sec=None,min=None,hour=None,day=None,month=None,year=None,
- *args,**kwargs
-):
- """
- Call a callback at a given in-game time.
-
- Args:
- callback (function): The callback function that will be called. Note
- that the callback must be a module-level function, since the script will
- be persistent. The callable should be on form `callable(*args, **kwargs)`
- where args/kwargs are passed into this schedule.
- repeat (bool, optional): Defines if the callback should be called regularly
- at the specified time.
- sec (int or None): Number of absolute game seconds at which to run repeat.
- min (int or None): Number of absolute minutes.
- hour (int or None): Number of absolute hours.
- day (int or None): Number of absolute days.
- month (int or None): Number of absolute months.
- year (int or None): Number of absolute years.
- *args, **kwargs: Will be passed into the callable. These must be possible
- to store in Attributes on the generated scheduling Script.
-
- Returns:
- Script: The created Script handling the scheduling.
-
- Examples:
- ::
-
- schedule(func, min=5, sec=0) # Will call 5 minutes past the next (in-game) hour.
- schedule(func, hour=2, min=30, sec=0) # Will call the next (in-game) day at 02:30.
-
- """
- seconds=real_seconds_until(sec=sec,min=min,hour=hour,day=day,month=month,year=year)
- script=create_script(
- "evennia.utils.gametime.TimeScript",
- key="TimeScript",
- desc="A gametime-sensitive script",
- interval=seconds,
- start_delay=True,
- repeats=-1ifrepeatelse1,
- )
- script.db.callback=callback
- script.db.gametime={
- "sec":sec,
- "min":min,
- "hour":hour,
- "day":day,
- "month":month,
- "year":year,
- }
- script.db.schedule_args=args
- script.db.schedule_kwargs=kwargs
- returnscript
-
-
-
[docs]defreset_gametime():
- """
- Resets the game time to make it start from the current time. Note that
- the epoch set by `settings.TIME_GAME_EPOCH` will still apply.
-
- """
- globalGAME_TIME_OFFSET
- GAME_TIME_OFFSET=runtime()
- ServerConfig.objects.conf("gametime_offset",GAME_TIME_OFFSET)
-"""
-IDmapper extension to the default manager.
-"""
-fromdjango.db.models.managerimportManager
-
-
-
[docs]classSharedMemoryManager(Manager):
- # TODO: improve on this implementation
- # We need a way to handle reverse lookups so that this model can
- # still use the singleton cache, but the active model isn't required
- # to be a SharedMemoryModel.
-
[docs]defget(self,*args,**kwargs):
- """
- Data entity lookup.
- """
- items=list(kwargs)
- inst=None
- iflen(items)==1:
- # CL: support __exact
- key=items[0]
- ifkey.endswith("__exact"):
- key=key[:-len("__exact")]
- ifkeyin("pk",self.model._meta.pk.attname):
- try:
- inst=self.model.get_cached_instance(kwargs[items[0]])
- # we got the item from cache, but if this is a fk, check it's ours
- ifgetattr(inst,str(self.field).split(".")[-1])!=self.instance:
- inst=None
- exceptException:
- pass
- ifinstisNone:
- inst=super().get(*args,**kwargs)
- returninst
-"""
-Django ID mapper
-
-Modified for Evennia by making sure that no model references
-leave caching unexpectedly (no use of WeakRefs).
-
-Also adds `cache_size()` for monitoring the size of the cache.
-"""
-
-importos
-importthreading
-importgc
-importtime
-fromweakrefimportWeakValueDictionary
-fromtwisted.internet.reactorimportcallFromThread
-fromdjango.core.exceptionsimportObjectDoesNotExist,FieldError
-fromdjango.db.models.signalsimportpost_save
-fromdjango.db.models.baseimportModel,ModelBase
-fromdjango.db.models.signalsimportpre_delete,post_migrate
-fromdjango.db.utilsimportDatabaseError
-fromevennia.utilsimportlogger
-fromevennia.utils.utilsimportdbref,get_evennia_pids,to_str
-
-from.managerimportSharedMemoryManager
-
-AUTO_FLUSH_MIN_INTERVAL=60.0*5# at least 5 mins between cache flushes
-
-_GA=object.__getattribute__
-_SA=object.__setattr__
-_DA=object.__delattr__
-_MONITOR_HANDLER=None
-
-# References to db-updated objects are stored here so the
-# main process can be informed to re-cache itself.
-PROC_MODIFIED_COUNT=0
-PROC_MODIFIED_OBJS=WeakValueDictionary()
-
-# get info about the current process and thread; determine if our
-# current pid is different from the server PID (i.e. # if we are in a
-# subprocess or not)
-_SELF_PID=os.getpid()
-_SERVER_PID,_PORTAL_PID=get_evennia_pids()
-_IS_SUBPROCESS=(_SERVER_PIDand_PORTAL_PID)and_SELF_PIDnotin(_SERVER_PID,_PORTAL_PID)
-_IS_MAIN_THREAD=threading.currentThread().getName()=="MainThread"
-
-
-
[docs]classSharedMemoryModelBase(ModelBase):
- # CL: upstream had a __new__ method that skipped ModelBase's __new__ if
- # SharedMemoryModelBase was not in the model class's ancestors. It's not
- # clear what was the intended purpose, but skipping ModelBase.__new__
- # broke things; in particular, default manager inheritance.
-
- def__call__(cls,*args,**kwargs):
- """
- this method will either create an instance (by calling the default implementation)
- or try to retrieve one from the class-wide cache by inferring the pk value from
- `args` and `kwargs`. If instance caching is enabled for this class, the cache is
- populated whenever possible (ie when it is possible to infer the pk value).
-
- """
-
- defnew_instance():
- 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
- ifinstance_keyisNone:
- returnnew_instance()
- cached_instance=cls.get_cached_instance(instance_key)
- ifcached_instanceisNone:
- cached_instance=new_instance()
- cls.cache_instance(cached_instance,new=True)
- returncached_instance
-
- def_prepare(cls):
- """
- Prepare the cache, making sure that proxies of the same db base
- share the same cache.
-
- """
- # the dbmodel is either the proxy base or ourselves
- dbmodel=cls._meta.concrete_modelifcls._meta.proxyelsecls
- cls.__dbclass__=dbmodel
- ifnothasattr(dbmodel,"__instance_cache__"):
- # we store __instance_cache__ only on the dbmodel base
- dbmodel.__instance_cache__={}
- super()._prepare()
-
- def__new__(cls,name,bases,attrs):
- """
- Field shortcut creation:
-
- Takes field names `db_*` and creates property wrappers named
- without the `db_` prefix. So db_key -> key
-
- This wrapper happens on the class level, so there is no
- overhead when creating objects. If a class already has a
- wrapper of the given name, the automatic creation is skipped.
-
- Notes:
- Remember to document this auto-wrapping in the class
- header, this could seem very much like magic to the user
- otherwise.
- """
-
- attrs["typename"]=cls.__name__
- attrs["path"]="%s.%s"%(attrs["__module__"],name)
- attrs["_is_deleted"]=False
-
- # set up the typeclass handling only if a variable _is_typeclass is set on the class
- defcreate_wrapper(cls,fieldname,wrappername,editable=True,foreignkey=False):
- "Helper method to create property wrappers with unique names (must be in separate call)"
-
- def_get(cls,fname):
- "Wrapper for getting database field"
- if_GA(cls,"_is_deleted"):
- raiseObjectDoesNotExist(
- "Cannot access %s: Hosting object was already deleted."%fname
- )
- return_GA(cls,fieldname)
-
- def_get_foreign(cls,fname):
- "Wrapper for returning foreignkey fields"
- if_GA(cls,"_is_deleted"):
- raiseObjectDoesNotExist(
- "Cannot access %s: Hosting object was already deleted."%fname
- )
- return_GA(cls,fieldname)
-
- def_set_nonedit(cls,fname,value):
- "Wrapper for blocking editing of field"
- raiseFieldError("Field %s cannot be edited."%fname)
-
- def_set(cls,fname,value):
- "Wrapper for setting database field"
- if_GA(cls,"_is_deleted"):
- raiseObjectDoesNotExist(
- "Cannot set %s to %s: Hosting object was already deleted!"%(fname,value)
- )
- _SA(cls,fname,value)
- # only use explicit update_fields in save if we actually have a
- # primary key assigned already (won't be set when first creating object)
- update_fields=(
- [fname]if_GA(cls,"_get_pk_val")(_GA(cls,"_meta"))isnotNoneelseNone
- )
- _GA(cls,"save")(update_fields=update_fields)
-
- def_set_foreign(cls,fname,value):
- "Setter only used on foreign key relations, allows setting with #dbref"
- if_GA(cls,"_is_deleted"):
- raiseObjectDoesNotExist(
- "Cannot set %s to %s: Hosting object was already deleted!"%(fname,value)
- )
- 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)
- dbid=dbref(value,reqhash=False)
- ifdbid:
- model=_GA(cls,"_meta").get_field(fname).model
- try:
- value=model._default_manager.get(id=dbid)
- exceptObjectDoesNotExist:
- # maybe it is just a name that happens to look like a dbid
- pass
- _SA(cls,fname,value)
- # only use explicit update_fields in save if we actually have a
- # primary key assigned already (won't be set when first creating object)
- update_fields=(
- [fname]if_GA(cls,"_get_pk_val")(_GA(cls,"_meta"))isnotNoneelseNone
- )
- _GA(cls,"save")(update_fields=update_fields)
-
- def_del_nonedit(cls,fname):
- "wrapper for not allowing deletion"
- raiseFieldError("Field %s cannot be edited."%fname)
-
- def_del(cls,fname):
- "Wrapper for clearing database field - sets it to None"
- _SA(cls,fname,None)
- update_fields=(
- [fname]if_GA(cls,"_get_pk_val")(_GA(cls,"_meta"))isnotNoneelseNone
- )
- _GA(cls,"save")(update_fields=update_fields)
-
- # wrapper factories
- ifnoteditable:
-
- deffget(cls):
- return_get(cls,fieldname)
-
- deffset(cls,val):
- return_set_nonedit(cls,fieldname,val)
-
- elifforeignkey:
-
- deffget(cls):
- return_get_foreign(cls,fieldname)
-
- deffset(cls,val):
- return_set_foreign(cls,fieldname,val)
-
- else:
-
- deffget(cls):
- return_get(cls,fieldname)
-
- deffset(cls,val):
- return_set(cls,fieldname,val)
-
- deffdel(cls):
- return_del(cls,fieldname)ifeditableelse_del_nonedit(cls,fieldname)
-
- # set docstrings for auto-doc
- fget.__doc__="A wrapper for getting database field `%s`."%fieldname
- fset.__doc__="A wrapper for setting (and saving) database field `%s`."%fieldname
- fdel.__doc__="A wrapper for deleting database field `%s`."%fieldname
- # assigning
- attrs[wrappername]=property(fget,fset,fdel)
- # type(cls).__setattr__(cls, wrappername, property(fget, fset, fdel))#, doc))
-
- # exclude some models that should not auto-create wrapper fields
- ifcls.__name__in("ServerConfig","TypeNick"):
- return
- # dynamically create the wrapper properties for all fields not already handled
- # (manytomanyfields are always handlers)
- forfieldname,fieldin(
- (fname,field)
- forfname,fieldinlist(attrs.items())
- iffname.startswith("db_")andtype(field).__name__!="ManyToManyField"
- ):
- foreignkey=type(field).__name__=="ForeignKey"
- wrappername="dbid"iffieldname=="id"elsefieldname.replace("db_","",1)
- ifwrappernamenotinattrs:
- # makes sure not to overload manually created wrappers on the model
- create_wrapper(
- cls,fieldname,wrappername,editable=field.editable,foreignkey=foreignkey
- )
-
- returnsuper().__new__(cls,name,bases,attrs)
-
-
-
[docs]classSharedMemoryModel(Model,metaclass=SharedMemoryModelBase):
- """
- Base class for idmapped objects. Inherit from `this`.
- """
-
- objects=SharedMemoryManager()
-
-
-
- @classmethod
- def_get_cache_key(cls,args,kwargs):
- """
- This method is used by the caching subsystem to infer the PK
- value from the constructor arguments. It is used to decide if
- an instance has to be built or is already in the cache.
-
- """
- result=None
- # Quick hack for my composites work for now.
- ifhasattr(cls._meta,"pks"):
- 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
- pk_position=cls._meta.fields.index(pk)
- iflen(args)>pk_position:
- # if it's in the args, we can get it easily by index
- result=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.
- 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
- result=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
- result=result._get_pk_val()
- returnresult
-
-
[docs]@classmethod
- defget_cached_instance(cls,id):
- """
- Method to retrieve a cached instance by pk value. Returns None
- when not found (which will always be the case when caching is
- disabled for this class). Please note that the lookup will be
- done even when instance caching is disabled.
-
- """
- returncls.__dbclass__.__instance_cache__.get(id)
-
-
[docs]@classmethod
- defcache_instance(cls,instance,new=False):
- """
- Method to store an instance in the cache.
-
- Args:
- instance (Class instance): the instance to cache.
- new (bool, optional): this is the first time this instance is
- cached (i.e. this is not an update operation like after a
- db save).
-
- """
- pk=instance._get_pk_val()
- ifpkisnotNone:
- cls.__dbclass__.__instance_cache__[pk]=instance
- ifnew:
- try:
- # trigger the at_init hook only
- # at first initialization
- instance.at_init()
- exceptAttributeError:
- # The at_init hook is not assigned to all entities
- pass
-
-
[docs]@classmethod
- defget_all_cached_instances(cls):
- """
- Return the objects so far cached by idmapper for this class.
-
- """
- returnlist(cls.__dbclass__.__instance_cache__.values())
-
- @classmethod
- def_flush_cached_by_key(cls,key,force=True):
- """
- Remove the cached reference.
-
- """
- try:
- ifforceorcls.at_idmapper_flush():
- delcls.__dbclass__.__instance_cache__[key]
- else:
- cls._dbclass__.__instance_cache__[key].refresh_from_db()
- exceptKeyError:
- # No need to remove if cache doesn't contain it already
- pass
-
-
[docs]@classmethod
- defflush_cached_instance(cls,instance,force=True):
- """
- Method to flush an instance from the cache. The instance will
- always be flushed from the cache, since this is most likely
- called from delete(), and we want to make sure we don't cache
- dead objects.
-
- """
- cls._flush_cached_by_key(instance._get_pk_val(),force=force)
[docs]defat_idmapper_flush(self):
- """
- This is called when the idmapper cache is flushed and
- allows customized actions when this happens.
-
- Returns:
- do_flush (bool): If True, flush this object as normal. If
- False, don't flush and expect this object to handle
- the flushing on its own.
- """
- returnTrue
-
-
[docs]defflush_from_cache(self,force=False):
- """
- Flush this instance from the instance cache. Use
- `force` to override the result of at_idmapper_flush() for the object.
-
- """
- pk=self._get_pk_val()
- ifpk:
- ifforceorself.at_idmapper_flush():
- self.__class__.__dbclass__.__instance_cache__.pop(pk,None)
[docs]defsave(self,*args,**kwargs):
- """
- Central database save operation.
-
- Notes:
- Arguments as per Django documentation.
- Calls `self.at_<fieldname>_postsave(new)`
- (this is a wrapper set by oobhandler:
- self._oob_at_<fieldname>_postsave())
-
- """
- global_MONITOR_HANDLER
- ifnot_MONITOR_HANDLER:
- fromevennia.scripts.monitorhandlerimportMONITOR_HANDLERas_MONITOR_HANDLER
-
- if_IS_SUBPROCESS:
- # we keep a store of objects modified in subprocesses so
- # we know to update their caches in the central process
- globalPROC_MODIFIED_COUNT,PROC_MODIFIED_OBJS
- PROC_MODIFIED_COUNT+=1
- PROC_MODIFIED_OBJS[PROC_MODIFIED_COUNT]=self
-
- if_IS_MAIN_THREAD:
- # in main thread - normal operation
- try:
- super().save(*args,**kwargs)
- exceptDatabaseError:
- # we handle the 'update_fields did not update any rows' error that
- # may happen due to timing issues with attributes
- ufields_removed=kwargs.pop("update_fields",None)
- ifufields_removed:
- super().save(*args,**kwargs)
- else:
- raise
- else:
- # in another thread; make sure to save in reactor thread
- def_save_callback(cls,*args,**kwargs):
- super().save(*args,**kwargs)
-
- callFromThread(_save_callback,self,*args,**kwargs)
-
- ifnotself.pk:
- # this can happen if some of the startup methods immediately
- # delete the object (an example are Scripts that start and die immediately)
- return
-
- # update field-update hooks and eventual OOB watchers
- new=False
- if"update_fields"inkwargsandkwargs["update_fields"]:
- # get field objects from their names
- update_fields=(
- self._meta.get_field(fieldname)forfieldnameinkwargs.get("update_fields")
- )
- else:
- # meta.fields are already field objects; get them all
- new=True
- update_fields=self._meta.fields
- forfieldinupdate_fields:
- fieldname=field.name
- # trigger eventual monitors
- _MONITOR_HANDLER.at_update(self,fieldname)
- # if a hook is defined it must be named exactly on this form
- hookname="at_%s_postsave"%fieldname
- ifhasattr(self,hookname)andcallable(_GA(self,hookname)):
- _GA(self,hookname)(new)
-
- # # if a trackerhandler is set on this object, update it with the
- # # fieldname and the new value
- # fieldtracker = "_oob_at_%s_postsave" % fieldname
- # if hasattr(self, fieldtracker):
- # _GA(self, fieldtracker)(fieldname)
- pass
-
-
-
[docs]classWeakSharedMemoryModelBase(SharedMemoryModelBase):
- """
- Uses a WeakValue dictionary for caching instead of a regular one.
-
- """
-
- def_prepare(cls):
- super()._prepare()
- cls.__dbclass__.__instance_cache__=WeakValueDictionary()
-
-
-
[docs]classWeakSharedMemoryModel(SharedMemoryModel,metaclass=WeakSharedMemoryModelBase):
- """
- Uses a WeakValue dictionary for caching instead of a regular one
-
- """
-
-
[docs]defflush_cache(**kwargs):
- """
- Flush idmapper cache. When doing so the cache will fire the
- at_idmapper_flush hook to allow the object to optionally handle
- its own flushing.
-
- Uses a signal so we make sure to catch cascades.
-
- """
-
- defclass_hierarchy(clslist):
- """Recursively yield a class hierarchy"""
- forclsinclslist:
- subclass_list=cls.__subclasses__()
- ifsubclass_list:
- forsubclsinclass_hierarchy(subclass_list):
- yieldsubcls
- else:
- yieldcls
-
- forclsinclass_hierarchy([SharedMemoryModel]):
- cls.flush_instance_cache()
- # run the python garbage collector
- returngc.collect()
[docs]defflush_cached_instance(sender,instance,**kwargs):
- """
- Flush the idmapper cache only for a given instance.
-
- """
- # XXX: Is this the best way to make sure we can flush?
- ifnothasattr(instance,"flush_cached_instance"):
- return
- sender.flush_cached_instance(instance,force=True)
[docs]defconditional_flush(max_rmem,force=False):
- """
- Flush the cache if the estimated memory usage exceeds `max_rmem`.
-
- The flusher has a timeout to avoid flushing over and over
- in particular situations (this means that for some setups
- the memory usage will exceed the requirement and a server with
- more memory is probably required for the given game).
-
- Args:
- max_rmem (int): memory-usage estimation-treshold after which
- cache is flushed.
- force (bool, optional): forces a flush, regardless of timeout.
- Defaults to `False`.
-
- """
- globalLAST_FLUSH
-
- defmem2cachesize(desired_rmem):
- """
- Estimate the size of the idmapper cache based on the memory
- desired. This is used to optionally cap the cache size.
-
- desired_rmem - memory in MB (minimum 50MB)
-
- The formula is empirically estimated from usage tests (Linux)
- and is
- Ncache = RMEM - 35.0 / 0.0157
- where RMEM is given in MB and Ncache is the size of the cache
- for this memory usage. VMEM tends to be about 100MB higher
- than RMEM for large memory usage.
- """
- vmem=max(desired_rmem,50.0)
- Ncache=int(abs(float(vmem)-35.0)/0.0157)
- returnNcache
-
- ifnotmax_rmem:
- # auto-flush is disabled
- return
-
- now=time.time()
- ifnotLAST_FLUSH:
- # server is just starting
- LAST_FLUSH=now
- return
-
- if((now-LAST_FLUSH)<AUTO_FLUSH_MIN_INTERVAL)andnotforce:
- # too soon after last flush.
- logger.log_warn(
- "Warning: Idmapper flush called more than "
- "once in %s min interval. Check memory usage."%(AUTO_FLUSH_MIN_INTERVAL/60.0)
- )
- return
-
- ifos.name=="nt":
- # we can't look for mem info in Windows at the moment
- return
-
- # check actual memory usage
- Ncache_max=mem2cachesize(max_rmem)
- Ncache,_=cache_size()
- actual_rmem=(
- float(os.popen("ps -p %d -o %s | tail -1"%(os.getpid(),"rss")).read())/1000.0
- )# resident memory
-
- ifNcache>=Ncache_maxandactual_rmem>max_rmem*0.9:
- # flush cache when number of objects in cache is big enough and our
- # actual memory use is within 10% of our set max
- flush_cache()
- LAST_FLUSH=now
-
-
-
[docs]defcache_size(mb=True):
- """
- Calculate statistics about the cache.
-
- Note: we cannot get reliable memory statistics from the cache -
- whereas we could do `getsizof` each object in cache, the result is
- highly imprecise and for a large number of objects the result is
- many times larger than the actual memory usage of the entire server;
- Python is clearly reusing memory behind the scenes that we cannot
- catch in an easy way here. Ideas are appreciated. /Griatch
-
- Returns:
- total_num, {objclass:total_num, ...}
-
- """
- numtotal=[0]# use mutable to keep reference through recursion
- classdict={}
-
- defget_recurse(submodels):
- forsubmodelinsubmodels:
- subclasses=submodel.__subclasses__()
- ifnotsubclasses:
- num=len(submodel.get_all_cached_instances())
- numtotal[0]+=num
- classdict[submodel.__dbclass__.__name__]=num
- else:
- get_recurse(subclasses)
-
- get_recurse(SharedMemoryModel.__subclasses__())
- returnnumtotal[0],classdict
-
- # article_list = Article.objects.all().select_related('category')
- # last_article = article_list[0]
- # for article in article_list[1:]:
- # self.assertEquals(article.category2 is last_article.category2, False)
- # last_article = article
-
-
[docs]deftestObjectDeletion(self):
- # This must execute first so its guaranteed to be in memory.
- list(Article.objects.all().select_related("category"))
-
- article=Article.objects.all()[0:1].get()
- pk=article.pk
- article.delete()
- self.assertEqual(pknotinArticle.__instance_cache__,True)
-"""
-Logging facilities
-
-These are thin wrappers on top of Twisted's logging facilities; logs
-are all directed either to stdout (if Evennia is running in
-interactive mode) or to $GAME_DIR/server/logs.
-
-The log_file() function uses its own threading system to log to
-arbitrary files in $GAME_DIR/server/logs.
-
-Note: All logging functions have two aliases, log_type() and
-log_typemsg(). This is for historical, back-compatible reasons.
-
-"""
-
-
-importos
-importtime
-fromdatetimeimportdatetime
-fromtracebackimportformat_exc
-fromtwisted.pythonimportlog,logfile
-fromtwisted.pythonimportutilastwisted_util
-fromtwisted.internet.threadsimportdeferToThread
-
-
-_LOGDIR=None
-_LOG_ROTATE_SIZE=None
-_TIMEZONE=None
-_CHANNEL_LOG_NUM_TAIL_LINES=None
-
-
-# logging overrides
-
-
-
[docs]deftimeformat(when=None):
- """
- This helper function will format the current time in the same
- way as the twisted logger does, including time zone info. Only
- difference from official logger is that we only use two digits
- for the year and don't show timezone for CET times.
-
- Args:
- when (int, optional): This is a time in POSIX seconds on the form
- given by time.time(). If not given, this function will
- use the current time.
-
- Returns:
- timestring (str): A formatted string of the given time.
-
- """
- when=whenifwhenelsetime.time()
-
- # time zone offset: UTC - the actual offset
- tz_offset=datetime.utcfromtimestamp(when)-datetime.fromtimestamp(when)
- tz_offset=tz_offset.days*86400+tz_offset.seconds
- # correct given time to utc
- when=datetime.utcfromtimestamp(when-tz_offset)
-
- iftz_offset==0:
- tz=""
- else:
- tz_hour=abs(int(tz_offset//3600))
- tz_mins=abs(int(tz_offset//60%60))
- tz_sign="-"iftz_offset>=0else"+"
- tz="%s%02d%s"%(tz_sign,tz_hour,(":%02d"%tz_minsiftz_minselse""))
-
- return"%d-%02d-%02d%02d:%02d:%02d%s"%(
- when.year-2000,
- when.month,
- when.day,
- when.hour,
- when.minute,
- when.second,
- tz,
- )
-
-
-
[docs]classWeeklyLogFile(logfile.DailyLogFile):
- """
- Log file that rotates once per week by default. Overrides key methods to change format.
-
- """
-
-
[docs]def__init__(self,name,directory,defaultMode=None,day_rotation=7,max_size=1000000):
- """
- Args:
- name (str): Name of log file.
- directory (str): Directory holding the file.
- defaultMode (str): Permissions used to create file. Defaults to
- current permissions of this file if it exists.
- day_rotation (int): How often to rotate the file.
- max_size (int): Max size of log file before rotation (regardless of
- time). Defaults to 1M.
-
- """
- self.day_rotation=day_rotation
- self.max_size=max_size
- self.size=0
- logfile.DailyLogFile.__init__(self,name,directory,defaultMode=defaultMode)
[docs]defshouldRotate(self):
- """Rotate when the date has changed since last write"""
- # all dates here are tuples (year, month, day)
- now=self.toDate()
- then=self.lastDate
- return(
- now[0]>then[0]
- ornow[1]>then[1]
- ornow[2]>(then[2]+self.day_rotation)
- orself.size>=self.max_size
- )
-
-
[docs]defsuffix(self,tupledate):
- """Return the suffix given a (year, month, day) tuple or unixtime.
- Format changed to have 03 for march instead of 3 etc (retaining unix
- file order)
-
- If we get duplicate suffixes in location (due to hitting size limit),
- we append __1, __2 etc.
-
- Examples:
- server.log.2020_01_29
- server.log.2020_01_29__1
- server.log.2020_01_29__2
-
- """
- suffix=""
- copy_suffix=0
- whileTrue:
- try:
- suffix="_".join(["{:02d}".format(part)forpartintupledate])
- exceptException:
- # try taking a float unixtime
- suffix="_".join(["{:02d}".format(part)forpartinself.toDate(tupledate)])
-
- suffix+=f"__{copy_suffix}"ifcopy_suffixelse""
-
- ifos.path.exists(f"{self.path}.{suffix}"):
- # Append a higher copy_suffix to try to break the tie (starting from 2)
- copy_suffix+=1
- else:
- break
- returnsuffix
-
-
[docs]defwrite(self,data):
- """
- Write data to log file
-
- """
- logfile.BaseLogFile.write(self,data)
- self.lastDate=max(self.lastDate,self.toDate())
- self.size+=len(data)
[docs]deflog_msg(msg):
- """
- Wrapper around log.msg call to catch any exceptions that might
- occur in logging. If an exception is raised, we'll print to
- stdout instead.
-
- Args:
- msg: The message that was passed to log.msg
-
- """
- try:
- log.msg(msg)
- exceptException:
- print("Exception raised while writing message to log. Original message: %s"%msg)
-
-
-
[docs]deflog_trace(errmsg=None):
- """
- Log a traceback to the log. This should be called from within an
- exception.
-
- Args:
- errmsg (str, optional): Adds an extra line with added info
- at the end of the traceback in the log.
-
- """
- tracestring=format_exc()
- try:
- iftracestring:
- forlineintracestring.splitlines():
- log.msg("[::] %s"%line)
- iferrmsg:
- try:
- errmsg=str(errmsg)
- exceptExceptionase:
- errmsg=str(e)
- forlineinerrmsg.splitlines():
- log_msg("[EE] %s"%line)
- exceptException:
- log_msg("[EE] %s"%errmsg)
-
-
-log_tracemsg=log_trace
-
-
-
[docs]deflog_err(errmsg):
- """
- Prints/logs an error message to the server log.
-
- Args:
- errmsg (str): The message to be logged.
-
- """
- try:
- errmsg=str(errmsg)
- exceptExceptionase:
- errmsg=str(e)
- forlineinerrmsg.splitlines():
- log_msg("[EE] %s"%line)
[docs]deflog_server(servermsg):
- """
- This is for the Portal to log captured Server stdout messages (it's
- usually only used during startup, before Server log is open)
-
- """
- try:
- servermsg=str(servermsg)
- exceptExceptionase:
- servermsg=str(e)
- forlineinservermsg.splitlines():
- log_msg("[Server] %s"%line)
-
-
-
[docs]deflog_warn(warnmsg):
- """
- Prints/logs any warnings that aren't critical but should be noted.
-
- Args:
- warnmsg (str): The message to be logged.
-
- """
- try:
- warnmsg=str(warnmsg)
- exceptExceptionase:
- warnmsg=str(e)
- forlineinwarnmsg.splitlines():
- log_msg("[WW] %s"%line)
[docs]deflog_info(infomsg):
- """
- Prints any generic debugging/informative info that should appear in the log.
-
- infomsg: (string) The message to be logged.
-
- """
- try:
- infomsg=str(infomsg)
- exceptExceptionase:
- infomsg=str(e)
- forlineininfomsg.splitlines():
- log_msg("[..] %s"%line)
-
-
-log_infomsg=log_info
-
-
-
[docs]deflog_dep(depmsg):
- """
- Prints a deprecation message.
-
- Args:
- depmsg (str): The deprecation message to log.
-
- """
- try:
- depmsg=str(depmsg)
- exceptExceptionase:
- depmsg=str(e)
- forlineindepmsg.splitlines():
- log_msg("[DP] %s"%line)
-
-
-log_depmsg=log_dep
-
-
-
[docs]deflog_sec(secmsg):
- """
- Prints a security-related message.
-
- Args:
- secmsg (str): The security message to log.
-
- """
- try:
- secmsg=str(secmsg)
- exceptExceptionase:
- secmsg=str(e)
- forlineinsecmsg.splitlines():
- log_msg("[SS] %s"%line)
[docs]classEvenniaLogFile(logfile.LogFile):
- """
- A rotating logfile based off Twisted's LogFile. It overrides
- 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
- # from django as possible.
- global_CHANNEL_LOG_NUM_TAIL_LINES
- if_CHANNEL_LOG_NUM_TAIL_LINESisNone:
- fromdjango.confimportsettings
-
- _CHANNEL_LOG_NUM_TAIL_LINES=settings.CHANNEL_LOG_NUM_TAIL_LINES
- num_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=(num_lines_to_append
- ifnum_lines_to_appendisnotNone
- elseself.num_lines_to_append)
- ifnotappend_tail:
- logfile.LogFile.rotate(self)
- return
- lines=tail_log_file(self.path,0,self.num_lines_to_append)
- super().rotate()
- forlineinlines:
- self.write(line)
-
-
[docs]defseek(self,*args,**kwargs):
- """
- 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)
-
-
[docs]defreadlines(self,*args,**kwargs):
- """
- 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.
-
- """
- lines=[]
- forlineinself._file.readlines(*args,**kwargs):
- try:
- lines.append(line.decode("utf-8"))
- exceptUnicodeDecodeError:
- try:
- lines.append(str(line))
- exceptException:
- lines.append("")
- returnlines
-
-
-_LOG_FILE_HANDLES={}# holds open log handles
-_LOG_FILE_HANDLE_COUNTS={}
-_LOG_FILE_HANDLE_RESET=500
-
-
-def_open_log_file(filename):
- """
- Helper to open the log file (always in the log dir) and cache its
- handle. Will create a new file in the log dir if one didn't
- exist.
-
- To avoid keeping the filehandle open indefinitely we reset it every
- _LOG_FILE_HANDLE_RESET accesses. This may help resolve issues for very
- long uptimes and heavy log use.
-
- """
- # we delay import of settings to keep logger module as free
- # from django as possible.
- global_LOG_FILE_HANDLES,_LOG_FILE_HANDLE_COUNTS,_LOGDIR,_LOG_ROTATE_SIZE
- ifnot_LOGDIR:
- fromdjango.confimportsettings
-
- _LOGDIR=settings.LOG_DIR
- _LOG_ROTATE_SIZE=settings.CHANNEL_LOG_ROTATE_SIZE
-
- filename=os.path.join(_LOGDIR,filename)
- iffilenamein_LOG_FILE_HANDLES:
- _LOG_FILE_HANDLE_COUNTS[filename]+=1
- if_LOG_FILE_HANDLE_COUNTS[filename]>_LOG_FILE_HANDLE_RESET:
- # close/refresh handle
- _LOG_FILE_HANDLES[filename].close()
- del_LOG_FILE_HANDLES[filename]
- else:
- # return cached handle
- return_LOG_FILE_HANDLES[filename]
- try:
- filehandle=EvenniaLogFile.fromFullPath(filename,rotateLength=_LOG_ROTATE_SIZE)
- # filehandle = open(filename, "a+") # append mode + reading
- _LOG_FILE_HANDLES[filename]=filehandle
- _LOG_FILE_HANDLE_COUNTS[filename]=0
- returnfilehandle
- exceptIOError:
- log_trace()
- returnNone
-
-
-
[docs]deflog_file(msg,filename="game.log"):
- """
- Arbitrary file logger using threads.
-
- Args:
- msg (str): String to append to logfile.
- filename (str, optional): Defaults to 'game.log'. All logs
- will appear in the logs directory and log entries will start
- on new lines following datetime info.
-
- """
-
- defcallback(filehandle,msg):
- """Writing to file and flushing result"""
- msg="\n%s [-] %s"%(timeformat(),msg.strip())
- filehandle.write(msg)
- # since we don't close the handle, we need to flush
- # manually or log file won't be written to until the
- # write buffer is full.
- filehandle.flush()
-
- deferrback(failure):
- """Catching errors to normal log"""
- log_trace()
-
- # save to server/logs/ directory
- filehandle=_open_log_file(filename)
- iffilehandle:
- deferToThread(callback,filehandle,msg).addErrback(errback)
-
-
-
[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.
-
- Args:
- filename (str): The name of the log file, presumed to be in
- the Evennia log dir.
- offset (int): The line offset *from the end of the file* to start
- reading from. 0 means to start at the latest entry.
- nlines (int): How many lines to return, counting backwards
- from the offset. If file is shorter, will get all lines.
- callback (callable, optional): A function to manage the result of the
- asynchronous file access. This will get a list of lines. If unset,
- the tail will happen synchronously.
-
- Returns:
- lines (deferred or list): This will be a deferred if `callable` is given,
- otherwise it will be a list with The nline entries from the end of the file, or
- all if the file is shorter than nlines.
-
- """
-
- defseek_file(filehandle,offset,nlines,callback):
- """step backwards in chunks and stop only when we have enough lines"""
- lines_found=[]
- buffer_size=4098
- block_count=-1
- whilelen(lines_found)<(offset+nlines):
- try:
- # scan backwards in file, starting from the end
- filehandle.seek(block_count*buffer_size,os.SEEK_END)
- exceptIOError:
- # file too small for this seek, take what we've got
- filehandle.seek(0)
- lines_found=filehandle.readlines()
- break
- lines_found=filehandle.readlines()
- block_count-=1
- # return the right number of lines
- lines_found=lines_found[-nlines-offset:-offsetifoffsetelseNone]
- ifcallback:
- callback(lines_found)
- returnNone
- else:
- returnlines_found
-
- deferrback(failure):
- """Catching errors to normal log"""
- log_trace()
-
- filehandle=_open_log_file(filename)
- iffilehandle:
- ifcallback:
- returndeferToThread(seek_file,filehandle,offset,nlines,callback).addErrback(
- errback
- )
- else:
- returnseek_file(filehandle,offset,nlines,callback)
- else:
- returnNone
[docs]classBaseOption:
- """
- Abstract Class to deal with encapsulating individual Options. An Option has
- a name/key, a description to display in relevant commands and menus, and a
- default value. It saves to the owner's Attributes using its Handler's save
- category.
-
- Designed to be extremely overloadable as some options can be cantankerous.
-
- Properties:
- valid: Shortcut to the loaded VALID_HANDLER.
- validator_key (str): The key of the Validator this uses.
-
- """
-
- def__str__(self):
- return"<Option {key}: {value}>".format(key=self.key,value=crop(str(self.value),width=10))
-
- def__repr__(self):
- returnstr(self)
-
-
[docs]def__init__(self,handler,key,description,default):
- """
-
- Args:
- handler (OptionHandler): The OptionHandler that 'owns' this Option.
- key (str): The name this will be used for storage in a dictionary.
- Must be unique per OptionHandler.
- description (str): What this Option's text will show in commands and menus.
- default: A default value for this Option.
-
- """
- self.handler=handler
- self.key=key
- self.default_value=default
- self.description=description
-
- # Value Storage contains None until the Option is loaded.
- self.value_storage=None
-
- # And it's not loaded until it's called upon to spit out its contents.
- self.loaded=False
[docs]defset(self,value,**kwargs):
- """
- Takes user input and stores appropriately. This method allows for
- passing extra instructions into the validator.
-
- Args:
- value (str): The new value of this Option.
- kwargs (any): Any kwargs will be passed into
- `self.validate(value, **kwargs)` and `self.save(**kwargs)`.
-
- """
- final_value=self.validate(value,**kwargs)
- self.value_storage=final_value
- self.loaded=True
- self.save(**kwargs)
-
-
[docs]defload(self):
- """
- Takes the provided save data, validates it, and gets this Option ready to use.
-
- Returns:
- Boolean: Whether loading was successful.
-
- """
- loadfunc=self.handler.loadfunc
- load_kwargs=self.handler.load_kwargs
-
- try:
- self.value_storage=self.deserialize(
- loadfunc(self.key,default=self.default_value,**load_kwargs)
- )
- exceptException:
- logger.log_trace()
- returnFalse
- self.loaded=True
- returnTrue
-
-
[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`.
-
- Keyword Args:
- any (any): Not used by default. These are passed in from self.set
- and allows the option to let the caller customize saving by
- overriding or extend the default save kwargs
-
- """
- value=self.serialize()
- save_kwargs={**self.handler.save_kwargs,**kwargs}
- savefunc=self.handler.savefunc
- savefunc(self.key,value=value,**save_kwargs)
-
-
[docs]defdeserialize(self,save_data):
- """
- Perform sanity-checking on the save data as it is loaded from storage.
- This isn't the same as what validator-functions provide (those work on
- user input). For example, save data might be a timedelta or a list or
- some other object.
-
- Args:
- save_data: The data to check.
-
- Returns:
- any (any): Whatever the Option needs to track, like a string or a
- datetime. The display hook is responsible for what is actually
- displayed to user.
- """
- returnsave_data
-
-
[docs]defserialize(self):
- """
- Serializes the save data for Attribute storage.
-
- Returns:
- any (any): Whatever is best for storage.
-
- """
- returnself.value_storage
-
-
[docs]defvalidate(self,value,**kwargs):
- """
- Validate user input, which is presumed to be a string.
-
- Args:
- value (str): User input.
- account (AccountDB): The Account that is performing the validation.
- This is necessary because of other settings which may affect the
- check, such as an Account's timezone affecting how their datetime
- entries are processed.
- Returns:
- any (any): The results of the validation.
- Raises:
- ValidationError: If input value failed validation.
-
- """
- returnvalidatorfuncs.text(value,option_key=self.key,**kwargs)
-
-
[docs]defdisplay(self,**kwargs):
- """
- Renders the Option's value as something pretty to look at.
-
- Keyword Args:
- any (any): These are options passed by the caller to potentially
- customize display dynamically.
-
- Returns:
- str: How the stored value should be projected to users (e.g. a raw
- timedelta is pretty ugly).
-
- """
- returnself.value
[docs]defdeserialize(self,save_data):
- ifisinstance(save_data,int):
- returndatetime.datetime.utcfromtimestamp(save_data)
- raiseValueError(f"{self.key} expected UTC Datetime in EPOCH format, got '{save_data}'")
[docs]classInMemorySaveHandler:
- """
- Fallback SaveHandler, implementing a minimum of the required save mechanism
- and storing data in memory.
-
- """
-
-
[docs]classOptionHandler:
- """
- This is a generic Option handler. Retrieve options either as properties on
- this handler or by using the .get method.
-
- This is used for Account.options but it could be used by Scripts or Objects
- just as easily. All it needs to be provided is an options_dict.
-
- """
-
-
[docs]def__init__(
- self,
- obj,
- options_dict=None,
- savefunc=None,
- loadfunc=None,
- save_kwargs=None,
- load_kwargs=None,
- ):
- """
- Initialize an OptionHandler.
-
- Args:
- obj (object): The object this handler sits on. This is usually a TypedObject.
- options_dict (dict): A dictionary of option keys, where the values
- are options. The format of those tuples is: ('key', "Description to
- show", 'option_type', <default value>)
- savefunc (callable): A callable for all options to call when saving itself.
- It will be called as `savefunc(key, value, **save_kwargs)`. A common one
- to pass would be AttributeHandler.add.
- loadfunc (callable): A callable for all options to call when loading data into
- itself. It will be called as `loadfunc(key, default=default, **load_kwargs)`.
- 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.
-
- """
- self.obj=obj
- self.options_dict={}ifoptions_dictisNoneelseoptions_dict
-
- ifnotsavefuncandloadfunc:
- self._in_memory_handler=InMemorySaveHandler()
- savefunc=InMemorySaveHandler.add
- loadfunc=InMemorySaveHandler.get
- self.savefunc=savefunc
- self.loadfunc=loadfunc
- self.save_kwargs={}ifsave_kwargsisNoneelsesave_kwargs
- self.load_kwargs={}ifload_kwargsisNoneelseload_kwargs
-
- # This dictionary stores the in-memory Options objects by their key for
- # quick lookup.
- self.options={}
-
- def__getattr__(self,key):
- """
- Allow for obj.options.key
-
- """
- returnself.get(key)
-
- def__setattr__(self,key,value):
- """
- Allow for obj.options.key = value
-
- But we must be careful to avoid infinite loops!
-
- """
- try:
- ifkeyin_GA(self,"options_dict"):
- _GA(self,"set")(key,value)
- exceptAttributeError:
- pass
- _SA(self,key,value)
-
- def_load_option(self,key):
- """
- Loads option on-demand if it has not been loaded yet.
-
- Args:
- key (str): The option being loaded.
-
- Returns:
-
- """
- desc,clsname,default_val=self.options_dict[key]
- loaded_option=OPTION_CLASSES.get(clsname)(self,key,desc,default_val)
- # store the value for future easy access
- self.options[key]=loaded_option
- returnloaded_option
-
-
[docs]defget(self,key,default=None,return_obj=False,raise_error=False):
- """
- Retrieves an Option stored in the handler. Will load it if it doesn't exist.
-
- Args:
- key (str): The option key to retrieve.
- default (any): What to return if the option is defined.
- return_obj (bool, optional): If True, returns the actual option
- object instead of its value.
- raise_error (bool, optional): Raise Exception if key is not found in options.
- Returns:
- option_value (any or Option): An option value the Option itself.
- Raises:
- KeyError: If option is not defined.
-
- """
- ifkeynotinself.options_dict:
- ifraise_error:
- raiseKeyError(_("Option not found!"))
- returndefault
- # get the options or load/recache it
- op_found=self.options.get(key)orself._load_option(key)
- returnop_foundifreturn_objelseop_found.value
-
-
[docs]defset(self,key,value,**kwargs):
- """
- Change an individual option.
-
- Args:
- key (str): The key of an option that can be changed. Allows partial matching.
- value (str): The value that should be checked, coerced, and stored.:
- kwargs (any, optional): These are passed into the Option's validation function,
- save function and display function and allows to customize either.
-
- Returns:
- value (any): Value stored in option, after validation.
-
- """
- ifnotkey:
- raiseValueError(_("Option field blank!"))
- match=string_partial_matching(list(self.options_dict.keys()),key,ret_index=False)
- ifnotmatch:
- raiseValueError(_("Option not found!"))
- iflen(match)>1:
- raiseValueError(_("Multiple matches:")
- +f"{', '.join(match)}. "
- +_("Please be more specific."))
- match=match[0]
- op=self.get(match,return_obj=True)
- op.set(value,**kwargs)
- returnop.value
-
-
[docs]defall(self,return_objs=False):
- """
- Get all options defined on this handler.
-
- Args:
- return_objs (bool, optional): Return the actual Option objects rather
- than their values.
- Returns:
- all_options (dict): All options on this handler, either `{key: value}`
- or `{key: <Option>}` if `return_objs` is `True`.
-
- """
- return[self.get(key,return_obj=return_objs)forkeyinself.options_dict]
-#
-# Copyright (c) 2009-2010 Gintautas Miliauskas
-#
-# Permission is hereby granted, free of charge, to any person
-# obtaining a copy of this software and associated documentation
-# files (the "Software"), to deal in the Software without
-# restriction, including without limitation the rights to use,
-# copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the
-# Software is furnished to do so, subject to the following
-# conditions:
-#
-# The above copyright notice and this permission notice shall be
-# included in all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
-# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
-# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
-# OTHER DEALINGS IN THE SOFTWARE.
-
-"""
-Pickle field implementation for Django.
-
-Modified for Evennia by Griatch and the Evennia community.
-
-"""
-fromastimportliteral_eval
-fromdatetimeimportdatetime
-
-fromcopyimportdeepcopy,ErrorasCopyError
-frombase64importb64encode,b64decode
-fromzlibimportcompress,decompress
-
-# import six # this is actually a pypy component, not in default syslib
-fromdjango.core.exceptionsimportValidationError
-fromdjango.dbimportmodels
-
-fromdjango.forms.fieldsimportCharField
-fromdjango.forms.widgetsimportTextarea
-
-frompickleimportloads,dumps
-fromdjango.utils.encodingimportforce_str
-fromevennia.utils.dbserializeimportpack_dbobj
-
-
-DEFAULT_PROTOCOL=4
-
-
-
[docs]classPickledObject(str):
- """
- A subclass of string so it can be told whether a string is a pickled
- object or not (if the object is an instance of this class then it must
- [well, should] be a pickled one).
-
- Only really useful for passing pre-encoded values to ``default``
- with ``dbsafe_encode``, not that doing so is necessary. If you
- remove PickledObject and its references, you won't be able to pass
- in pre-encoded values anymore, but you can always just pass in the
- python objects themselves.
- """
-
-
-class_ObjectWrapper(object):
- """
- A class used to wrap object that have properties that may clash with the
- ORM internals.
-
- For example, objects with the `prepare_database_save` property such as
- `django.db.Model` subclasses won't work under certain conditions and the
- same apply for trying to retrieve any `callable` object.
- """
-
- __slots__=("_obj",)
-
- def__init__(self,obj):
- self._obj=obj
-
-
-
[docs]defdbsafe_encode(value,compress_object=False,pickle_protocol=DEFAULT_PROTOCOL):
- # We use deepcopy() here to avoid a problem with cPickle, where dumps
- # can generate different character streams for same lookup value if
- # they are referenced differently.
- # The reason this is important is because we do all of our lookups as
- # simple string matches, thus the character streams must be the same
- # for the lookups to work properly. See tests.py for more information.
- try:
- value=deepcopy(value)
- exceptCopyError:
- # this can happen on a manager query where the search query string is a
- # database model.
- value=pack_dbobj(value)
-
- value=dumps(value,protocol=pickle_protocol)
-
- ifcompress_object:
- value=compress(value)
- value=b64encode(value).decode()# decode bytes to str
- returnPickledObject(value)
[docs]classPickledWidget(Textarea):
- """
- This is responsible for outputting HTML representing a given field.
- """
-
-
[docs]defrender(self,name,value,attrs=None,renderer=None):
- """Display of the PickledField in django admin"""
-
- repr_value=repr(value)
-
- # analyze represented value to see how big the field should be
- ifattrsisnotNone:
- attrs["name"]=name
- else:
- attrs={"name":name}
- attrs["cols"]=30
- # adapt number of rows to number of lines in string
- rows=1
- ifisinstance(value,str)and"\n"inrepr_value:
- rows=max(1,len(value.split("\n")))
- attrs["rows"]=rows
- attrs=self.build_attrs(attrs)
-
- try:
- # necessary to convert it back after repr(), otherwise validation errors will mutate it
- value=literal_eval(repr_value)
- except(ValueError,SyntaxError):
- # we could not eval it, just show its prepresentation
- value=repr_value
- returnsuper().render(name,value,attrs=attrs,renderer=renderer)
[docs]classPickledFormField(CharField):
- """
- This represents one input field for the form.
-
- """
-
- widget=PickledWidget
- default_error_messages=dict(CharField.default_error_messages)
- default_error_messages["invalid"]=(
- "This is not a Python Literal. You can store things like strings, "
- "integers, or floats, but you must do it by typing them as you would "
- "type them in the Python Interpreter. For instance, strings must be "
- "surrounded by quote marks. We have converted it to a string for your "
- "convenience. If it is acceptable, please hit save again."
- )
-
-
[docs]def__init__(self,*args,**kwargs):
- # This needs to fall through to literal_eval.
- kwargs["required"]=False
- super().__init__(*args,**kwargs)
-
-
[docs]defclean(self,value):
- value=super().clean(value)
-
- # handle empty input
- try:
- ifnotvalue.strip():
- # Field was left blank. Make this None.
- value="None"
- exceptAttributeError:
- pass
-
- # parse raw Python
- try:
- returnliteral_eval(value)
- except(ValueError,SyntaxError):
- pass
-
- # fall through to parsing the repr() of the data
- try:
- value=repr(value)
- returnliteral_eval(value)
- except(ValueError,SyntaxError):
- raiseValidationError(self.error_messages["invalid"])
-
-
-
[docs]classPickledObjectField(models.Field):
- """
- A field that will accept *any* python object and store it in the
- database. PickledObjectField will optionally compress its values if
- declared with the keyword argument ``compress=True``.
-
- Does not actually encode and compress ``None`` objects (although you
- can still do lookups using None). This way, it is still possible to
- use the ``isnull`` lookup type correctly.
- """
-
-
[docs]defget_default(self):
- """
- Returns the default value for this field.
-
- The default implementation on models.Field calls force_str
- on the default, which means you can't set arbitrary Python
- objects as the default. To fix this, we just return the value
- without calling force_str on it. Note that if you set a
- callable as a default, the field will still call it. It will
- *not* try to pickle and encode it.
-
- """
- ifself.has_default():
- ifcallable(self.default):
- returnself.default()
- returnself.default
- # If the field doesn't have a default, then we punt to models.Field.
- returnsuper().get_default()
-
-
[docs]deffrom_db_value(self,value,*args):
- """
- B64decode and unpickle the object, optionally decompressing it.
-
- If an error is raised in de-pickling and we're sure the value is
- a definite pickle, the error is allowed to propagate. If we
- aren't sure if the value is a pickle or not, then we catch the
- error and return the original value instead.
-
- """
- ifvalueisnotNone:
- try:
- value=dbsafe_decode(value,self.compress)
- exceptException:
- # If the value is a definite pickle; and an error is raised in
- # de-pickling it should be allowed to propogate.
- ifisinstance(value,PickledObject):
- raise
- else:
- ifisinstance(value,_ObjectWrapper):
- returnvalue._obj
- returnvalue
[docs]defget_db_prep_value(self,value,connection=None,prepared=False):
- """
- Pickle and b64encode the object, optionally compressing it.
-
- The pickling protocol is specified explicitly (by default 2),
- rather than as -1 or HIGHEST_PROTOCOL, because we don't want the
- protocol to change over time. If it did, ``exact`` and ``in``
- lookups would likely fail, since pickle would now be generating
- a different string.
-
- """
- ifvalueisnotNoneandnotisinstance(value,PickledObject):
- # We call force_str here explicitly, so that the encoded string
- # isn't rejected by the postgresql backend. Alternatively,
- # we could have just registered PickledObject with the psycopg
- # marshaller (telling it to store it like it would a string), but
- # since both of these methods result in the same value being stored,
- # doing things this way is much easier.
- value=force_str(dbsafe_encode(value,self.compress,self.protocol))
- returnvalue
[docs]defget_db_prep_lookup(self,lookup_type,value,connection=None,prepared=False):
- iflookup_typenotin["exact","in","isnull"]:
- raiseTypeError("Lookup type %s is not supported."%lookup_type)
- # The Field model already calls get_db_prep_value before doing the
- # actual lookup, so all we need to do is limit the lookup types.
- returnsuper().get_db_prep_lookup(
- lookup_type,value,connection=connection,prepared=prepared
- )
-"""
-This is a convenient container gathering all the main
-search methods for the various database tables.
-
-It is intended to be used e.g. as
-
-> from evennia.utils import search
-> match = search.objects(...)
-
-Note that this is not intended to be a complete listing of all search
-methods! You need to refer to the respective manager to get all
-possible search methods. To get to the managers from your code, import
-the database model and call its 'objects' property.
-
-Also remember that all commands in this file return lists (also if
-there is only one match) unless noted otherwise.
-
-Example: To reach the search method 'get_object_with_account'
- in evennia/objects/managers.py:
-
-> from evennia.objects.models import ObjectDB
-> match = Object.objects.get_object_with_account(...)
-
-
-"""
-
-# Import the manager methods to be wrapped
-
-fromdjango.db.utilsimportOperationalError,ProgrammingError
-fromdjango.contrib.contenttypes.modelsimportContentType
-
-# limit symbol import from API
-__all__=(
- "search_object",
- "search_account",
- "search_script",
- "search_message",
- "search_channel",
- "search_help_entry",
- "search_tag",
- "search_script_tag",
- "search_account_tag",
- "search_channel_tag",
-)
-
-
-# import objects this way to avoid circular import problems
-try:
- ObjectDB=ContentType.objects.get(app_label="objects",model="objectdb").model_class()
- AccountDB=ContentType.objects.get(app_label="accounts",model="accountdb").model_class()
- ScriptDB=ContentType.objects.get(app_label="scripts",model="scriptdb").model_class()
- Msg=ContentType.objects.get(app_label="comms",model="msg").model_class()
- ChannelDB=ContentType.objects.get(app_label="comms",model="channeldb").model_class()
- HelpEntry=ContentType.objects.get(app_label="help",model="helpentry").model_class()
- Tag=ContentType.objects.get(app_label="typeclasses",model="tag").model_class()
-except(OperationalError,ProgrammingError):
- # this is a fallback used during tests/doc building
- print("Database not available yet - using temporary fallback for search managers.")
- fromevennia.objects.modelsimportObjectDB
- fromevennia.accounts.modelsimportAccountDB
- fromevennia.scripts.modelsimportScriptDB
- fromevennia.comms.modelsimportMsg,ChannelDB
- fromevennia.help.modelsimportHelpEntry
- fromevennia.typeclasses.tagsimportTag# noqa
-
-# -------------------------------------------------------------------
-# Search manager-wrappers
-# -------------------------------------------------------------------
-
-#
-# Search objects as a character
-#
-# NOTE: A more powerful wrapper of this method
-# is reachable from within each command class
-# by using self.caller.search()!
-#
-# def object_search(self, ostring=None,
-# attribute_name=None,
-# typeclass=None,
-# candidates=None,
-# exact=True):
-#
-# Search globally or in a list of candidates and return results.
-# The result is always a list of Objects (or the empty list)
-#
-# Arguments:
-# ostring: (str) The string to compare names against. By default (if
-# not attribute_name is set), this will search object.key
-# and object.aliases in order. Can also be on the form #dbref,
-# which will, if exact=True be matched against primary key.
-# attribute_name: (str): Use this named ObjectAttribute to match ostring
-# against, instead of the defaults.
-# typeclass (str or TypeClass): restrict matches to objects having
-# this typeclass. This will help speed up global searches.
-# candidates (list obj ObjectDBs): If supplied, search will only be
-# performed among the candidates in this list. A common list
-# of candidates is the contents of the current location.
-# exact (bool): Match names/aliases exactly or partially. Partial
-# matching matches the beginning of words in the names/aliases,
-# using a matching routine to separate multiple matches in
-# names with multiple components (so "bi sw" will match
-# "Big sword"). Since this is more expensive than exact
-# matching, it is recommended to be used together with
-# the objlist keyword to limit the number of possibilities.
-# This keyword has no meaning if attribute_name is set.
-#
-# Returns:
-# A list of matching objects (or a list with one unique match)
-# def object_search(self, ostring, caller=None,
-# candidates=None,
-# attribute_name=None):
-#
-search_object=ObjectDB.objects.search_object
-search_objects=search_object
-object_search=search_object
-objects=search_objects
-
-#
-# Search for accounts
-#
-# account_search(self, ostring)
-
-# Searches for a particular account by name or
-# database id.
-#
-# ostring = a string or database id.
-#
-
-search_account=AccountDB.objects.search_account
-search_accounts=search_account
-account_search=search_account
-accounts=search_accounts
-
-#
-# Searching for scripts
-#
-# script_search(self, ostring, obj=None, only_timed=False)
-#
-# Search for a particular script.
-#
-# ostring - search criterion - a script ID or key
-# obj - limit search to scripts defined on this object
-# only_timed - limit search only to scripts that run
-# on a timer.
-#
-
-search_script=ScriptDB.objects.search_script
-search_scripts=search_script
-script_search=search_script
-scripts=search_scripts
-#
-# Searching for communication messages
-#
-#
-# message_search(self, sender=None, receiver=None, channel=None, freetext=None)
-#
-# Search the message database for particular messages. At least one
-# of the arguments must be given to do a search.
-#
-# sender - get messages sent by a particular account
-# receiver - get messages received by a certain account
-# channel - get messages sent to a particular channel
-# freetext - Search for a text string in a message.
-# NOTE: This can potentially be slow, so make sure to supply
-# one of the other arguments to limit the search.
-#
-
-search_message=Msg.objects.search_message
-search_messages=search_message
-message_search=search_message
-messages=search_messages
-
-#
-# Search for Communication Channels
-#
-# channel_search(self, ostring)
-#
-# Search the channel database for a particular channel.
-#
-# ostring - the key or database id of the channel.
-# exact - requires an exact ostring match (not case sensitive)
-#
-
-search_channel=ChannelDB.objects.search_channel
-search_channels=search_channel
-channel_search=search_channel
-channels=search_channels
-
-#
-# Find help entry objects.
-#
-# search_help(self, ostring, help_category=None)
-#
-# Retrieve a search entry object.
-#
-# ostring - the help topic to look for
-# category - limit the search to a particular help topic
-#
-
-search_help=HelpEntry.objects.search_help
-search_help_entry=search_help
-search_help_entries=search_help
-help_entry_search=search_help
-help_entries=search_help
-
-
-# Locate Attributes
-
-# search_object_attribute(key, category, value, strvalue) (also search_attribute works)
-# search_account_attribute(key, category, value, strvalue) (also search_attribute works)
-# search_script_attribute(key, category, value, strvalue) (also search_attribute works)
-# search_channel_attribute(key, category, value, strvalue) (also search_attribute works)
-
-# Note that these return the object attached to the Attribute,
-# not the attribute object itself (this is usually what you want)
-
-
-defsearch_object_attribute(
- key=None,category=None,value=None,strvalue=None,attrtype=None,**kwargs
-):
- returnObjectDB.objects.get_by_attribute(
- key=key,category=category,value=value,strvalue=strvalue,attrtype=attrtype,**kwargs
- )
-
-
-defsearch_account_attribute(
- key=None,category=None,value=None,strvalue=None,attrtype=None,**kwargs
-):
- returnAccountDB.objects.get_by_attribute(
- key=key,category=category,value=value,strvalue=strvalue,attrtype=attrtype,**kwargs
- )
-
-
-defsearch_script_attribute(
- key=None,category=None,value=None,strvalue=None,attrtype=None,**kwargs
-):
- returnScriptDB.objects.get_by_attribute(
- key=key,category=category,value=value,strvalue=strvalue,attrtype=attrtype,**kwargs
- )
-
-
-defsearch_channel_attribute(
- key=None,category=None,value=None,strvalue=None,attrtype=None,**kwargs
-):
- returnChannelDB.objects.get_by_attribute(
- key=key,category=category,value=value,strvalue=strvalue,attrtype=attrtype,**kwargs
- )
-
-
-# search for attribute objects
-search_attribute_object=ObjectDB.objects.get_attribute
-
-# Locate Tags
-
-# search_object_tag(key=None, category=None) (also search_tag works)
-# search_account_tag(key=None, category=None)
-# search_script_tag(key=None, category=None)
-# search_channel_tag(key=None, category=None)
-
-# Note that this returns the object attached to the tag, not the tag
-# object itself (this is usually what you want)
-
-
-defsearch_object_by_tag(key=None,category=None,tagtype=None,**kwargs):
- """
- Find object based on tag or category.
-
- Args:
- key (str, optional): The tag key to search for.
- category (str, optional): The category of tag
- to search for. If not set, uncategorized
- tags will be searched.
- tagtype (str, optional): 'type' of Tag, by default
- this is either `None` (a normal Tag), `alias` or
- `permission`. This always apply to all queried tags.
- kwargs (any): Other optional parameter that may be supported
- by the manager method.
-
- Returns:
- matches (list): List of Objects with tags matching
- the search criteria, or an empty list if no
- matches were found.
-
- """
- returnObjectDB.objects.get_by_tag(key=key,category=category,tagtype=tagtype,**kwargs)
-
-
-search_tag=search_object_by_tag# this is the most common case
-
-
-
[docs]defsearch_account_tag(key=None,category=None,tagtype=None,**kwargs):
- """
- Find account based on tag or category.
-
- Args:
- key (str, optional): The tag key to search for.
- category (str, optional): The category of tag
- to search for. If not set, uncategorized
- tags will be searched.
- tagtype (str, optional): 'type' of Tag, by default
- this is either `None` (a normal Tag), `alias` or
- `permission`. This always apply to all queried tags.
- kwargs (any): Other optional parameter that may be supported
- by the manager method.
-
- Returns:
- matches (list): List of Accounts with tags matching
- the search criteria, or an empty list if no
- matches were found.
-
- """
- returnAccountDB.objects.get_by_tag(key=key,category=category,tagtype=tagtype,**kwargs)
-
-
-
[docs]defsearch_script_tag(key=None,category=None,tagtype=None,**kwargs):
- """
- Find script based on tag or category.
-
- Args:
- key (str, optional): The tag key to search for.
- category (str, optional): The category of tag
- to search for. If not set, uncategorized
- tags will be searched.
- tagtype (str, optional): 'type' of Tag, by default
- this is either `None` (a normal Tag), `alias` or
- `permission`. This always apply to all queried tags.
- kwargs (any): Other optional parameter that may be supported
- by the manager method.
-
- Returns:
- matches (list): List of Scripts with tags matching
- the search criteria, or an empty list if no
- matches were found.
-
- """
- returnScriptDB.objects.get_by_tag(key=key,category=category,tagtype=tagtype,**kwargs)
-
-
-
[docs]defsearch_channel_tag(key=None,category=None,tagtype=None,**kwargs):
- """
- Find channel based on tag or category.
-
- Args:
- key (str, optional): The tag key to search for.
- category (str, optional): The category of tag
- to search for. If not set, uncategorized
- tags will be searched.
- tagtype (str, optional): 'type' of Tag, by default
- this is either `None` (a normal Tag), `alias` or
- `permission`. This always apply to all queried tags.
- kwargs (any): Other optional parameter that may be supported
- by the manager method.
-
- Returns:
- matches (list): List of Channels with tags matching
- the search criteria, or an empty list if no
- matches were found.
-
- """
- returnChannelDB.objects.get_by_tag(key=key,category=category,tagtype=tagtype,**kwargs)
-
-
-# search for tag objects (not the objects they are attached to
-search_tag_object=ObjectDB.objects.get_tag
-
-"""
-Various helper resources for writing unittests.
-
-Classes for testing Evennia core:
-
-- `BaseEvenniaTestCase` - no default objects, only enforced default settings
-- `BaseEvenniaTest` - all default objects, enforced default settings
-- `BaseEvenniaCommandTest` - for testing Commands, enforced default settings
-
-Classes for testing game folder content:
-
-- `EvenniaTestCase` - no default objects, using gamedir settings (identical to
- standard Python TestCase)
-- `EvenniaTest` - all default objects, using gamedir settings
-- `EvenniaCommandTest` - for testing game folder commands, using gamedir settings
-
-Other:
-
-- `EvenniaTestMixin` - A class mixin for creating the test environment objects, for
- making custom tests.
-- `EvenniaCommandMixin` - A class mixin that adds support for command testing with the .call()
- helper. Used by the command-test classes, but can be used for making a customt test class.
-
-"""
-importsys
-importre
-importtypes
-fromtwisted.internet.deferimportDeferred
-fromdjango.confimportsettings
-fromdjango.testimportTestCase,override_settings
-frommockimportMock,patch,MagicMock
-fromevennia.objects.objectsimportDefaultObject,DefaultCharacter,DefaultRoom,DefaultExit
-fromevennia.accounts.accountsimportDefaultAccount
-fromevennia.scripts.scriptsimportDefaultScript
-fromevennia.server.serversessionimportServerSession
-fromevennia.server.sessionhandlerimportSESSIONS
-fromevennia.utilsimportcreate
-fromevennia.utils.idmapper.modelsimportflush_cache
-fromevennia.utils.utilsimportall_from_module,to_str
-fromevennia.utilsimportansi
-fromevenniaimportsettings_default
-fromevennia.commands.default.muxcommandimportMuxCommand
-fromevennia.commands.commandimportInterruptCommand
-
-
-_RE_STRIP_EVMENU=re.compile(r"^\+|-+\+|\+-+|--+|\|(?:\s|$)",re.MULTILINE)
-
-
-# set up a 'pristine' setting, unaffected by any changes in mygame
-DEFAULT_SETTING_RESETS=dict(
- CONNECTION_SCREEN_MODULE="evennia.game_template.server.conf.connection_screens",
- AT_SERVER_STARTSTOP_MODULE="evennia.game_template.server.conf.at_server_startstop",
- AT_SERVICES_PLUGINS_MODULES=["evennia.game_template.server.conf.server_services_plugins"],
- PORTAL_SERVICES_PLUGIN_MODULES=["evennia.game_template.server.conf.portal_services_plugins"],
- MSSP_META_MODULE="evennia.game_template.server.conf.mssp",
- WEB_PLUGINS_MODULE="server.conf.web_plugins",
- LOCK_FUNC_MODULES=("evennia.locks.lockfuncs","evennia.game_template.server.conf.lockfuncs"),
- INPUT_FUNC_MODULES=["evennia.server.inputfuncs",
- "evennia.game_template.server.conf.inputfuncs"],
- PROTOTYPE_MODULES=["evennia.game_template.world.prototypes"],
- CMDSET_UNLOGGEDIN="evennia.game_template.commands.default_cmdsets.UnloggedinCmdSet",
- CMDSET_SESSION="evennia.game_template.commands.default_cmdsets.SessionCmdSet",
- CMDSET_CHARACTER="evennia.game_template.commands.default_cmdsets.CharacterCmdSet",
- CMDSET_ACCOUNT="evennia.game_template.commands.default_cmdsets.AccountCmdSet",
- CMDSET_PATHS=["evennia.game_template.commands","evennia","evennia.contrib"],
- TYPECLASS_PATHS=[
- "evennia",
- "evennia.contrib",
- "evennia.contrib.game_systems",
- "evennia.contrib.base_systems",
- "evennia.contrib.full_systems",
- "evennia.contrib.tutorials",
- "evennia.contrib.utils"],
- BASE_ACCOUNT_TYPECLASS="evennia.accounts.accounts.DefaultAccount",
- BASE_OBJECT_TYPECLASS="evennia.objects.objects.DefaultObject",
- BASE_CHARACTER_TYPECLASS="evennia.objects.objects.DefaultCharacter",
- BASE_ROOM_TYPECLASS="evennia.objects.objects.DefaultRoom",
- BASE_EXIT_TYPECLASS="evennia.objects.objects.DefaultExit",
- BASE_CHANNEL_TYPECLASS="evennia.comms.comms.DefaultChannel",
- BASE_SCRIPT_TYPECLASS="evennia.scripts.scripts.DefaultScript",
- BASE_BATCHPROCESS_PATHS=["evennia.game_template.world",
- "evennia.contrib","evennia.contrib.tutorials"],
- FILE_HELP_ENTRY_MODULES=["evennia.game_template.world.help_entries"],
- FUNCPARSER_OUTGOING_MESSAGES_MODULES=["evennia.utils.funcparser",
- "evennia.game_template.server.conf.inlinefuncs"],
- FUNCPARSER_PROTOTYPE_PARSING_MODULES=["evennia.prototypes.protfuncs",
- "evennia.game_template.server.conf.prototypefuncs"],
- BASE_GUEST_TYPECLASS="evennia.accounts.accounts.DefaultGuest",
- # a special flag; test with settings._TEST_ENVIRONMENT to see if code runs in a test
- _TEST_ENVIRONMENT=True,
-)
-
-DEFAULT_SETTINGS={
- **all_from_module(settings_default),
- **DEFAULT_SETTING_RESETS
-}
-DEFAULT_SETTINGS.pop("DATABASES")# we want different dbs tested in CI
-
-
-# mocking of evennia.utils.utils.delay
-
[docs]defunload_module(module):
- """
- Reset import so one can mock global constants.
-
- Args:
- module (module, object or str): The module will
- be removed so it will have to be imported again. If given
- an object, the module in which that object sits will be unloaded. A string
- should directly give the module pathname to unload.
-
- Example:
-
- ```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'
- ```
-
- 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):
- modulename=module
- elifhasattr(module,"__module__"):
- modulename=module.__module__
- else:
- modulename=module.__name__
-
- ifmodulenameinsys.modules:
- delsys.modules[modulename]
[docs]defsetup_session(self):
- dummysession=ServerSession()
- dummysession.init_session("telnet",("localhost","testmode"),SESSIONS)
- dummysession.sessid=1
- SESSIONS.portal_connect(
- dummysession.get_sync_data()
- )# note that this creates a new Session!
- session=SESSIONS.session_from_sessid(1)# the real session
- SESSIONS.login(session,self.account,testmode=True)
- self.session=session
[docs]deftearDown(self):
- flush_cache()
- try:
- SESSIONS.data_out=self.backups[0]
- SESSIONS.disconnect=self.backups[1]
- settings.DEFAULT_HOME=self.backups[2]
- settings.PROTOTYPE_MODULES=self.backups[3]
- exceptAttributeErroraserr:
- raiseAttributeError(f"{err}: Teardown error. If you overrode the `setUp()` method "
- "in your test, make sure you also added `super().setUp()`!")
-
- delSESSIONS[self.session.sessid]
- self.teardown_accounts()
- super().tearDown()
-
-
-
[docs]@patch("evennia.server.portal.portal.LoopingCall",new=MagicMock())
-classEvenniaCommandTestMixin:
- """
- Mixin to add to a test in order to provide the `.call` helper for
- testing the execution and returns of 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(EvenniaTest, CommandTestMixin):
-
- 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,
- input_args,
- msg=None,
- cmdset=None,
- noansi=True,
- caller=None,
- receiver=None,
- cmdstring=None,
- obj=None,
- inputs=None,
- raw_string=None,
- ):
- """
- 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:
- 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.obj
- caller=callerifcallerelseself.char1
- cmdobj.caller=caller
- cmdobj.cmdname=cmdstringifcmdstringelsecmdobj.key
- cmdobj.raw_cmdname=cmdobj.cmdname
- cmdobj.cmdstring=cmdobj.cmdname# deprecated
- cmdobj.args=input_args
- cmdobj.cmdset=cmdset
- cmdobj.session=SESSIONS.session_from_sessid(1)
- cmdobj.account=self.account
- cmdobj.raw_string=raw_stringifraw_stringisnotNoneelsecmdobj.key+" "+input_args
- cmdobj.obj=objor(callerifcallerelseself.char1)
- inputs=inputsor[]
-
- # 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 mock
- receiver.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():
- return
- cmdobj.parse()
- ret=cmdobj.func()
-
- # handle func's with yield in them (making them generators)
- ifisinstance(ret,types.GeneratorType):
- whileTrue:
- try:
- inp=inputs.pop()ifinputselseNone
- ifinp:
- try:
- # this mimics a user's reply to a prompt
- ret.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
-
- cmdobj.at_post_cmd()
- exceptStopIteration:
- pass
- exceptInterruptCommand:
- pass
-
- 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)
-
- # 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.
-
- 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",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]
-
- # 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
-
-
-# Base testing classes
-
-
[docs]@override_settings(**DEFAULT_SETTINGS)
-classBaseEvenniaTestCase(TestCase):
- """
- Base test (with no default objects) but with enforced default settings.
-
- """
-
-
[docs]classEvenniaTestCase(TestCase):
- """
- For use with gamedir settings; Just like the normal test case, only for naming consistency.
-
- """
- pass
-
-
-
[docs]@override_settings(**DEFAULT_SETTINGS)
-classBaseEvenniaTest(EvenniaTestMixin,TestCase):
- """
- This class parent has all default objects and uses only default settings.
-
- """
-
-
[docs]classEvenniaTest(EvenniaTestMixin,TestCase):
- """
- This test class is intended for inheriting in mygame tests.
- It helps ensure your tests are run with your own objects
- and settings from your game folder.
-
- """
-
- account_typeclass=settings.BASE_ACCOUNT_TYPECLASS
- object_typeclass=settings.BASE_OBJECT_TYPECLASS
- character_typeclass=settings.BASE_CHARACTER_TYPECLASS
- exit_typeclass=settings.BASE_EXIT_TYPECLASS
- room_typeclass=settings.BASE_ROOM_TYPECLASS
- script_typeclass=settings.BASE_SCRIPT_TYPECLASS
-
-
-
[docs]@patch("evennia.commands.account.COMMAND_DEFAULT_CLASS",MuxCommand)
-@patch("evennia.commands.admin.COMMAND_DEFAULT_CLASS",MuxCommand)
-@patch("evennia.commands.batchprocess.COMMAND_DEFAULT_CLASS",MuxCommand)
-@patch("evennia.commands.building.COMMAND_DEFAULT_CLASS",MuxCommand)
-@patch("evennia.commands.comms.COMMAND_DEFAULT_CLASS",MuxCommand)
-@patch("evennia.commands.general.COMMAND_DEFAULT_CLASS",MuxCommand)
-@patch("evennia.commands.help.COMMAND_DEFAULT_CLASS",MuxCommand)
-@patch("evennia.commands.syscommands.COMMAND_DEFAULT_CLASS",MuxCommand)
-@patch("evennia.commands.system.COMMAND_DEFAULT_CLASS",MuxCommand)
-@patch("evennia.commands.unloggedin.COMMAND_DEFAULT_CLASS",MuxCommand)
-classBaseEvenniaCommandTest(BaseEvenniaTest,EvenniaCommandTestMixin):
- """
- Commands only using the default settings.
-
- """
-
-
-
[docs]classEvenniaCommandTest(EvenniaTest,EvenniaCommandTestMixin):
- """
- Parent class to inherit from - makes tests use your own
- classes and settings in mygame.
-
- """
-"""
-ANSI -> html converter
-
-Credit for original idea and implementation
-goes to Muhammad Alkarouri and his
-snippet #577349 on http://code.activestate.com.
-
-(extensively modified by Griatch 2010)
-"""
-
-importre
-fromhtmlimportescapeashtml_escape
-from.ansiimport*
-
-
-# All xterm256 RGB equivalents
-
-XTERM256_FG="\033[38;5;%sm"
-XTERM256_BG="\033[48;5;%sm"
-
-
-
[docs]classTextToHTMLparser(object):
- """
- This class describes a parser for converting from ANSI to html.
- """
-
- tabstop=4
- # mapping html color name <-> ansi code.
- hilite=ANSI_HILITE
- unhilite=ANSI_UNHILITE# this will be stripped - there is no css equivalent.
- normal=ANSI_NORMAL# "
- underline=ANSI_UNDERLINE
- blink=ANSI_BLINK
- inverse=ANSI_INVERSE# this will produce an outline; no obvious css equivalent?
- colorcodes=[
- ("color-000",unhilite+ANSI_BLACK),# pure black
- ("color-001",unhilite+ANSI_RED),
- ("color-002",unhilite+ANSI_GREEN),
- ("color-003",unhilite+ANSI_YELLOW),
- ("color-004",unhilite+ANSI_BLUE),
- ("color-005",unhilite+ANSI_MAGENTA),
- ("color-006",unhilite+ANSI_CYAN),
- ("color-007",unhilite+ANSI_WHITE),# light grey
- ("color-008",hilite+ANSI_BLACK),# dark grey
- ("color-009",hilite+ANSI_RED),
- ("color-010",hilite+ANSI_GREEN),
- ("color-011",hilite+ANSI_YELLOW),
- ("color-012",hilite+ANSI_BLUE),
- ("color-013",hilite+ANSI_MAGENTA),
- ("color-014",hilite+ANSI_CYAN),
- ("color-015",hilite+ANSI_WHITE),# pure white
- ]+[("color-%03i"%(i+16),XTERM256_FG%("%i"%(i+16)))foriinrange(240)]
-
- colorback=[
- ("bgcolor-000",ANSI_BACK_BLACK),# pure black
- ("bgcolor-001",ANSI_BACK_RED),
- ("bgcolor-002",ANSI_BACK_GREEN),
- ("bgcolor-003",ANSI_BACK_YELLOW),
- ("bgcolor-004",ANSI_BACK_BLUE),
- ("bgcolor-005",ANSI_BACK_MAGENTA),
- ("bgcolor-006",ANSI_BACK_CYAN),
- ("bgcolor-007",ANSI_BACK_WHITE),# light grey
- ("bgcolor-008",hilite+ANSI_BACK_BLACK),# dark grey
- ("bgcolor-009",hilite+ANSI_BACK_RED),
- ("bgcolor-010",hilite+ANSI_BACK_GREEN),
- ("bgcolor-011",hilite+ANSI_BACK_YELLOW),
- ("bgcolor-012",hilite+ANSI_BACK_BLUE),
- ("bgcolor-013",hilite+ANSI_BACK_MAGENTA),
- ("bgcolor-014",hilite+ANSI_BACK_CYAN),
- ("bgcolor-015",hilite+ANSI_BACK_WHITE),# pure white
- ]+[("bgcolor-%03i"%(i+16),XTERM256_BG%("%i"%(i+16)))foriinrange(240)]
-
- # make sure to escape [
- # colorcodes = [(c, code.replace("[", r"\[")) for c, code in colorcodes]
- # colorback = [(c, code.replace("[", r"\[")) for c, code in colorback]
- fg_colormap=dict((code,clr)forclr,codeincolorcodes)
- bg_colormap=dict((code,clr)forclr,codeincolorback)
-
- # create stop markers
- fgstop="(?:\033\[1m|\033\[22m){0,1}\033\[3[0-8].*?m|\033\[0m|$"
- bgstop="(?:\033\[1m|\033\[22m){0,1}\033\[4[0-8].*?m|\033\[0m|$"
- bgfgstop=bgstop[:-2]+r"(\s*)"+fgstop
-
- fgstart="((?:\033\[1m|\033\[22m){0,1}\033\[3[0-8].*?m)"
- bgstart="((?:\033\[1m|\033\[22m){0,1}\033\[4[0-8].*?m)"
- bgfgstart=bgstart+r"(\s*)"+"((?:\033\[1m|\033\[22m){0,1}\033\[[3-4][0-8].*?m){0,1}"
-
- # extract color markers, tagging the start marker and the text marked
- re_fgs=re.compile(fgstart+"(.*?)(?="+fgstop+")")
- re_bgs=re.compile(bgstart+"(.*?)(?="+bgstop+")")
- re_bgfg=re.compile(bgfgstart+"(.*?)(?="+bgfgstop+")")
-
- re_normal=re.compile(normal.replace("[",r"\["))
- re_hilite=re.compile("(?:%s)(.*)(?=%s|%s)"%(hilite.replace("[",r"\["),fgstop,bgstop))
- re_unhilite=re.compile("(?:%s)(.*)(?=%s|%s)"%(unhilite.replace("[",r"\["),fgstop,bgstop))
- re_uline=re.compile("(?:%s)(.*?)(?=%s|%s)"%(underline.replace("[",r"\["),fgstop,bgstop))
- re_blink=re.compile("(?:%s)(.*?)(?=%s|%s)"%(blink.replace("[",r"\["),fgstop,bgstop))
- re_inverse=re.compile("(?:%s)(.*?)(?=%s|%s)"%(inverse.replace("[",r"\["),fgstop,bgstop))
- re_string=re.compile(
- r"(?P<htmlchars>[<&>])|(?P<tab>[\t]+)|(?P<space> +)|"
- r"(?P<spacestart>^ )|(?P<lineend>\r\n|\r|\n)",
- re.S|re.M|re.I,
- )
- re_dblspace=re.compile(r" {2,}",re.M)
- re_url=re.compile(
- r'(?<!=")((?:ftp|www|https?)\W+(?:(?!\.(?:\s|$)|&\w+;)[^"\',;$*^\\(){}<>\[\]\s])+)(\.(?:\s|$)|&\w+;|)'
- )
- re_mxplink=re.compile(r"\|lc(.*?)\|lt(.*?)\|le",re.DOTALL)
- re_mxpurl=re.compile(r"\|lu(.*?)\|lt(.*?)\|le",re.DOTALL)
-
- def_sub_bgfg(self,colormatch):
- # print("colormatch.groups()", colormatch.groups())
- bgcode,prespace,fgcode,text,postspace=colormatch.groups()
- ifnotfgcode:
- ret=r"""<span class="%s">%s%s%s</span>"""%(
- self.bg_colormap.get(bgcode,self.fg_colormap.get(bgcode,"err")),
- prespaceand" "*len(prespace)or"",
- postspaceand" "*len(postspace)or"",
- text,
- )
- else:
- ret=r"""<span class="%s"><span class="%s">%s%s%s</span></span>"""%(
- self.bg_colormap.get(bgcode,self.fg_colormap.get(bgcode,"err")),
- self.fg_colormap.get(fgcode,self.bg_colormap.get(fgcode,"err")),
- prespaceand" "*len(prespace)or"",
- postspaceand" "*len(postspace)or"",
- text,
- )
- returnret
-
- def_sub_fg(self,colormatch):
- code,text=colormatch.groups()
- returnr"""<span class="%s">%s</span>"""%(self.fg_colormap.get(code,"err"),text)
-
- def_sub_bg(self,colormatch):
- code,text=colormatch.groups()
- returnr"""<span class="%s">%s</span>"""%(self.bg_colormap.get(code,"err"),text)
-
-
[docs]defre_color(self,text):
- """
- Replace ansi colors with html color class names. Let the
- client choose how it will display colors, if it wishes to.
-
- Args:
- text (str): the string with color to replace.
-
- Returns:
- text (str): Re-colored text.
-
- """
- text=self.re_bgfg.sub(self._sub_bgfg,text)
- text=self.re_fgs.sub(self._sub_fg,text)
- text=self.re_bgs.sub(self._sub_bg,text)
- text=self.re_normal.sub("",text)
- returntext
-
-
[docs]defre_bold(self,text):
- """
- Clean out superfluous hilights rather than set <strong>to make
- it match the look of telnet.
-
- Args:
- text (str): Text to process.
-
- Returns:
- text (str): Processed text.
-
- """
- text=self.re_hilite.sub(r"<strong>\1</strong>",text)
- returnself.re_unhilite.sub(r"\1",text)# strip unhilite - there is no equivalent in css.
-
-
[docs]defre_underline(self,text):
- """
- Replace ansi underline with html underline class name.
-
- Args:
- text (str): Text to process.
-
- Returns:
- text (str): Processed text.
-
- """
- returnself.re_uline.sub(r'<span class="underline">\1</span>',text)
-
-
[docs]defre_blinking(self,text):
- """
- Replace ansi blink with custom blink css class
-
- Args:
- text (str): Text to process.
-
- Returns:
- text (str): Processed text.
- """
- returnself.re_blink.sub(r'<span class="blink">\1</span>',text)
-
-
[docs]defre_inversing(self,text):
- """
- Replace ansi inverse with custom inverse css class
-
- Args:
- text (str): Text to process.
-
- Returns:
- text (str): Processed text.
- """
- returnself.re_inverse.sub(r'<span class="inverse">\1</span>',text)
-
-
[docs]defremove_bells(self,text):
- """
- Remove ansi specials
-
- Args:
- text (str): Text to process.
-
- Returns:
- text (str): Processed text.
-
- """
- returntext.replace("\07","")
-
-
[docs]defremove_backspaces(self,text):
- """
- Removes special escape sequences
-
- Args:
- text (str): Text to process.
-
- Returns:
- text (str): Processed text.
-
- """
- backspace_or_eol=r"(.\010)|(\033\[K)"
- n=1
- whilen>0:
- text,n=re.subn(backspace_or_eol,"",text,1)
- returntext
-
-
[docs]defconvert_linebreaks(self,text):
- """
- Extra method for cleaning linebreaks
-
- Args:
- text (str): Text to process.
-
- Returns:
- text (str): Processed text.
-
- """
- returntext.replace("\n",r"<br>")
-
-
[docs]defconvert_urls(self,text):
- """
- Replace urls (http://...) by valid HTML.
-
- Args:
- text (str): Text to process.
-
- Returns:
- text (str): Processed text.
-
- """
- # -> added target to output prevent the web browser from attempting to
- # change pages (and losing our webclient session).
- returnself.re_url.sub(r'<a href="\1" target="_blank">\1</a>\2',text)
-
-
[docs]defre_double_space(self,text):
- """
- HTML will swallow any normal space after the first, so if any slipped
- through we must make sure to replace them with " "
- """
- returnself.re_dblspace.sub(self.sub_dblspace,text)
-
-
[docs]defsub_mxp_links(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.
-
- """
- cmd,text=[grp.replace('"',"\\"")forgrpinmatch.groups()]
- val=(
- r"""<a id="mxplink" href="#" """
- """onclick="Evennia.msg("text",["{cmd}"],{{}});"""
- """return false;">{text}</a>""".format(cmd=cmd,text=text)
- )
- returnval
-
-
[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,
- for handling all substitutions.
-
- Args:
- match (re.Matchobject): Match for substitution.
-
- Returns:
- text (str): Processed text.
-
- """
- cdict=match.groupdict()
- ifcdict["htmlchars"]:
- returnhtml_escape(cdict["htmlchars"])
- elifcdict["lineend"]:
- return"<br>"
- elifcdict["tab"]:
- text=cdict["tab"].replace("\t"," "+" "*(self.tabstop-1))
- returntext
- elifcdict["space"]orcdict["spacestart"]:
- text=cdict["space"]
- text=" "iflen(text)==1else" "+text[1:].replace(" "," ")
- returntext
- returnNone
-
-
[docs]defsub_dblspace(self,match):
- "clean up double-spaces"
- return" "+" "*(len(match.group())-1)
-
-
[docs]defparse(self,text,strip_ansi=False):
- """
- Main access function, converts a text containing ANSI codes
- into html statements.
-
- Args:
- text (str): Text to process.
- strip_ansi (bool, optional):
-
- Returns:
- text (str): Parsed text.
- """
- # parse everything to ansi first
- text=parse_ansi(text,strip_ansi=strip_ansi,xterm256=True,mxp=True)
- # convert all ansi to html
- result=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)
- result=self.re_blinking(result)
- result=self.re_inversing(result)
- result=self.remove_bells(result)
- result=self.convert_linebreaks(result)
- result=self.remove_backspaces(result)
- result=self.convert_urls(result)
- result=self.re_double_space(result)
- # clean out eventual ansi that was missed
- # result = parse_ansi(result, strip_ansi=True)
-
- returnresult
-# -*- encoding: utf-8 -*-
-"""
-General helper functions that don't fit neatly under any given category.
-
-They provide some useful string and conversion methods that might
-be of use when designing your own game.
-
-"""
-importos
-importgc
-importsys
-importtypes
-importmath
-importre
-importtextwrap
-importrandom
-importinspect
-importtraceback
-importimportlib
-importimportlib.util
-importimportlib.machinery
-fromastimportliteral_eval
-fromsimpleevalimportsimple_eval
-fromunicodedataimporteast_asian_width
-fromtwisted.internet.taskimportdeferLater
-fromtwisted.internet.deferimportreturnValue# noqa - used as import target
-fromtwisted.internetimportthreads,reactor
-fromos.pathimportjoinasosjoin
-frominspectimportismodule,trace,getmembers,getmodule,getmro
-fromcollectionsimportdefaultdict,OrderedDict
-fromdjango.confimportsettings
-fromdjango.utilsimporttimezone
-fromdjango.utils.htmlimportstrip_tags
-fromdjango.utils.translationimportgettextas_
-fromdjango.appsimportapps
-fromdjango.core.validatorsimportvalidate_emailasdjango_validate_email
-fromdjango.core.exceptionsimportValidationErrorasDjangoValidationError
-
-fromevennia.utilsimportlogger
-
-_MULTIMATCH_TEMPLATE=settings.SEARCH_MULTIMATCH_TEMPLATE
-_EVENNIA_DIR=settings.EVENNIA_DIR
-_GAME_DIR=settings.GAME_DIR
-ENCODINGS=settings.ENCODINGS
-
-_TASK_HANDLER=None
-_TICKER_HANDLER=None
-_STRIP_UNSAFE_TOKENS=None
-
-_GA=object.__getattribute__
-_SA=object.__setattr__
-_DA=object.__delattr__
-
-
-
[docs]defis_iter(obj):
- """
- Checks if an object behaves iterably.
-
- Args:
- obj (any): Entity to check for iterability.
-
- Returns:
- is_iterable (bool): If `obj` is iterable or not.
-
- Notes:
- Strings are *not* accepted as iterable (although they are
- actually iterable), since string iterations are usually not
- what we want to do with a string.
-
- """
- ifisinstance(obj,(str,bytes)):
- returnFalse
-
- try:
- returniter(obj)andTrue
- exceptTypeError:
- returnFalse
-
-
-
[docs]defmake_iter(obj):
- """
- Makes sure that the object is always iterable.
-
- Args:
- obj (any): Object to make iterable.
-
- Returns:
- iterable (list or iterable): The same object
- passed-through or made iterable.
-
- """
- returnnotis_iter(obj)and[obj]orobj
-
-
-
[docs]defwrap(text,width=None,indent=0):
- """
- Safely wrap text to a certain number of characters.
-
- Args:
- text (str): The text to wrap.
- width (int, optional): The number of characters to wrap to.
- indent (int): How much to indent each line (with whitespace).
-
- Returns:
- text (str): Properly wrapped text.
-
- """
- width=widthifwidthelsesettings.CLIENT_DEFAULT_WIDTH
- ifnottext:
- return""
- indent=" "*indent
- returnto_str(textwrap.fill(text,width,initial_indent=indent,subsequent_indent=indent))
-
-
-# alias - fill
-fill=wrap
-
-
-
[docs]defpad(text,width=None,align="c",fillchar=" "):
- """
- Pads to a given width.
-
- Args:
- text (str): Text to pad.
- width (int, optional): The width to pad to, in characters.
- align (str, optional): This is one of 'c', 'l' or 'r' (center,
- left or right).
- fillchar (str, optional): The character to fill with.
-
- Returns:
- text (str): The padded text.
-
- """
- width=widthifwidthelsesettings.CLIENT_DEFAULT_WIDTH
- align=alignifalignin("c","l","r")else"c"
- fillchar=fillchar[0]iffillcharelse" "
- ifalign=="l":
- returntext.ljust(width,fillchar)
- elifalign=="r":
- returntext.rjust(width,fillchar)
- else:
- returntext.center(width,fillchar)
-
-
-
[docs]defcrop(text,width=None,suffix="[...]"):
- """
- Crop text to a certain width, throwing away text from too-long
- lines.
-
- Args:
- text (str): Text to crop.
- width (int, optional): Width of line to crop, in characters.
- suffix (str, optional): This is appended to the end of cropped
- lines to show that the line actually continues. Cropping
- will be done so that the suffix will also fit within the
- given width. If width is too small to fit both crop and
- suffix, the suffix will be dropped.
-
- Returns:
- text (str): The cropped text.
-
- """
- width=widthifwidthelsesettings.CLIENT_DEFAULT_WIDTH
- ltext=len(text)
- ifltext<=width:
- returntext
- else:
- lsuffix=len(suffix)
- text=text[:width]iflsuffix>=widthelse"%s%s"%(text[:width-lsuffix],suffix)
- 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, 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.
-
- Notes:
- This is useful for preserving triple-quoted string indentation
- while still shifting it all to be next to the left edge of the
- display.
-
- """
- ifnottext:
- return""
- 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
- )
-
-
-
[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
- words with extra whitespace where necessary. Paragraphs will
- be retained.
-
- Args:
- text (str): Text to justify.
- width (int, optional): The length of each line, in characters.
- align (str, optional): The alignment, 'l', 'c', 'r' or 'f'
- for left, center, right or full justification respectively.
- indent (int, optional): Number of characters indentation of
- entire justified text block.
-
- Returns:
- justified (str): The justified and indented block of text.
-
- """
- width=widthifwidthelsesettings.CLIENT_DEFAULT_WIDTH
-
- def_process_line(line):
- """
- helper function that distributes extra spaces between words. The number
- of gaps is nwords - 1 but must be at least 1 for single-word lines. We
- distribute odd spaces randomly to one of the gaps.
- """
- line_rest=width-(wlen+ngaps)
- gap=" "# minimum gap between words
- ifline_rest>0:
- ifalign=="l":
- ifline[-1]=="\n\n":
- line[-1]=" "*(line_rest-1)+"\n"+" "*width+"\n"+" "*width
- else:
- line[-1]+=" "*line_rest
- elifalign=="r":
- line[0]=" "*line_rest+line[0]
- elifalign=="c":
- pad=" "*(line_rest//2)
- line[0]=pad+line[0]
- ifline[-1]=="\n\n":
- line[-1]+=(
- pad+" "*(line_rest%2-1)+"\n"+" "*width+"\n"+" "*width
- )
- else:
- line[-1]=line[-1]+pad+" "*(line_rest%2)
- else:# align 'f'
- gap+=" "*(line_rest//max(1,ngaps))
- rest_gap=line_rest%max(1,ngaps)
- foriinrange(rest_gap):
- line[i]+=" "
- elifnotany(line):
- return[" "*width]
- returngap.join(line)
-
- # split into paragraphs and words
- paragraphs=re.split("\n\s*?\n",text,re.MULTILINE)
- words=[]
- forip,paragraphinenumerate(paragraphs):
- ifip>0:
- words.append(("\n",0))
- words.extend((word,len(word))forwordinparagraph.split())
- ngaps,wlen,line=0,0,[]
-
- lines=[]
- whilewords:
- ifnotline:
- # start a new line
- word=words.pop(0)
- wlen=word[1]
- line.append(word[0])
- elif(words[0][1]+wlen+ngaps)>=width:
- # next word would exceed word length of line + smallest gaps
- lines.append(_process_line(line))
- ngaps,wlen,line=0,0,[]
- else:
- # put a new word on the line
- word=words.pop(0)
- line.append(word[0])
- ifword[1]==0:
- # a new paragraph, process immediately
- lines.append(_process_line(line))
- ngaps,wlen,line=0,0,[]
- else:
- wlen+=word[1]
- ngaps+=1
-
- ifline:# catch any line left behind
- lines.append(_process_line(line))
- indentstring=" "*indent
- 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.
-
- Args:
- string (str): The string to columnize.
- columns (int, optional): The number of columns to use.
- spacing (int, optional): How much space to have between columns.
- width (int, optional): The max width of the columns.
- Defaults to client's default width.
-
- Returns:
- columns (str): Text divided into columns.
-
- Raises:
- RuntimeError: If given invalid values.
-
- """
- columns=max(1,columns)
- spacing=max(1,spacing)
- width=widthifwidthelsesettings.CLIENT_DEFAULT_WIDTH
-
- w_spaces=(columns-1)*spacing
- w_txt=max(1,width-w_spaces)
-
- ifw_spaces+columns>width:# require at least 1 char per column
- raiseRuntimeError("Width too small to fit columns")
-
- colwidth=int(w_txt/(1.0*columns))
-
- # first make a single column which we then split
- onecol=justify(string,width=colwidth,align=align)
- onecol=onecol.split("\n")
-
- nrows,dangling=divmod(len(onecol),columns)
- nrows=[nrows+1ifi<danglingelsenrowsforiinrange(columns)]
-
- height=max(nrows)
- cols=[]
- istart=0
- forirowsinnrows:
- cols.append(onecol[istart:istart+irows])
- istart=istart+irows
- forcolincols:
- iflen(col)<height:
- col.append(" "*colwidth)
-
- sep=" "*spacing
- rows=[]
- forirowinrange(height):
- rows.append(sep.join(col[irow]forcolincols))
-
- return"\n".join(rows)
-
-
-
[docs]defiter_to_str(iterable,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`
- is `True`, the outgoing strings will be surrounded by quotes.
-
- Args:
- iterable (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.
-
- Notes:
- Default is to use 'Oxford comma', like 1, 2, 3, and 4. To remove, give
- `endsep` as just `and`.
-
- Examples:
-
- ```python
- >>> 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"'
- ```
-
- """
- ifnotiterable:
- return""
- iterable=list(make_iter(iterable))
- len_iter=len(iterable)
-
- ifaddquote:
- iterable=tuple(f'"{val}"'forvaliniterable)
- else:
- iterable=tuple(str(val)forvaliniterable)
-
- ifendsep.startswith(","):
- # oxford comma alternative
- endsep=endsep[1:]iflen_iter<3elseendsep
- elifendsep:
- # normal space-separated end separator
- endsep=" "+str(endsep).strip()
- else:
- # no separator given - use comma
- endsep=','
-
- iflen_iter==1:
- returnstr(iterable[0])
- eliflen_iter==2:
- returnf"{endsep} ".join(str(v)forviniterable)
- else:
- return", ".join(str(v)forviniterable[:-1])+f"{endsep}{iterable[-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.
-
- Args:
- instring (string): A string that may potentially contain
- wildcards (`*` or `?`).
-
- Returns:
- regex (str): A string where wildcards were replaced with
- regular expressions.
-
- """
- regexp_string=""
-
- # If the string starts with an asterisk, we can't impose the beginning of
- # string (^) limiter.
- ifinstring[0]!="*":
- regexp_string+="^"
-
- # Replace any occurances of * or ? with the appropriate groups.
- regexp_string+=instring.replace("*","(.*)").replace("?","(.{1})")
-
- # If there's an asterisk at the end of the string, we can't impose the
- # end of string ($) limiter.
- ifinstring[-1]!="*":
- regexp_string+="$"
-
- returnregexp_string
-
-
-
[docs]deftime_format(seconds,style=0):
- """
- Function to return a 'prettified' version of a value in seconds.
-
- Args:
- seconds (int): Number if seconds to format.
- style (int): One of the following styles:
- 0. "1d 08:30"
- 1. "1d"
- 2. "1 day, 8 hours, 30 minutes"
- 3. "1 day, 8 hours, 30 minutes, 10 seconds"
- 4. highest unit (like "3 years" or "8 months" or "1 second")
- Returns:
- timeformatted (str): A pretty time string.
- """
- ifseconds<0:
- seconds=0
- else:
- # We'll just use integer math, no need for decimal precision.
- seconds=int(seconds)
-
- days=seconds//86400
- seconds-=days*86400
- hours=seconds//3600
- seconds-=hours*3600
- minutes=seconds//60
- seconds-=minutes*60
-
- retval=""
- ifstyle==0:
- """
- Standard colon-style output.
- """
- ifdays>0:
- retval="%id %02i:%02i"%(days,hours,minutes)
- else:
- retval="%02i:%02i"%(hours,minutes)
- returnretval
-
- elifstyle==1:
- """
- Simple, abbreviated form that only shows the highest time amount.
- """
- ifdays>0:
- return"%id"%(days,)
- elifhours>0:
- return"%ih"%(hours,)
- elifminutes>0:
- return"%im"%(minutes,)
- else:
- return"%is"%(seconds,)
- elifstyle==2:
- """
- Full-detailed, long-winded format. We ignore seconds.
- """
- days_str=hours_str=""
- minutes_str="0 minutes"
-
- ifdays>0:
- ifdays==1:
- days_str="%i day, "%days
- else:
- days_str="%i days, "%days
- ifdaysorhours>0:
- ifhours==1:
- hours_str="%i hour, "%hours
- else:
- hours_str="%i hours, "%hours
- ifhoursorminutes>0:
- ifminutes==1:
- minutes_str="%i minute "%minutes
- else:
- minutes_str="%i minutes "%minutes
- retval="%s%s%s"%(days_str,hours_str,minutes_str)
- elifstyle==3:
- """
- Full-detailed, long-winded format. Includes seconds.
- """
- days_str=hours_str=minutes_str=seconds_str=""
- ifdays>0:
- ifdays==1:
- days_str="%i day, "%days
- else:
- days_str="%i days, "%days
- ifdaysorhours>0:
- ifhours==1:
- hours_str="%i hour, "%hours
- else:
- hours_str="%i hours, "%hours
- ifhoursorminutes>0:
- ifminutes==1:
- minutes_str="%i minute "%minutes
- else:
- minutes_str="%i minutes "%minutes
- ifminutesorseconds>0:
- ifseconds==1:
- seconds_str="%i second "%seconds
- else:
- seconds_str="%i seconds "%seconds
- retval="%s%s%s%s"%(days_str,hours_str,minutes_str,seconds_str)
- elifstyle==4:
- """
- Only return the highest unit.
- """
- ifdays>=730:# Several years
- return"{} years".format(days//365)
- elifdays>=365:# One year
- return"a year"
- elifdays>=62:# Several months
- return"{} months".format(days//31)
- elifdays>=31:# One month
- return"a month"
- elifdays>=2:# Several days
- return"{} days".format(days)
- elifdays>0:
- return"a day"
- elifhours>=2:# Several hours
- return"{} hours".format(hours)
- elifhours>0:# One hour
- return"an hour"
- elifminutes>=2:# Several minutes
- return"{} minutes".format(minutes)
- elifminutes>0:# One minute
- return"a minute"
- elifseconds>=2:# Several seconds
- return"{} seconds".format(seconds)
- elifseconds==1:
- return"a second"
- else:
- return"0 seconds"
- else:
- raiseValueError("Unknown style for time format: %s"%style)
-
- returnretval.strip()
-
-
-
[docs]defdatetime_format(dtobj):
- """
- Pretty-prints the time since a given time.
-
- Args:
- dtobj (datetime): An datetime object, e.g. from Django's
- `DateTimeField`.
-
- Returns:
- deltatime (str): A string describing how long ago `dtobj`
- took place.
-
- """
-
- now=timezone.now()
-
- ifdtobj.year<now.year:
- # another year (Apr 5, 2019)
- timestring=dtobj.strftime(f"%b {dtobj.day}, %Y")
- elifdtobj.date()<now.date():
- # another date, same year (Apr 5)
- timestring=dtobj.strftime(f"%b {dtobj.day}")
- elifdtobj.hour<now.hour-1:
- # same day, more than 1 hour ago (10:45)
- timestring=dtobj.strftime("%H:%M")
- else:
- # same day, less than 1 hour ago (10:45:33)
- timestring=dtobj.strftime("%H:%M:%S")
- returntimestring
-
-
-
[docs]defhost_os_is(osname):
- """
- Check to see if the host OS matches the query.
-
- Args:
- osname (str): Common names are "posix" (linux/unix/mac) and
- "nt" (windows).
-
- Args:
- is_os (bool): If the os matches or not.
-
- """
- returnos.name==osname
-
-
-
[docs]defget_evennia_version(mode="long"):
- """
- Helper method for getting the current evennia version.
-
- Args:
- mode (str, optional): One of:
- - long: 0.9.0 rev342453534
- - short: 0.9.0
- - pretty: Evennia 0.9.0
-
- Returns:
- version (str): The version string.
-
- """
- importevennia
-
- vers=evennia.__version__
- ifmode=="short":
- returnvers.split()[0].strip()
- elifmode=="pretty":
- vers=vers.split()[0].strip()
- returnf"Evennia {vers}"
- else:# mode "long":
- returnvers
-
-
-
[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.
-
- Args:
- python_path (str): A dot-python path
- file_ending (str): A file ending, including the period.
- pypath_prefixes (list): A list of paths to test for existence. These
- should be on python.path form. EVENNIA_DIR and GAME_DIR are automatically
- checked, they need not be added to this list.
-
- Returns:
- abspaths (list): All existing, absolute paths created by
- converting `python_path` to an absolute paths and/or
- prepending `python_path` by `settings.EVENNIA_DIR`,
- `settings.GAME_DIR` and by`pypath_prefixes` respectively.
-
- Notes:
- This will also try a few combinations of paths to allow cases
- where pypath is given including the "evennia." or "mygame."
- prefixes.
-
- """
- path=python_path.strip().split(".")
- plong=osjoin(*path)+file_ending
- pshort=(
- osjoin(*path[1:])+file_endingiflen(path)>1elseplong
- )# in case we had evennia. or mygame.
- prefixlong=(
- [osjoin(*ppath.strip().split("."))forppathinmake_iter(pypath_prefixes)]
- ifpypath_prefixes
- else[]
- )
- prefixshort=(
- [
- osjoin(*ppath.strip().split(".")[1:])
- forppathinmake_iter(pypath_prefixes)
- iflen(ppath.strip().split("."))>1
- ]
- ifpypath_prefixes
- else[]
- )
- paths=(
- [plong]
- +[osjoin(_EVENNIA_DIR,prefix,plong)forprefixinprefixlong]
- +[osjoin(_GAME_DIR,prefix,plong)forprefixinprefixlong]
- +[osjoin(_EVENNIA_DIR,prefix,plong)forprefixinprefixshort]
- +[osjoin(_GAME_DIR,prefix,plong)forprefixinprefixshort]
- +[osjoin(_EVENNIA_DIR,plong),osjoin(_GAME_DIR,plong)]
- +[osjoin(_EVENNIA_DIR,prefix,pshort)forprefixinprefixshort]
- +[osjoin(_GAME_DIR,prefix,pshort)forprefixinprefixshort]
- +[osjoin(_EVENNIA_DIR,prefix,pshort)forprefixinprefixlong]
- +[osjoin(_GAME_DIR,prefix,pshort)forprefixinprefixlong]
- +[osjoin(_EVENNIA_DIR,pshort),osjoin(_GAME_DIR,pshort)]
- )
- # filter out non-existing paths
- returnlist(set(pforpinpathsifos.path.isfile(p)))
-
-
-
[docs]defdbref(inp,reqhash=True):
- """
- Converts/checks if input is a valid dbref.
-
- Args:
- inp (int, str): A database ref on the form N or #N.
- reqhash (bool, optional): Require the #N form to accept
- input as a valid dbref.
-
- Returns:
- dbref (int or None): The integer part of the dbref or `None`
- if input was not a valid dbref.
-
- """
- ifreqhash:
- num=(
- int(inp.lstrip("#"))
- if(isinstance(inp,str)andinp.startswith("#")andinp.lstrip("#").isdigit())
- elseNone
- )
- returnnumifisinstance(num,int)andnum>0elseNone
- elifisinstance(inp,str):
- inp=inp.lstrip("#")
- returnint(inp)ifinp.isdigit()andint(inp)>0elseNone
- else:
- returninpifisinstance(inp,int)elseNone
-
-
-
[docs]defdbref_to_obj(inp,objclass,raise_errors=True):
- """
- Convert a #dbref to a valid object.
-
- Args:
- inp (str or int): A valid #dbref.
- objclass (class): A valid django model to filter against.
- raise_errors (bool, optional): Whether to raise errors
- or return `None` on errors.
-
- Returns:
- obj (Object or None): An entity loaded from the dbref.
-
- Raises:
- Exception: If `raise_errors` is `True` and
- `objclass.objects.get(id=dbref)` did not return a valid
- object.
-
- """
- dbid=dbref(inp)
- ifnotdbid:
- # we only convert #dbrefs
- returninp
- try:
- ifdbid<0:
- returnNone
- exceptValueError:
- returnNone
-
- # if we get to this point, inp is an integer dbref; get the matching object
- try:
- returnobjclass.objects.get(id=dbid)
- exceptException:
- ifraise_errors:
- raise
- returninp
-
-
-# legacy alias
-dbid_to_obj=dbref_to_obj
-
-
-# some direct translations for the latinify
-_UNICODE_MAP={
- "EM DASH":"-",
- "FIGURE DASH":"-",
- "EN DASH":"-",
- "HORIZONTAL BAR":"-",
- "HORIZONTAL ELLIPSIS":"...",
- "LEFT SINGLE QUOTATION MARK":"'",
- "RIGHT SINGLE QUOTATION MARK":"'",
- "LEFT DOUBLE QUOTATION MARK":'"',
- "RIGHT DOUBLE QUOTATION MARK":'"',
-}
-
-
-
[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.
-
- Arguments:
- string (str): A string to convert to 'safe characters' convertable
- to an latin-1 bytestring later.
- default (str, optional): Characters resisting mapping will be replaced
- with this character or string. The intent is to apply an encode operation
- on the string soon after.
-
- Returns:
- string (str): A 'latinified' string where each unicode character has been
- replaced with a 'safe' equivalent available in the ascii/latin-1 charset.
- Notes:
- This is inspired by the gist by Ricardo Murri:
- https://gist.github.com/riccardomurri/3c3ccec30f037be174d3
-
- """
-
- fromunicodedataimportname
-
- ifisinstance(string,bytes):
- string=string.decode("utf8")
-
- converted=[]
- forunichiniter(string):
- try:
- ch=unich.encode("utf8").decode("ascii")
- exceptUnicodeDecodeError:
- # deduce a latin letter equivalent from the Unicode data
- # point name; e.g., since `name(u'á') == 'LATIN SMALL
- # LETTER A WITH ACUTE'` translate `á` to `a`. However, in
- # some cases the unicode name is still "LATIN LETTER"
- # although no direct equivalent in the Latin alphabet
- # exists (e.g., Þ, "LATIN CAPITAL LETTER THORN") -- we can
- # avoid these cases by checking that the letter name is
- # composed of one letter only.
- # We also supply some direct-translations for some particular
- # common cases.
- what=name(unich)
- ifwhatin_UNICODE_MAP:
- ch=_UNICODE_MAP[what]
- else:
- what=what.split()
- ifwhat[0]=="LATIN"andwhat[2]=="LETTER"andlen(what[3])==1:
- ch=what[3].lower()ifwhat[1]=="SMALL"elsewhat[3].upper()
- else:
- ch=default
- converted.append(chr(ord(ch)))
- 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.
-
- Args:
- text (any): The text to encode to bytes. If bytes, return unchanged. If not a str, convert
- to str before converting.
- session (Session, optional): A Session to get encoding info from. Will try this before
- falling back to settings.ENCODINGS.
-
- Returns:
- encoded_text (bytes): the encoded text following the session's protocol flag followed by the
- encodings specified in settings.ENCODINGS. If all attempt fail, log the error and send
- 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.
-
- Notes:
- If `text` is already bytes, return it as is.
-
- """
- ifisinstance(text,bytes):
- returntext
- ifnotisinstance(text,str):
- # convert to a str representation before encoding
- try:
- text=str(text)
- exceptException:
- text=repr(text)
-
- default_encoding=session.protocol_flags.get("ENCODING","utf-8")ifsessionelse"utf-8"
- try:
- returntext.encode(default_encoding)
- except(LookupError,UnicodeEncodeError):
- forencodinginsettings.ENCODINGS:
- try:
- returntext.encode(encoding)
- except(LookupError,UnicodeEncodeError):
- pass
- # no valid encoding found. Replace unconvertable parts with ?
- 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.
-
- Args:
- text (any): The text to encode to bytes. If a str, return it. If also not bytes, convert
- to str using str() or repr() as a fallback.
- session (Session, optional): A Session to get encoding info from. Will try this before
- falling back to settings.ENCODINGS.
-
- Returns:
- decoded_text (str): The decoded text.
-
- Notes:
- If `text` is already str, return it as is.
- """
- ifisinstance(text,str):
- returntext
- ifnotisinstance(text,bytes):
- # not a byte, convert directly to str
- try:
- returnstr(text)
- exceptException:
- returnrepr(text)
-
- default_encoding=session.protocol_flags.get("ENCODING","utf-8")ifsessionelse"utf-8"
- try:
- returntext.decode(default_encoding)
- except(LookupError,UnicodeDecodeError):
- forencodinginsettings.ENCODINGS:
- try:
- returntext.decode(encoding)
- except(LookupError,UnicodeDecodeError):
- pass
- # no valid encoding found. Replace unconvertable parts with ?
- 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.
-
- Args:
- emailaddress (str): Email address to validate.
-
- Returns:
- bool: If this is a valid email or not.
-
- """
- try:
- django_validate_email(str(emailaddress))
- exceptDjangoValidationError:
- returnFalse
- exceptException:
- logger.log_trace()
- returnFalse
- else:
- 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 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 differentiates this function from Python's `isinstance()` is the
- flexibility in the types allowed for the object and parent being compared.
-
- """
-
- ifcallable(obj):
- # this is a class
- obj_paths=["%s.%s"%(mod.__module__,mod.__name__)formodinobj.mro()]
- else:
- obj_paths=["%s.%s"%(mod.__module__,mod.__name__)formodinobj.__class__.mro()]
-
- ifisinstance(parent,str):
- # a given string path, for direct matching
- parent_path=parent
- elifcallable(parent):
- # this is a class
- parent_path="%s.%s"%(parent.__module__,parent.__name__)
- else:
- parent_path="%s.%s"%(parent.__class__.__module__,parent.__class__.__name__)
- 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
- any results if called from inside the game.
-
- Returns:
- services (dict): A dict of available services.
-
- """
- fromevennia.server.sessionhandlerimportSESSIONS
-
- ifhasattr(SESSIONS,"server")andhasattr(SESSIONS.server,"services"):
- server=SESSIONS.server.services.namedServices
- else:
- # This function must be called from inside the evennia process.
- server={}
- delSESSIONS
- 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'.
-
- Returns:
- uses (bool): If the given database is used or not.
-
- """
- try:
- engine=settings.DATABASES["default"]["ENGINE"]
- exceptKeyError:
- engine=settings.DATABASE_ENGINE
- returnengine=="django.db.backends.%s"%name
-
-
-
[docs]defdelay(timedelay,callback,*args,**kwargs):
- """
- Delay the calling of a callback (function).
-
- Args:
- timedelay (int or float): The delay in seconds.
- callback (callable): Will be called as `callback(*args, **kwargs)`
- after `timedelay` seconds.
- *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.
- any (any): Will be used as keyword arguments to callback.
-
- Returns:
- task (TaskHandlerTask): An instance of a task.
- Refer to, evennia.scripts.taskhandler.TaskHandlerTask
-
- 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
- and other keyword arguments will be saved (serialized) in the database,
- assuming they can be. The callback will be executed even after
- a server restart/reload, taking into account the specified delay
- (and server down time).
- Keep in mind that persistent tasks arguments and callback should not
- use memory references.
- 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.
-
- """
- global_TASK_HANDLER
- if_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.
-
- Args:
- to_execute (callable): If this is a callable, it will be
- 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.
- at_return_kwargs (dict): This dictionary will be used as
- 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.
- at_err_kwargs (dict): This dictionary will be used as keyword
- arguments to the at_err errback.
-
- Notes:
- All other `*args` and `**kwargs` will be passed on to
- `to_execute`. Run_async will relay executed code to a thread
- or procpool.
-
- Use this function with restrain and only for features/commands
- that you know has no influence on the cause-and-effect order of your
- game (commands given after the async function might be executed before
- it has finished). Accessing the same property from different threads
- can lead to unpredicted behaviour if you are not careful (this is called a
- "race condition").
-
- Also note that some databases, notably sqlite3, don't support access from
- multiple threads simultaneously, so if you do heavy database access from
- your `to_execute` under sqlite3 you will probably run very slow or even get
- tracebacks.
-
- """
-
- # handle special reserved input kwargs
- callback=kwargs.pop("at_return",None)
- errback=kwargs.pop("at_err",None)
- callback_kwargs=kwargs.pop("at_return_kwargs",{})
- errback_kwargs=kwargs.pop("at_err_kwargs",{})
-
- ifcallable(to_execute):
- # no process pool available, fall back to old deferToThread mechanism.
- deferred=threads.deferToThread(to_execute,*args,**kwargs)
- else:
- # no appropriate input for this server setup
- raiseRuntimeError("'%s' could not be handled by run_async"%to_execute)
-
- # attach callbacks
- ifcallback:
- deferred.addCallback(callback,**callback_kwargs)
- deferred.addErrback(errback,**errback_kwargs)
-
-
-
[docs]defcheck_evennia_dependencies():
- """
- Checks the versions of Evennia's dependencies including making
- some checks for runtime libraries.
-
- Returns:
- result (bool): `False` if a show-stopping version mismatch is
- found.
-
- """
-
- # check main dependencies
- fromevennia.server.evennia_launcherimportcheck_main_evennia_dependencies
-
- not_error=check_main_evennia_dependencies()
-
- errstring=""
- # South is no longer used ...
- if"south"insettings.INSTALLED_APPS:
- errstring+=(
- "\n ERROR: 'south' found in settings.INSTALLED_APPS. "
- "\n South is no longer used. If this was added manually, remove it."
- )
- not_error=False
- # IRC support
- ifsettings.IRC_ENABLED:
- try:
- importtwisted.words
-
- twisted.words# set to avoid debug info about not-used import
- 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', "
- "\n others can get it from http://twistedmatrix.com/trac/wiki/TwistedWords."
- )
- not_error=False
- errstring=errstring.strip()
- iferrstring:
- mlen=max(len(line)forlineinerrstring.split("\n"))
- logger.log_err("%s\n%s\n%s"%("-"*mlen,errstring,"-"*mlen))
- returnnot_error
-
-
-
[docs]defhas_parent(basepath,obj):
- """
- Checks if `basepath` is somewhere in obj's parent tree.
-
- Args:
- basepath (str): Python dotpath to compare against obj path.
- obj (any): Object whose path is to be checked.
-
- Returns:
- has_parent (bool): If the check was successful or not.
-
- """
- try:
- returnany(
- cls
- forclsinobj.__class__.mro()
- ifbasepath=="%s.%s"%(cls.__module__,cls.__name__)
- )
- except(TypeError,AttributeError):
- # this can occur if we tried to store a class object, not an
- # instance. Not sure if one should defend against this.
- returnFalse
-
-
-
[docs]defmod_import_from_path(path):
- """
- Load a Python module at the specified path.
-
- Args:
- path (str): An absolute path to a Python module to load.
-
- Returns:
- (module or None): An imported module if the path was a valid
- Python module. Returns `None` if the import failed.
-
- """
- ifnotos.path.isabs(path):
- path=os.path.abspath(path)
- dirpath,filename=path.rsplit(os.path.sep,1)
- modname=filename.rstrip(".py")
-
- try:
- returnimportlib.machinery.SourceFileLoader(modname,path).load_module()
- exceptOSError:
- logger.log_trace(f"Could not find module '{modname}' ({modname}.py) at path '{dirpath}'")
- returnNone
-
-
-
[docs]defmod_import(module):
- """
- A generic Python module loader.
-
- Args:
- module (str, module): This can be either a Python path
- (dot-notation like `evennia.objects.models`), an absolute path
- (e.g. `/home/eve/evennia/evennia/objects/models.py`) or an
- already imported module object (e.g. `models`)
- Returns:
- (module or None): An imported module. If the input argument was
- already a module, this is returned as-is, otherwise the path is
- parsed and imported. Returns `None` and logs error if import failed.
-
- """
- ifnotmodule:
- returnNone
-
- ifisinstance(module,types.ModuleType):
- # if this is already a module, we are done
- returnmodule
-
- ifmodule.endswith(".py")andos.path.exists(module):
- returnmod_import_from_path(module)
-
- try:
- returnimportlib.import_module(module)
- exceptImportError:
- returnNone
-
-
-
[docs]defall_from_module(module):
- """
- Return all global-level variables defined in a module.
-
- Args:
- module (str, module): This can be either a Python path
- (dot-notation like `evennia.objects.models`), an absolute path
- (e.g. `/home/eve/evennia/evennia/objects.models.py`) or an
- already imported module object (e.g. `models`)
-
- Returns:
- dict: A dict of {variablename: variable} for all
- variables in the given module.
-
- Notes:
- 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 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.
-
- Args:
- module (str, module): A python-path to a module or an actual
- module object.
-
- Returns:
- callables (dict): A dict of {name: callable, ...} from the module.
-
- Notes:
- Will ignore callables whose names start with underscore "_".
-
- """
- mod=mod_import(module)
- ifnotmod:
- return{}
- # make sure to only return callables actually defined in this module (not imports)
- members=getmembers(mod,predicate=lambdaobj:callable(obj)andgetmodule(obj)==mod)
- 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
- is given (or a list entry is `None`), all global variables are
- extracted from the module.
-
- Args:
- module (string or module): Python path, absolute path or a module.
- variable (string or iterable, optional): Single variable name or iterable
- of variable names to extract. If not given, all variables in
- the module will be returned.
- default (string, optional): Default value to use if a variable fails to
- be extracted. Ignored if `variable` is not given.
-
- Returns:
- variables (value or list): A single value or a list of values
- depending on if `variable` is given or not. Errors in lists
- are replaced by the `default` argument.
-
- """
-
- ifnotmodule:
- returndefault
-
- mod=mod_import(module)
-
- ifnotmod:
- returndefault
-
- ifvariable:
- result=[]
- forvarinmake_iter(variable):
- ifvar:
- # try to pick a named variable
- result.append(mod.__dict__.get(var,default))
- else:
- # get all
- result=[
- valforkey,valinmod.__dict__.items()ifnot(key.startswith("_")orismodule(val))
- ]
-
- iflen(result)==1:
- returnresult[0]
- 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.
-
- Args:
- module (string or module): Python path, absolute path or a module.
- variable (string or iterable, optional): Single variable name or iterable
- of variable names to extract. If not given, all variables in
- the module will be returned.
- default (string, optional): Default value to use if a variable fails to
- be extracted. Ignored if `variable` is not given.
-
- Returns:
- variables (value or list): A single (string) value or a list of values
- depending on if `variable` is given or not. Errors in lists (such
- as the value not being a string) are replaced by the `default` argument.
-
- """
- val=variable_from_module(module,variable=variable,default=default)
- ifval:
- ifvariable:
- returnval
- else:
- result=[vforvinmake_iter(val)ifisinstance(v,str)]
- returnresultifresultelsedefault
- returndefault
-
-
-
[docs]defrandom_string_from_module(module):
- """
- Returns a random global string from a module.
-
- Args:
- module (string or module): Python path, absolute path or a module.
-
- Returns:
- random (string): A random stribg variable from `module`.
- """
- 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
- prepended to see a match is found.
-
- Args:
- path (str): Full or partial python path.
- variable (str): Name of variable to import from module.
- default (string, optional): Default value to use if a variable fails to
- be extracted. Ignored if `variable` is not given.
- defaultpaths (iterable, options): Python paths to attempt in order if
- importing directly from `path` doesn't work.
-
- Returns:
- value (any): The variable imported from the module, or `default`, if
- not found.
-
- """
- paths=[path]+make_iter(defaultpaths)
- formodpathinpaths:
- try:
- mod=importlib.import_module(modpath)
- exceptImportErrorasex:
- ifnotstr(ex).startswith("No module named %s"%modpath):
- # this means the module was found but it
- # triggers an ImportError on import.
- raiseex
- returngetattr(mod,variable,default)
- returndefault
-
-
-
[docs]defclass_from_module(path,defaultpaths=None,fallback=None):
- """
- 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:
- path (str): Full Python dot-path to module.
- defaultpaths (iterable, optional): If a direct import from `path` fails,
- try subsequent imports by prepending those paths to `path`.
- fallback (str): If all other attempts fail, use this path as a fallback.
- This is intended as a last-resport. In the example of Evennia
- loading, this would be a path to a default parent class in the
- evennia repo itself.
-
- Returns:
- class (Class): An uninstatiated class recovered from path.
-
- Raises:
- ImportError: If all loading failed.
-
- """
- cls=None
- err=""
- ifdefaultpaths:
- paths=(
- [path]+["%s.%s"%(dpath,path)fordpathinmake_iter(defaultpaths)]
- ifdefaultpaths
- else[]
- )
- else:
- paths=[path]
-
- fortestpathinpaths:
- if"."inpath:
- testpath,clsname=testpath.rsplit(".",1)
- else:
- raiseImportError("the path '%s' is not on the form modulepath.Classname."%path)
-
- try:
- ifnotimportlib.util.find_spec(testpath,package="evennia"):
- continue
- exceptModuleNotFoundError:
- continue
-
- try:
- mod=importlib.import_module(testpath,package="evennia")
- exceptModuleNotFoundError:
- err=traceback.format_exc(30)
- break
-
- try:
- cls=getattr(mod,clsname)
- break
- exceptAttributeError:
- iflen(trace())>2:
- # AttributeError within the module, don't hide it
- err=traceback.format_exc(30)
- break
- ifnotcls:
- err="\nCould not load typeclass '{}'{}".format(
- path," with the following traceback:\n"+erriferrelse""
- )
- ifdefaultpaths:
- err+="\nPaths searched:\n%s"%"\n ".join(paths)
- else:
- err+="."
- logger.log_err(err)
- iffallback:
- logger.log_warn(f"Falling back to {fallback}.")
- returnclass_from_module(fallback)
- else:
- # even fallback fails
- raiseImportError(err)
- returncls
-
-
-# alias
-object_from_module=class_from_module
-
-
-
[docs]definit_new_account(account):
- """
- Deprecated.
- """
- fromevennia.utilsimportlogger
-
- 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
- Linguistics* (Coling 2008), pages 593-600, Manchester, August 2008.
- The measure-vectors used is simply a "bag of words" type histogram
- (but for letters).
-
- Args:
- string1 (str): String to compare (may contain any number of words).
- string2 (str): Second string to compare (any number of words).
-
- Returns:
- similarity (float): A value 0...1 rating how similar the two
- strings are.
-
- """
- vocabulary=set(list(string1+string2))
- vec1=[string1.count(v)forvinvocabulary]
- vec2=[string2.count(v)forvinvocabulary]
- try:
- returnfloat(sum(vec1[i]*vec2[i]foriinrange(len(vocabulary))))/(
- math.sqrt(sum(v1**2forv1invec1))*math.sqrt(sum(v2**2forv2invec2))
- )
- exceptZeroDivisionError:
- # can happen if empty-string cmdnames appear for some reason.
- # This is a no-match.
- 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.
-
- Args:
- string (str): A string to search for.
- vocabulary (iterable): A list of available strings.
- cutoff (int, 0-1): Limit the similarity matches (the higher
- the value, the more exact a match is required).
- maxnum (int): Maximum number of suggestions to return.
-
- 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.
-
- """
- return[
- tup[1]
- fortupinsorted(
- [(string_similarity(string,sugg),sugg)forsugginvocabulary],
- key=lambdatup:tup[0],
- reverse=True,
- )
- iftup[0]>=cutoff
- ][: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
- alternative. Case is not important. So e.g. "bi sh sw" or just
- "big" or "shiny" or "sw" will match "Big shiny sword". Scoring is
- done to allow to separate by most common demoninator. You will get
- multiple matches returned if appropriate.
-
- Args:
- alternatives (list of str): A list of possible strings to
- match.
- inp (str): Search criterion.
- ret_index (bool, optional): Return list of indices (from alternatives
- array) instead of strings.
- Returns:
- matches (list): String-matches or indices if `ret_index` is `True`.
-
- """
- ifnotalternativesornotinp:
- return[]
-
- matches=defaultdict(list)
- inp_words=inp.lower().split()
- foraltindex,altinenumerate(alternatives):
- alt_words=alt.lower().split()
- last_index=0
- score=0
- forinp_wordininp_words:
- # loop over parts, making sure only to visit each part once
- # (this will invalidate input in the wrong word order)
- submatch=[
- last_index+alt_num
- foralt_num,alt_wordinenumerate(alt_words[last_index:])
- ifalt_word.startswith(inp_word)
- ]
- ifsubmatch:
- last_index=min(submatch)+1
- score+=1
- else:
- score=0
- break
- ifscore:
- ifret_index:
- matches[score].append(altindex)
- else:
- matches[score].append(alt)
- ifmatches:
- returnmatches[max(matches)]
- return[]
-
-
-
[docs]defformat_table(table,extra_space=1):
- """
- Format a 2D array of strings into a multi-column table.
-
- 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
- column. All columns must have the same number of rows (some
- positions may be empty though).
- extra_space (int, optional): Sets how much *minimum* extra
- padding (in characters) should be left between columns.
-
- 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.
-
- `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 = ""
- 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[[]]
-
- max_widths=[max([len(str(val))forvalincol])forcolintable]
- ftable=[]
- forirowinrange(len(table[0])):
- ftable.append(
- [
- str(col[irow]).ljust(max_widths[icol])+" "*extra_space
- foricol,colinenumerate(table)
- ]
- )
- 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)
- 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.
-
- Returns:
- server, portal (tuple): The PIDs of the respective processes,
- or two `None` values if not found.
-
- Examples:
- This can be used to determine if we are in a subprocess by
-
- ```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")
- server_pid,portal_pid=None,None
- ifos.path.exists(server_pidfile):
- withopen(server_pidfile,"r")asf:
- server_pid=f.read()
- ifos.path.exists(portal_pidfile):
- withopen(portal_pidfile,"r")asf:
- portal_pid=f.read()
- ifserver_pidandportal_pid:
- returnint(server_pid),int(portal_pid)
- 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
- from the object.
-
- Args:
- obj (object): the object to be measured.
- max_depth (int, optional): maximum referential distance
- from `obj` that `deepsize()` should cover for
- measuring objects referenced by `obj`.
-
- Returns:
- size (int): deepsize of `obj` in Bytes.
-
- Notes:
- This measure is necessarily approximate since some
- memory is shared between objects. The `max_depth` of 4 is roughly
- tested to give reasonable size information about database models
- and their handlers.
-
- """
-
- def_recurse(o,dct,depth):
- if0<=max_depth<depth:
- return
- forrefingc.get_referents(o):
- idr=id(ref)
- ifidrnotindct:
- dct[idr]=(ref,sys.getsizeof(ref,default=0))
- _recurse(ref,dct,depth+1)
-
- sizedict={}
- _recurse(obj,sizedict,0)
- size=sys.getsizeof(obj)+sum([p[1]forpinsizedict.values()])
- returnsize
-
-
-# lazy load handler
-_missing=object()
-
-
-
[docs]classlazy_property:
- """
- Delays loading of property until first access. Credit goes to the
- Implementation in the werkzeug suite:
- http://werkzeug.pocoo.org/docs/utils/#werkzeug.utils.cached_property
-
- This should be used as a decorator in a class and in Evennia is
- mainly used to lazy-load handlers:
-
- ```python
- @lazy_property
- def attributes(self):
- return AttributeHandler(self)
- ```
-
- Once initialized, the `AttributeHandler` will be available as a
- property "attributes" on the object. This is read-only since
- this functionality is pretty much exclusively used by handlers.
-
- """
-
-
[docs]def__init__(self,func,name=None,doc=None):
- """Store all properties for now"""
- self.__name__=nameorfunc.__name__
- self.__module__=func.__module__
- self.__doc__=docorfunc.__doc__
- self.func=func
-
- def__get__(self,obj,type=None):
- """Triggers initialization"""
- ifobjisNone:
- returnself
- value=obj.__dict__.get(self.__name__,_missing)
- ifvalueis_missing:
- value=self.func(obj)
- obj.__dict__[self.__name__]=value
- returnvalue
-
- def__set__(self,obj,value):
- """Protect against setting"""
- handlername=self.__name__
- raiseAttributeError(
- _("{obj}.{handlername} is a handler and can't be set directly. "
- "To add values, use `{obj}.{handlername}.add()` instead.").format(
- obj=obj,handlername=handlername)
- )
-
- def__delete__(self,obj):
- """Protect against deleting"""
- handlername=self.__name__
- raiseAttributeError(
- _("{obj}.{handlername} is a handler and can't be deleted directly. "
- "To remove values, use `{obj}.{handlername}.remove()` instead.").format(
- obj=obj,handlername=handlername)
- )
[docs]defstrip_control_sequences(string):
- """
- Remove non-print text sequences.
-
- Args:
- string (str): Text to strip.
-
- Returns.
- text (str): Stripped text.
-
- """
- global_STRIP_ANSI
- ifnot_STRIP_ANSI:
- fromevennia.utils.ansiimportstrip_raw_ansias_STRIP_ANSI
- return_RE_CONTROL_CHAR.sub("",_STRIP_ANSI(string))
-
-
-
[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.
-
- Args:
- callerdepth (int): Must be larger than 0. When > 1, it will
- print the caller of the caller etc.
-
- Returns:
- calledby (str): A debug string detailing which routine called
- us.
-
- """
- importinspect
-
- stack=inspect.stack()
- # we must step one extra level back in stack since we don't want
- # to include the call of this function itself.
- callerdepth=min(max(2,callerdepth+1),len(stack)-1)
- frame=inspect.stack()[callerdepth]
- path=os.path.sep.join(frame[1].rsplit(os.path.sep,2)[-2:])
- 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.
-
- Args:
- target (str): A string with potential MXP components
- to search.
-
- Returns:
- length (int): The length of `target`, ignoring MXP components.
-
- """
- # Would create circular import if in module root.
- fromevennia.utils.ansiimportANSI_PARSER
-
- ifinherits_from(target,str)and"|lt"intarget:
- returnlen(ANSI_PARSER.strip_mxp(target))
- 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
- strip MXP patterns.
-
- Args:
- target (any): Something to measure the length of. If a string, it will be
- measured keeping asian-character and MXP links in mind.
-
- Return:
- int: The visible width of the target.
-
- """
- # Would create circular import if in module root.
- fromevennia.utils.ansiimportANSI_PARSER
-
- ifinherits_from(target,str):
- # str or ANSIString
- target=ANSI_PARSER.strip_mxp(target)
- target=ANSI_PARSER.parse_ansi(target,strip_ansi=True)
- extra_wide=("F","W")
- returnsum(2ifeast_asian_width(char)inextra_wideelse1forcharintarget)
- else:
- returnlen(target)
-
-
-# -------------------------------------------------------------------
-# Search handler function
-# -------------------------------------------------------------------
-#
-# 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
- to manage errors in command lookup.
-
- Args:
- matches (list): This is a list of 0, 1 or more typeclass
- instances or Command instances, the matched result of the
- search. If 0, a nomatch error should be echoed, and if >1,
- multimatch errors should be given. Only if a single match
- should the result pass through.
- caller (Object): The object performing the search and/or which should
- receive error messages.
- 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)
-
- """
-
- error=""
- ifnotmatches:
- # no results.
- error=kwargs.get("nofound_string")or_("Could not find '{query}'.").format(query=query)
- matches=None
- eliflen(matches)>1:
- multimatch_string=kwargs.get("multimatch_string")
- ifmultimatch_string:
- error="%s\n"%multimatch_string
- else:
- error=_("More than one match for '{query}' (please narrow target):\n").format(
- query=query
- )
-
- fornum,resultinenumerate(matches):
- # we need to consider Commands, where .aliases is a list
- aliases=result.aliases.all()ifhasattr(result.aliases,"all")elseresult.aliases
- # remove any pluralization aliases
- aliases=[
- alias
- foraliasinaliases
- ifhasattr(alias,"category")andalias.categorynotin("plural_key",)
- ]
- error+=_MULTIMATCH_TEMPLATE.format(
- number=num+1,
- name=result.get_display_name(caller)
- ifhasattr(result,"get_display_name")
- elsequery,
- aliases=" [{alias}]".format(alias=";".join(aliases)ifaliaseselse""),
- info=result.get_extra_info(caller),
- )
- matches=None
- else:
- # exactly one match
- matches=matches[0]
-
- iferrorandnotquiet:
- caller.msg(error.strip())
- 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
- grow out of bounds.
-
- """
-
-
[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.
- fifo (bool, optional): Defaults to `True`. Remove overshooting elements
- in FIFO order. If `False`, remove in FILO order.
-
- """
- super().__init__()
- self.size_limit=kwargs.get("size_limit",None)
- self.filo=notkwargs.get("fifo",True)# FIFO inverse of FILO
- self._check_size()
[docs]defget_game_dir_path():
- """
- This is called by settings_default in order to determine the path
- of the game directory.
-
- Returns:
- path (str): Full OS path to the game dir
-
- """
- # current working directory, assumed to be somewhere inside gamedir.
- forinuminrange(10):
- gpath=os.getcwd()
- if"server"inos.listdir(gpath):
- ifos.path.isfile(os.path.join("server","conf","settings.py")):
- returngpath
- else:
- os.chdir(os.pardir)
- 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.
-
- Returns:
- dict: On the form `{"typeclass.path": typeclass, ...}`
-
- Notes:
- 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
-
- typeclasses={
- "{}.{}".format(model.__module__,model.__name__):model
- formodelinapps.get_models()
- ifTypedObjectingetmro(model)
- }
- ifparent:
- typeclasses={
- name:typeclass
- forname,typeclassintypeclasses.items()
- ifinherits_from(typeclass,parent)
- }
- returntypeclasses
-
-
-
[docs]defget_all_cmdsets(parent=None):
- """
- List available cmdsets from all available modules.
-
- 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")
- # wait five seconds
- yield(5)
- # ask user (caller) a question
- response = yield("Do you want to continue waiting?")
- if response == "yes":
- yield(5)
- else:
- # ...
- ```
-
- Notes:
- This turns the decorated function or method into a generator.
-
- """
- fromevennia.utils.evmenuimportget_input
-
- def_process_input(caller,prompt,result,generator):
- deferLater(reactor,0,_iterate,generator,caller,response=result)
- returnFalse
-
- def_iterate(generator,caller=None,response=None):
- try:
- ifresponseisNone:
- value=next(generator)
- else:
- value=generator.send(response)
- exceptStopIteration:
- pass
- else:
- ifisinstance(value,(int,float)):
- delay(value,_iterate,generator,caller=caller)
- elifisinstance(value,str):
- ifnotcaller:
- raiseValueError(
- "To retrieve input from a @pausable method, that method "
- "must be called with a 'caller' argument)"
- )
- get_input(caller,value,_process_input,generator=generator)
- else:
- raiseValueError("yield(val) in a @pausable method must have an int/float as arg.")
-
- defdecorator(*args,**kwargs):
- argnames=inspect.getfullargspec(func).args
- caller=None
- if"caller"inargnames:
- # we assume this is an object
- caller=args[argnames.index("caller")]
-
- ret=func(*args,**kwargs)
- ifisinstance(ret,types.GeneratorType):
- _iterate(ret,caller)
- else:
- returnret
-
- returndecorator
-
-
-
[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
-
-
-
[docs]defstrip_unsafe_input(txt,session=None,bypass_perms=None):
- """
- Remove 'unsafe' text codes from text; these are used to elimitate
- exploits in user-provided data, such as html-tags, line breaks etc.
-
- Args:
- txt (str): The text to clean.
- session (Session, optional): A Session in order to determine if
- the check should be bypassed by permission (will be checked
- with the 'perm' lock, taking permission hierarchies into account).
- bypass_perms (list, optional): Iterable of permission strings
- to check for bypassing the strip. If not given, use
- `settings.INPUT_CLEANUP_BYPASS_PERMISSIONS`.
-
- Returns:
- str: The cleaned string.
-
- Notes:
- The `INPUT_CLEANUP_BYPASS_PERMISSIONS` list defines what account
- permissions are required to bypass this strip.
-
- """
- global_STRIP_UNSAFE_TOKENS
- ifnot_STRIP_UNSAFE_TOKENS:
- fromevennia.utils.ansiimportstrip_unsafe_tokensas_STRIP_UNSAFE_TOKENS
-
- ifsession:
- obj=session.puppetifsession.puppetelsesession.account
- bypass_perms=bypass_permsorsettings.INPUT_CLEANUP_BYPASS_PERMISSIONS
- ifobj.permissions.check(*bypass_perms):
- returntxt
-
- # remove html codes
- txt=strip_tags(txt)
- txt=_STRIP_UNSAFE_TOKENS(txt)
- returntxt
-
-
-
[docs]defcopy_word_case(base_word,new_word):
- """
- Converts a word to use the same capitalization as a first word.
-
- Args:
- base_word (str): A word to get the capitalization from.
- new_word (str): A new word to capitalize in the same way as `base_word`.
-
- Returns:
- str: The `new_word` with capitalization matching the first word.
-
- Notes:
- This is meant for words. Longer sentences may get unexpected results.
-
- If the two words have a mix of capital/lower letters _and_ `new_word`
- is longer than `base_word`, the excess will retain its original case.
-
- """
-
- # Word
- ifbase_word.istitle():
- returnnew_word.title()
- # word
- elifbase_word.islower():
- returnnew_word.lower()
- # WORD
- elifbase_word.isupper():
- returnnew_word.upper()
- else:
- # WorD - a mix. Handle each character
- maxlen=len(base_word)
- shared,excess=new_word[:maxlen],new_word[maxlen-1:]
- return"".join(char.upper()ifbase_word[ic].isupper()elsechar.lower()
- foric,charinenumerate(new_word))+excess
-"""
-Contains all the validation functions.
-
-All validation functions must have a checker (probably a session) and entry arg.
-
-They can employ more paramters at your leisure.
-
-
-"""
-
-importreas_re
-importpytzas_pytz
-importdatetimeas_dt
-fromevennia.utils.ansiimportstrip_ansi
-fromevennia.utils.utilsimportstring_partial_matchingas_partial,validate_email_address
-fromdjango.utils.translationimportgettextas_
-
-_TZ_DICT={str(tz):_pytz.timezone(tz)fortzin_pytz.common_timezones}
-
-
-
[docs]deftext(entry,option_key="Text",**kwargs):
- try:
- returnstr(entry)
- exceptExceptionaserr:
- 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(_("Nothing entered for a {option_key}!").format(option_key=option_key))
- test_str=strip_ansi(f"|{entry}|n")
- iftest_str:
- raiseValueError(_("'{entry}' is not a valid {option_key}.").format(
- entry=entry,option_key=option_key))
- returnentry
-
-
-
[docs]defdatetime(entry,option_key="Datetime",account=None,from_tz=None,**kwargs):
- """
- Process a datetime string in standard forms while accounting for the
- inputer's timezone. Always returns a result in UTC.
-
- Args:
- entry (str): A date string from a user.
- option_key (str): Name to display this datetime as.
- 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 `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.
-
- """
- ifnotentry:
- raiseValueError(_("No {option_key} entered!").format(option_key=option_key))
- ifnotfrom_tz:
- from_tz=_pytz.UTC
- ifaccount:
- acct_tz=account.options.get("timezone","UTC")
- try:
- from_tz=_pytz.timezone(acct_tz)
- exceptExceptionaserr:
- raiseValueError(
- _("Timezone string '{acct_tz}' is not a valid timezone ({err})").format(
- acct_tz=acct_tz,err=err
- )
- )
- else:
- from_tz=_pytz.UTC
-
- utc=_pytz.UTC
- now=_dt.datetime.utcnow().replace(tzinfo=utc)
- cur_year=now.strftime("%Y")
- split_time=entry.split(" ")
- iflen(split_time)==3:
- entry=f"{split_time[0]}{split_time[1]}{split_time[2]}{cur_year}"
- eliflen(split_time)==4:
- entry=f"{split_time[0]}{split_time[1]}{split_time[2]}{split_time[3]}"
- else:
- raiseValueError(
- _("{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(
- _("{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)
-
-
-
[docs]defduration(entry,option_key="Duration",**kwargs):
- """
- 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.
- option_key (str): Name to display this query as.
-
- Returns:
- timedelta
-
- """
- time_string=entry.lower().split(" ")
- seconds=0
- minutes=0
- hours=0
- days=0
- weeks=0
-
- forintervalintime_string:
- if_re.match(r"^[\d]+s$",interval):
- seconds+=int(interval.rstrip("s"))
- elif_re.match(r"^[\d]+m$",interval):
- minutes+=int(interval.rstrip("m"))
- elif_re.match(r"^[\d]+h$",interval):
- hours+=int(interval.rstrip("h"))
- elif_re.match(r"^[\d]+d$",interval):
- days+=int(interval.rstrip("d"))
- elif_re.match(r"^[\d]+w$",interval):
- weeks+=int(interval.rstrip("w"))
- elif_re.match(r"^[\d]+y$",interval):
- days+=int(interval.rstrip("y"))*365
- else:
- 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)
-
-
-
[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(_("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(_("Must enter a whole number for {option_key}!").format(
- option_key=option_key))
- try:
- num=int(entry)
- exceptValueError:
- 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(_("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(_("{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=(_("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()
- ifentryin("1","TRUE","ON","ENABLED","ENABLE","YES"):
- returnTrue
- ifentryin("0","FALSE","OFF","DISABLED","DISABLE","NO"):
- returnFalse
- raiseValueError(error)
-
-
-
[docs]deftimezone(entry,option_key="Timezone",**kwargs):
- """
- Takes user input as string, and partial matches a Timezone.
-
- Args:
- entry (str): The name of the Timezone.
- option_key (str): What this Timezone is used for.
-
- Returns:
- A PYTZ timezone.
-
- """
- ifnotentry:
- raiseValueError(_("No {option_key} entered!").format(option_key=option_key))
- found=_partial(list(_TZ_DICT.keys()),entry,ret_index=False)
- iflen(found)>1:
- raiseValueError(
- _("That matched: {matches}. Please be more specific!").format(
- matches=', '.join(str(t)fortinfound)))
- iffound:
- return_TZ_DICT[found[0]]
- raiseValueError(_("Could not find timezone '{entry}' for {option_key}!").format(
- entry=entry,option_key=option_key))
[docs]classCaseInsensitiveModelBackend(ModelBackend):
- """
- By default ModelBackend does case _sensitive_ username
- authentication, which isn't what is generally expected. This
- backend supports case insensitive username authentication.
-
- """
-
-
[docs]defauthenticate(self,request,username=None,password=None,autologin=None):
- """
- Custom authenticate with bypass for auto-logins
-
- Args:
- request (Request): Request object.
- username (str, optional): Name of user to authenticate.
- password (str, optional): Password of user
- autologin (Account, optional): If given, assume this is
- an already authenticated account and bypass authentication.
- """
- ifautologin:
- # Note: Setting .backend on account is critical in order to
- # be allowed to call django.auth.login(account) later. This
- # is necessary for the auto-login feature of the webclient,
- # but it's important to make sure Django doesn't change this
- # requirement or the name of the property down the line. /Griatch
- autologin.backend="evennia.web.utils.backends.CaseInsensitiveModelBackend"
- returnautologin
- else:
- # In this case .backend will be assigned automatically
- # somewhere along the way.
- Account=get_user_model()
- try:
- account=Account.objects.get(username__iexact=username)
- ifaccount.check_password(password):
- returnaccount
- else:
- returnNone
- exceptAccount.DoesNotExist:
- returnNone
-"""
-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.
-
-"""
-
-
-importos
-fromdjango.confimportsettings
-fromevennia.utils.utilsimportget_evennia_version
-
-# Setup lists of the most relevant apps so
-# the adminsite becomes more readable.
-
-GAME_NAME=None
-GAME_SLOGAN=None
-SERVER_VERSION=None
-SERVER_HOSTNAME=None
-
-TELNET_ENABLED=None
-TELNET_PORTS=None
-TELNET_SSL_ENABLED=None
-TELNET_SSL_PORTS=None
-
-SSH_ENABLED=None
-SSH_PORTS=None
-
-WEBCLIENT_ENABLED=None
-WEBSOCKET_CLIENT_ENABLED=None
-WEBSOCKET_PORT=None
-WEBSOCKET_URL=None
-
-REST_API_ENABLED=False
-
-ACCOUNT_RELATED=["Accounts"]
-GAME_ENTITIES=["Objects","Scripts","Comms","Help"]
-GAME_SETUP=["Permissions","Config"]
-CONNECTIONS=["Irc"]
-WEBSITE=["Flatpages","News","Sites"]
-
-
-
[docs]defload_game_settings():
- """
- Load and cache game settings.
-
- """
- globalGAME_NAME,GAME_SLOGAN,SERVER_VERSION,SERVER_HOSTNAME
- globalTELNET_ENABLED,TELNET_PORTS
- globalTELNET_SSL_ENABLED,TELNET_SSL_PORTS
- globalSSH_ENABLED,SSH_PORTS
- globalWEBCLIENT_ENABLED,WEBSOCKET_CLIENT_ENABLED,WEBSOCKET_PORT,WEBSOCKET_URL
- globalREST_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
- SERVER_HOSTNAME=settings.SERVER_HOSTNAME
-
- TELNET_ENABLED=settings.TELNET_ENABLED
- TELNET_PORTS=settings.TELNET_PORTS
- TELNET_SSL_ENABLED=settings.SSL_ENABLED
- TELNET_SSL_PORTS=settings.SSL_PORTS
-
- SSH_ENABLED=settings.SSH_ENABLED
- SSH_PORTS=settings.SSH_PORTS
-
- WEBCLIENT_ENABLED=settings.WEBCLIENT_ENABLED
- WEBSOCKET_CLIENT_ENABLED=settings.WEBSOCKET_CLIENT_ENABLED
- # if we are working through a proxy or uses docker port-remapping, the webclient port encoded
- # in the webclient should be different than the one the server expects. Use the environment
- # variable WEBSOCKET_CLIENT_PROXY_PORT if this is the case.
- WEBSOCKET_PORT=int(
- os.environ.get("WEBSOCKET_CLIENT_PROXY_PORT",settings.WEBSOCKET_CLIENT_PORT)
- )
- # this is determined dynamically by the client and is less of an issue
- WEBSOCKET_URL=settings.WEBSOCKET_CLIENT_URL
-
- REST_API_ENABLED=settings.REST_API_ENABLED
-
-
-load_game_settings()
-
-
-# The main context processor function
-
[docs]classSharedLoginMiddleware(object):
- """
- Handle the shared login between website and webclient.
-
- """
-
-
[docs]def__init__(self,get_response):
- # One-time configuration and initialization.
- self.get_response=get_response
-
- def__call__(self,request):
- # Code to be executed for each request before
- # the view (and later middleware) are called.
-
- # Synchronize credentials between webclient and website
- # Must be performed *before* rendering the view (issue #1723)
- self.make_shared_login(request)
-
- # Process view
- response=self.get_response(request)
-
- # Code to be executed for each request/response after
- # the view is called.
-
- # Return processed view
- returnresponse
-
-
[docs]@classmethod
- defmake_shared_login(cls,request):
- csession=request.session
- account=request.user
- website_uid=csession.get("website_authenticated_uid",None)
- webclient_uid=csession.get("webclient_authenticated_uid",None)
-
- ifnotcsession.session_key:
- # this is necessary to build the sessid key
- csession.save()
-
- ifaccount.is_authenticated:
- # Logged into website
- ifwebsite_uidisNone:
- # fresh website login (just from login page)
- csession["website_authenticated_uid"]=account.id
- ifwebclient_uidisNone:
- # auto-login web client
- csession["webclient_authenticated_uid"]=account.id
-
- elifwebclient_uid:
- # Not logged into website, but logged into webclient
- ifwebsite_uidisNone:
- csession["website_authenticated_uid"]=account.id
- account=AccountDB.objects.get(id=webclient_uid)
- try:
- # calls our custom authenticate, in web/utils/backend.py
- authenticate(autologin=account)
- login(request,account)
- exceptAttributeError:
- logger.log_trace()
-
- ifcsession.get("webclient_authenticated_uid",None):
- # set a nonce to prevent the webclient from erasing the webclient_authenticated_uid value
- csession["webclient_authenticated_nonce"]=(
- csession.get("webclient_authenticated_nonce",0)+1
- )
- # wrap around to prevent integer overflows
- ifcsession["webclient_authenticated_nonce"]>32:
- csession["webclient_authenticated_nonce"]=0
-"""
-This contains a simple view for rendering the webclient
-page and serve it eventual static content.
-
-"""
-
-fromdjango.confimportsettings
-fromdjango.httpimportHttp404
-fromdjango.shortcutsimportrender
-fromdjango.contrib.authimportlogin,authenticate
-
-fromevennia.accounts.modelsimportAccountDB
-fromevennia.utilsimportlogger
-
-
-
[docs]defwebclient(request):
- """
- Webclient page template loading.
-
- """
- # auto-login is now handled by evennia.web.utils.middleware
-
- # check if webclient should be enabled
- ifnotsettings.WEBCLIENT_ENABLED:
- raiseHttp404
-
- # make sure to store the browser session's hash so the webclient can get to it!
- pagevars={"browser_sessid":request.session.session_key}
-
- returnrender(request,"webclient.html",pagevars)
[docs]classEvenniaForm(forms.Form):
- """
- This is a stock Django form, but modified so that all values provided
- through it are escaped (sanitized). Validation is performed by the fields
- you define in the form.
-
- This has little to do with Evennia itself and is more general web security-
- related.
-
- https://www.owasp.org/index.php/Input_Validation_Cheat_Sheet#Goals_of_Input_Validation
-
- """
-
-
[docs]defclean(self):
- """
- Django hook. Performed on form submission.
-
- Returns:
- cleaned (dict): Dictionary of key:value pairs submitted on the form.
-
- """
- # Call parent function
- cleaned=super().clean()
-
- # Escape all values provided by user
- cleaned={k:escape(v)fork,vincleaned.items()}
- returncleaned
-
-
-
[docs]classAccountForm(UserCreationForm):
- """
- This is a generic Django form tailored to the Account model.
-
- In this incarnation it does not allow getting/setting of attributes, only
- core User model fields (username, email, password).
-
- """
-
-
[docs]classMeta:
- """
- This is a Django construct that provides additional configuration to
- the form.
-
- """
-
- # The model/typeclass this form creates
- model=class_from_module(settings.BASE_ACCOUNT_TYPECLASS,
- fallback=settings.FALLBACK_ACCOUNT_TYPECLASS)
-
- # The fields to display on the form, in the given order
- fields=("username","email")
-
- # Any overrides of field classes
- field_classes={"username":UsernameField}
-
- # Username is collected as part of the core UserCreationForm, so we just need
- # to add a field to (optionally) capture email.
- email=forms.EmailField(
- help_text="A valid email address. Optional; used for password resets.",required=False
- )
-
-
-
[docs]classObjectForm(EvenniaForm,ModelForm):
- """
- This is a Django form for generic Evennia Objects that allows modification
- of attributes when called from a descendent of ObjectUpdate or ObjectCreate
- views.
-
- It defines no fields by default; you have to do that by extending this class
- and defining what fields you want to be recorded. See the CharacterForm for
- a simple example of how to do this.
-
- """
-
-
[docs]classMeta:
- """
- This is a Django construct that provides additional configuration to
- the form.
-
- """
-
- # The model/typeclass this form creates
- model=class_from_module(settings.BASE_OBJECT_TYPECLASS,
- fallback=settings.FALLBACK_OBJECT_TYPECLASS)
-
- # The fields to display on the form, in the given order
- fields=("db_key",)
-
- # This lets us rename ugly db-specific keys to something more human
- labels={"db_key":"Name"}
-
-
-
[docs]classCharacterForm(ObjectForm):
- """
- This is a Django form for Evennia Character objects.
-
- Since Evennia characters only have one attribute by default, this form only
- defines a field for that single attribute. The names of fields you define should
- correspond to their names as stored in the dbhandler; you can display
- 'prettier' versions of the fieldname on the form using the 'label' kwarg.
-
- The basic field types are CharFields and IntegerFields, which let you enter
- text and numbers respectively. IntegerFields have some neat validation tricks
- they can do, like mandating values fall within a certain range.
-
- For example, a complete "age" field (which stores its value to
- `character.db.age` might look like:
-
- age = forms.IntegerField(
- label="Your Age",
- min_value=18, max_value=9000,
- help_text="Years since your birth.")
-
- Default input fields are generic single-line text boxes. You can control what
- sort of input field users will see by specifying a "widget." An example of
- this is used for the 'desc' field to show a Textarea box instead of a Textbox.
-
- For help in building out your form, please see:
- https://docs.djangoproject.com/en/1.11/topics/forms/#building-a-form-in-django
-
- For more information on fields and their capabilities, see:
- https://docs.djangoproject.com/en/1.11/ref/forms/fields/
-
- For more on widgets, see:
- https://docs.djangoproject.com/en/1.11/ref/forms/widgets/
-
- """
-
-
[docs]classMeta:
- """
- This is a Django construct that provides additional configuration to
- the form.
-
- """
-
- # Get the correct object model
- model=class_from_module(settings.BASE_CHARACTER_TYPECLASS,
- fallback=settings.FALLBACK_CHARACTER_TYPECLASS)
-
- # Allow entry of the 'key' field
- fields=("db_key",)
-
- # Rename 'key' to something more intelligible
- labels={"db_key":"Name"}
-
- # Fields pertaining to configurable attributes on the Character object.
- desc=forms.CharField(
- label="Description",
- max_length=2048,
- required=False,
- widget=forms.Textarea(attrs={"rows":3}),
- help_text="A brief description of your character.",
- )
-
-
-
[docs]classCharacterUpdateForm(CharacterForm):
- """
- This is a Django form for updating Evennia Character objects.
-
- By default it is the same as the CharacterForm, but if there are circumstances
- in which you don't want to let players edit all the same attributes they had
- access to during creation, you can redefine this form with those fields you do
- wish to allow.
-
- """
-
- pass
[docs]deftest_get(self):
- # Try accessing page while not logged in
- response=self.client.get(reverse(self.url_name,kwargs=self.get_kwargs()))
- self.assertEqual(response.status_code,self.unauthenticated_response)
[docs]defsetUp(self):
- super().setUp()
-
- # create a db help entry
- create_help_entry('unit test db entry','unit test db entry text',category="General")
-
-
[docs]defget_kwargs(self):
- return{"category":slugify("general"),
- "topic":slugify('unit test db entry')}
-
-
[docs]deftest_view(self):
- response=self.client.get(reverse(self.url_name,kwargs=self.get_kwargs()),follow=True)
- self.assertEqual(response.context["entry_text"],'unit test db entry text')
-
-
[docs]deftest_object_cache(self):
- # clear file help entries, use local HELP_ENTRY_DICTS to recreate new entries
- global_FILE_HELP_ENTRIES
- if_FILE_HELP_ENTRIESisNone:
- fromevennia.help.filehelpimportFILE_HELP_ENTRIESas_FILE_HELP_ENTRIES
- help_module='evennia.web.website.tests'
- self.file_help_store=_FILE_HELP_ENTRIES.__init__(help_file_modules=[help_module])
-
- # request access to an entry
- response=self.client.get(reverse(self.url_name,kwargs=self.get_kwargs()),follow=True)
- self.assertEqual(response.context["entry_text"],'unit test db entry text')
- # request a second entry, verifing the cached object is not provided on a new topic request
- entry_two_args={"category":slugify("general"),"topic":slugify('unit test file entry')}
- response=self.client.get(reverse(self.url_name,kwargs=entry_two_args),follow=True)
- self.assertEqual(response.context["entry_text"],'cache test file entry text')
[docs]defsetUp(self):
- super(HelpLockedDetailTest,self).setUp()
-
- # create a db entry with a lock
- self.db_help_entry=create_help_entry('unit test locked topic','unit test locked entrytext',
- category="General",locks='read:perm(Developer)')
-
-
[docs]defget_kwargs(self):
- return{"category":slugify("general"),
- "topic":slugify('unit test locked topic')}
-
-
[docs]deftest_locked_entry(self):
- # request access to an entry for permission the account does not have
- response=self.client.get(reverse(self.url_name,kwargs=self.get_kwargs()),follow=True)
- self.assertEqual(response.context["entry_text"],'Failed to find entry.')
-
-
[docs]deftest_lock_with_perm(self):
- # log TestAccount in, grant permission required, read the entry
- self.login()
- self.account.permissions.add("Developer")
- response=self.client.get(reverse(self.url_name,kwargs=self.get_kwargs()),follow=True)
- self.assertEqual(response.context["entry_text"],'unit test locked entrytext')
[docs]@override_settings(MULTISESSION_MODE=0)
- deftest_valid_access_multisession_0(self):
- "Account1 with no characters should be able to create a new one"
- self.account.db._playable_characters=[]
-
- # Login account
- self.login()
-
- # Post data for a new character
- data={"db_key":"gannon","desc":"Some dude."}
-
- response=self.client.post(reverse(self.url_name),data=data,follow=True)
- self.assertEqual(response.status_code,200)
-
- # Make sure the character was actually created
- self.assertTrue(
- len(self.account.db._playable_characters)==1,
- "Account only has the following characters attributed to it: %s"
- %self.account.db._playable_characters,
- )
-
-
[docs]@override_settings(MULTISESSION_MODE=2)
- @override_settings(MAX_NR_CHARACTERS=10)
- deftest_valid_access_multisession_2(self):
- "Account1 should be able to create a new character"
- # Login account
- self.login()
-
- # Post data for a new character
- data={"db_key":"gannon","desc":"Some dude."}
-
- response=self.client.post(reverse(self.url_name),data=data,follow=True)
- self.assertEqual(response.status_code,200)
-
- # Make sure the character was actually created
- self.assertTrue(
- len(self.account.db._playable_characters)>1,
- "Account only has the following characters attributed to it: %s"
- %self.account.db._playable_characters,
- )
[docs]deftest_invalid_access(self):
- "Account1 should not be able to puppet Account2:Char2"
- # Login account
- self.login()
-
- # Try to access puppet page for char2
- kwargs={"pk":self.char2.pk,"slug":slugify(self.char2.name)}
- response=self.client.get(reverse(self.url_name,kwargs=kwargs),follow=True)
- self.assertTrue(
- response.status_code>=400,
- "Invalid access should return a 4xx code-- either obj not found or permission denied! (Returned %s)"
- %response.status_code,
- )
[docs]deftest_valid_access(self):
- "Account1 should be able to update Account1:Char1"
- # Login account
- self.login()
-
- # Try to access update page for char1
- response=self.client.get(reverse(self.url_name,kwargs=self.get_kwargs()),follow=True)
- self.assertEqual(response.status_code,200)
-
- # Try to update char1 desc
- data={"db_key":self.char1.db_key,"desc":"Just a regular type of dude."}
- response=self.client.post(
- reverse(self.url_name,kwargs=self.get_kwargs()),data=data,follow=True
- )
- self.assertEqual(response.status_code,200)
-
- # Make sure the change was made successfully
- self.assertEqual(self.char1.db.desc,data["desc"])
-
-
[docs]deftest_invalid_access(self):
- "Account1 should not be able to update Account2:Char2"
- # Login account
- self.login()
-
- # Try to access update page for char2
- kwargs={"pk":self.char2.pk,"slug":slugify(self.char2.name)}
- response=self.client.get(reverse(self.url_name,kwargs=kwargs),follow=True)
- self.assertEqual(response.status_code,403)
[docs]deftest_valid_access(self):
- "Account1 should be able to delete Account1:Char1"
- # Login account
- self.login()
-
- # Try to access delete page for char1
- response=self.client.get(reverse(self.url_name,kwargs=self.get_kwargs()),follow=True)
- self.assertEqual(response.status_code,200)
-
- # Proceed with deleting it
- data={"value":"yes"}
- response=self.client.post(
- reverse(self.url_name,kwargs=self.get_kwargs()),data=data,follow=True
- )
- self.assertEqual(response.status_code,200)
-
- # Make sure it deleted
- self.assertFalse(
- self.char1inself.account.db._playable_characters,
- "Char1 is still in Account playable characters list.",
- )
-
-
[docs]deftest_invalid_access(self):
- "Account1 should not be able to delete Account2:Char2"
- # Login account
- self.login()
-
- # Try to access delete page for char2
- kwargs={"pk":self.char2.pk,"slug":slugify(self.char2.name)}
- response=self.client.get(reverse(self.url_name,kwargs=kwargs),follow=True)
- self.assertEqual(response.status_code,403)
-"""functools.py - Tools for working with functions and callable objects
-"""
-# Python module wrapper for _functools C module
-# to allow utilities written in Python to be added
-# to the functools module.
-# Written by Nick Coghlan <ncoghlan at gmail.com>,
-# Raymond Hettinger <python at rcn.com>,
-# and Łukasz Langa <lukasz at langa.pl>.
-# Copyright (C) 2006-2013 Python Software Foundation.
-# See C source code for _functools credits/copyright
-
-__all__=['update_wrapper','wraps','WRAPPER_ASSIGNMENTS','WRAPPER_UPDATES',
- 'total_ordering','cmp_to_key','lru_cache','reduce','partial',
- 'partialmethod','singledispatch','singledispatchmethod',
- "cached_property"]
-
-fromabcimportget_cache_token
-fromcollectionsimportnamedtuple
-# import types, weakref # Deferred to single_dispatch()
-fromreprlibimportrecursive_repr
-from_threadimportRLock
-
-
-################################################################################
-### update_wrapper() and wraps() decorator
-################################################################################
-
-# update_wrapper() and wraps() are tools to help write
-# wrapper functions that can handle naive introspection
-
-WRAPPER_ASSIGNMENTS=('__module__','__name__','__qualname__','__doc__',
- '__annotations__')
-WRAPPER_UPDATES=('__dict__',)
-defupdate_wrapper(wrapper,
- wrapped,
- assigned=WRAPPER_ASSIGNMENTS,
- updated=WRAPPER_UPDATES):
- """Update a wrapper function to look like the wrapped function
-
- wrapper is the function to be updated
- wrapped is the original function
- assigned is a tuple naming the attributes assigned directly
- from the wrapped function to the wrapper function (defaults to
- functools.WRAPPER_ASSIGNMENTS)
- updated is a tuple naming the attributes of the wrapper that
- are updated with the corresponding attribute from the wrapped
- function (defaults to functools.WRAPPER_UPDATES)
- """
- forattrinassigned:
- try:
- value=getattr(wrapped,attr)
- exceptAttributeError:
- pass
- else:
- setattr(wrapper,attr,value)
- forattrinupdated:
- getattr(wrapper,attr).update(getattr(wrapped,attr,{}))
- # Issue #17482: set __wrapped__ last so we don't inadvertently copy it
- # from the wrapped function when updating __dict__
- wrapper.__wrapped__=wrapped
- # Return the wrapper so this can be used as a decorator via partial()
- returnwrapper
-
-defwraps(wrapped,
- assigned=WRAPPER_ASSIGNMENTS,
- updated=WRAPPER_UPDATES):
- """Decorator factory to apply update_wrapper() to a wrapper function
-
- Returns a decorator that invokes update_wrapper() with the decorated
- function as the wrapper argument and the arguments to wraps() as the
- remaining arguments. Default arguments are as for update_wrapper().
- This is a convenience function to simplify applying partial() to
- update_wrapper().
- """
- returnpartial(update_wrapper,wrapped=wrapped,
- assigned=assigned,updated=updated)
-
-
-################################################################################
-### total_ordering class decorator
-################################################################################
-
-# The total ordering functions all invoke the root magic method directly
-# rather than using the corresponding operator. This avoids possible
-# infinite recursion that could occur when the operator dispatch logic
-# detects a NotImplemented result and then calls a reflected method.
-
-def_gt_from_lt(self,other,NotImplemented=NotImplemented):
- 'Return a > b. Computed by @total_ordering from (not a < b) and (a != b).'
- op_result=self.__lt__(other)
- ifop_resultisNotImplemented:
- returnop_result
- returnnotop_resultandself!=other
-
-def_le_from_lt(self,other,NotImplemented=NotImplemented):
- 'Return a <= b. Computed by @total_ordering from (a < b) or (a == b).'
- op_result=self.__lt__(other)
- returnop_resultorself==other
-
-def_ge_from_lt(self,other,NotImplemented=NotImplemented):
- 'Return a >= b. Computed by @total_ordering from (not a < b).'
- op_result=self.__lt__(other)
- ifop_resultisNotImplemented:
- returnop_result
- returnnotop_result
-
-def_ge_from_le(self,other,NotImplemented=NotImplemented):
- 'Return a >= b. Computed by @total_ordering from (not a <= b) or (a == b).'
- op_result=self.__le__(other)
- ifop_resultisNotImplemented:
- returnop_result
- returnnotop_resultorself==other
-
-def_lt_from_le(self,other,NotImplemented=NotImplemented):
- 'Return a < b. Computed by @total_ordering from (a <= b) and (a != b).'
- op_result=self.__le__(other)
- ifop_resultisNotImplemented:
- returnop_result
- returnop_resultandself!=other
-
-def_gt_from_le(self,other,NotImplemented=NotImplemented):
- 'Return a > b. Computed by @total_ordering from (not a <= b).'
- op_result=self.__le__(other)
- ifop_resultisNotImplemented:
- returnop_result
- returnnotop_result
-
-def_lt_from_gt(self,other,NotImplemented=NotImplemented):
- 'Return a < b. Computed by @total_ordering from (not a > b) and (a != b).'
- op_result=self.__gt__(other)
- ifop_resultisNotImplemented:
- returnop_result
- returnnotop_resultandself!=other
-
-def_ge_from_gt(self,other,NotImplemented=NotImplemented):
- 'Return a >= b. Computed by @total_ordering from (a > b) or (a == b).'
- op_result=self.__gt__(other)
- returnop_resultorself==other
-
-def_le_from_gt(self,other,NotImplemented=NotImplemented):
- 'Return a <= b. Computed by @total_ordering from (not a > b).'
- op_result=self.__gt__(other)
- ifop_resultisNotImplemented:
- returnop_result
- returnnotop_result
-
-def_le_from_ge(self,other,NotImplemented=NotImplemented):
- 'Return a <= b. Computed by @total_ordering from (not a >= b) or (a == b).'
- op_result=self.__ge__(other)
- ifop_resultisNotImplemented:
- returnop_result
- returnnotop_resultorself==other
-
-def_gt_from_ge(self,other,NotImplemented=NotImplemented):
- 'Return a > b. Computed by @total_ordering from (a >= b) and (a != b).'
- op_result=self.__ge__(other)
- ifop_resultisNotImplemented:
- returnop_result
- returnop_resultandself!=other
-
-def_lt_from_ge(self,other,NotImplemented=NotImplemented):
- 'Return a < b. Computed by @total_ordering from (not a >= b).'
- op_result=self.__ge__(other)
- ifop_resultisNotImplemented:
- returnop_result
- returnnotop_result
-
-_convert={
- '__lt__':[('__gt__',_gt_from_lt),
- ('__le__',_le_from_lt),
- ('__ge__',_ge_from_lt)],
- '__le__':[('__ge__',_ge_from_le),
- ('__lt__',_lt_from_le),
- ('__gt__',_gt_from_le)],
- '__gt__':[('__lt__',_lt_from_gt),
- ('__ge__',_ge_from_gt),
- ('__le__',_le_from_gt)],
- '__ge__':[('__le__',_le_from_ge),
- ('__gt__',_gt_from_ge),
- ('__lt__',_lt_from_ge)]
-}
-
-deftotal_ordering(cls):
- """Class decorator that fills in missing ordering methods"""
- # Find user-defined comparisons (not those inherited from object).
- roots={opforopin_convertifgetattr(cls,op,None)isnotgetattr(object,op,None)}
- ifnotroots:
- raiseValueError('must define at least one ordering operation: < > <= >=')
- root=max(roots)# prefer __lt__ to __le__ to __gt__ to __ge__
- foropname,opfuncin_convert[root]:
- ifopnamenotinroots:
- opfunc.__name__=opname
- setattr(cls,opname,opfunc)
- returncls
-
-
-################################################################################
-### cmp_to_key() function converter
-################################################################################
-
-defcmp_to_key(mycmp):
- """Convert a cmp= function into a key= function"""
- classK(object):
- __slots__=['obj']
- def__init__(self,obj):
- self.obj=obj
- def__lt__(self,other):
- returnmycmp(self.obj,other.obj)<0
- def__gt__(self,other):
- returnmycmp(self.obj,other.obj)>0
- def__eq__(self,other):
- returnmycmp(self.obj,other.obj)==0
- def__le__(self,other):
- returnmycmp(self.obj,other.obj)<=0
- def__ge__(self,other):
- returnmycmp(self.obj,other.obj)>=0
- __hash__=None
- returnK
-
-try:
- from_functoolsimportcmp_to_key
-exceptImportError:
- pass
-
-
-################################################################################
-### reduce() sequence to a single item
-################################################################################
-
-_initial_missing=object()
-
-defreduce(function,sequence,initial=_initial_missing):
- """
- reduce(function, sequence[, initial]) -> value
-
- Apply a function of two arguments cumulatively to the items of a sequence,
- from left to right, so as to reduce the sequence to a single value.
- For example, reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]) calculates
- ((((1+2)+3)+4)+5). If initial is present, it is placed before the items
- of the sequence in the calculation, and serves as a default when the
- sequence is empty.
- """
-
- it=iter(sequence)
-
- ifinitialis_initial_missing:
- try:
- value=next(it)
- exceptStopIteration:
- raiseTypeError("reduce() of empty sequence with no initial value")fromNone
- else:
- value=initial
-
- forelementinit:
- value=function(value,element)
-
- returnvalue
-
-try:
- from_functoolsimportreduce
-exceptImportError:
- pass
-
-
-################################################################################
-### partial() argument application
-################################################################################
-
-# Purely functional, no descriptor behaviour
-classpartial:
- """New function with partial application of the given arguments
- and keywords.
- """
-
- __slots__="func","args","keywords","__dict__","__weakref__"
-
- def__new__(cls,func,/,*args,**keywords):
- ifnotcallable(func):
- raiseTypeError("the first argument must be callable")
-
- ifhasattr(func,"func"):
- args=func.args+args
- keywords={**func.keywords,**keywords}
- func=func.func
-
- self=super(partial,cls).__new__(cls)
-
- self.func=func
- self.args=args
- self.keywords=keywords
- returnself
-
- def__call__(self,/,*args,**keywords):
- keywords={**self.keywords,**keywords}
- returnself.func(*self.args,*args,**keywords)
-
- @recursive_repr()
- def__repr__(self):
- qualname=type(self).__qualname__
- args=[repr(self.func)]
- args.extend(repr(x)forxinself.args)
- args.extend(f"{k}={v!r}"for(k,v)inself.keywords.items())
- iftype(self).__module__=="functools":
- returnf"functools.{qualname}({', '.join(args)})"
- returnf"{qualname}({', '.join(args)})"
-
- def__reduce__(self):
- returntype(self),(self.func,),(self.func,self.args,
- self.keywordsorNone,self.__dict__orNone)
-
- def__setstate__(self,state):
- ifnotisinstance(state,tuple):
- raiseTypeError("argument to __setstate__ must be a tuple")
- iflen(state)!=4:
- raiseTypeError(f"expected 4 items in state, got {len(state)}")
- func,args,kwds,namespace=state
- if(notcallable(func)ornotisinstance(args,tuple)or
- (kwdsisnotNoneandnotisinstance(kwds,dict))or
- (namespaceisnotNoneandnotisinstance(namespace,dict))):
- raiseTypeError("invalid partial state")
-
- args=tuple(args)# just in case it's a subclass
- ifkwdsisNone:
- kwds={}
- eliftype(kwds)isnotdict:# XXX does it need to be *exactly* dict?
- kwds=dict(kwds)
- ifnamespaceisNone:
- namespace={}
-
- self.__dict__=namespace
- self.func=func
- self.args=args
- self.keywords=kwds
-
-try:
- from_functoolsimportpartial
-exceptImportError:
- pass
-
-# Descriptor version
-classpartialmethod(object):
- """Method descriptor with partial application of the given arguments
- and keywords.
-
- Supports wrapping existing descriptors and handles non-descriptor
- callables as instance methods.
- """
-
- def__init__(*args,**keywords):
- iflen(args)>=2:
- self,func,*args=args
- elifnotargs:
- raiseTypeError("descriptor '__init__' of partialmethod "
- "needs an argument")
- elif'func'inkeywords:
- func=keywords.pop('func')
- self,*args=args
- importwarnings
- warnings.warn("Passing 'func' as keyword argument is deprecated",
- DeprecationWarning,stacklevel=2)
- else:
- raiseTypeError("type 'partialmethod' takes at least one argument, "
- "got %d"%(len(args)-1))
- args=tuple(args)
-
- ifnotcallable(func)andnothasattr(func,"__get__"):
- raiseTypeError("{!r} is not callable or a descriptor"
- .format(func))
-
- # func could be a descriptor like classmethod which isn't callable,
- # so we can't inherit from partial (it verifies func is callable)
- ifisinstance(func,partialmethod):
- # flattening is mandatory in order to place cls/self before all
- # other arguments
- # it's also more efficient since only one function will be called
- self.func=func.func
- self.args=func.args+args
- self.keywords={**func.keywords,**keywords}
- else:
- self.func=func
- self.args=args
- self.keywords=keywords
- __init__.__text_signature__='($self, func, /, *args, **keywords)'
-
- def__repr__(self):
- args=", ".join(map(repr,self.args))
- keywords=", ".join("{}={!r}".format(k,v)
- fork,vinself.keywords.items())
- format_string="{module}.{cls}({func}, {args}, {keywords})"
- returnformat_string.format(module=self.__class__.__module__,
- cls=self.__class__.__qualname__,
- func=self.func,
- args=args,
- keywords=keywords)
-
- def_make_unbound_method(self):
- def_method(cls_or_self,/,*args,**keywords):
- keywords={**self.keywords,**keywords}
- returnself.func(cls_or_self,*self.args,*args,**keywords)
- _method.__isabstractmethod__=self.__isabstractmethod__
- _method._partialmethod=self
- return_method
-
- def__get__(self,obj,cls=None):
- get=getattr(self.func,"__get__",None)
- result=None
- ifgetisnotNone:
- new_func=get(obj,cls)
- ifnew_funcisnotself.func:
- # Assume __get__ returning something new indicates the
- # creation of an appropriate callable
- result=partial(new_func,*self.args,**self.keywords)
- try:
- result.__self__=new_func.__self__
- exceptAttributeError:
- pass
- ifresultisNone:
- # If the underlying descriptor didn't do anything, treat this
- # like an instance method
- result=self._make_unbound_method().__get__(obj,cls)
- returnresult
-
- @property
- def__isabstractmethod__(self):
- returngetattr(self.func,"__isabstractmethod__",False)
-
-# Helper functions
-
-def_unwrap_partial(func):
- whileisinstance(func,partial):
- func=func.func
- returnfunc
-
-################################################################################
-### LRU Cache function decorator
-################################################################################
-
-_CacheInfo=namedtuple("CacheInfo",["hits","misses","maxsize","currsize"])
-
-class_HashedSeq(list):
- """ This class guarantees that hash() will be called no more than once
- per element. This is important because the lru_cache() will hash
- the key multiple times on a cache miss.
-
- """
-
- __slots__='hashvalue'
-
- def__init__(self,tup,hash=hash):
- self[:]=tup
- self.hashvalue=hash(tup)
-
- def__hash__(self):
- returnself.hashvalue
-
-def_make_key(args,kwds,typed,
- kwd_mark=(object(),),
- fasttypes={int,str},
- tuple=tuple,type=type,len=len):
- """Make a cache key from optionally typed positional and keyword arguments
-
- The key is constructed in a way that is flat as possible rather than
- as a nested structure that would take more memory.
-
- If there is only a single argument and its data type is known to cache
- its hash value, then that argument is returned without a wrapper. This
- saves space and improves lookup speed.
-
- """
- # All of code below relies on kwds preserving the order input by the user.
- # Formerly, we sorted() the kwds before looping. The new way is *much*
- # faster; however, it means that f(x=1, y=2) will now be treated as a
- # distinct call from f(y=2, x=1) which will be cached separately.
- key=args
- ifkwds:
- key+=kwd_mark
- foriteminkwds.items():
- key+=item
- iftyped:
- key+=tuple(type(v)forvinargs)
- ifkwds:
- key+=tuple(type(v)forvinkwds.values())
- eliflen(key)==1andtype(key[0])infasttypes:
- returnkey[0]
- return_HashedSeq(key)
-
-deflru_cache(maxsize=128,typed=False):
- """Least-recently-used cache decorator.
-
- If *maxsize* is set to None, the LRU features are disabled and the cache
- can grow without bound.
-
- If *typed* is True, arguments of different types will be cached separately.
- For example, f(3.0) and f(3) will be treated as distinct calls with
- distinct results.
-
- Arguments to the cached function must be hashable.
-
- View the cache statistics named tuple (hits, misses, maxsize, currsize)
- with f.cache_info(). Clear the cache and statistics with f.cache_clear().
- Access the underlying function with f.__wrapped__.
-
- See: http://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU)
-
- """
-
- # Users should only access the lru_cache through its public API:
- # cache_info, cache_clear, and f.__wrapped__
- # The internals of the lru_cache are encapsulated for thread safety and
- # to allow the implementation to change (including a possible C version).
-
- ifisinstance(maxsize,int):
- # Negative maxsize is treated as 0
- ifmaxsize<0:
- maxsize=0
- elifcallable(maxsize)andisinstance(typed,bool):
- # The user_function was passed in directly via the maxsize argument
- user_function,maxsize=maxsize,128
- wrapper=_lru_cache_wrapper(user_function,maxsize,typed,_CacheInfo)
- returnupdate_wrapper(wrapper,user_function)
- elifmaxsizeisnotNone:
- raiseTypeError(
- 'Expected first argument to be an integer, a callable, or None')
-
- defdecorating_function(user_function):
- wrapper=_lru_cache_wrapper(user_function,maxsize,typed,_CacheInfo)
- returnupdate_wrapper(wrapper,user_function)
-
- returndecorating_function
-
-def_lru_cache_wrapper(user_function,maxsize,typed,_CacheInfo):
- # Constants shared by all lru cache instances:
- sentinel=object()# unique object used to signal cache misses
- make_key=_make_key# build a key from the function arguments
- PREV,NEXT,KEY,RESULT=0,1,2,3# names for the link fields
-
- cache={}
- hits=misses=0
- full=False
- cache_get=cache.get# bound method to lookup a key or return None
- cache_len=cache.__len__# get cache size without calling len()
- lock=RLock()# because linkedlist updates aren't threadsafe
- root=[]# root of the circular doubly linked list
- root[:]=[root,root,None,None]# initialize by pointing to self
-
- ifmaxsize==0:
-
- defwrapper(*args,**kwds):
- # No caching -- just a statistics update
- nonlocalmisses
- misses+=1
- result=user_function(*args,**kwds)
- returnresult
-
- elifmaxsizeisNone:
-
- defwrapper(*args,**kwds):
- # Simple caching without ordering or size limit
- nonlocalhits,misses
- key=make_key(args,kwds,typed)
- result=cache_get(key,sentinel)
- ifresultisnotsentinel:
- hits+=1
- returnresult
- misses+=1
- result=user_function(*args,**kwds)
- cache[key]=result
- returnresult
-
- else:
-
- defwrapper(*args,**kwds):
- # Size limited caching that tracks accesses by recency
- nonlocalroot,hits,misses,full
- key=make_key(args,kwds,typed)
- withlock:
- link=cache_get(key)
- iflinkisnotNone:
- # Move the link to the front of the circular queue
- link_prev,link_next,_key,result=link
- link_prev[NEXT]=link_next
- link_next[PREV]=link_prev
- last=root[PREV]
- last[NEXT]=root[PREV]=link
- link[PREV]=last
- link[NEXT]=root
- hits+=1
- returnresult
- misses+=1
- result=user_function(*args,**kwds)
- withlock:
- ifkeyincache:
- # Getting here means that this same key was added to the
- # cache while the lock was released. Since the link
- # update is already done, we need only return the
- # computed result and update the count of misses.
- pass
- eliffull:
- # Use the old root to store the new key and result.
- oldroot=root
- oldroot[KEY]=key
- oldroot[RESULT]=result
- # Empty the oldest link and make it the new root.
- # Keep a reference to the old key and old result to
- # prevent their ref counts from going to zero during the
- # update. That will prevent potentially arbitrary object
- # clean-up code (i.e. __del__) from running while we're
- # still adjusting the links.
- root=oldroot[NEXT]
- oldkey=root[KEY]
- oldresult=root[RESULT]
- root[KEY]=root[RESULT]=None
- # Now update the cache dictionary.
- delcache[oldkey]
- # Save the potentially reentrant cache[key] assignment
- # for last, after the root and links have been put in
- # a consistent state.
- cache[key]=oldroot
- else:
- # Put result in a new link at the front of the queue.
- last=root[PREV]
- link=[last,root,key,result]
- last[NEXT]=root[PREV]=cache[key]=link
- # Use the cache_len bound method instead of the len() function
- # which could potentially be wrapped in an lru_cache itself.
- full=(cache_len()>=maxsize)
- returnresult
-
- defcache_info():
- """Report cache statistics"""
- withlock:
- return_CacheInfo(hits,misses,maxsize,cache_len())
-
- defcache_clear():
- """Clear the cache and cache statistics"""
- nonlocalhits,misses,full
- withlock:
- cache.clear()
- root[:]=[root,root,None,None]
- hits=misses=0
- full=False
-
- wrapper.cache_info=cache_info
- wrapper.cache_clear=cache_clear
- returnwrapper
-
-try:
- from_functoolsimport_lru_cache_wrapper
-exceptImportError:
- pass
-
-
-################################################################################
-### singledispatch() - single-dispatch generic function decorator
-################################################################################
-
-def_c3_merge(sequences):
- """Merges MROs in *sequences* to a single MRO using the C3 algorithm.
-
- Adapted from http://www.python.org/download/releases/2.3/mro/.
-
- """
- result=[]
- whileTrue:
- sequences=[sforsinsequencesifs]# purge empty sequences
- ifnotsequences:
- returnresult
- fors1insequences:# find merge candidates among seq heads
- candidate=s1[0]
- fors2insequences:
- ifcandidateins2[1:]:
- candidate=None
- break# reject the current head, it appears later
- else:
- break
- ifcandidateisNone:
- raiseRuntimeError("Inconsistent hierarchy")
- result.append(candidate)
- # remove the chosen candidate
- forseqinsequences:
- ifseq[0]==candidate:
- delseq[0]
-
-def_c3_mro(cls,abcs=None):
- """Computes the method resolution order using extended C3 linearization.
-
- If no *abcs* are given, the algorithm works exactly like the built-in C3
- linearization used for method resolution.
-
- If given, *abcs* is a list of abstract base classes that should be inserted
- into the resulting MRO. Unrelated ABCs are ignored and don't end up in the
- result. The algorithm inserts ABCs where their functionality is introduced,
- i.e. issubclass(cls, abc) returns True for the class itself but returns
- False for all its direct base classes. Implicit ABCs for a given class
- (either registered or inferred from the presence of a special method like
- __len__) are inserted directly after the last ABC explicitly listed in the
- MRO of said class. If two implicit ABCs end up next to each other in the
- resulting MRO, their ordering depends on the order of types in *abcs*.
-
- """
- fori,baseinenumerate(reversed(cls.__bases__)):
- ifhasattr(base,'__abstractmethods__'):
- boundary=len(cls.__bases__)-i
- break# Bases up to the last explicit ABC are considered first.
- else:
- boundary=0
- abcs=list(abcs)ifabcselse[]
- explicit_bases=list(cls.__bases__[:boundary])
- abstract_bases=[]
- other_bases=list(cls.__bases__[boundary:])
- forbaseinabcs:
- ifissubclass(cls,base)andnotany(
- issubclass(b,base)forbincls.__bases__
- ):
- # If *cls* is the class that introduces behaviour described by
- # an ABC *base*, insert said ABC to its MRO.
- abstract_bases.append(base)
- forbaseinabstract_bases:
- abcs.remove(base)
- explicit_c3_mros=[_c3_mro(base,abcs=abcs)forbaseinexplicit_bases]
- abstract_c3_mros=[_c3_mro(base,abcs=abcs)forbaseinabstract_bases]
- other_c3_mros=[_c3_mro(base,abcs=abcs)forbaseinother_bases]
- return_c3_merge(
- [[cls]]+
- explicit_c3_mros+abstract_c3_mros+other_c3_mros+
- [explicit_bases]+[abstract_bases]+[other_bases]
- )
-
-def_compose_mro(cls,types):
- """Calculates the method resolution order for a given class *cls*.
-
- Includes relevant abstract base classes (with their respective bases) from
- the *types* iterable. Uses a modified C3 linearization algorithm.
-
- """
- bases=set(cls.__mro__)
- # Remove entries which are already present in the __mro__ or unrelated.
- defis_related(typ):
- return(typnotinbasesandhasattr(typ,'__mro__')
- andissubclass(cls,typ))
- types=[nfornintypesifis_related(n)]
- # Remove entries which are strict bases of other entries (they will end up
- # in the MRO anyway.
- defis_strict_base(typ):
- forotherintypes:
- iftyp!=otherandtypinother.__mro__:
- returnTrue
- returnFalse
- types=[nfornintypesifnotis_strict_base(n)]
- # Subclasses of the ABCs in *types* which are also implemented by
- # *cls* can be used to stabilize ABC ordering.
- type_set=set(types)
- mro=[]
- fortypintypes:
- found=[]
- forsubintyp.__subclasses__():
- ifsubnotinbasesandissubclass(cls,sub):
- found.append([sforsinsub.__mro__ifsintype_set])
- ifnotfound:
- mro.append(typ)
- continue
- # Favor subclasses with the biggest number of useful bases
- found.sort(key=len,reverse=True)
- forsubinfound:
- forsubclsinsub:
- ifsubclsnotinmro:
- mro.append(subcls)
- return_c3_mro(cls,abcs=mro)
-
-def_find_impl(cls,registry):
- """Returns the best matching implementation from *registry* for type *cls*.
-
- Where there is no registered implementation for a specific type, its method
- resolution order is used to find a more generic implementation.
-
- Note: if *registry* does not contain an implementation for the base
- *object* type, this function may return None.
-
- """
- mro=_compose_mro(cls,registry.keys())
- match=None
- fortinmro:
- ifmatchisnotNone:
- # If *match* is an implicit ABC but there is another unrelated,
- # equally matching implicit ABC, refuse the temptation to guess.
- if(tinregistryandtnotincls.__mro__
- andmatchnotincls.__mro__
- andnotissubclass(match,t)):
- raiseRuntimeError("Ambiguous dispatch: {} or {}".format(
- match,t))
- break
- iftinregistry:
- match=t
- returnregistry.get(match)
-
-defsingledispatch(func):
- """Single-dispatch generic function decorator.
-
- Transforms a function into a generic function, which can have different
- behaviours depending upon the type of its first argument. The decorated
- function acts as the default implementation, and additional
- implementations can be registered using the register() attribute of the
- generic function.
- """
- # There are many programs that use functools without singledispatch, so we
- # trade-off making singledispatch marginally slower for the benefit of
- # making start-up of such applications slightly faster.
- importtypes,weakref
-
- registry={}
- dispatch_cache=weakref.WeakKeyDictionary()
- cache_token=None
-
- defdispatch(cls):
- """generic_func.dispatch(cls) -> <function implementation>
-
- Runs the dispatch algorithm to return the best available implementation
- for the given *cls* registered on *generic_func*.
-
- """
- nonlocalcache_token
- ifcache_tokenisnotNone:
- current_token=get_cache_token()
- ifcache_token!=current_token:
- dispatch_cache.clear()
- cache_token=current_token
- try:
- impl=dispatch_cache[cls]
- exceptKeyError:
- try:
- impl=registry[cls]
- exceptKeyError:
- impl=_find_impl(cls,registry)
- dispatch_cache[cls]=impl
- returnimpl
-
- defregister(cls,func=None):
- """generic_func.register(cls, func) -> func
-
- Registers a new implementation for the given *cls* on a *generic_func*.
-
- """
- nonlocalcache_token
- iffuncisNone:
- ifisinstance(cls,type):
- returnlambdaf:register(cls,f)
- ann=getattr(cls,'__annotations__',{})
- ifnotann:
- raiseTypeError(
- f"Invalid first argument to `register()`: {cls!r}. "
- f"Use either `@register(some_class)` or plain `@register` "
- f"on an annotated function."
- )
- func=cls
-
- # only import typing if annotation parsing is necessary
- fromtypingimportget_type_hints
- argname,cls=next(iter(get_type_hints(func).items()))
- ifnotisinstance(cls,type):
- raiseTypeError(
- f"Invalid annotation for {argname!r}. "
- f"{cls!r} is not a class."
- )
- registry[cls]=func
- ifcache_tokenisNoneandhasattr(cls,'__abstractmethods__'):
- cache_token=get_cache_token()
- dispatch_cache.clear()
- returnfunc
-
- defwrapper(*args,**kw):
- ifnotargs:
- raiseTypeError(f'{funcname} requires at least '
- '1 positional argument')
-
- returndispatch(args[0].__class__)(*args,**kw)
-
- funcname=getattr(func,'__name__','singledispatch function')
- registry[object]=func
- wrapper.register=register
- wrapper.dispatch=dispatch
- wrapper.registry=types.MappingProxyType(registry)
- wrapper._clear_cache=dispatch_cache.clear
- update_wrapper(wrapper,func)
- returnwrapper
-
-
-# Descriptor version
-classsingledispatchmethod:
- """Single-dispatch generic method descriptor.
-
- Supports wrapping existing descriptors and handles non-descriptor
- callables as instance methods.
- """
-
- def__init__(self,func):
- ifnotcallable(func)andnothasattr(func,"__get__"):
- raiseTypeError(f"{func!r} is not callable or a descriptor")
-
- self.dispatcher=singledispatch(func)
- self.func=func
-
- defregister(self,cls,method=None):
- """generic_method.register(cls, func) -> func
-
- Registers a new implementation for the given *cls* on a *generic_method*.
- """
- returnself.dispatcher.register(cls,func=method)
-
- def__get__(self,obj,cls=None):
- def_method(*args,**kwargs):
- method=self.dispatcher.dispatch(args[0].__class__)
- returnmethod.__get__(obj,cls)(*args,**kwargs)
-
- _method.__isabstractmethod__=self.__isabstractmethod__
- _method.register=self.register
- update_wrapper(_method,self.func)
- return_method
-
- @property
- def__isabstractmethod__(self):
- returngetattr(self.func,'__isabstractmethod__',False)
-
-
-################################################################################
-### cached_property() - computed once per instance, cached as attribute
-################################################################################
-
-_NOT_FOUND=object()
-
-
-classcached_property:
- def__init__(self,func):
- self.func=func
- self.attrname=None
- self.__doc__=func.__doc__
- self.lock=RLock()
-
- def__set_name__(self,owner,name):
- ifself.attrnameisNone:
- self.attrname=name
- elifname!=self.attrname:
- raiseTypeError(
- "Cannot assign the same cached_property to two different names "
- f"({self.attrname!r} and {name!r})."
- )
-
- def__get__(self,instance,owner=None):
- ifinstanceisNone:
- returnself
- ifself.attrnameisNone:
- raiseTypeError(
- "Cannot use cached_property instance without calling __set_name__ on it.")
- try:
- cache=instance.__dict__
- exceptAttributeError:# not all objects have __dict__ (e.g. class defines slots)
- msg=(
- f"No '__dict__' attribute on {type(instance).__name__!r} "
- f"instance to cache {self.attrname!r} property."
- )
- raiseTypeError(msg)fromNone
- val=cache.get(self.attrname,_NOT_FOUND)
- ifvalis_NOT_FOUND:
- withself.lock:
- # check if another thread filled cache while we awaited lock
- val=cache.get(self.attrname,_NOT_FOUND)
- ifvalis_NOT_FOUND:
- val=self.func(instance)
- try:
- cache[self.attrname]=val
- exceptTypeError:
- msg=(
- f"The '__dict__' attribute on {type(instance).__name__!r} instance "
- f"does not support item assignment for caching {self.attrname!r} property."
- )
- raiseTypeError(msg)fromNone
- returnval
-
-
-
-
\ No newline at end of file
diff --git a/docs/0.9.5/_sources/A-voice-operated-elevator-using-events.md.txt b/docs/0.9.5/_sources/A-voice-operated-elevator-using-events.md.txt
deleted file mode 100644
index 24f6bdef33..0000000000
--- a/docs/0.9.5/_sources/A-voice-operated-elevator-using-events.md.txt
+++ /dev/null
@@ -1,436 +0,0 @@
-# A voice operated elevator using events
-
-
-- Previous tutorial: [Adding dialogues in events](./Dialogues-in-events.md)
-
-This tutorial will walk you through the steps to create a voice-operated elevator, using the [in-
-game Python
-system](https://github.com/evennia/evennia/blob/master/evennia/contrib/ingame_python/README.md).
-This tutorial assumes the in-game Python system is installed in your game. If it isn't, you can
-follow the installation steps given in [the documentation on in-game
-Python](https://github.com/evennia/evennia/blob/master/evennia/contrib/ingame_python/README.md), and
-come back on this tutorial once the system is installed. **You do not need to read** the entire
-documentation, it's a good reference, but not the easiest way to learn about it. Hence these
-tutorials.
-
-The in-game Python system allows to run code on individual objects in some situations. You don't
-have to modify the source code to add these features, past the installation. The entire system
-makes it easy to add specific features to some objects, but not all.
-
-> What will we try to do?
-
-In this tutorial, we are going to create a simple voice-operated elevator. In terms of features, we
-will:
-
-- Explore events with parameters.
-- Work on more interesting callbacks.
-- Learn about chained events.
-- Play with variable modification in callbacks.
-
-## Our study case
-
-Let's summarize what we want to achieve first. We would like to create a room that will represent
-the inside of our elevator. In this room, a character could just say "1", "2" or "3", and the
-elevator will start moving. The doors will close and open on the new floor (the exits leading in
-and out of the elevator will be modified).
-
-We will work on basic features first, and then will adjust some, showing you how easy and powerfully
-independent actions can be configured through the in-game Python system.
-
-## Creating the rooms and exits we need
-
-We'll create an elevator right in our room (generally called "Limbo", of ID 2). You could easily
-adapt the following instructions if you already have some rooms and exits, of course, just remember
-to check the IDs.
-
-> Note: the in-game Python system uses IDs for a lot of things. While it is not mandatory, it is
-good practice to know the IDs you have for your callbacks, because it will make manipulation much
-quicker. There are other ways to identify objects, but as they depend on many factors, IDs are
-usually the safest path in our callbacks.
-
-Let's go into limbo (`#2`) to add our elevator. We'll add it to the north. To create this room,
-in-game you could type:
-
- tunnel n = Inside of an elevator
-
-The game should respond by telling you:
-
- Created room Inside of an elevator(#3) of type typeclasses.rooms.Room.
- Created Exit from Limbo to Inside of an elevator: north(#4) (n).
- Created Exit back from Inside of an elevator to Limbo: south(#5) (s).
-
-Note the given IDs:
-
-- `#2` is limbo, the first room the system created.
-- `#3` is our room inside of an elevator.
-- `#4` is the north exit from Limbo to our elevator.
-- `#5` is the south exit from an elevator to Limbo.
-
-Keep these IDs somewhere for the demonstration. You will shortly see why they are important.
-
-> Why have we created exits to our elevator and back to Limbo? Isn't the elevator supposed to move?
-
-It is. But we need to have exits that will represent the way inside the elevator and out. What we
-will do, at every floor, will be to change these exits so they become connected to the right room.
-You'll see this process a bit later.
-
-We have two more rooms to create: our floor 2 and 3. This time, we'll use `dig`, because we don't
-need exits leading there, not yet anyway.
-
- dig The second floor
- dig The third floor
-
-Evennia should answer with:
-
- Created room The second floor(#6) of type typeclasses.rooms.Room.
- Created room The third floor(#7) of type typeclasses.rooms.Room.
-
-Add these IDs to your list, we will use them too.
-
-## Our first callback in the elevator
-
-Let's go to the elevator (you could use `tel #3` if you have the same IDs I have).
-
-This is our elevator room. It looks a bit empty, feel free to add a prettier description or other
-things to decorate it a bit.
-
-But what we want now is to be able to say "1", "2" or "3" and have the elevator move in that
-direction.
-
-If you have read [the previous tutorial about adding dialogues in events](./Dialogues-in-events.md), you
-may remember what we need to do. If not, here's a summary: we need to run some code when somebody
-speaks in the room. So we need to create a callback (the callback will contain our lines of code).
-We just need to know on which event this should be set. You can enter `call here` to see the
-possible events in this room.
-
-In the table, you should see the "say" event, which is called when somebody says something in the
-room. So we'll need to add a callback to this event. Don't worry if you're a bit lost, just follow
-the following steps, the way they connect together will become more obvious.
-
- call/add here = say 1, 2, 3
-
-1. We need to add a callback. A callback contains the code that will be executed at a given time.
-So we use the `call/add` command and switch.
-2. `here` is our object, the room in which we are.
-3. An equal sign.
-4. The name of the event to which the callback should be connected. Here, the event is "say".
-Meaning this callback will be executed every time somebody says something in the room.
-5. But we add an event parameter to indicate the keywords said in the room that should execute our
-callback. Otherwise, our callback would be called every time somebody speaks, no matter what. Here
-we limit, indicating our callback should be executed only if the spoken message contains "1", "2" or
-"3".
-
-An editor should open, inviting you to enter the Python code that should be executed. The first
-thing to remember is to read the text provided (it can contain important information) and, most of
-all, the list of variables that are available in this callback:
-
-```
-Variables you can use in this event:
-
- character: the character having spoken in this room.
- room: the room connected to this event.
- message: the text having been spoken by the character.
-
-----------Line Editor [Callback say of Inside of an elevator]---------------------
-01|
-----------[l:01 w:000 c:0000]------------(:h for help)----------------------------
-```
-
-This is important, in order to know what variables we can use in our callback out-of-the-box. Let's
-write a single line to be sure our callback is called when we expect it to:
-
-```python
-character.msg("You just said {}.".format(message))
-```
-
-You can paste this line in-game, then type the `:wq` command to exit the editor and save your
-modifications.
-
-Let's check. Try to say "hello" in the room. You should see the standard message, but nothing
-more. Now try to say "1". Below the standard message, you should see:
-
- You just said 1.
-
-You can try it. Our callback is only called when we say "1", "2" or "3". Which is just what we
-want.
-
-Let's go back in our code editor and add something more useful.
-
- call/edit here = say
-
-> Notice that we used the "edit" switch this time, since the callback exists, we just want to edit
-it.
-
-The editor opens again. Let's empty it first:
-
- :DD
-
-And turn off automatic indentation, which will help us:
-
- :=
-
-> Auto-indentation is an interesting feature of the code editor, but we'd better not use it at this
-point, it will make copy/pasting more complicated.
-
-## Our entire callback in the elevator
-
-So here's the time to truly code our callback in-game. Here's a little reminder:
-
-1. We have all the IDs of our three rooms and two exits.
-2. When we say "1", "2" or "3", the elevator should move to the right room, that is change the
-exits. Remember, we already have the exits, we just need to change their location and destination.
-
-It's a good idea to try to write this callback yourself, but don't feel bad about checking the
-solution right now. Here's a possible code that you could paste in the code editor:
-
-```python
-# First let's have some constants
-ELEVATOR = get(id=3)
-FLOORS = {
- "1": get(id=2),
- "2": get(id=6),
- "3": get(id=7),
-}
-TO_EXIT = get(id=4)
-BACK_EXIT = get(id=5)
-
-# Now we check that the elevator isn't already at this floor
-floor = FLOORS.get(message)
-if floor is None:
- character.msg("Which floor do you want?")
-elif TO_EXIT.location is floor:
- character.msg("The elevator already is at this floor.")
-else:
- # 'floor' contains the new room where the elevator should be
- room.msg_contents("The doors of the elevator close with a clank.")
- TO_EXIT.location = floor
- BACK_EXIT.destination = floor
- room.msg_contents("The doors of the elevator open to {floor}.",
- mapping=dict(floor=floor))
-```
-
-Let's review this longer callback:
-
-1. We first obtain the objects of both exits and our three floors. We use the `get()` eventfunc,
-which is a shortcut to obtaining objects. We usually use it to retrieve specific objects with an
-ID. We put the floors in a dictionary. The keys of the dictionary are the floor number (as str),
-the values are room objects.
-2. Remember, the `message` variable contains the message spoken in the room. So either "1", "2", or
-"3". We still need to check it, however, because if the character says something like "1 2" in the
-room, our callback will be executed. Let's be sure what she says is a floor number.
-3. We then check if the elevator is already at this floor. Notice that we use `TO_EXIT.location`.
-`TO_EXIT` contains our "north" exit, leading inside of our elevator. Therefore, its `location` will
-be the room where the elevator currently is.
-4. If the floor is a different one, have the elevator "move", changing just the location and
-destination of both exits.
- - The `BACK_EXIT` (that is "north") should change its location. The elevator shouldn't be
-accessible through our old floor.
- - The `TO_EXIT` (that is "south", the exit leading out of the elevator) should have a different
-destination. When we go out of the elevator, we should find ourselves in the new floor, not the old
-one.
-
-Feel free to expand on this example, changing messages, making further checks. Usage and practice
-are keys.
-
-You can quit the editor as usual with `:wq` and test it out.
-
-## Adding a pause in our callback
-
-Let's improve our callback. One thing that's worth adding would be a pause: for the time being,
-when we say the floor number in the elevator, the doors close and open right away. It would be
-better to have a pause of several seconds. More logical.
-
-This is a great opportunity to learn about chained events. Chained events are very useful to create
-pauses. Contrary to the events we have seen so far, chained events aren't called automatically.
-They must be called by you, and can be called after some time.
-
-- Chained events always have the name "chain_X". Usually, X is a number, but you can give the
-chained event a more explicit name.
-- In our original callback, we will call our chained events in, say, 15 seconds.
-- We'll also have to make sure the elevator isn't already moving.
-
-Other than that, a chained event can be connected to a callback as usual. We'll create a chained
-event in our elevator, that will only contain the code necessary to open the doors to the new floor.
-
- call/add here = chain_1
-
-The callback is added to the "chain_1" event, an event that will not be automatically called by the
-system when something happens. Inside this event, you can paste the code to open the doors at the
-new floor. You can notice a few differences:
-
-```python
-TO_EXIT.location = floor
-TO_EXIT.destination = ELEVATOR
-BACK_EXIT.location = ELEVATOR
-BACK_EXIT.destination = floor
-room.msg_contents("The doors of the elevator open to {floor}.",
- mapping=dict(floor=floor))
-```
-
-Paste this code into the editor, then use `:wq` to save and quit the editor.
-
-Now let's edit our callback in the "say" event. We'll have to change it a bit:
-
-- The callback will have to check the elevator isn't already moving.
-- It must change the exits when the elevator move.
-- It has to call the "chain_1" event we have defined. It should call it 15 seconds later.
-
-Let's see the code in our callback.
-
- call/edit here = say
-
-Remove the current code and disable auto-indentation again:
-
- :DD
- :=
-
-And you can paste instead the following code. Notice the differences with our first attempt:
-
-```python
-# First let's have some constants
-ELEVATOR = get(id=3)
-FLOORS = {
- "1": get(id=2),
- "2": get(id=6),
- "3": get(id=7),
-}
-TO_EXIT = get(id=4)
-BACK_EXIT = get(id=5)
-
-# Now we check that the elevator isn't already at this floor
-floor = FLOORS.get(message)
-if floor is None:
- character.msg("Which floor do you want?")
-elif BACK_EXIT.location is None:
- character.msg("The elevator is between floors.")
-elif TO_EXIT.location is floor:
- character.msg("The elevator already is at this floor.")
-else:
- # 'floor' contains the new room where the elevator should be
- room.msg_contents("The doors of the elevator close with a clank.")
- TO_EXIT.location = None
- BACK_EXIT.location = None
- call_event(room, "chain_1", 15)
-```
-
-What changed?
-
-1. We added a little test to make sure the elevator wasn't already moving. If it is, the
-`BACK_EXIT.location` (the "south" exit leading out of the elevator) should be `None`. We'll remove
-the exit while the elevator is moving.
-2. When the doors close, we set both exits' `location` to `None`. Which "removes" them from their
-room but doesn't destroy them. The exits still exist but they don't connect anything. If you say
-"2" in the elevator and look around while the elevator is moving, you won't see any exits.
-3. Instead of opening the doors immediately, we call `call_event`. We give it the object containing
-the event to be called (here, our elevator), the name of the event to be called (here, "chain_1")
-and the number of seconds from now when the event should be called (here, `15`).
-4. The `chain_1` callback we have created contains the code to "re-open" the elevator doors. That
-is, besides displaying a message, it reset the exits' `location` and `destination`.
-
-If you try to say "3" in the elevator, you should see the doors closing. Look around you and you
-won't see any exit. Then, 15 seconds later, the doors should open, and you can leave the elevator
-to go to the third floor. While the elevator is moving, the exit leading to it will be
-inaccessible.
-
-> Note: we don't define the variables again in our chained event, we just call them. When we
-execute `call_event`, a copy of our current variables is placed in the database. These variables
-will be restored and accessible again when the chained event is called.
-
-You can use the `call/tasks` command to see the tasks waiting to be executed. For instance, say "2"
-in the room, notice the doors closing, and then type the `call/tasks` command. You will see a task
-in the elevator, waiting to call the `chain_1` event.
-
-## Changing exit messages
-
-Here's another nice little feature of events: you can modify the message of a single exit without
-altering the others. In this case, when someone goes north into our elevator, we'd like to see
-something like: "someone walks into the elevator." Something similar for the back exit would be
-great too.
-
-Inside of the elevator, you can look at the available events on the exit leading outside (south).
-
- call south
-
-You should see two interesting rows in this table:
-
-```
-| msg_arrive | 0 (0) | Customize the message when a character |
-| | | arrives through this exit. |
-| msg_leave | 0 (0) | Customize the message when a character leaves |
-| | | through this exit. |
-```
-
-So we can change the message others see when a character leaves, by editing the "msg_leave" event.
-Let's do that:
-
- call/add south = msg_leave
-
-Take the time to read the help. It gives you all the information you should need. We'll need to
-change the "message" variable, and use custom mapping (between braces) to alter the message. We're
-given an example, let's use it. In the code editor, you can paste the following line:
-
-```python
-message = "{character} walks out of the elevator."
-```
-
-Again, save and quit the editor by entering `:wq`. You can create a new character to see it leave.
-
- charcreate A beggar
- tel #8 = here
-
-(Obviously, adapt the ID if necessary.)
-
- py self.search("beggar").move_to(self.search("south"))
-
-This is a crude way to force our beggar out of the elevator, but it allows us to test. You should
-see:
-
- A beggar(#8) walks out of the elevator.
-
-Great! Let's do the same thing for the exit leading inside of the elevator. Follow the beggar,
-then edit "msg_leave" of "north":
-
- call/add north = msg_leave
-
-```python
-message = "{character} walks into the elevator."
-```
-
-Again, you can force our beggar to move and see the message we have just set. This modification
-applies to these two exits, obviously: the custom message won't be used for other exits. Since we
-use the same exits for every floor, this will be available no matter at what floor the elevator is,
-which is pretty neat!
-
-## Tutorial F.A.Q.
-
-- **Q:** what happens if the game reloads or shuts down while a task is waiting to happen?
-- **A:** if your game reloads while a task is in pause (like our elevator between floors), when the
-game is accessible again, the task will be called (if necessary, with a new time difference to take
-into account the reload). If the server shuts down, obviously, the task will not be called, but
-will be stored and executed when the server is up again.
-- **Q:** can I use all kinds of variables in my callback? Whether chained or not?
-- **A:** you can use every variable type you like in your original callback. However, if you
-execute `call_event`, since your variables are stored in the database, they will need to respect the
-constraints on persistent attributes. A callback will not be stored in this way, for instance.
-This variable will not be available in your chained event.
-- **Q:** when you say I can call my chained events something else than "chain_1", "chain_2" and
-such, what is the naming convention?
-- **A:** chained events have names beginning by "chain_". This is useful for you and for the
-system. But after the underscore, you can give a more useful name, like "chain_open_doors" in our
-case.
-- **Q:** do I have to pause several seconds to call a chained event?
-- **A:** no, you can call it right away. Just leave the third parameter of `call_event` out (it
-will default to 0, meaning the chained event will be called right away). This will not create a
-task.
-- **Q:** can I have chained events calling themselves?
-- **A:** you can. There's no limitation. Just be careful, a callback that calls itself,
-particularly without delay, might be a good recipe for an infinite loop. However, in some cases, it
-is useful to have chained events calling themselves, to do the same repeated action every X seconds
-for instance.
-- **Q:** what if I need several elevators, do I need to copy/paste these callbacks each time?
-- **A:** not advisable. There are definitely better ways to handle this situation. One of them is
-to consider adding the code in the source itself. Another possibility is to call chained events
-with the expected behavior, which makes porting code very easy. This side of chained events will be
-shown in the next tutorial.
-
-- Previous tutorial: [Adding dialogues in events](./Dialogues-in-events.md)
diff --git a/docs/0.9.5/_sources/API-refactoring.md.txt b/docs/0.9.5/_sources/API-refactoring.md.txt
deleted file mode 100644
index 689b5893f5..0000000000
--- a/docs/0.9.5/_sources/API-refactoring.md.txt
+++ /dev/null
@@ -1,46 +0,0 @@
-# API refactoring
-
-Building up to Evennia 1.0 and beyond, it's time to comb through the Evennia API for old cruft. This
-whitepage is for anyone interested to contribute with their views on what part of the API needs
-refactoring, cleanup or clarification (or extension!)
-
-Note that this is not a forum. To keep things clean, each opinion text should ideally present a
-clear argument or lay out a suggestion. Asking for clarification and any side-discussions should be
-held in chat or forum.
-
----
-
-### Griatch (Aug 13, 2019)
-
-This is how to enter an opinion. Use any markdown needed but stay within your section. Also remember
-to copy your text to the clipboard before saving since if someone else edited the wiki in the
-meantime you'll have to start over.
-
-### Griatch (Sept 2, 2019)
-
-I don't agree with removing explicit keywords as suggested by [Johnny on Aug 29 below](API-
-refactoring#reduce-usage-of-optionalpositional-arguments-aug-29-2019). Overriding such a method can
-still be done by `get(self, **kwargs)` if so desired, making the kwargs explicit helps IMO
-readability of the API. If just giving a generic `**kwargs`, one must read the docstring or even the
-code to see which keywords are valid.
-
-On the other hand, I think it makes sense to as a standard offer an extra `**kwargs` at the end of
-arg-lists for common methods that are expected to be over-ridden. This make the API more flexible by
-hinting to the dev that they could expand their own over-ridden implementation with their own
-keyword arguments if so desired.
-
----
-
-### Johnny
-
-#### Reduce usage of optional/positional arguments (Aug 29, 2019)
-```
-# AttributeHandler
-def get(self, key=None, default=None, category=None, return_obj=False,
- strattr=False, raise_exception=False, accessing_obj=None,
- default_access=True, return_list=False):
-```
-Many classes have methods requiring lengthy positional argument lists, which are tedious and error-
-prone to extend and override especially in cases where not all arguments are even required. It would
-be useful if arguments were reserved for required inputs and anything else relegated to kwargs for
-easier passthrough on extension.
diff --git a/docs/0.9.5/_sources/Accounts.md.txt b/docs/0.9.5/_sources/Accounts.md.txt
deleted file mode 100644
index 6d61136583..0000000000
--- a/docs/0.9.5/_sources/Accounts.md.txt
+++ /dev/null
@@ -1,108 +0,0 @@
-# Accounts
-
-
-All *users* (real people) that starts a game [Session](./Sessions.md) on Evennia are doing so through an
-object called *Account*. The Account object has no in-game representation, it represents a unique
-game account. In order to actually get on the game the Account must *puppet* an [Object](./Objects.md)
-(normally a [Character](./Objects.md#characters)).
-
-Exactly how many Sessions can interact with an Account and its Puppets at once is determined by
-Evennia's [MULTISESSION_MODE](./Sessions.md#multisession-mode) setting.
-
-Apart from storing login information and other account-specific data, the Account object is what is
-chatting on [Channels](./Communications.md). It is also a good place to store [Permissions](./Locks.md) to be
-consistent between different in-game characters as well as configuration options. The Account
-object also has its own [CmdSet](./Command-Sets.md), the `AccountCmdSet`.
-
-Logged into default evennia, you can use the `ooc` command to leave your current
-[character](./Objects.md) and go into OOC mode. You are quite limited in this mode, basically it works
-like a simple chat program. It acts as a staging area for switching between Characters (if your
-game supports that) or as a safety mode if your Character gets deleted. Use `ic` to attempt to
-(re)puppet a Character.
-
-Note that the Account object can have, and often does have, a different set of
-[Permissions](./Locks.md#permissions) from the Character they control. Normally you should put your
-permissions on the Account level - this will overrule permissions set on the Character level. For
-the permissions of the Character to come into play the default `quell` command can be used. This
-allows for exploring the game using a different permission set (but you can't escalate your
-permissions this way - for hierarchical permissions like `Builder`, `Admin` etc, the *lower* of the
-permissions on the Character/Account will always be used).
-
-## How to create your own Account types
-
-You will usually not want more than one Account typeclass for all new accounts (but you could in
-principle create a system that changes an account's typeclass dynamically).
-
-An Evennia Account is, per definition, a Python class that includes `evennia.DefaultAccount` among
-its parents. In `mygame/typeclasses/accounts.py` there is an empty class ready for you to modify.
-Evennia defaults to using this (it inherits directly from `DefaultAccount`).
-
-Here's an example of modifying the default Account class in code:
-
-```python
- # in mygame/typeclasses/accounts.py
-
- from evennia import DefaultAccount
-
- class Account(DefaultAccount): # [...]
-
- at_account_creation(self): "this is called only once, when account is first created"
- self.db.real_name = None # this is set later self.db.real_address = None #
-"
- self.db.config_1 = True # default config self.db.config_2 = False # "
- self.db.config_3 = 1 # "
-
- # ... whatever else our game needs to know ``` Reload the server with `reload`.
-
-```
-
-... However, if you use `examine *self` (the asterisk makes you examine your Account object rather
-than your Character), you won't see your new Attributes yet. This is because `at_account_creation`
-is only called the very *first* time the Account is called and your Account object already exists
-(any new Accounts that connect will see them though). To update yourself you need to make sure to
-re-fire the hook on all the Accounts you have already created. Here is an example of how to do this
-using `py`:
-
-
-``` py [account.at_account_creation() for account in evennia.managers.accounts.all()] ```
-
-You should now see the Attributes on yourself.
-
-
-> If you wanted Evennia to default to a completely *different* Account class located elsewhere, you
-> must point Evennia to it. Add `BASE_ACCOUNT_TYPECLASS` to your settings file, and give the python
-> path to your custom class as its value. By default this points to `typeclasses.accounts.Account`,
-> the empty template we used above.
-
-
-## Properties on Accounts
-
-Beyond those properties assigned to all typeclassed objects (see [Typeclasses](./Typeclasses.md)), the
-Account also has the following custom properties:
-
-- `user` - a unique link to a `User` Django object, representing the logged-in user.
-- `obj` - an alias for `character`.
-- `name` - an alias for `user.username`
-- `sessions` - an instance of
- [ObjectSessionHandler](github:evennia.objects.objects#objectsessionhandler)
- managing all connected Sessions (physical connections) this object listens to (Note: In older
- versions of Evennia, this was a list). The so-called `session-id` (used in many places) is found
-as
- a property `sessid` on each Session instance.
-- `is_superuser` (bool: True/False) - if this account is a superuser.
-
-Special handlers:
-- `cmdset` - This holds all the current [Commands](./Commands.md) of this Account. By default these are
- the commands found in the cmdset defined by `settings.CMDSET_ACCOUNT`.
-- `nicks` - This stores and handles [Nicks](./Nicks.md), in the same way as nicks it works on Objects.
- For Accounts, nicks are primarily used to store custom aliases for
-[Channels](./Communications.md#channels).
-
-Selection of special methods (see `evennia.DefaultAccount` for details):
-- `get_puppet` - get a currently puppeted object connected to the Account and a given session id, if
- any.
-- `puppet_object` - connect a session to a puppetable Object.
-- `unpuppet_object` - disconnect a session from a puppetable Object.
-- `msg` - send text to the Account
-- `execute_cmd` - runs a command as if this Account did it.
-- `search` - search for Accounts.
diff --git a/docs/0.9.5/_sources/Add-a-simple-new-web-page.md.txt b/docs/0.9.5/_sources/Add-a-simple-new-web-page.md.txt
deleted file mode 100644
index c146a54f10..0000000000
--- a/docs/0.9.5/_sources/Add-a-simple-new-web-page.md.txt
+++ /dev/null
@@ -1,100 +0,0 @@
-# Add a simple new web page
-
-
-Evennia leverages [Django](https://docs.djangoproject.com) which is a web development framework.
-Huge professional websites are made in Django and there is extensive documentation (and books) on it
-. You are encouraged to at least look at the Django basic tutorials. Here we will just give a brief
-introduction for how things hang together, to get you started.
-
-We assume you have installed and set up Evennia to run. A webserver and website comes out of the
-box. You can get to that by entering `http://localhost:4001` in your web browser - you should see a
-welcome page with some game statistics and a link to the web client. Let us add a new page that you
-can get to by going to `http://localhost:4001/story`.
-
-### Create the view
-
-A django "view" is a normal Python function that django calls to render the HTML page you will see
-in the web browser. Here we will just have it spit back the raw html, but Django can do all sorts of
-cool stuff with the page in the view, like adding dynamic content or change it on the fly. Open
-`mygame/web` folder and add a new module there named `story.py` (you could also put it in its own
-folder if you wanted to be neat. Don't forget to add an empty `__init__.py` file if you do, to tell
-Python you can import from the new folder). Here's how it looks:
-
-```python
-# in mygame/web/story.py
-
-from django.shortcuts import render
-
-def storypage(request):
- return render(request, "story.html")
-```
-
-This view takes advantage of a shortcut provided to use by Django, _render_. This shortcut gives the
-template some information from the request, for instance, the game name, and then renders it.
-
-### The HTML page
-
-We need to find a place where Evennia (and Django) looks for html files (called *templates* in
-Django parlance). You can specify such places in your settings (see the `TEMPLATES` variable in
-`default_settings.py` for more info), but here we'll use an existing one. Go to
-`mygame/template/overrides/website/` and create a page `story.html` there.
-
-This is not a HTML tutorial, so we'll go simple:
-
-```html
-{% extends "base.html" %}
-{% block content %}
-
-
-
A story about a tree
-
- This is a story about a tree, a classic tale ...
-
-
-
-{% endblock %}
-```
-
-Since we've used the _render_ shortcut, Django will allow us to extend our base styles easily.
-
-If you'd rather not take advantage of Evennia's base styles, you can do something like this instead:
-
-```html
-
-
-
A story about a tree
-
- This is a story about a tree, a classic tale ...
-
-
-```
-
-
-### The URL
-
-When you enter the address `http://localhost:4001/story` in your web browser, Django will parse that
-field to figure out which page you want to go to. You tell it which patterns are relevant in the
-file
-[mygame/web/urls.py](https://github.com/evennia/evennia/blob/master/evennia/game_template/web/urls.py).
-Open it now.
-
-Django looks for the variable `urlpatterns` in this file. You want to add your new pattern to the
-`custom_patterns` list we have prepared - that is then merged with the default `urlpatterns`. Here's
-how it could look:
-
-```python
-from web import story
-
-# ...
-
-custom_patterns = [
- url(r'story', story.storypage, name='Story'),
-]
-```
-
-That is, we import our story view module from where we created it earlier and then create an `url`
-instance. The first argument to `url` is the pattern of the url we want to find (`"story"`) (this is
-a regular expression if you are familiar with those) and then our view function we want to direct
-to.
-
-That should be it. Reload Evennia and you should be able to browse to your new story page!
diff --git a/docs/0.9.5/_sources/Add-a-wiki-on-your-website.md.txt b/docs/0.9.5/_sources/Add-a-wiki-on-your-website.md.txt
deleted file mode 100644
index a8f476d28e..0000000000
--- a/docs/0.9.5/_sources/Add-a-wiki-on-your-website.md.txt
+++ /dev/null
@@ -1,232 +0,0 @@
-# Add a wiki on your website
-
-
-**Before doing this tutorial you will probably want to read the intro in
-[Basic Web tutorial](./Web-Tutorial.md).** Reading the three first parts of the
-[Django tutorial](https://docs.djangoproject.com/en/1.9/intro/tutorial01/) might help as well.
-
-This tutorial will provide a step-by-step process to installing a wiki on your website.
-Fortunately, you don't have to create the features manually, since it has been done by others, and
-we can integrate their work quite easily with Django. I have decided to focus on
-the [Django-wiki](http://django-wiki.readthedocs.io/).
-
-> Note: this article has been updated for Evennia 0.9. If you're not yet using this version, be
-careful, as the django wiki doesn't support Python 2 anymore. (Remove this note when enough time
-has passed.)
-
-The [Django-wiki](http://django-wiki.readthedocs.io/) offers a lot of features associated with
-wikis, is
-actively maintained (at this time, anyway), and isn't too difficult to install in Evennia. You can
-see a [demonstration of Django-wiki here](https://demo.django.wiki).
-
-## Basic installation
-
-You should begin by shutting down the Evennia server if it is running. We will run migrations and
-alter the virtual environment just a bit. Open a terminal and activate your Python environment, the
-one you use to run the `evennia` command.
-
-* On Linux:
- ```
- source evenv/bin/activate
- ```
-* Or Windows:
- ```
- evenv\bin\activate
- ```
-
-### Installing with pip
-
-Install the wiki using pip:
-
- pip install wiki
-
-> Note: this will install the last version of Django wiki. Version >0.4 doesn't support Python 2, so
-install wiki 0.3 if you haven't updated to Python 3 yet.
-
-It might take some time, the Django-wiki having some dependencies.
-
-### Adding the wiki in the settings
-
-You will need to add a few settings to have the wiki app on your website. Open your
-`server/conf/settings.py` file and add the following at the bottom (but before importing
-`secret_settings`). Here's what you'll find in my own setting file (add the whole Django-wiki
-section):
-
-```python
-r"""
-Evennia settings file.
-
-...
-
-"""
-
-# Use the defaults from Evennia unless explicitly overridden
-from evennia.settings_default import *
-
-######################################################################
-# Evennia base server config
-######################################################################
-
-# This is the name of your game. Make it catchy!
-SERVERNAME = "demowiki"
-
-######################################################################
-# Django-wiki settings
-######################################################################
-INSTALLED_APPS += (
- 'django.contrib.humanize.apps.HumanizeConfig',
- 'django_nyt.apps.DjangoNytConfig',
- 'mptt',
- 'sorl.thumbnail',
- 'wiki.apps.WikiConfig',
- 'wiki.plugins.attachments.apps.AttachmentsConfig',
- 'wiki.plugins.notifications.apps.NotificationsConfig',
- 'wiki.plugins.images.apps.ImagesConfig',
- 'wiki.plugins.macros.apps.MacrosConfig',
-)
-
-# Disable wiki handling of login/signup
-WIKI_ACCOUNT_HANDLING = False
-WIKI_ACCOUNT_SIGNUP_ALLOWED = False
-
-######################################################################
-# Settings given in secret_settings.py override those in this file.
-######################################################################
-try:
- from server.conf.secret_settings import *
-except ImportError:
- print("secret_settings.py file not found or failed to import.")
-```
-
-### Adding the new URLs
-
-Next we need to add two URLs in our `web/urls.py` file. Open it and compare the following output:
-you will need to add two URLs in `custom_patterns` and add one import line:
-
-```python
-from django.conf.urls import url, include
-from django.urls import path # NEW!
-
-# default evenni a patterns
-from evennia.web.urls import urlpatterns
-
-# eventual custom patterns
-custom_patterns = [
- # url(r'/desired/url/', view, name='example'),
- url('notifications/', include('django_nyt.urls')), # NEW!
- url('wiki/', include('wiki.urls')), # NEW!
-]
-
-# this is required by Django.
-urlpatterns = custom_patterns + urlpatterns
-```
-
-You will probably need to copy line 2, 10, and 11. Be sure to place them correctly, as shown in
-the example above.
-
-### Running migrations
-
-It's time to run the new migrations. The wiki app adds a few tables in our database. We'll need to
-run:
-
- evennia migrate
-
-And that's it, you can start the server. If you go to http://localhost:4001/wiki , you should see
-the wiki. Use your account's username and password to connect to it. That's how simple it is.
-
-## Customizing privileges
-
-A wiki can be a great collaborative tool, but who can see it? Who can modify it? Django-wiki comes
-with a privilege system centered around four values per wiki page. The owner of an article can
-always read and write in it (which is somewhat logical). The group of the article defines who can
-read and who can write, if the user seeing the page belongs to this group. The topic of groups in
-wiki pages will not be discussed here. A last setting determines which other user (that is, these
-who aren't in the groups, and aren't the article's owner) can read and write. Each article has
-these four settings (group read, group write, other read, other write). Depending on your purpose,
-it might not be a good default choice, particularly if you have to remind every builder to keep the
-pages private. Fortunately, Django-wiki gives us additional settings to customize who can read, and
-who can write, a specific article.
-
-These settings must be placed, as usual, in your `server/conf/settings.py` file. They take a
-function as argument, said function (or callback) will be called with the article and the user.
-Remember, a Django user, for us, is an account. So we could check lockstrings on them if needed.
-Here is a default setting to restrict the wiki: only builders can write in it, but anyone (including
-non-logged in users) can read it. The superuser has some additional privileges.
-
-```python
-# In server/conf/settings.py
-# ...
-
-def is_superuser(article, user):
- """Return True if user is a superuser, False otherwise."""
- return not user.is_anonymous() and user.is_superuser
-
-def is_builder(article, user):
- """Return True if user is a builder, False otherwise."""
- return not user.is_anonymous() and user.locks.check_lockstring(user, "perm(Builders)")
-
-def is_anyone(article, user):
- """Return True even if the user is anonymous."""
- return True
-
-# Who can create new groups and users from the wiki?
-WIKI_CAN_ADMIN = is_superuser
-# Who can change owner and group membership?
-WIKI_CAN_ASSIGN = is_superuser
-# Who can change group membership?
-WIKI_CAN_ASSIGN_OWNER = is_superuser
-# Who can change read/write access to groups or others?
-WIKI_CAN_CHANGE_PERMISSIONS = is_superuser
-# Who can soft-delete an article?
-WIKI_CAN_DELETE = is_builder
-# Who can lock an article and permanently delete it?
-WIKI_CAN_MODERATE = is_superuser
-# Who can edit articles?
-WIKI_CAN_WRITE = is_builder
-# Who can read articles?
-WIKI_CAN_READ = is_anyone
-```
-
-Here, we have created three functions: one to return `True` if the user is the superuser, one to
-return `True` if the user is a builder, one to return `True` no matter what (this includes if the
-user is anonymous, E.G. if it's not logged-in). We then change settings to allow either the
-superuser or
-each builder to moderate, read, write, delete, and more. You can, of course, add more functions,
-adapting them to your need. This is just a demonstration.
-
-Providing the `WIKI_CAN*...` settings will bypass the original permission system. The superuser
-could change permissions of an article, but still, only builders would be able to write it. If you
-need something more custom, you will have to expand on the functions you use.
-
-### Managing wiki pages from Evennia
-
-Unfortunately, Django wiki doesn't provide a clear and clean entry point to read and write articles
-from Evennia and it doesn't seem to be a very high priority. If you really need to keep Django wiki
-and to create and manage wiki pages from your code, you can do so, but this article won't elaborate,
-as this is somewhat more technical.
-
-However, it is a good opportunity to present a small project that has been created more recently:
-[evennia-wiki](https://github.com/vincent-lg/evennia-wiki) has been created to provide a simple
-wiki, more tailored to Evennia and easier to connect. It doesn't, as yet, provide as many options
-as does Django wiki, but it's perfectly usable:
-
-- Pages have an inherent and much-easier to understand hierarchy based on URLs.
-- Article permissions are connected to Evennia groups and are much easier to accommodate specific
-requirements.
-- Articles can easily be created, read or updated from the Evennia code itself.
-- Markdown is fully-supported with a default integration to Bootstrap to look good on an Evennia
-website. Tables and table of contents are supported as well as wiki links.
-- The process to override wiki templates makes full use of the `template_overrides` directory.
-
-However evennia-wiki doesn't yet support:
-
-- Images in markdown and the uploading schema. If images are important to you, please consider
-contributing to this new project.
-- Modifying permissions on a per page/setting basis.
-- Moving pages to new locations.
-- Viewing page history.
-
-Considering the list of features in Django wiki, obviously other things could be added to the list.
-However, these features may be the most important and useful. Additional ones might not be that
-necessary. If you're interested in supporting this little project, you are more than welcome to
-[contribute to it](https://github.com/vincent-lg/evennia-wiki). Thanks!
\ No newline at end of file
diff --git a/docs/0.9.5/_sources/Adding-Command-Tutorial.md.txt b/docs/0.9.5/_sources/Adding-Command-Tutorial.md.txt
deleted file mode 100644
index 569c12252d..0000000000
--- a/docs/0.9.5/_sources/Adding-Command-Tutorial.md.txt
+++ /dev/null
@@ -1,171 +0,0 @@
-# Adding Command Tutorial
-
-This is a quick first-time tutorial expanding on the [Commands](./Commands.md) documentation.
-
-Let's assume you have just downloaded Evennia, installed it and created your game folder (let's call
-it just `mygame` here). Now you want to try to add a new command. This is the fastest way to do it.
-
-## Step 1: Creating a custom command
-
-1. Open `mygame/commands/command.py` in a text editor. This is just one place commands could be
-placed but you get it setup from the onset as an easy place to start. It also already contains some
-example code.
-1. Create a new class in `command.py` inheriting from `default_cmds.MuxCommand`. Let's call it
- `CmdEcho` in this example.
-1. Set the class variable `key` to a good command name, like `echo`.
-1. Give your class a useful _docstring_. A docstring is the string at the very top of a class or
-function/method. The docstring at the top of the command class is read by Evennia to become the help
-entry for the Command (see
- [Command Auto-help](./Help-System.md#command-auto-help-system)).
-1. Define a class method `func(self)` that echoes your input back to you.
-
-Below is an example how this all could look for the echo command:
-
-```python
- # file mygame/commands/command.py
- #[...]
- from evennia import default_cmds
- class CmdEcho(default_cmds.MuxCommand):
- """
- Simple command example
-
- Usage:
- echo [text]
-
- This command simply echoes text back to the caller.
- """
-
- key = "echo"
-
- def func(self):
- "This actually does things"
- if not self.args:
- self.caller.msg("You didn't enter anything!")
- else:
- self.caller.msg("You gave the string: '%s'" % self.args)
-```
-
-## Step 2: Adding the Command to a default Cmdset
-
-The command is not available to use until it is part of a [Command Set](./Command-Sets.md). In this
-example we will go the easiest route and add it to the default Character commandset that already
-exists.
-
-1. Edit `mygame/commands/default_cmdsets.py`
-1. Import your new command with `from commands.command import CmdEcho`.
-1. Add a line `self.add(CmdEcho())` to `CharacterCmdSet`, in the `at_cmdset_creation` method (the
- template tells you where).
-
-This is approximately how it should look at this point:
-
-```python
- # file mygame/commands/default_cmdsets.py
- #[...]
- from commands.command import CmdEcho
- #[...]
- class CharacterCmdSet(default_cmds.CharacterCmdSet):
-
- key = "DefaultCharacter"
-
- def at_cmdset_creation(self):
-
- # this first adds all default commands
- super().at_cmdset_creation()
-
- # all commands added after this point will extend or
- # overwrite the default commands.
- self.add(CmdEcho())
-```
-
-Next, run the `@reload` command. You should now be able to use your new `echo` command from inside
-the game. Use `help echo` to see the documentation for the command.
-
-If you have trouble, make sure to check the log for error messages (probably due to syntax errors in
-your command definition).
-
-> Note: Typing `echotest` will also work. It will be handled as the command `echo` directly followed
-by
-its argument `test` (which will end up in `self.args). To change this behavior, you can add the
-`arg_regex` property alongside `key`, `help_category` etc. [See the arg_regex
-documentation](./Commands.md#on-arg_regex) for more info.
-
-If you want to overload existing default commands (such as `look` or `get`), just add your new
-command with the same key as the old one - it will then replace it. Just remember that you must use
-`@reload` to see any changes.
-
-See [Commands](./Commands.md) for many more details and possibilities when defining Commands and using
-Cmdsets in various ways.
-
-
-## Adding the command to specific object types
-
-Adding your Command to the `CharacterCmdSet` is just one easy exapmple. The cmdset system is very
-generic. You can create your own cmdsets (let's say in a module `mycmdsets.py`) and add them to
-objects as you please (how to control their merging is described in detail in the [Command Set
-documentation](./Command-Sets.md)).
-
-```python
- # file mygame/commands/mycmdsets.py
- #[...]
- from commands.command import CmdEcho
- from evennia import CmdSet
- #[...]
- class MyCmdSet(CmdSet):
-
- key = "MyCmdSet"
-
- def at_cmdset_creation(self):
- self.add(CmdEcho())
-```
-Now you just need to add this to an object. To test things (as superuser) you can do
-
- @py self.cmdset.add("mycmdsets.MyCmdSet")
-
-This will add this cmdset (along with its echo command) to yourself so you can test it. Note that
-you cannot add a single Command to an object on its own, it must be part of a CommandSet in order to
-do so.
-
-The Command you added is not there permanently at this point. If you do a `@reload` the merger will
-be gone. You *could* add the `permanent=True` keyword to the `cmdset.add` call. This will however
-only make the new merged cmdset permanent on that *single* object. Often you want *all* objects of
-this particular class to have this cmdset.
-
-To make sure all new created objects get your new merged set, put the `cmdset.add` call in your
-custom [Typeclasses](./Typeclasses.md)' `at_object_creation` method:
-
-```python
- # e.g. in mygame/typeclasses/objects.py
-
- from evennia import DefaultObject
- class MyObject(DefaultObject):
-
- def at_object_creation(self):
- "called when the object is first created"
- self.cmdset.add("mycmdset.MyCmdSet", permanent=True)
-```
-
-All new objects of this typeclass will now start with this cmdset and it will survive a `@reload`.
-
-*Note:* An important caveat with this is that `at_object_creation` is only called *once*, when the
-object is first created. This means that if you already have existing objects in your databases
-using that typeclass, they will not have been initiated the same way. There are many ways to update
-them; since it's a one-time update you can usually just simply loop through them. As superuser, try
-the following:
-
- @py from typeclasses.objects import MyObject; [o.cmdset.add("mycmdset.MyCmdSet") for o in
-MyObject.objects.all()]
-
-This goes through all objects in your database having the right typeclass, adding the new cmdset to
-each. The good news is that you only have to do this if you want to post-add *cmdsets*. If you just
-want to add a new *command*, you can simply add that command to the cmdset's `at_cmdset_creation`
-and `@reload` to make the Command immediately available.
-
-## Change where Evennia looks for command sets
-
-Evennia uses settings variables to know where to look for its default command sets. These are
-normally not changed unless you want to re-organize your game folder in some way. For example, the
-default character cmdset defaults to being defined as
-
- CMDSET_CHARACTER="commands.default_cmdset.CharacterCmdSet"
-
-See `evennia/settings_default.py` for the other settings.
diff --git a/docs/0.9.5/_sources/Adding-Object-Typeclass-Tutorial.md.txt b/docs/0.9.5/_sources/Adding-Object-Typeclass-Tutorial.md.txt
deleted file mode 100644
index 4e4e0b66bb..0000000000
--- a/docs/0.9.5/_sources/Adding-Object-Typeclass-Tutorial.md.txt
+++ /dev/null
@@ -1,109 +0,0 @@
-# Adding Object Typeclass Tutorial
-
-Evennia comes with a few very basic classes of in-game entities:
-
- DefaultObject
- |
- DefaultCharacter
- DefaultRoom
- DefaultExit
- DefaultChannel
-
-When you create a new Evennia game (with for example `evennia --init mygame`) Evennia will
-automatically create empty child classes `Object`, `Character`, `Room` and `Exit` respectively. They
-are found `mygame/typeclasses/objects.py`, `mygame/typeclasses/rooms.py` etc.
-
-> Technically these are all [Typeclassed](./Typeclasses.md), which can be ignored for now. In
-> `mygame/typeclasses` are also base typeclasses for out-of-character things, notably
-> [Channels](./Communications.md), [Accounts](./Accounts.md) and [Scripts](./Scripts.md). We don't cover those in
-> this tutorial.
-
-For your own game you will most likely want to expand on these very simple beginnings. It's normal
-to want your Characters to have various attributes, for example. Maybe Rooms should hold extra
-information or even *all* Objects in your game should have properties not included in basic Evennia.
-
-## Change Default Rooms, Exits, Character Typeclass
-
-This is the simplest case.
-
-The default build commands of a new Evennia game is set up to use the `Room`, `Exit` and `Character`
-classes found in the same-named modules under `mygame/typeclasses/`. By default these are empty and
-just implements the default parents from the Evennia library (`DefaultRoom`etc). Just add the
-changes you want to these classes and run `@reload` to add your new functionality.
-
-## Create a new type of object
-
-Say you want to create a new "Heavy" object-type that characters should not have the ability to pick
-up.
-
-1. Edit `mygame/typeclasses/objects.py` (you could also create a new module there, named something
- like `heavy.py`, that's up to how you want to organize things).
-1. Create a new class inheriting at any distance from `DefaultObject`. It could look something like
- this:
-```python
- # end of file mygame/typeclasses/objects.py
- from evennia import DefaultObject
-
- class Heavy(DefaultObject):
- "Heavy object"
- def at_object_creation(self):
- "Called whenever a new object is created"
- # lock the object down by default
- self.locks.add("get:false()")
- # the default "get" command looks for this Attribute in order
- # to return a customized error message (we just happen to know
- # this, you'd have to look at the code of the 'get' command to
- # find out).
- self.db.get_err_msg = "This is too heavy to pick up."
-```
-1. Once you are done, log into the game with a build-capable account and do `@create/drop
- rock:objects.Heavy` to drop a new heavy "rock" object in your location. Next try to pick it up
-(`@quell` yourself first if you are a superuser). If you get errors, look at your log files where
-you will find the traceback. The most common error is that you have some sort of syntax error in
-your class.
-
-Note that the [Locks](./Locks.md) and [Attribute](./Attributes.md) which are set in the typeclass could just
-as well have been set using commands in-game, so this is a *very* simple example.
-
-## Storing data on initialization
-
-The `at_object_creation` is only called once, when the object is first created. This makes it ideal
-for database-bound things like [Attributes](./Attributes.md). But sometimes you want to create temporary
-properties (things that are not to be stored in the database but still always exist every time the
-object is created). Such properties can be initialized in the `at_init` method on the object.
-`at_init` is called every time the object is loaded into memory.
-
-> Note: It's usually pointless and wasteful to assign database data in `at_init`, since this will
-> hit the database with the same value over and over. Put those in `at_object_creation` instead.
-
-You are wise to use `ndb` (non-database Attributes) to store these non-persistent properties, since
-ndb-properties are protected against being cached out in various ways and also allows you to list
-them using various in-game tools:
-
-```python
-def at_init(self):
- self.ndb.counter = 0
- self.ndb.mylist = []
-```
-
-> Note: As mentioned in the [Typeclasses](./Typeclasses.md) documentation, `at_init` replaces the use of
-> the standard `__init__` method of typeclasses due to how the latter may be called in situations
-> other than you'd expect. So use `at_init` where you would normally use `__init__`.
-
-
-## Updating existing objects
-
-If you already have some `Heavy` objects created and you add a new `Attribute` in
-`at_object_creation`, you will find that those existing objects will not have this Attribute. This
-is not so strange, since `at_object_creation` is only called once, it will not be called again just
-because you update it. You need to update existing objects manually.
-
-If the number of objects is limited, you can use `@typeclass/force/reload objectname` to force a
-re-load of the `at_object_creation` method (only) on the object. This case is common enough that
-there is an alias `@update objectname` you can use to get the same effect. If there are multiple
-objects you can use `@py` to loop over the objects you need:
-
-```
-@py from typeclasses.objects import Heavy; [obj.at_object_creation() for obj in Heavy.objects.all()]
-
-```
diff --git a/docs/0.9.5/_sources/Administrative-Docs.md.txt b/docs/0.9.5/_sources/Administrative-Docs.md.txt
deleted file mode 100644
index 154fbd83ec..0000000000
--- a/docs/0.9.5/_sources/Administrative-Docs.md.txt
+++ /dev/null
@@ -1,76 +0,0 @@
-# Administrative Docs
-
-The following pages are aimed at game administrators -- the higher-ups that possess shell access and
-are responsible for managing the game.
-
-### Installation and Early Life
-
-- [Choosing (and installing) an SQL Server](./Choosing-An-SQL-Server.md)
-- [Getting Started - Installing Evennia](./Getting-Started.md)
-- [Running Evennia in Docker Containers](./Running-Evennia-in-Docker.md)
-- [Starting, stopping, reloading and resetting Evennia](./Start-Stop-Reload.md)
-- [Keeping your game up to date](./Updating-Your-Game.md)
- - [Resetting your database](./Updating-Your-Game.md#resetting-your-database)
-- [Making your game available online](./Online-Setup.md)
- - [Hosting options](./Online-Setup.md#hosting-options)
- - [Securing your server with SSL/Let's Encrypt](./Online-Setup.md#ssl)
-- [Listing your game](./Evennia-Game-Index.md) at the online [Evennia game
-index](http://games.evennia.com)
-
-### Customizing the server
-
-- [Changing the Settings](./Server-Conf.md#settings-file)
- - [Available Master
-Settings](https://github.com/evennia/evennia/blob/master/evennia/settings_default.py)
-- [Change Evennia's language](./Internationalization.md) (internationalization)
-- [Apache webserver configuration](./Apache-Config.md) (optional)
-- [Changing text encodings used by the server](./Text-Encodings.md)
-- [The Connection Screen](./Connection-Screen.md)
-- [Guest Logins](./Guest-Logins.md)
-- [How to connect Evennia to IRC channels](./IRC.md)
-- [How to connect Evennia to RSS feeds](./RSS.md)
-- [How to connect Evennia to Grapevine](./Grapevine.md)
-- [How to connect Evennia to Twitter](./How-to-connect-Evennia-to-Twitter.md)
-
-### Administrating the running game
-
-- [Supported clients](./Client-Support-Grid.md) (grid of known client issues)
-- [Changing Permissions](./Building-Permissions.md) of users
-- [Banning](./Banning.md) and deleting users
- - [Summary of abuse-handling tools](./Banning.md#summary-of-abuse-handling-tools) in the default cmdset
-
-### Working with Evennia
-
-- [Setting up your work environment with version control](./Version-Control.md)
-- [First steps coding with Evennia](./First-Steps-Coding.md)
-- [Setting up a continuous integration build environment](./Continuous-Integration.md)
-
-
-```{toctree}
- :hidden:
-
- Choosing-An-SQL-Server
- Getting-Started
- Running-Evennia-in-Docker
- Start-Stop-Reload
- Updating-Your-Game
- Online-Setup
- Evennia-Game-Index
- Server-Conf
- Internationalization
- Apache-Config
- Text-Encodings
- Connection-Screen
- Guest-Logins
- IRC
- RSS
- Grapevine
- How-to-connect-Evennia-to-Twitter
- Client-Support-Grid
- Building-Permissions
- Banning
- Version-Control
- First-Steps-Coding
- Continuous-Integration
-
-```
\ No newline at end of file
diff --git a/docs/0.9.5/_sources/Apache-Config.md.txt b/docs/0.9.5/_sources/Apache-Config.md.txt
deleted file mode 100644
index 76a21ee24f..0000000000
--- a/docs/0.9.5/_sources/Apache-Config.md.txt
+++ /dev/null
@@ -1,171 +0,0 @@
-# Apache Config
-
-
-**Warning**: This information is presented as a convenience, using another webserver than Evennia's
-own is not directly supported and you are on your own if you want to do so. Evennia's webserver
-works out of the box without any extra configuration and also runs in-process making sure to avoid
-caching race conditions. The browser web client will most likely not work (at least not without
-tweaking) on a third-party web server.
-
-One reason for wanting to use an external webserver like Apache would be to act as a *proxy* in
-front of the Evennia webserver. Getting this working with TLS (encryption) requires some extra work
-covered at the end of this page.
-
-Note that the Apache instructions below might be outdated. If something is not working right, or you
-use Evennia with a different server, please let us know. Also, if there is a particular Linux distro
-you would like covered, please let us know.
-
-## `mod_wsgi` Setup
-
-### Install `mod_wsgi`
-
-- *Fedora/RHEL* - Apache HTTP Server and `mod_wsgi` are available in the standard package
-repositories for Fedora and RHEL:
- ```
- $ dnf install httpd mod_wsgi
- or
- $ yum install httpd mod_wsgi
- ```
-- *Ubuntu/Debian* - Apache HTTP Server and `mod_wsgi` are available in the standard package
-repositories for Ubuntu and Debian:
- ```
- $ apt-get update
- $ apt-get install apache2 libapache2-mod-wsgi
- ```
-
-### Copy and modify the VHOST
-
-After `mod_wsgi` is installed, copy the `evennia/web/utils/evennia_wsgi_apache.conf` file to your
-apache2 vhosts/sites folder. On Debian/Ubuntu, this is `/etc/apache2/sites-enabled/`. Make your
-modifications **after** copying the file there.
-
-Read the comments and change the paths to point to the appropriate locations within your setup.
-
-### Restart/Reload Apache
-
-You'll then want to reload or restart apache2 after changing the configurations.
-
-- *Fedora/RHEL/Ubuntu*
- ```
- $ systemctl restart httpd
- ```
-- *Ubuntu/Debian*
- ```
- $ systemctl restart apache2
- ```
-
-### Enjoy
-
-With any luck, you'll be able to point your browser at your domain or subdomain that you set up in
-your vhost and see the nifty default Evennia webpage. If not, read the hopefully informative error
-message and work from there. Questions may be directed to our [Evennia Community
-site](http://evennia.com).
-
-### A note on code reloading
-
-If your `mod_wsgi` is set up to run on daemon mode (as will be the case by default on Debian and
-Ubuntu), you may tell `mod_wsgi` to reload by using the `touch` command on
-`evennia/game/web/utils/apache_wsgi.conf`. When `mod_wsgi` sees that the file modification time has
-changed, it will force a code reload. Any modifications to the code will not be propagated to the
-live instance of your site until reloaded.
-
-If you are not running in daemon mode or want to force the issue, simply restart or reload apache2
-to apply your changes.
-
-### Further notes and hints:
-
-If you get strange (and usually uninformative) `Permission denied` errors from Apache, make sure
-that your `evennia` directory is located in a place the webserver may actually access. For example,
-some Linux distributions may default to very restrictive access permissions on a user's `/home`
-directory.
-
-One user commented that they had to add the following to their Apache config to get things to work.
-Not confirmed, but worth trying if there are trouble.
-
- /evennia/game/web">
- Options +ExecCGI
- Allow from all
-
-
-## `mod_proxy` and `mod_ssl` setup
-
-Below are steps on running Evennia using a front-end proxy (Apache HTTP), `mod_proxy_http`,
-`mod_proxy_wstunnel`, and `mod_ssl`. `mod_proxy_http` and `mod_proxy_wstunnel` will simply be
-referred to as
-`mod_proxy` below.
-
-### Install `mod_ssl`
-
-- *Fedora/RHEL* - Apache HTTP Server and `mod_ssl` are available in the standard package
-repositories for Fedora and RHEL:
- ```
- $ dnf install httpd mod_ssl
- or
- $ yum install httpd mod_ssl
-
- ```
-- *Ubuntu/Debian* - Apache HTTP Server and `mod_sslj`kl are installed together in the `apache2`
-package and available in the
-standard package repositories for Ubuntu and Debian. `mod_ssl` needs to be enabled after
-installation:
- ```
- $ apt-get update
- $ apt-get install apache2
- $ a2enmod ssl
-
- ```
-
-### TLS proxy+websocket configuration
-
-Below is a sample configuration for Evennia with a TLS-enabled http and websocket proxy.
-
-#### Apache HTTP Server Configuration
-
-```
-
- # Always redirect to https/443
- ServerName mud.example.com
- Redirect / https://mud.example.com
-
-
-
- ServerName mud.example.com
-
- SSLEngine On
-
- # Location of certificate and key
- SSLCertificateFile /etc/pki/tls/certs/mud.example.com.crt
- SSLCertificateKeyFile /etc/pki/tls/private/mud.example.com.key
-
- # Use a tool https://www.ssllabs.com/ssltest/ to scan your set after setting up.
- SSLProtocol TLSv1.2
- SSLCipherSuite HIGH:!eNULL:!NULL:!aNULL
-
- # Proxy all websocket traffic to port 4002 in Evennia
- ProxyPass /ws ws://127.0.0.1:4002/
- ProxyPassReverse /ws ws://127.0.0.1:4002/
-
- # Proxy all HTTP traffic to port 4001 in Evennia
- ProxyPass / http://127.0.0.1:4001/
- ProxyPassReverse / http://127.0.0.1:4001/
-
- # Configure separate logging for this Evennia proxy
- ErrorLog logs/evennia_error.log
- CustomLog logs/evennia_access.log combined
-
-```
-
-#### Evennia secure websocket configuration
-
-There is a slight trick in setting up Evennia so websocket traffic is handled correctly by the
-proxy. You must set the `WEBSOCKET_CLIENT_URL` setting in your `mymud/server/conf/settings.py` file:
-
-```
-WEBSOCKET_CLIENT_URL = "wss://external.example.com/ws"
-```
-
-The setting above is what the client's browser will actually use. Note the use of `wss://` is
-because our client will be communicating over an encrypted connection ("wss" indicates websocket
-over SSL/TLS). Also, especially note the additional path `/ws` at the end of the URL. This is how
-Apache HTTP Server identifies that a particular request should be proxied to Evennia's websocket
-port but this should be applicable also to other types of proxies (like nginx).
diff --git a/docs/0.9.5/_sources/Arxcode-installing-help.md.txt b/docs/0.9.5/_sources/Arxcode-installing-help.md.txt
deleted file mode 100644
index e7efdfae28..0000000000
--- a/docs/0.9.5/_sources/Arxcode-installing-help.md.txt
+++ /dev/null
@@ -1,272 +0,0 @@
-# Arxcode installing help
-
-## Introduction
-
-[Arx - After the Reckoning](http://play.arxmush.org/) is a big and very popular
-[Evennia](http://www.evennia.com)-based game. Arx is heavily roleplaying-centric, relying on game
-masters to drive the story. Technically it's maybe best described as "a MUSH, but with more coded
-systems". In August of 2018, the game's developer, Tehom, generously released the [source code of
-Arx on github](https://github.com/Arx-Game/arxcode). This is a treasure-trove for developers wanting
-to pick ideas or even get a starting game to build on. These instructions are based on the Arx-code
-released as of *Aug 12, 2018*.
-
-If you are not familiar with what Evennia is, you can read
-[an introduction here](./Evennia-Introduction.md).
-
-It's not too hard to run Arx from the sources (of course you'll start with an empty database) but
-since part of Arx has grown organically, it doesn't follow standard Evennia paradigms everywhere.
-This page covers one take on installing and setting things up while making your new Arx-based game
-better match with the vanilla Evennia install.
-
-## Installing Evennia
-
-Firstly, set aside a folder/directory on your drive for everything to follow.
-
-You need to start by installing [Evennia](http://www.evennia.com) by following most of the [Getting
-Started
-Instructions](./Getting-Started.md) for your OS. The difference is that you need to `git clone
-https://github.com/TehomCD/evennia.git` instead of Evennia's repo because Arx uses TehomCD's older
-Evennia 0.8 [fork](https://github.com/TehomCD/evennia), notably still using Python2. This detail is
-important if referring to newer Evennia documentation.
-
-If you are new to Evennia it's *highly* recommended that you run through the
-instructions in full - including initializing and starting a new empty game and connecting to it.
-That way you can be sure Evennia works correctly as a base line. If you have trouble, make sure to
-read the [Troubleshooting instructions](./Getting-Started.md#troubleshooting) for your
-operating system. You can also drop into our
-[forums](https://groups.google.com/forum/#%21forum/evennia), join `#evennia` on `irc.freenode.net`
-or chat from the linked [Discord Server](https://discord.gg/NecFePw).
-
-After installing you should have a `virtualenv` running and you should have the following file
-structure in your set-aside folder:
-
-```
-vienv/
-evennia/
-mygame/
-
-```
-
-Here `mygame` is the empty game you created during the Evennia install, with `evennia --init`. Go to
-that and run `evennia stop` to make sure your empty game is not running. We'll instead let Evenna
-run Arx, so in principle you could erase `mygame` - but it could also be good to have a clean game
-to compare to.
-
-## Installing Arxcode
-
-### Clone the arxcode repo
-
-Cd to the root of your directory and clone the released source code from github:
-
- git clone https://github.com/Arx-Game/arxcode.git myarx
-
-A new folder `myarx` should appear next to the ones you already had. You could rename this to
-something else if you want.
-
-Cd into `myarx`. If you wonder about the structure of the game dir, you can [read more about it
-here](./Directory-Overview.md).
-
-### Clean up settings
-
-Arx has split evennia's normal settings into `base_settings.py` and `production_settings.py`. It
-also has its own solution for managing 'secret' parts of the settings file. We'll keep most of Arx
-way but remove the secret-handling and replace it with the normal Evennia method.
-
-Cd into `myarx/server/conf/` and open the file `settings.py` in a text editor. The top part (within
-`"""..."""`) is just help text. Wipe everything underneath that and make it look like this instead
-(don't forget to save):
-
-```
-from base_settings import *
-
-TELNET_PORTS = [4000]
-SERVERNAME = "MyArx"
-GAME_SLOGAN = "The cool game"
-
-try:
- from server.conf.secret_settings import *
-except ImportError:
- print("secret_settings.py file not found or failed to import.")
-```
-
-> Note: Indents and capitalization matter in Python. Make indents 4 spaces (not tabs) for your own
-> sanity. If you want a starter on Python in Evennia, [you can look here](Python-basic-
-introduction).
-
-This will import Arx' base settings and override them with the Evennia-default telnet port and give
-the game a name. The slogan changes the sub-text shown under the name of your game in the website
-header. You can tweak these to your own liking later.
-
-Next, create a new, empty file `secret_settings.py` in the same location as the `settings.py` file.
-This can just contain the following:
-
-```python
-SECRET_KEY = "sefsefiwwj3 jnwidufhjw4545_oifej whewiu hwejfpoiwjrpw09&4er43233fwefwfw"
-
-```
-
-Replace the long random string with random ASCII characters of your own. The secret key should not
-be shared.
-
-Next, open `myarx/server/conf/base_settings.py` in your text editor. We want to remove/comment out
-all mentions of the `decouple` package, which Evennia doesn't use (we use `private_settings.py` to
-hide away settings that should not be shared).
-
-Comment out `from decouple import config` by adding a `#` to the start of the line: `# from decouple
-import config`. Then search for `config(` in the file and comment out all lines where this is used.
-Many of these are specific to the server environment where the original Arx runs, so is not that
-relevant to us.
-
-### Install Arx dependencies
-
-Arx has some further dependencies beyond vanilla Evennia. Start by `cd`:ing to the root of your
-`myarx` folder.
-
-> If you run *Linux* or *Mac*: Edit `myarx/requirements.txt` and comment out the line
-> `pypiwin32==219` - it's only needed on Windows and will give an error on other platforms.
-
-Make sure your `virtualenv` is active, then run
-
- pip install -r requirements.txt
-
-The needed Python packages will be installed for you.
-
-### Adding logs/ folder
-
-The Arx repo does not contain the `myarx/server/logs/` folder Evennia expects for storing server
-logs. This is simple to add:
-
- # linux/mac
- mkdir server/logs
- # windows
- mkdir server\logs
-
-### Setting up the database and starting
-
-From the `myarx` folder, run
-
- evennia migrate
-
-This creates the database and will step through all database migrations needed.
-
- evennia start
-
-If all goes well Evennia will now start up, running Arx! You can connect to it on `localhost` (or
-`127.0.0.1` if your platform doesn't alias `localhost`), port `4000` using a Telnet client.
-Alternatively, you can use your web browser to browse to `http://localhost:4001` to see the game's
-website and get to the web client.
-
-When you log in you'll get the standard Evennia greeting (since the database is empty), but you can
-try `help` to see that it's indeed Arx that is running.
-
-### Additional Setup Steps
-
-The first time you start Evennia after creating the database with the `evennia migrate` step above,
-it should create a few starting objects for you - your superuser account, which it will prompt you
-to enter, a starting room (Limbo), and a character object for you. If for some reason this does not
-occur, you may have to follow the steps below. For the first time Superuser login you may have to
-run steps 7-8 and 10 to create and connect to your in-came Character.
-
-1. Login to the game website with your Superuser account.
-2. Press the `Admin` button to get into the (Django-) Admin Interface.
-3. Navigate to the `Accounts` section.
-4. Add a new Account named for the new staffer. Use a place holder password and dummy e-mail
- address.
-5. Flag account as `Staff` and apply the `Admin` permission group (This assumes you have already set
- up an Admin Group in Django).
-6. Add Tags named `player` and `developer`.
-7. Log into the game using the web client (or a third-party telnet client) using your superuser
- account. Move to where you want the new staffer character to appear.
-8. In the game client, run `@create/drop :typeclasses.characters.Character`, where
- `` is usually the same name you used for the Staffer account you created in the
- Admin earlier (if you are creating a Character for your superuser, use your superuser account
-name).
- This creates a new in-game Character and places it in your current location.
-9. Have the new Admin player log into the game.
-10. Have the new Admin puppet the character with `@ic StafferName`.
-11. Have the new Admin change their password - `@password = `.
-
-Now that you have a Character and an Account object, there's a few additional things you may need to
-do in order for some commands to function properly. You can either execute these as in-game commands
-while `@ic` (controlling your character object).
-
-1. `@py from web.character.models import RosterEntry;RosterEntry.objects.create(player=self.player,
-character=self)`
-2. `@py from world.dominion.models import PlayerOrNpc, AssetOwner;dompc =
-PlayerOrNpc.objects.create(player = self.player);AssetOwner.objects.create(player=dompc)`
-
-Those steps will give you a 'RosterEntry', 'PlayerOrNpc', and 'AssetOwner' objects. RosterEntry
-explicitly connects a character and account object together, even while offline, and contains
-additional information about a character's current presence in game (such as which 'roster' they're
-in, if you choose to use an active roster of characters). PlayerOrNpc are more character extensions,
-as well as support for npcs with no in-game presence and just represented by a name which can be
-offscreen members of a character's family. It also allows for membership in Organizations.
-AssetOwner holds information about a character or organization's money and resources.
-
-## Alternate guide by Pax for installing on Windows
-
-If for some reason you cannot use the Windows Subsystem for Linux (which would use instructions
-identical to the ones above), it's possible to get Evennia running under Anaconda for Windows. The
-process is a little bit trickier.
-
- Make sure you have:
- * Git for Windows https://git-scm.com/download/win
- * Anaconda for Windows https://www.anaconda.com/distribution/
- * VC++ Compiler for Python 2.7 http://aka.ms/vcpython27
-
-conda update conda
-conda create -n arx python=2.7
-source activate arx
-
- Set up a convenient repository place for things.
-
-cd ~
-mkdir Source
-cd Source
-mkdir Arx
-cd Arx
-
- Replace the SSH git clone links below with your own github forks.
- If you don't plan to change Evennia at all, you can use the
- evennia/evennia.git repo instead of a forked one.
-
-git clone git@github.com:/evennia.git
-git clone git@github.com:/arxcode.git
-
- Evennia is a package itself, so we want to install it and all of its
- prerequisites, after switching to the appropriately-tagged branch for
- Arxcode.
-
-cd evennia
-git checkout tags/v0.7 -b arx-master
-pip install -e .
-
- Arx has some dependencies of its own, so now we'll go install them
- As it is not a package, we'll use the normal requirements file.
-
-cd ../arxcode
-pip install -r requirements.txt
-
- The git repo doesn't include the empty log directory and Evennia is unhappy if you
- don't have it, so while still in the arxcode directory...
-
-mkdir server/logs
-
- Now hit https://github.com/evennia/evennia/wiki/Arxcode-installing-help and
- change the setup stuff as in the 'Clean up settings' section.
-
- Then we will create our default database...
-
-../evennia/bin/windows/evennia.bat migrate
-
- ...and do the first run. You need winpty because Windows does not have a TTY/PTY
- by default, and so the Python console input commands (used for prompts on first
- run) will fail and you will end up in an unhappy place. Future runs, you should
- not need winpty.
-
-winpty ../evennia/bin/windows/evennia.bat start
-
- Once this is done, you should have your Evennia server running Arxcode up
- on localhost at port 4000, and the webserver at http://localhost:4001/
-
- And you are done! Huzzah!
\ No newline at end of file
diff --git a/docs/0.9.5/_sources/Async-Process.md.txt b/docs/0.9.5/_sources/Async-Process.md.txt
deleted file mode 100644
index e44e9330a2..0000000000
--- a/docs/0.9.5/_sources/Async-Process.md.txt
+++ /dev/null
@@ -1,233 +0,0 @@
-# Async Process
-
-
-*This is considered an advanced topic.*
-
-## Synchronous versus Asynchronous
-
-Most program code operates *synchronously*. This means that each statement in your code gets
-processed and finishes before the next can begin. This makes for easy-to-understand code. It is also
-a *requirement* in many cases - a subsequent piece of code often depend on something calculated or
-defined in a previous statement.
-
-Consider this piece of code in a traditional Python program:
-
-```python
- print("before call ...")
- long_running_function()
- print("after call ...")
-
-```
-
-When run, this will print `"before call ..."`, after which the `long_running_function` gets to work
-for however long time. Only once that is done, the system prints `"after call ..."`. Easy and
-logical to follow. Most of Evennia work in this way and often it's important that commands get
-executed in the same strict order they were coded.
-
-Evennia, via Twisted, is a single-process multi-user server. In simple terms this means that it
-swiftly switches between dealing with player input so quickly that each player feels like they do
-things at the same time. This is a clever illusion however: If one user, say, runs a command
-containing that `long_running_function`, *all* other players are effectively forced to wait until it
-finishes.
-
-Now, it should be said that on a modern computer system this is rarely an issue. Very few commands
-run so long that other users notice it. And as mentioned, most of the time you *want* to enforce
-all commands to occur in strict sequence.
-
-When delays do become noticeable and you don't care in which order the command actually completes,
-you can run it *asynchronously*. This makes use of the `run_async()` function in
-`src/utils/utils.py`:
-
-```python
- run_async(function, *args, **kwargs)
-```
-
-Where `function` will be called asynchronously with `*args` and `**kwargs`. Example:
-
-```python
- from evennia import utils
- print("before call ...")
- utils.run_async(long_running_function)
- print("after call ...")
-```
-
-Now, when running this you will find that the program will not wait around for
-`long_running_function` to finish. In fact you will see `"before call ..."` and `"after call ..."`
-printed out right away. The long-running function will run in the background and you (and other
-users) can go on as normal.
-
-## Customizing asynchronous operation
-
-A complication with using asynchronous calls is what to do with the result from that call. What if
-`long_running_function` returns a value that you need? It makes no real sense to put any lines of
-code after the call to try to deal with the result from `long_running_function` above - as we saw
-the `"after call ..."` got printed long before `long_running_function` was finished, making that
-line quite pointless for processing any data from the function. Instead one has to use *callbacks*.
-
-`utils.run_async` takes reserved kwargs that won't be passed into the long-running function:
-
-- `at_return(r)` (the *callback*) is called when the asynchronous function (`long_running_function`
- above) finishes successfully. The argument `r` will then be the return value of that function (or
- `None`).
-
- ```python
- def at_return(r):
- print(r)
- ```
-
-- `at_return_kwargs` - an optional dictionary that will be fed as keyword arguments to the
-`at_return` callback.
-- `at_err(e)` (the *errback*) is called if the asynchronous function fails and raises an exception.
- This exception is passed to the errback wrapped in a *Failure* object `e`. If you do not supply an
- errback of your own, Evennia will automatically add one that silently writes errors to the evennia
- log. An example of an errback is found below:
-
-```python
- def at_err(e):
- print("There was an error:", str(e))
-```
-
-- `at_err_kwargs` - an optional dictionary that will be fed as keyword arguments to the `at_err`
- errback.
-
-An example of making an asynchronous call from inside a [Command](./Commands.md) definition:
-
-```python
- from evennia import utils, Command
-
- class CmdAsync(Command):
-
- key = "asynccommand"
-
- def func(self):
-
- def long_running_function():
- #[... lots of time-consuming code ...]
- return final_value
-
- def at_return_function(r):
- self.caller.msg("The final value is %s" % r)
-
- def at_err_function(e):
- self.caller.msg("There was an error: %s" % e)
-
- # do the async call, setting all callbacks
- utils.run_async(long_running_function, at_return=at_return_function,
-at_err=at_err_function)
-```
-
-That's it - from here on we can forget about `long_running_function` and go on with what else need
-to be done. *Whenever* it finishes, the `at_return_function` function will be called and the final
-value will
-pop up for us to see. If not we will see an error message.
-
-## delay
-
-The `delay` function is a much simpler sibling to `run_async`. It is in fact just a way to delay the
-execution of a command until a future time. This is equivalent to something like `time.sleep()`
-except delay is asynchronous while `sleep` would lock the entire server for the duration of the
-sleep.
-
-```python
- from evennia.utils import delay
-
- # [...]
- # e.g. inside a Command, where `self.caller` is available
- def callback(obj):
- obj.msg("Returning!")
- delay(10, callback, self.caller)
-```
-
-This will delay the execution of the callback for 10 seconds. This function is explored much more in
-the [Command Duration Tutorial](./Command-Duration.md).
-
-You can also try the following snippet just see how it works:
-
- @py from evennia.utils import delay; delay(10, lambda who: who.msg("Test!"), self)
-
-Wait 10 seconds and 'Test!' should be echoed back to you.
-
-
-## The @interactive decorator
-
-As of Evennia 0.9, the `@interactive` [decorator](https://realpython.com/primer-on-python-decorators/)
-is available. This makes any function or method possible to 'pause' and/or await player input
-in an interactive way.
-
-```python
- from evennia.utils import interactive
-
- @interactive
- def myfunc(caller):
-
- while True:
- caller.msg("Getting ready to wait ...")
- yield(5)
- caller.msg("Now 5 seconds have passed.")
-
- response = yield("Do you want to wait another 5 secs?")
-
- if response.lower() not in ("yes", "y"):
- break
-```
-
-The `@interactive` decorator gives the function the ability to pause. The use
-of `yield(seconds)` will do just that - it will asynchronously pause for the
-number of seconds given before continuing. This is technically equivalent to
-using `call_async` with a callback that continues after 5 secs. But the code
-with `@interactive` is a little easier to follow.
-
-Within the `@interactive` function, the `response = yield("question")` question
-allows you to ask the user for input. You can then process the input, just like
-you would if you used the Python `input` function. There is one caveat to this
-functionality though - _it will only work if the function/method has an
-argument named exactly `caller`_. This is because internally Evennia will look
-for the `caller` argument and treat that as the source of input.
-
-All of this makes the `@interactive` decorator very useful. But it comes with a
-few caveats. Notably, decorating a function/method with `@interactive` turns it
-into a Python [generator](https://wiki.python.org/moin/Generators). The most
-common issue is that you cannot use `return ` from a generator (just an
-empty `return` works). To return a value from a function/method you have decorated
-with `@interactive`, you must instead use a special Twisted function
-`twisted.internet.defer.returnValue`. Evennia also makes this function
-conveniently available from `evennia.utils`:
-
-```python
- from evennia.utils import interactive, returnValue
-
- @interactive
- def myfunc():
-
- # ...
- result = 10
-
- # this must be used instead of `return result`
- returnValue(result)
-
-```
-
-
-
-## Assorted notes
-
-Overall, be careful with choosing when to use asynchronous calls. It is mainly useful for large
-administration operations that have no direct influence on the game world (imports and backup
-operations come to mind). Since there is no telling exactly when an asynchronous call actually ends,
-using them for in-game commands is to potentially invite confusion and inconsistencies (and very
-hard-to-reproduce bugs).
-
-The very first synchronous example above is not *really* correct in the case of Twisted, which is
-inherently an asynchronous server. Notably you might find that you will *not* see the first `before
-call ...` text being printed out right away. Instead all texts could end up being delayed until
-after the long-running process finishes. So all commands will retain their relative order as
-expected, but they may appear with delays or in groups.
-
-## Further reading
-
-Technically, `run_async` is just a very thin and simplified wrapper around a
-[Twisted Deferred](http://twistedmatrix.com/documents/9.0.0/core/howto/defer.html) object; the
-wrapper sets
-up a default errback also if none is supplied. If you know what you are doing there is nothing
-stopping you from bypassing the utility function, building a more sophisticated callback chain after
-your own liking.
diff --git a/docs/0.9.5/_sources/Attributes.md.txt b/docs/0.9.5/_sources/Attributes.md.txt
deleted file mode 100644
index b04d18f654..0000000000
--- a/docs/0.9.5/_sources/Attributes.md.txt
+++ /dev/null
@@ -1,393 +0,0 @@
-# Attributes
-
-
-When performing actions in Evennia it is often important that you store data for later. If you write
-a menu system, you have to keep track of the current location in the menu tree so that the player
-can give correct subsequent commands. If you are writing a combat system, you might have a
-combattant's next roll get easier dependent on if their opponent failed. Your characters will
-probably need to store roleplaying-attributes like strength and agility. And so on.
-
-[Typeclassed](./Typeclasses.md) game entities ([Accounts](./Accounts.md), [Objects](./Objects.md),
-[Scripts](./Scripts.md) and [Channels](./Communications.md)) always have *Attributes* associated with them.
-Attributes are used to store any type of data 'on' such entities. This is different from storing
-data in properties already defined on entities (such as `key` or `location`) - these have very
-specific names and require very specific types of data (for example you couldn't assign a python
-*list* to the `key` property no matter how hard you tried). `Attributes` come into play when you
-want to assign arbitrary data to arbitrary names.
-
-**Attributes are _not_ secure by default and any player may be able to change them unless you
-[prevent this behavior](./Attributes.md#locking-and-checking-attributes).**
-
-## The .db and .ndb shortcuts
-
-To save persistent data on a Typeclassed object you normally use the `db` (DataBase) operator. Let's
-try to save some data to a *Rose* (an [Object](./Objects.md)):
-
-```python
- # saving
- rose.db.has_thorns = True
- # getting it back
- is_ouch = rose.db.has_thorns
-
-```
-
-This looks like any normal Python assignment, but that `db` makes sure that an *Attribute* is
-created behind the scenes and is stored in the database. Your rose will continue to have thorns
-throughout the life of the server now, until you deliberately remove them.
-
-To be sure to save **non-persistently**, i.e. to make sure NOT to create a database entry, you use
-`ndb` (NonDataBase). It works in the same way:
-
-```python
- # saving
- rose.ndb.has_thorns = True
- # getting it back
- is_ouch = rose.ndb.has_thorns
-```
-
-Technically, `ndb` has nothing to do with `Attributes`, despite how similar they look. No
-`Attribute` object is created behind the scenes when using `ndb`. In fact the database is not
-invoked at all since we are not interested in persistence. There is however an important reason to
-use `ndb` to store data rather than to just store variables direct on entities - `ndb`-stored data
-is tracked by the server and will not be purged in various cache-cleanup operations Evennia may do
-while it runs. Data stored on `ndb` (as well as `db`) will also be easily listed by example the
-`@examine` command.
-
-You can also `del` properties on `db` and `ndb` as normal. This will for example delete an
-`Attribute`:
-
-```python
- del rose.db.has_thorns
-```
-
-Both `db` and `ndb` defaults to offering an `all` property on themselves. This returns all
-associated attributes or non-persistent properties.
-
-```python
- list_of_all_rose_attributes = rose.db.all
- list_of_all_rose_ndb_attrs = rose.ndb.all
-```
-
-If you use `all` as the name of an attribute, this will be used instead. Later deleting your custom
-`all` will return the default behaviour.
-
-## The AttributeHandler
-
-The `.db` and `.ndb` properties are very convenient but if you don't know the name of the Attribute
-beforehand they cannot be used. Behind the scenes `.db` actually accesses the `AttributeHandler`
-which sits on typeclassed entities as the `.attributes` property. `.ndb` does the same for the
-`.nattributes` property.
-
-The handlers have normal access methods that allow you to manage and retrieve `Attributes` and
-`NAttributes`:
-
-- `has('attrname')` - this checks if the object has an Attribute with this key. This is equivalent
- to doing `obj.db.attrname`.
-- `get(...)` - this retrieves the given Attribute. Normally the `value` property of the Attribute is
- returned, but the method takes keywords for returning the Attribute object itself. By supplying an
- `accessing_object` to the call one can also make sure to check permissions before modifying
- anything.
-- `add(...)` - this adds a new Attribute to the object. An optional [lockstring](./Locks.md) can be
- supplied here to restrict future access and also the call itself may be checked against locks.
-- `remove(...)` - Remove the given Attribute. This can optionally be made to check for permission
- before performing the deletion. - `clear(...)` - removes all Attributes from object.
-- `all(...)` - returns all Attributes (of the given category) attached to this object.
-
-See [this section](./Attributes.md#locking-and-checking-attributes) for more about locking down Attribute
-access and editing. The `Nattribute` offers no concept of access control.
-
-Some examples:
-
-```python
- import evennia
- obj = evennia.search_object("MyObject")
-
- obj.attributes.add("test", "testvalue")
- print(obj.db.test) # prints "testvalue"
- print(obj.attributes.get("test")) # "
- print(obj.attributes.all()) # prints []
- obj.attributes.remove("test")
-```
-
-
-## Properties of Attributes
-
-An Attribute object is stored in the database. It has the following properties:
-
-- `key` - the name of the Attribute. When doing e.g. `obj.db.attrname = value`, this property is set
- to `attrname`.
-- `value` - this is the value of the Attribute. This value can be anything which can be pickled -
- objects, lists, numbers or what have you (see
- [this section](./Attributes.md#what-types-of-data-can-i-save-in-an-attribute) for more info). In the
-example
- `obj.db.attrname = value`, the `value` is stored here.
-- `category` - this is an optional property that is set to None for most Attributes. Setting this
- allows to use Attributes for different functionality. This is usually not needed unless you want
- to use Attributes for very different functionality ([Nicks](./Nicks.md) is an example of using
-Attributes
- in this way). To modify this property you need to use the [Attribute
-Handler](#the-attributehandler).
-- `strvalue` - this is a separate value field that only accepts strings. This severely limits the
- data possible to store, but allows for easier database lookups. This property is usually not used
- except when re-using Attributes for some other purpose ([Nicks](./Nicks.md) use it). It is only
- accessible via the [Attribute Handler](./Attributes.md#the-attributehandler).
-
-There are also two special properties:
-
-- `attrtype` - this is used internally by Evennia to separate [Nicks](./Nicks.md), from Attributes (Nicks
- use Attributes behind the scenes).
-- `model` - this is a *natural-key* describing the model this Attribute is attached to. This is on
- the form *appname.modelclass*, like `objects.objectdb`. It is used by the Attribute and
- NickHandler to quickly sort matches in the database. Neither this nor `attrtype` should normally
- need to be modified.
-
-Non-database attributes have no equivalence to `category` nor `strvalue`, `attrtype` or `model`.
-
-## Persistent vs non-persistent
-
-So *persistent* data means that your data will survive a server reboot, whereas with
-*non-persistent* data it will not ...
-
-... So why would you ever want to use non-persistent data? The answer is, you don't have to. Most of
-the time you really want to save as much as you possibly can. Non-persistent data is potentially
-useful in a few situations though.
-
-- You are worried about database performance. Since Evennia caches Attributes very aggressively,
- this is not an issue unless you are reading *and* writing to your Attribute very often (like many
- times per second). Reading from an already cached Attribute is as fast as reading any Python
- property. But even then this is not likely something to worry about: Apart from Evennia's own
- caching, modern database systems themselves also cache data very efficiently for speed. Our
-default
- database even runs completely in RAM if possible, alleviating much of the need to write to disk
- during heavy loads.
-- A more valid reason for using non-persistent data is if you *want* to lose your state when logging
- off. Maybe you are storing throw-away data that are re-initialized at server startup. Maybe you
- are implementing some caching of your own. Or maybe you are testing a buggy [Script](./Scripts.md) that
- does potentially harmful stuff to your character object. With non-persistent storage you can be
-sure
- that whatever is messed up, it's nothing a server reboot can't clear up.
-- NAttributes have no restrictions at all on what they can store (see next section), since they
- don't need to worry about being saved to the database - they work very well for temporary storage.
-- You want to implement a fully or partly *non-persistent world*. Who are we to argue with your
- grand vision!
-
-## What types of data can I save in an Attribute?
-
-> None of the following affects NAttributes, which does not invoke the database at all. There are no
-> restrictions to what can be stored in a NAttribute.
-
-The database doesn't know anything about Python objects, so Evennia must *serialize* Attribute
-values into a string representation in order to store it to the database. This is done using the
-`pickle` module of Python (the only exception is if you use the `strattr` keyword of the
-AttributeHandler to save to the `strvalue` field of the Attribute. In that case you can only save
-*strings* which will not be pickled).
-
-It's important to note that when you access the data in an Attribute you are *always* de-serializing
-it from the database representation every time. This is because we allow for storing
-database-entities in Attributes too. If we cached it as its Python form, we might end up with
-situations where the database entity was deleted since we last accessed the Attribute.
-De-serializing data with a database-entity in it means querying the database for that object and
-making sure it still exists (otherwise it will be set to `None`). Performance-wise this is usually
-not a big deal. But if you are accessing the Attribute as part of some big loop or doing a large
-amount of reads/writes you should first extract it to a temporary variable, operate on *that* and
-then save the result back to the Attribute. If you are storing a more complex structure like a
-`dict` or a `list` you should make sure to "disconnect" it from the database before looping over it,
-as mentioned in the [Retrieving Mutable Objects](./Attributes.md#retrieving-mutable-objects) section
-below.
-
-### Storing single objects
-
-With a single object, we mean anything that is *not iterable*, like numbers, strings or custom class
-instances without the `__iter__` method.
-
-* You can generally store any non-iterable Python entity that can be
- [pickled](http://docs.python.org/library/pickle.html).
-* Single database objects/typeclasses can be stored as any other in the Attribute. These can
- normally *not* be pickled, but Evennia will behind the scenes convert them to an internal
- representation using their classname, database-id and creation-date with a microsecond precision,
- guaranteeing you get the same object back when you access the Attribute later.
-* If you *hide* a database object inside a non-iterable custom class (like stored as a variable
- inside it), Evennia will not know it's there and won't convert it safely. Storing classes with
- such hidden database objects is *not* supported and will lead to errors!
-
-```python
-# Examples of valid single-value attribute data:
-obj.db.test1 = 23
-obj.db.test1 = False
-# a database object (will be stored as an internal representation)
-obj.db.test2 = myobj
-
-# example of an invalid, "hidden" dbobject
-class Invalid(object):
- def __init__(self, dbobj):
- # no way for Evennia to know this is a dbobj
- self.dbobj = dbobj
-invalid = Invalid(myobj)
-obj.db.invalid = invalid # will cause error!
-```
-
-### Storing multiple objects
-
-This means storing objects in a collection of some kind and are examples of *iterables*, pickle-able
-entities you can loop over in a for-loop. Attribute-saving supports the following iterables:
-
-* [Tuples](https://docs.python.org/2/library/functions.html#tuple), like `(1,2,"test", )`.
-* [Lists](https://docs.python.org/2/tutorial/datastructures.html#more-on-lists), like `[1,2,"test",
-]`.
-* [Dicts](https://docs.python.org/2/tutorial/datastructures.html#dictionaries), like `{1:2,
-"test":]`.
-* [Sets](https://docs.python.org/2/tutorial/datastructures.html#sets), like `{1,2,"test",}`.
-* [collections.OrderedDict](https://docs.python.org/2/library/collections.html#collections.OrderedDict), like `OrderedDict((1,2), ("test", ))`.
-* [collections.Deque](https://docs.python.org/2/library/collections.html#collections.deque), like
-`deque((1,2,"test",))`.
-* *Nestings* of any combinations of the above, like lists in dicts or an OrderedDict of tuples, each
-containing dicts, etc.
-* All other iterables (i.e. entities with the `__iter__` method) will be converted to a *list*.
- Since you can use any combination of the above iterables, this is generally not much of a
- limitation.
-
-Any entity listed in the [Single object](./Attributes.md#storing-single-objects) section above can be
-stored in the iterable.
-
-> As mentioned in the previous section, database entities (aka typeclasses) are not possible to
-> pickle. So when storing an iterable, Evennia must recursively traverse the iterable *and all its
-> nested sub-iterables* in order to find eventual database objects to convert. This is a very fast
-> process but for efficiency you may want to avoid too deeply nested structures if you can.
-
-```python
-# examples of valid iterables to store
-obj.db.test3 = [obj1, 45, obj2, 67]
-# a dictionary
-obj.db.test4 = {'str':34, 'dex':56, 'agi':22, 'int':77}
-# a mixed dictionary/list
-obj.db.test5 = {'members': [obj1,obj2,obj3], 'enemies':[obj4,obj5]}
-# a tuple with a list in it
-obj.db.test6 = (1,3,4,8, ["test", "test2"], 9)
-# a set
-obj.db.test7 = set([1,2,3,4,5])
-# in-situ manipulation
-obj.db.test8 = [1,2,{"test":1}]
-obj.db.test8[0] = 4
-obj.db.test8[2]["test"] = 5
-# test8 is now [4,2,{"test":5}]
-```
-
-### Retrieving Mutable objects
-
-A side effect of the way Evennia stores Attributes is that *mutable* iterables (iterables that can
-be modified in-place after they were created, which is everything except tuples) are handled by
-custom objects called `_SaverList`, `_SaverDict` etc. These `_Saver...` classes behave just like the
-normal variant except that they are aware of the database and saves to it whenever new data gets
-assigned to them. This is what allows you to do things like `self.db.mylist[7] = val` and be sure
-that the new version of list is saved. Without this you would have to load the list into a temporary
-variable, change it and then re-assign it to the Attribute in order for it to save.
-
-There is however an important thing to remember. If you retrieve your mutable iterable into another
-variable, e.g. `mylist2 = obj.db.mylist`, your new variable (`mylist2`) will *still* be a
-`_SaverList`. This means it will continue to save itself to the database whenever it is updated!
-
-
-```python
- obj.db.mylist = [1,2,3,4]
- mylist = obj.db.mylist
- mylist[3] = 5 # this will also update database
- print(mylist) # this is now [1,2,3,5]
- print(obj.db.mylist) # this is also [1,2,3,5]
-```
-
-To "disconnect" your extracted mutable variable from the database you simply need to convert the
-`_Saver...` iterable to a normal Python structure. So to convert a `_SaverList`, you use the
-`list()` function, for a `_SaverDict` you use `dict()` and so on.
-
-```python
- obj.db.mylist = [1,2,3,4]
- mylist = list(obj.db.mylist) # convert to normal list
- mylist[3] = 5
- print(mylist) # this is now [1,2,3,5]
- print(obj.db.mylist) # this is still [1,2,3,4]
-```
-
-A further problem comes with *nested mutables*, like a dict containing lists of dicts or something
-like that. Each of these nested mutables would be `_Saver*` structures connected to the database and
-disconnecting the outermost one of them would not disconnect those nested within. To make really
-sure you disonnect a nested structure entirely from the database, Evennia provides a special
-function `evennia.utils.dbserialize.deserialize`:
-
-```
-from evennia.utils.dbserialize import deserialize
-
-decoupled_mutables = deserialize(nested_mutables)
-
-```
-
-The result of this operation will be a structure only consisting of normal Python mutables (`list`
-instead of `_SaverList` and so on).
-
-
-Remember, this is only valid for *mutable* iterables.
-[Immutable](http://en.wikipedia.org/wiki/Immutable) objects (strings, numbers, tuples etc) are
-already disconnected from the database from the onset.
-
-```python
- obj.db.mytup = (1,2,[3,4])
- obj.db.mytup[0] = 5 # this fails since tuples are immutable
-
- # this works but will NOT update database since outermost is a tuple
- obj.db.mytup[2][1] = 5
- print(obj.db.mytup[2][1]) # this still returns 4, not 5
-
- mytup1 = obj.db.mytup # mytup1 is already disconnected from database since outermost
- # iterable is a tuple, so we can edit the internal list as we want
- # without affecting the database.
-```
-
-> Attributes will fetch data fresh from the database whenever you read them, so
-> if you are performing big operations on a mutable Attribute property (such as looping over a list
-> or dict) you should make sure to "disconnect" the Attribute's value first and operate on this
-> rather than on the Attribute. You can gain dramatic speed improvements to big loops this
-> way.
-
-
-## Locking and checking Attributes
-
-Attributes are normally not locked down by default, but you can easily change that for individual
-Attributes (like those that may be game-sensitive in games with user-level building).
-
-First you need to set a *lock string* on your Attribute. Lock strings are specified [Locks](./Locks.md).
-The relevant lock types are
-
-- `attrread` - limits who may read the value of the Attribute
-- `attredit` - limits who may set/change this Attribute
-
-You cannot use the `db` handler to modify Attribute object (such as setting a lock on them) - The
-`db` handler will return the Attribute's *value*, not the Attribute object itself. Instead you use
-the AttributeHandler and set it to return the object instead of the value:
-
-```python
- lockstring = "attread:all();attredit:perm(Admins)"
- obj.attributes.get("myattr", return_obj=True).locks.add(lockstring)
-```
-
-Note the `return_obj` keyword which makes sure to return the `Attribute` object so its LockHandler
-could be accessed.
-
-A lock is no good if nothing checks it -- and by default Evennia does not check locks on Attributes.
-You have to add a check to your commands/code wherever it fits (such as before setting an
-Attribute).
-
-```python
- # in some command code where we want to limit
- # setting of a given attribute name on an object
- attr = obj.attributes.get(attrname,
- return_obj=True,
- accessing_obj=caller,
- default=None,
- default_access=False)
- if not attr:
- caller.msg("You cannot edit that Attribute!")
- return
- # edit the Attribute here
-```
-
-The same keywords are available to use with `obj.attributes.set()` and `obj.attributes.remove()`,
-those will check for the `attredit` lock type.
diff --git a/docs/0.9.5/_sources/Banning.md.txt b/docs/0.9.5/_sources/Banning.md.txt
deleted file mode 100644
index 09baae2b95..0000000000
--- a/docs/0.9.5/_sources/Banning.md.txt
+++ /dev/null
@@ -1,148 +0,0 @@
-# Banning
-
-
-Whether due to abuse, blatant breaking of your rules, or some other reason, you will eventually find
-no other recourse but to kick out a particularly troublesome player. The default command set has
-admin tools to handle this, primarily `ban`, `unban`, and `boot`.
-
-## Creating a ban
-
-Say we have a troublesome player "YouSuck" - this is a person that refuses common courtesy - an
-abusive
-and spammy account that is clearly created by some bored internet hooligan only to cause grief. You
-have tried to be nice. Now you just want this troll gone.
-
-### Name ban
-
-The easiest recourse is to block the account YouSuck from ever connecting again.
-
- ban YouSuck
-
-This will lock the name YouSuck (as well as 'yousuck' and any other capitalization combination), and
-next time they try to log in with this name the server will not let them!
-
-You can also give a reason so you remember later why this was a good thing (the banned account will
-never see this)
-
- ban YouSuck:This is just a troll.
-
-If you are sure this is just a spam account, you might even consider deleting the player account
-outright:
-
- account/delete YouSuck
-
-Generally, banning the name is the easier and safer way to stop the use of an account -- if you
-change your mind you can always remove the block later whereas a deletion is permanent.
-
-### IP ban
-
-Just because you block YouSuck's name might not mean the trolling human behind that account gives
-up. They can just create a new account YouSuckMore and be back at it. One way to make things harder
-for them is to tell the server to not allow connections from their particular IP address.
-
-First, when the offending account is online, check which IP address they use. This you can do with
-the `who` command, which will show you something like this:
-
- Account Name On for Idle Room Cmds Host
- YouSuckMore 01:12 2m 22 212 237.333.0.223
-
-The "Host" bit is the IP address from which the account is connecting. Use this to define the ban
-instead of the name:
-
- ban 237.333.0.223
-
-This will stop YouSuckMore connecting from their computer. Note however that IP address might change
-easily - either due to how the player's Internet Service Provider operates or by the user simply
-changing computers. You can make a more general ban by putting asterisks `*` as wildcards for the
-groups of three digits in the address. So if you figure out that !YouSuckMore mainly connects from
-237.333.0.223, 237.333.0.225, and 237.333.0.256 (only changes in their subnet), it might be an idea
-to put down a ban like this to include any number in that subnet:
-
- ban 237.333.0.*
-
-You should combine the IP ban with a name-ban too of course, so the account YouSuckMore is truly
-locked regardless of where they connect from.
-
-Be careful with too general IP bans however (more asterisks above). If you are unlucky you could be
-blocking out innocent players who just happen to connect from the same subnet as the offender.
-
-## Booting
-
-YouSuck is not really noticing all this banning yet though - and won't until having logged out and
-trying to log back in again. Let's help the troll along.
-
- boot YouSuck
-
-Good riddance. You can give a reason for booting too (to be echoed to the player before getting
-kicked out).
-
- boot YouSuck:Go troll somewhere else.
-
-### Lifting a ban
-
-Use the `unban` (or `ban`) command without any arguments and you will see a list of all currently
-active bans:
-
- Active bans
- id name/ip date reason
- 1 yousuck Fri Jan 3 23:00:22 2020 This is just a Troll.
- 2 237.333.0.* Fri Jan 3 23:01:03 2020 YouSuck's IP.
-
-Use the `id` from this list to find out which ban to lift.
-
- unban 2
-
- Cleared ban 2: 237.333.0.*
-
-## Summary of abuse-handling tools
-
-Below are other useful commands for dealing with annoying players.
-
-- **who** -- (as admin) Find the IP of a account. Note that one account can be connected to from
-multiple IPs depending on what you allow in your settings.
-- **examine/account thomas** -- Get all details about an account. You can also use `*thomas` to get
-the account. If not given, you will get the *Object* thomas if it exists in the same location, which
-is not what you want in this case.
-- **boot thomas** -- Boot all sessions of the given account name.
-- **boot 23** -- Boot one specific client session/IP by its unique id.
-- **ban** -- List all bans (listed with ids)
-- **ban thomas** -- Ban the user with the given account name
-- **ban/ip `134.233.2.111`** -- Ban by IP
-- **ban/ip `134.233.2.*`** -- Widen IP ban
-- **ban/ip `134.233.*.*`** -- Even wider IP ban
-- **unban 34** -- Remove ban with id #34
-
-- **cboot mychannel = thomas** -- Boot a subscriber from a channel you control
-- **clock mychannel = control:perm(Admin);listen:all();send:all()** -- Fine control of access to
-your channel using [lock definitions](./Locks.md).
-
-Locking a specific command (like `page`) is accomplished like so:
-1. Examine the source of the command. [The default `page` command class](
-https://github.com/evennia/evennia/blob/master/evennia/commands/default/comms.py#L686) has the lock
-string **"cmd:not pperm(page_banned)"**. This means that unless the player has the 'permission'
-"page_banned" they can use this command. You can assign any lock string to allow finer customization
-in your commands. You might look for the value of an [Attribute](./Attributes.md) or [Tag](./Tags.md), your
-current location etc.
-2. **perm/account thomas = page_banned** -- Give the account the 'permission' which causes (in this
-case) the lock to fail.
-
-- **perm/del/account thomas = page_banned** -- Remove the given permission
-
-- **tel thomas = jail** -- Teleport a player to a specified location or #dbref
-- **type thomas = FlowerPot** -- Turn an annoying player into a flower pot (assuming you have a
-`FlowerPot` typeclass ready)
-- **userpassword thomas = fooBarFoo** -- Change a user's password
-- **account/delete thomas** -- Delete a player account (not recommended, use **ban** instead)
-
-- **server** -- Show server statistics, such as CPU load, memory usage, and how many objects are
-cached
-- **time** -- Gives server uptime, runtime, etc
-- **reload** -- Reloads the server without disconnecting anyone
-- **reset** -- Restarts the server, kicking all connections
-- **shutdown** -- Stops the server cold without it auto-starting again
-- **py** -- Executes raw Python code, allows for direct inspection of the database and account
-objects on the fly. For advanced users.
-
-
-**Useful Tip:** `evennia changepassword ` entered into the command prompt will reset the
-password of any account, including the superuser or admin accounts. This is a feature of Django.
diff --git a/docs/0.9.5/_sources/Batch-Code-Processor.md.txt b/docs/0.9.5/_sources/Batch-Code-Processor.md.txt
deleted file mode 100644
index df8eaff1ff..0000000000
--- a/docs/0.9.5/_sources/Batch-Code-Processor.md.txt
+++ /dev/null
@@ -1,229 +0,0 @@
-# Batch Code Processor
-
-
-For an introduction and motivation to using batch processors, see [here](./Batch-Processors.md). This
-page describes the Batch-*code* processor. The Batch-*command* one is covered [here](Batch-Command-
-Processor).
-
-## Basic Usage
-
-The batch-code processor is a superuser-only function, invoked by
-
- > @batchcode path.to.batchcodefile
-
-Where `path.to.batchcodefile` is the path to a *batch-code file*. Such a file should have a name
-ending in "`.py`" (but you shouldn't include that in the path). The path is given like a python path
-relative to a folder you define to hold your batch files, set by `BATCH_IMPORT_PATH` in your
-settings. Default folder is (assuming your game is called "mygame") `mygame/world/`. So if you want
-to run the example batch file in `mygame/world/batch_code.py`, you could simply use
-
- > @batchcode batch_code
-
-This will try to run through the entire batch file in one go. For more gradual, *interactive*
-control you can use the `/interactive` switch. The switch `/debug` will put the processor in
-*debug* mode. Read below for more info.
-
-## The batch file
-
-A batch-code file is a normal Python file. The difference is that since the batch processor loads
-and executes the file rather than importing it, you can reliably update the file, then call it
-again, over and over and see your changes without needing to `@reload` the server. This makes for
-easy testing. In the batch-code file you have also access to the following global variables:
-
-- `caller` - This is a reference to the object running the batchprocessor.
-- `DEBUG` - This is a boolean that lets you determine if this file is currently being run in debug-
-mode or not. See below how this can be useful.
-
-Running a plain Python file through the processor will just execute the file from beginning to end.
-If you want to get more control over the execution you can use the processor's *interactive* mode.
-This runs certain code blocks on their own, rerunning only that part until you are happy with it. In
-order to do this you need to add special markers to your file to divide it up into smaller chunks.
-These take the form of comments, so the file remains valid Python.
-
-Here are the rules of syntax of the batch-code `*.py` file.
-
-- `#CODE` as the first on a line marks the start of a *code* block. It will last until the beginning
-of another marker or the end of the file. Code blocks contain functional python code. Each `#CODE`
-block will be run in complete isolation from other parts of the file, so make sure it's self-
-contained.
-- `#HEADER` as the first on a line marks the start of a *header* block. It lasts until the next
-marker or the end of the file. This is intended to hold imports and variables you will need for all
-other blocks .All python code defined in a header block will always be inserted at the top of every
-`#CODE` blocks in the file. You may have more than one `#HEADER` block, but that is equivalent to
-having one big one. Note that you can't exchange data between code blocks, so editing a header-
-variable in one code block won't affect that variable in any other code block!
-- `#INSERT path.to.file` will insert another batchcode (Python) file at that position.
-- A `#` that is not starting a `#HEADER`, `#CODE` or `#INSERT` instruction is considered a comment.
-- Inside a block, normal Python syntax rules apply. For the sake of indentation, each block acts as
-a separate python module.
-
-Below is a version of the example file found in `evennia/contrib/tutorial_examples/`.
-
-```python
- #
- # This is an example batch-code build file for Evennia.
- #
-
- #HEADER
-
- # This will be included in all other #CODE blocks
-
- from evennia import create_object, search_object
- from evennia.contrib.tutorial_examples import red_button
- from typeclasses.objects import Object
-
- limbo = search_object('Limbo')[0]
-
-
- #CODE
-
- red_button = create_object(red_button.RedButton, key="Red button",
- location=limbo, aliases=["button"])
-
- # caller points to the one running the script
- caller.msg("A red button was created.")
-
- # importing more code from another batch-code file
- #INSERT batch_code_insert
-
- #CODE
-
- table = create_object(Object, key="Blue Table", location=limbo)
- chair = create_object(Object, key="Blue Chair", location=limbo)
-
- string = "A %s and %s were created."
- if DEBUG:
- table.delete()
- chair.delete()
- string += " Since debug was active, " \
- "they were deleted again."
- caller.msg(string % (table, chair))
-```
-
-This uses Evennia's Python API to create three objects in sequence.
-
-## Debug mode
-
-Try to run the example script with
-
- > @batchcode/debug tutorial_examples.example_batch_code
-
-The batch script will run to the end and tell you it completed. You will also get messages that the
-button and the two pieces of furniture were created. Look around and you should see the button
-there. But you won't see any chair nor a table! This is because we ran this with the `/debug`
-switch, which is directly visible as `DEBUG==True` inside the script. In the above example we
-handled this state by deleting the chair and table again.
-
-The debug mode is intended to be used when you test out a batchscript. Maybe you are looking for
-bugs in your code or try to see if things behave as they should. Running the script over and over
-would then create an ever-growing stack of chairs and tables, all with the same name. You would have
-to go back and painstakingly delete them later.
-
-## Interactive mode
-
-Interactive mode works very similar to the [batch-command processor counterpart](Batch-Command-
-Processor). It allows you more step-wise control over how the batch file is executed. This is useful
-for debugging or for picking and choosing only particular blocks to run. Use `@batchcode` with the
-`/interactive` flag to enter interactive mode.
-
- > @batchcode/interactive tutorial_examples.example_batch_code
-
-You should see the following:
-
- 01/02: red_button = create_object(red_button.RedButton, [...] (hh for help)
-
-This shows that you are on the first `#CODE` block, the first of only two commands in this batch
-file. Observe that the block has *not* actually been executed at this point!
-
-To take a look at the full code snippet you are about to run, use `ll` (a batch-processor version of
-`look`).
-
-```python
- from evennia.utils import create, search
- from evennia.contrib.tutorial_examples import red_button
- from typeclasses.objects import Object
-
- limbo = search.objects(caller, 'Limbo', global_search=True)[0]
-
- red_button = create.create_object(red_button.RedButton, key="Red button",
- location=limbo, aliases=["button"])
-
- # caller points to the one running the script
- caller.msg("A red button was created.")
-```
-
-Compare with the example code given earlier. Notice how the content of `#HEADER` has been pasted at
-the top of the `#CODE` block. Use `pp` to actually execute this block (this will create the button
-and give you a message). Use `nn` (next) to go to the next command. Use `hh` for a list of commands.
-
-If there are tracebacks, fix them in the batch file, then use `rr` to reload the file. You will
-still be at the same code block and can rerun it easily with `pp` as needed. This makes for a simple
-debug cycle. It also allows you to rerun individual troublesome blocks - as mentioned, in a large
-batch file this can be very useful (don't forget the `/debug` mode either).
-
-Use `nn` and `bb` (next and back) to step through the file; e.g. `nn 12` will jump 12 steps forward
-(without processing any blocks in between). All normal commands of Evennia should work too while
-working in interactive mode.
-
-## Limitations and Caveats
-
-The batch-code processor is by far the most flexible way to build a world in Evennia. There are
-however some caveats you need to keep in mind.
-
-### Safety
-Or rather the lack of it. There is a reason only *superusers* are allowed to run the batch-code
-processor by default. The code-processor runs **without any Evennia security checks** and allows
-full access to Python. If an untrusted party could run the code-processor they could execute
-arbitrary python code on your machine, which is potentially a very dangerous thing. If you want to
-allow other users to access the batch-code processor you should make sure to run Evennia as a
-separate and very limited-access user on your machine (i.e. in a 'jail'). By comparison, the batch-
-command processor is much safer since the user running it is still 'inside' the game and can't
-really do anything outside what the game commands allow them to.
-
-### No communication between code blocks
-Global variables won't work in code batch files, each block is executed as stand-alone environments.
-`#HEADER` blocks are literally pasted on top of each `#CODE` block so updating some header-variable
-in your block will not make that change available in another block. Whereas a python execution
-limitation, allowing this would also lead to very hard-to-debug code when using the interactive mode
-- this would be a classical example of "spaghetti code".
-
-The main practical issue with this is when building e.g. a room in one code block and later want to
-connect that room with a room you built in the current block. There are two ways to do this:
-
-- Perform a database search for the name of the room you created (since you cannot know in advance
-which dbref it got assigned). The problem is that a name may not be unique (you may have a lot of "A
-dark forest" rooms). There is an easy way to handle this though - use [Tags](./Tags.md) or *Aliases*. You
-can assign any number of tags and/or aliases to any object. Make sure that one of those tags or
-aliases is unique to the room (like "room56") and you will henceforth be able to always uniquely
-search and find it later.
-- Use the `caller` global property as an inter-block storage. For example, you could have a
-dictionary of room references in an `ndb`:
- ```python
- #HEADER
- if caller.ndb.all_rooms is None:
- caller.ndb.all_rooms = {}
-
- #CODE
- # create and store the castle
- castle = create_object("rooms.Room", key="Castle")
- caller.ndb.all_rooms["castle"] = castle
-
- #CODE
- # in another node we want to access the castle
- castle = caller.ndb.all_rooms.get("castle")
- ```
-Note how we check in `#HEADER` if `caller.ndb.all_rooms` doesn't already exist before creating the
-dict. Remember that `#HEADER` is copied in front of every `#CODE` block. Without that `if` statement
-we'd be wiping the dict every block!
-
-### Don't treat a batchcode file like any Python file
-Despite being a valid Python file, a batchcode file should *only* be run by the batchcode processor.
-You should not do things like define Typeclasses or Commands in them, or import them into other
-code. Importing a module in Python will execute base level of the module, which in the case of your
-average batchcode file could mean creating a lot of new objects every time.
-### Don't let code rely on the batch-file's real file path
-
-When you import things into your batchcode file, don't use relative imports but always import with
-paths starting from the root of your game directory or evennia library. Code that relies on the
-batch file's "actual" location *will fail*. Batch code files are read as text and the strings
-executed. When the code runs it has no knowledge of what file those strings where once a part of.
diff --git a/docs/0.9.5/_sources/Batch-Command-Processor.md.txt b/docs/0.9.5/_sources/Batch-Command-Processor.md.txt
deleted file mode 100644
index 9278b4be7f..0000000000
--- a/docs/0.9.5/_sources/Batch-Command-Processor.md.txt
+++ /dev/null
@@ -1,182 +0,0 @@
-# Batch Command Processor
-
-
-For an introduction and motivation to using batch processors, see [here](./Batch-Processors.md). This
-page describes the Batch-*command* processor. The Batch-*code* one is covered [here](Batch-Code-
-Processor).
-
-## Basic Usage
-
-The batch-command processor is a superuser-only function, invoked by
-
- > @batchcommand path.to.batchcmdfile
-
-Where `path.to.batchcmdfile` is the path to a *batch-command file* with the "`.ev`" file ending.
-This path is given like a python path relative to a folder you define to hold your batch files, set
-with `BATCH_IMPORT_PATH` in your settings. Default folder is (assuming your game is in the `mygame`
-folder) `mygame/world`. So if you want to run the example batch file in
-`mygame/world/batch_cmds.ev`, you could use
-
- > @batchcommand batch_cmds
-
-A batch-command file contains a list of Evennia in-game commands separated by comments. The
-processor will run the batch file from beginning to end. Note that *it will not stop if commands in
-it fail* (there is no universal way for the processor to know what a failure looks like for all
-different commands). So keep a close watch on the output, or use *Interactive mode* (see below) to
-run the file in a more controlled, gradual manner.
-
-## The batch file
-
-The batch file is a simple plain-text file containing Evennia commands. Just like you would write
-them in-game, except you have more freedom with line breaks.
-
-Here are the rules of syntax of an `*.ev` file. You'll find it's really, really simple:
-
-- All lines having the `#` (hash)-symbol *as the first one on the line* are considered *comments*.
-All non-comment lines are treated as a command and/or their arguments.
-- Comment lines have an actual function -- they mark the *end of the previous command definition*.
-So never put two commands directly after one another in the file - separate them with a comment, or
-the second of the two will be considered an argument to the first one. Besides, using plenty of
-comments is good practice anyway.
-- A line that starts with the word `#INSERT` is a comment line but also signifies a special
-instruction. The syntax is `#INSERT ` and tries to import a given batch-cmd file
-into this one. The inserted batch file (file ending `.ev`) will run normally from the point of the
-`#INSERT` instruction.
-- Extra whitespace in a command definition is *ignored*. - A completely empty line translates in to
-a line break in texts. Two empty lines thus means a new paragraph (this is obviously only relevant
-for commands accepting such formatting, such as the `@desc` command).
-- The very last command in the file is not required to end with a comment.
-- You *cannot* nest another `@batchcommand` statement into your batch file. If you want to link many
-batch-files together, use the `#INSERT` batch instruction instead. You also cannot launch the
-`@batchcode` command from your batch file, the two batch processors are not compatible.
-
-Below is a version of the example file found in `evennia/contrib/tutorial_examples/batch_cmds.ev`.
-
-```bash
- #
- # This is an example batch build file for Evennia.
- #
-
- # This creates a red button
- @create button:tutorial_examples.red_button.RedButton
- # (This comment ends input for @create)
- # Next command. Let's create something.
- @set button/desc =
- This is a large red button. Now and then
- it flashes in an evil, yet strangely tantalizing way.
-
- A big sign sits next to it. It says:
-
-
- -----------
-
- Press me!
-
- -----------
-
-
- ... It really begs to be pressed! You
- know you want to!
-
- # This inserts the commands from another batch-cmd file named
- # batch_insert_file.ev.
- #INSERT examples.batch_insert_file
-
-
- # (This ends the @set command). Note that single line breaks
- # and extra whitespace in the argument are ignored. Empty lines
- # translate into line breaks in the output.
- # Now let's place the button where it belongs (let's say limbo #2 is
- # the evil lair in our example)
- @teleport #2
- # (This comments ends the @teleport command.)
- # Now we drop it so others can see it.
- # The very last command in the file needs not be ended with #.
- drop button
-```
-
-To test this, run `@batchcommand` on the file:
-
- > @batchcommand contrib.tutorial_examples.batch_cmds
-
-A button will be created, described and dropped in Limbo. All commands will be executed by the user
-calling the command.
-
-> Note that if you interact with the button, you might find that its description changes, loosing
-your custom-set description above. This is just the way this particular object works.
-
-## Interactive mode
-
-Interactive mode allows you to more step-wise control over how the batch file is executed. This is
-useful for debugging and also if you have a large batch file and is only updating a small part of it
--- running the entire file again would be a waste of time (and in the case of `@create`-ing objects
-you would to end up with multiple copies of same-named objects, for example). Use `@batchcommand`
-with the `/interactive` flag to enter interactive mode.
-
- > @batchcommand/interactive tutorial_examples.batch_cmds
-
-You will see this:
-
- 01/04: @create button:tutorial_examples.red_button.RedButton (hh for help)
-
-This shows that you are on the `@create` command, the first out of only four commands in this batch
-file. Observe that the command `@create` has *not* been actually processed at this point!
-
-To take a look at the full command you are about to run, use `ll` (a batch-processor version of
-`look`). Use `pp` to actually process the current command (this will actually `@create` the button)
--- and make sure it worked as planned. Use `nn` (next) to go to the next command. Use `hh` for a
-list of commands.
-
-If there are errors, fix them in the batch file, then use `rr` to reload the file. You will still be
-at the same command and can rerun it easily with `pp` as needed. This makes for a simple debug
-cycle. It also allows you to rerun individual troublesome commands - as mentioned, in a large batch
-file this can be very useful. Do note that in many cases, commands depend on the previous ones (e.g.
-if `@create` in the example above had failed, the following commands would have had nothing to
-operate on).
-
-Use `nn` and `bb` (next and back) to step through the file; e.g. `nn 12` will jump 12 steps forward
-(without processing any command in between). All normal commands of Evennia should work too while
-working in interactive mode.
-
-## Limitations and Caveats
-
-The batch-command processor is great for automating smaller builds or for testing new commands and
-objects repeatedly without having to write so much. There are several caveats you have to be aware
-of when using the batch-command processor for building larger, complex worlds though.
-
-The main issue is that when you run a batch-command script you (*you*, as in your superuser
-character) are actually moving around in the game creating and building rooms in sequence, just as
-if you had been entering those commands manually, one by one. You have to take this into account
-when creating the file, so that you can 'walk' (or teleport) to the right places in order.
-
-This also means there are several pitfalls when designing and adding certain types of objects. Here
-are some examples:
-
-- *Rooms that change your [Command Set](./Command-Sets.md)*: Imagine that you build a 'dark' room, which
-severely limits the cmdsets of those entering it (maybe you have to find the light switch to
-proceed). In your batch script you would create this room, then teleport to it - and promptly be
-shifted into the dark state where none of your normal build commands work ...
-- *Auto-teleportation*: Rooms that automatically teleport those that enter them to another place
-(like a trap room, for example). You would be teleported away too.
-- *Mobiles*: If you add aggressive mobs, they might attack you, drawing you into combat. If they
-have AI they might even follow you around when building - or they might move away from you before
-you've had time to finish describing and equipping them!
-
-The solution to all these is to plan ahead. Make sure that superusers are never affected by whatever
-effects are in play. Add an on/off switch to objects and make sure it's always set to *off* upon
-creation. It's all doable, one just needs to keep it in mind.
-
-## Assorted notes
-
-The fact that you build as 'yourself' can also be considered an advantage however, should you ever
-decide to change the default command to allow others than superusers to call the processor. Since
-normal access-checks are still performed, a malevolent builder with access to the processor should
-not be able to do all that much damage (this is the main drawback of the [Batch Code
-Processor](./Batch-Code-Processor.md))
-
-- [GNU Emacs](https://www.gnu.org/software/emacs/) users might find it interesting to use emacs'
-*evennia mode*. This is an Emacs major mode found in `evennia/utils/evennia-mode.el`. It offers
-correct syntax highlighting and indentation with `` when editing `.ev` files in Emacs. See the
-header of that file for installation instructions.
-- [VIM](http://www.vim.org/) users can use amfl's [vim-evennia](https://github.com/amfl/vim-evennia)
-mode instead, see its readme for install instructions.
\ No newline at end of file
diff --git a/docs/0.9.5/_sources/Batch-Processors.md.txt b/docs/0.9.5/_sources/Batch-Processors.md.txt
deleted file mode 100644
index 85a4c8a30f..0000000000
--- a/docs/0.9.5/_sources/Batch-Processors.md.txt
+++ /dev/null
@@ -1,82 +0,0 @@
-# Batch Processors
-
-
-Building a game world is a lot of work, especially when starting out. Rooms should be created,
-descriptions have to be written, objects must be detailed and placed in their proper places. In many
-traditional MUD setups you had to do all this online, line by line, over a telnet session.
-
-Evennia already moves away from much of this by shifting the main coding work to external Python
-modules. But also building would be helped if one could do some or all of it externally. Enter
-Evennia's *batch processors* (there are two of them). The processors allows you, as a game admin, to
-build your game completely offline in normal text files (*batch files*) that the processors
-understands. Then, when you are ready, you use the processors to read it all into Evennia (and into
-the database) in one go.
-
-You can of course still build completely online should you want to - this is certainly the easiest
-way to go when learning and for small build projects. But for major building work, the advantages of
-using the batch-processors are many:
-- It's hard to compete with the comfort of a modern desktop text editor; Compared to a traditional
-MUD line input, you can get much better overview and many more features. Also, accidentally pressing
-Return won't immediately commit things to the database.
-- You might run external spell checkers on your batch files. In the case of one of the batch-
-processors (the one that deals with Python code), you could also run external debuggers and code
-analyzers on your file to catch problems before feeding it to Evennia.
-- The batch files (as long as you keep them) are records of your work. They make a natural starting
-point for quickly re-building your world should you ever decide to start over.
-- If you are an Evennia developer, using a batch file is a fast way to setup a test-game after
-having reset the database.
-- The batch files might come in useful should you ever decide to distribute all or part of your
-world to others.
-
-
-There are two batch processors, the Batch-*command* processor and the Batch-*code* processor. The
-first one is the simpler of the two. It doesn't require any programming knowledge - you basically
-just list in-game commands in a text file. The code-processor on the other hand is much more
-powerful but also more complex - it lets you use Evennia's API to code your world in full-fledged
-Python code.
-
-- The [Batch Command Processor](./Batch-Command-Processor.md)
-- The [Batch Code Processor](./Batch-Code-Processor.md)
-
-If you plan to use international characters in your batchfiles you are wise to read about *file
-encodings* below.
-
-## A note on File Encodings
-
-As mentioned, both the processors take text files as input and then proceed to process them. As long
-as you stick to the standard [ASCII](http://en.wikipedia.org/wiki/Ascii) character set (which means
-the normal English characters, basically) you should not have to worry much about this section.
-
-Many languages however use characters outside the simple `ASCII` table. Common examples are various
-apostrophes and umlauts but also completely different symbols like those of the greek or cyrillic
-alphabets.
-
-First, we should make it clear that Evennia itself handles international characters just fine. It
-(and Django) uses [unicode](http://en.wikipedia.org/wiki/Unicode) strings internally.
-
-The problem is that when reading a text file like the batchfile, we need to know how to decode the
-byte-data stored therein to universal unicode. That means we need an *encoding* (a mapping) for how
-the file stores its data. There are many, many byte-encodings used around the world, with opaque
-names such as `Latin-1`, `ISO-8859-3` or `ARMSCII-8` to pick just a few examples. Problem is that
-it's practially impossible to determine which encoding was used to save a file just by looking at it
-(it's just a bunch of bytes!). You have to *know*.
-
-With this little introduction it should be clear that Evennia can't guess but has to *assume* an
-encoding when trying to load a batchfile. The text editor and Evennia must speak the same "language"
-so to speak. Evennia will by default first try the international `UTF-8` encoding, but you can have
-Evennia try any sequence of different encodings by customizing the `ENCODINGS` list in your settings
-file. Evennia will use the first encoding in the list that do not raise any errors. Only if none
-work will the server give up and return an error message.
-
-You can often change the text editor encoding (this depends on your editor though), otherwise you
-need to add the editor's encoding to Evennia's `ENCODINGS` list. If you are unsure, write a test
-file with lots of non-ASCII letters in the editor of your choice, then import to make sure it works
-as it should.
-
-More help with encodings can be found in the entry [Text Encodings](./Text-Encodings.md) and also in the
-Wikipedia article [here](http://en.wikipedia.org/wiki/Text_encodings).
-
-**A footnote for the batch-code processor**: Just because *Evennia* can parse your file and your
-fancy special characters, doesn't mean that *Python* allows their use. Python syntax only allows
-international characters inside *strings*. In all other source code only `ASCII` set characters are
-allowed.
diff --git a/docs/0.9.5/_sources/Bootstrap-&-Evennia.md.txt b/docs/0.9.5/_sources/Bootstrap-&-Evennia.md.txt
deleted file mode 100644
index fd7db78909..0000000000
--- a/docs/0.9.5/_sources/Bootstrap-&-Evennia.md.txt
+++ /dev/null
@@ -1,100 +0,0 @@
-# Bootstrap & Evennia
-
-# What is Bootstrap?
-Evennia's new default web page uses a framework called [Bootstrap](https://getbootstrap.com/). This
-framework is in use across the internet - you'll probably start to recognize its influence once you
-learn some of the common design patterns. This switch is great for web developers, perhaps like
-yourself, because instead of wondering about setting up different grid systems or what custom class
-another designer used, we have a base, a bootstrap, to work from. Bootstrap is responsive by
-default, and comes with some default styles that Evennia has lightly overrode to keep some of the
-same colors and styles you're used to from the previous design.
-
-For your reading pleasure, a brief overview of Bootstrap follows. For more in-depth info, please
-read [the documentation](https://getbootstrap.com/docs/4.0/getting-started/introduction/).
-***
-
-## The Layout System
-Other than the basic styling Bootstrap includes, it also includes [a built in layout and grid
-system](https://getbootstrap.com/docs/4.0/layout/overview/).
-The first part of this system is [the
-container](https://getbootstrap.com/docs/4.0/layout/overview/#containers).
-
-The container is meant to hold all your page content. Bootstrap provides two types: fixed-width and
-full-width.
-Fixed-width containers take up a certain max-width of the page - they're useful for limiting the
-width on Desktop or Tablet platforms, instead of making the content span the width of the page.
-```
-
-
-
-```
-Full width containers take up the maximum width available to them - they'll span across a wide-
-screen desktop or a smaller screen phone, edge-to-edge.
-```
-
-
-
-```
-
-The second part of the layout system is [the grid](https://getbootstrap.com/docs/4.0/layout/grid/).
-This is the bread-and-butter of the layout of Bootstrap - it allows you to change the size of
-elements depending on the size of the screen, without writing any media queries. We'll briefly go
-over it - to learn more, please read the docs or look at the source code for Evennia's home page in
-your browser.
-> Important! Grid elements should be in a .container or .container-fluid. This will center the
-contents of your site.
-
-Bootstrap's grid system allows you to create rows and columns by applying classes based on
-breakpoints. The default breakpoints are extra small, small, medium, large, and extra-large. If
-you'd like to know more about these breakpoints, please [take a look at the documentation for
-them.](https://getbootstrap.com/docs/4.0/layout/overview/#responsive-breakpoints)
-
-To use the grid system, first create a container for your content, then add your rows and columns
-like so:
-```
-
-
-
- 1 of 3
-
-
- 2 of 3
-
-
- 3 of 3
-
-
-
-```
-This layout would create three equal-width columns.
-
-To specify your sizes - for instance, Evennia's default site has three columns on desktop and
-tablet, but reflows to single-column on smaller screens. Try it out!
-```
-
-
-
- 1 of 4
-
-
- 2 of 4
-
-
- 3 of 4
-
-
- 4 of 4
-
-
-
-```
-This layout would be 4 columns on large screens, 2 columns on medium screens, and 1 column on
-anything smaller.
-
-To learn more about Bootstrap's grid, please [take a look at the
-docs](https://getbootstrap.com/docs/4.0/layout/grid/)
-***
-
-## More Bootstrap
-Bootstrap also provides a huge amount of utilities, as well as styling and content elements. To
-learn more about them, please [read the Bootstrap docs](https://getbootstrap.com/docs/4.0/getting-started/introduction/) or read one of our other web tutorials.
diff --git a/docs/0.9.5/_sources/Bootstrap-Components-and-Utilities.md.txt b/docs/0.9.5/_sources/Bootstrap-Components-and-Utilities.md.txt
deleted file mode 100644
index 5e46964766..0000000000
--- a/docs/0.9.5/_sources/Bootstrap-Components-and-Utilities.md.txt
+++ /dev/null
@@ -1,82 +0,0 @@
-# Bootstrap Components and Utilities
-
-Bootstrap provides many utilities and components you can use when customizing Evennia's web
-presence. We'll go over a few examples here that you might find useful.
-> Please take a look at either [the basic web tutorial](./Add-a-simple-new-web-page.md) or [the web
-character view tutorial](./Web-Character-View-Tutorial.md)
-> to get a feel for how to add pages to Evennia's website to test these examples.
-
-## General Styling
-Bootstrap provides base styles for your site. These can be customized through CSS, but the default
-styles are intended to provide a consistent, clean look for sites.
-
-### Color
-Most elements can be styled with default colors. [Take a look at the
-documentation](https://getbootstrap.com/docs/4.0/utilities/colors/) to learn more about these colors
-- suffice to say, adding a class of text-* or bg-*, for instance, text-primary, sets the text color
-or background color.
-
-### Borders
-Simply adding a class of 'border' to an element adds a border to the element. For more in-depth
-info, please [read the documentation on
-borders.](https://getbootstrap.com/docs/4.0/utilities/borders/).
-```
-
-```
-You can also easily round corners just by adding a class.
-```
-
-```
-
-### Spacing
-Bootstrap provides classes to easily add responsive margin and padding. Most of the time, you might
-like to add margins or padding through CSS itself - however these classes are used in the default
-Evennia site. [Take a look at the docs](https://getbootstrap.com/docs/4.0/utilities/spacing/) to
-learn more.
-
-***
-## Components
-
-### Buttons
-[Buttons](https://getbootstrap.com/docs/4.0/components/buttons/) in Bootstrap are very easy to use -
-button styling can be added to `