Merged the devel-branch.
This commit is contained in:
commit
96ac3f967b
413 changed files with 15558 additions and 20456 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -10,7 +10,6 @@ dist
|
|||
build
|
||||
eggs
|
||||
parts
|
||||
bin
|
||||
var
|
||||
sdist
|
||||
develop-eggs
|
||||
|
|
@ -26,12 +25,6 @@ __pycache__
|
|||
*.restart
|
||||
*.db3
|
||||
|
||||
# Installation-specific
|
||||
game/settings.py
|
||||
game/logs/*.log.*
|
||||
game/gamesrc/web/static/*
|
||||
game/gamesrc/web/media/*
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
|
||||
|
|
|
|||
10
.travis.yml
Normal file
10
.travis.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
language: python
|
||||
python:
|
||||
- "2.7"
|
||||
install: pip install .
|
||||
script:
|
||||
- evennia --init dummy
|
||||
- cd dummy
|
||||
- evennia migrate
|
||||
- evennia test evennia
|
||||
|
||||
|
|
@ -1,7 +1,15 @@
|
|||
Changelog
|
||||
---------
|
||||
# Evennia Changelog
|
||||
|
||||
Sept 2014:
|
||||
## Feb 2015:
|
||||
Development currently in devel/ branch. Moved typeclasses to use
|
||||
django's proxy functionality. Changed the Evennia folder layout to a
|
||||
library format with a stand-alone launcher, in preparation for making
|
||||
an 'evennia' pypy package and using versioning. The version we will
|
||||
merge with will likely be 0.5. There is also work with an expanded
|
||||
testing structure and the use of threading for saves. We also now
|
||||
use Travis for automatic build checking.
|
||||
|
||||
## Sept 2014:
|
||||
Updated to Django 1.7+ which means South dependency was dropped and
|
||||
minimum Python version upped to 2.7. MULTISESSION_MODE=3 was added
|
||||
and the web customization system was overhauled using the latest
|
||||
|
|
@ -9,28 +17,28 @@ functionality of django. Otherwise, mostly bug-fixes and
|
|||
implementation of various smaller feature requests as we got used
|
||||
to github. Many new users have appeared.
|
||||
|
||||
Jan 2014:
|
||||
## Jan 2014:
|
||||
Moved Evennia project from Google Code to github.com/evennia/evennia.
|
||||
|
||||
Nov 2013:
|
||||
## Nov 2013:
|
||||
Moved the internal webserver into the Server and added support for
|
||||
out-of-band protocols (MSDP initially). This large development push
|
||||
also meant fixes and cleanups of the way attributes were handled.
|
||||
Tags were added, along with proper handlers for permissions, nicks
|
||||
and aliases.
|
||||
|
||||
May 2013:
|
||||
## May 2013:
|
||||
Made players able to control more than one Character at the same
|
||||
time, through the MULTISESSION_MODE=2 addition. This lead to a lot
|
||||
of internal changes for the server.
|
||||
|
||||
Oct 2012:
|
||||
## Oct 2012:
|
||||
Changed Evennia from the Modified Artistic 1.0 license to the more
|
||||
standard and permissive BSD license. Lots of updates and bug fixes as
|
||||
more people start to use it in new ways. Lots of new caching and
|
||||
speed-ups.
|
||||
|
||||
March 2012:
|
||||
## March 2012:
|
||||
Evennia's API has changed and simplified slightly in that the
|
||||
base-modules where removed from game/gamesrc. Instead admins are
|
||||
encouraged to explicitly create new modules under game/gamesrc/ when
|
||||
|
|
@ -42,7 +50,7 @@ extensions, notably the MSDP and GMCP out-of-band extensions. On the
|
|||
community side, evennia's dev blog was started and linked on planet
|
||||
Mud-dev aggregator.
|
||||
|
||||
Nov 2011:
|
||||
## Nov 2011:
|
||||
After creating several different proof-of-concept game systems (in
|
||||
contrib and privately) as well testing lots of things to make sure the
|
||||
implementation is basically sound, we are declaring Evennia out of
|
||||
|
|
@ -51,7 +59,7 @@ development is still heavy but the issue list is at an all-time low
|
|||
and the server is slowly stabilizing as people try different things
|
||||
with it. So Beta it is!
|
||||
|
||||
Aug 2011:
|
||||
## Aug 2011:
|
||||
Split Evennia into two processes: Portal and Server. After a lot of
|
||||
work trying to get in-memory code-reloading to work, it's clear this
|
||||
is not Python's forte - it's impossible to catch all exceptions,
|
||||
|
|
@ -60,21 +68,21 @@ hackish, flakey and unstable code. With the Portal-Server split, the
|
|||
Server can simply be rebooted while players connected to the Portal
|
||||
remain connected. The two communicates over twisted's AMP protocol.
|
||||
|
||||
May 2011:
|
||||
## May 2011:
|
||||
The new version of Evennia, originally hitting trunk in Aug2010, is
|
||||
maturing. All commands from the pre-Aug version, including IRC/IMC2
|
||||
support works again. An ajax web-client was added earlier in the year,
|
||||
including moving Evennia to be its own webserver (no more need for
|
||||
Apache or django-testserver). Contrib-folder added.
|
||||
|
||||
Aug 2010:
|
||||
## Aug 2010:
|
||||
Evennia-griatch-branch is ready for merging with trunk. This marks a
|
||||
rather big change in the inner workings of the server, such as the
|
||||
introduction of TypeClasses and Scripts (as compared to the old
|
||||
ScriptParents and Events) but should hopefully bring everything
|
||||
together into one consistent package as code development continues.
|
||||
|
||||
May 2010:
|
||||
## May 2010:
|
||||
Evennia is currently being heavily revised and cleaned from
|
||||
the years of gradual piecemeal development. It is thus in a very
|
||||
'Alpha' stage at the moment. This means that old code snippets
|
||||
|
|
@ -82,85 +90,28 @@ will not be backwards compatabile. Changes touch almost all
|
|||
parts of Evennia's innards, from the way Objects are handled
|
||||
to Events, Commands and Permissions.
|
||||
|
||||
April 2010:
|
||||
## April 2010:
|
||||
Griatch takes over Maintainership of the Evennia project from
|
||||
the original creator Greg Taylor.
|
||||
|
||||
(Earlier revisions, with previous maintainer, go back to 2005)
|
||||
|
||||
|
||||
Contact, Support and Development
|
||||
-----------------------
|
||||
This is still alpha software, but we try to give support best we can
|
||||
if you have questions. Make a post to the mailing list or chat us up
|
||||
on IRC. We also have a bug tracker if you want to report
|
||||
bugs. Finally, if you are willing to help with the code work, we much
|
||||
appreciate all help! Visit either of the following resources:
|
||||
# Contact, Support and Development
|
||||
|
||||
Make a post to the mailing list or chat us up on IRC. We also have a
|
||||
bug tracker if you want to report bugs. Finally, if you are willing to
|
||||
help with the code work, we much appreciate all help! Visit either of
|
||||
the following resources:
|
||||
|
||||
* Evennia Webpage
|
||||
http://evennia.com
|
||||
|
||||
* Evennia manual (wiki)
|
||||
https://github.com/evennia/evennia/wiki
|
||||
|
||||
* Evennia Code Page (See INSTALL text for installation)
|
||||
https://github.com/evennia/evennia
|
||||
|
||||
* Bug tracker
|
||||
https://github.com/evennia/evennia/issues
|
||||
|
||||
* IRC channel
|
||||
visit channel #evennia on irc.freenode.com
|
||||
or online client: http://tinyurl.com/evchat
|
||||
|
||||
|
||||
Directory structure
|
||||
-------------------
|
||||
evennia
|
||||
|
|
||||
| ev.py
|
||||
|_______game (start the server, settings)
|
||||
| |___gamesrc
|
||||
| |___(game-related dirs)
|
||||
|_______src
|
||||
| |___(engine-related dirs)
|
||||
| |
|
||||
|_______contrib
|
||||
|
|
||||
|_______docs
|
||||
|
|
||||
|_______locales
|
||||
|
||||
ev.py is the API file. It contains easy shortcuts to most
|
||||
of Evennia's functionality. Import ev into a python interpreter
|
||||
(like ipython) and explore what's available.
|
||||
|
||||
The game/ folder is where you develop your game. The root
|
||||
of this directory contains the settings file and the executables
|
||||
to start the server. Under game/gamesrc you will create the
|
||||
modules that defines your game.
|
||||
|
||||
src/ contains the Evennia library. As a normal user you should
|
||||
not edit anything in this folder - you will run into conflicts
|
||||
conflicts as we update things from our end. If you see code
|
||||
you like (such as that of a default command), copy&paste it
|
||||
into a new module in game/gamesrc/. If you find that src/ doesn't
|
||||
support a functionality you need, issue a Feature
|
||||
request or a bug report appropriately. If you do add functionality
|
||||
or fix bugs in src/ yourself, please consider contributing it to
|
||||
Evennia's main repo to help us improve!
|
||||
|
||||
contrib/ contains optional code snippets. These are potentially useful
|
||||
but are deemed to be too game-specific to be part of the server itself.
|
||||
Modules in contrib are not used unless you yourself decide to import
|
||||
and use them.
|
||||
|
||||
docs/ contain offline versions of the documentation, you can use
|
||||
python-sphinx to convert the raw data to nice-looking output for
|
||||
printing etc. The online wiki is however the most updated version
|
||||
of the documentation.
|
||||
|
||||
locales/ holds translations of the server strings to other languages
|
||||
than English.
|
||||
|
||||
Enjoy!
|
||||
or the webclient: http://tinyurl.com/evchat
|
||||
42
CODING_STYLE.md
Normal file
42
CODING_STYLE.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# Evennia Code Style
|
||||
|
||||
All code submitted or committed to the Evennia project should aim to
|
||||
follow the guidelines outlined in [Python PEP
|
||||
8](http://www.python.org/dev/peps/pep-0008). Keeping the code style
|
||||
uniform makes it much easier for people to collaborate and read the
|
||||
code.
|
||||
|
||||
A good way to check if your code follows PEP8 is to use the [PEP8
|
||||
tool](https://pypi.python.org/pypi/pep8) on your sources.
|
||||
|
||||
## A quick list of code style points
|
||||
|
||||
* 4-space indendation, NO TABS!
|
||||
* Unix line endings.
|
||||
* CamelCase is only used for classes, nothing else.
|
||||
* All non-global variable names and all function names are to be
|
||||
lowercase, words separated by underscores. Variable names should
|
||||
always be more than two letters long.
|
||||
* Module-level global variables (only) are to be in CAPITAL letters.
|
||||
* (Evennia-specific): Imports should normally be done in this order:
|
||||
- Python modules (builtins and standard library)
|
||||
- Twisted modules
|
||||
- Django modules
|
||||
- Evennia src/ modules
|
||||
- Evennia game/ modules
|
||||
- Evennia 'ev' API imports
|
||||
|
||||
## Documentation
|
||||
|
||||
Remember that Evennia's source code is intended to be read - and will
|
||||
be read - by game admins trying to implement their game. Evennia
|
||||
prides itself with being extensively documented. Modules, functions,
|
||||
classes and class methods should all start with at least one line of
|
||||
docstring summing up the function's purpose. Ideally also explain
|
||||
eventual arguments and caveats. Add comments where appropriate.
|
||||
|
||||
## Ask Questions!
|
||||
|
||||
If any of the rules outlined in PEP 8 or in the sections above doesn't
|
||||
make sense, please don't hesitate to ask on the Evennia mailing list
|
||||
or in the chat.
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
Evennia Code Style
|
||||
------------------
|
||||
All code submitted or committed to the Evennia project needs to follow
|
||||
the guidelines outlined in Python PEP 8, which may be found at:
|
||||
|
||||
http://www.python.org/dev/peps/pep-0008/
|
||||
|
||||
A quick list of code style points
|
||||
---------------------------------
|
||||
* 4-space indendation, NO TABS!
|
||||
* Unix line endings.
|
||||
* CamelCase is only used for classes, nothing else.
|
||||
* All non-global variable names and all function names are to be
|
||||
lowercase, words separated by underscores. Variable names should
|
||||
always be more than two letters long.
|
||||
* Module-level global variables (only) are to be in CAPITAL letters.
|
||||
* (Evennia-specific): Imports should normally be done in this order:
|
||||
- Python modules (builtins and standard library)
|
||||
- Twisted modules
|
||||
- Django modules
|
||||
- Evennia src/ modules
|
||||
- Evennia game/ modules
|
||||
- Evennia 'ev' API imports
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
Remember that Evennia's source code is intended to be read - and will
|
||||
be read - by game admins trying to implement their game. Evennia
|
||||
prides itself with being extensively documented. Modules, functions,
|
||||
classes and class methods should all start with at least one line of
|
||||
docstring summing up the function's purpose. Ideally also explain
|
||||
eventual arguments and caveats. Add comments where appropriate.
|
||||
|
||||
Pylint
|
||||
------
|
||||
The program 'pylint' (http://www.logilab.org/857) is a useful tool for
|
||||
checking your Python code for errors. It will also check how well your
|
||||
code adheres to the PEP 8 guidelines (such as lack of docstrings) and
|
||||
tells you what can be improved.
|
||||
|
||||
Since pylint cannot catch dynamically created variables used in
|
||||
commands and elsewhere in Evennia, one needs to reduce some checks to
|
||||
avoid false errors and warnings. For best results, run pylint like
|
||||
this:
|
||||
|
||||
> pylint --disable=E1101,E0102,F0401,W0232,R0903 filename.py
|
||||
|
||||
To avoid entering the options every time, you can auto-create a
|
||||
pylintrc file by using the option --generate-rcfile. You need to dump
|
||||
this output into a file .pylintrc, for example like this (linux):
|
||||
|
||||
> pylint --disable=E1101,E0102,F0401,W0232,R0903 --generate-rcfile > ~/.pylintrc
|
||||
|
||||
From now on you can then just run
|
||||
|
||||
> pylint filename.py
|
||||
|
||||
Ask Questions!
|
||||
--------------
|
||||
If any of the rules outlined in PEP 8 or in the sections above doesn't
|
||||
make sense, please don't hesitate to ask on the Evennia mailing list
|
||||
at http://evennia.com. Keeping our code style uniform makes this
|
||||
project much easier for a wider group of people to participate in.
|
||||
137
INSTALL.md
Normal file
137
INSTALL.md
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
|
||||
# Evennia installation
|
||||
|
||||
The latest and more detailed installation instructions can be found
|
||||
[here](https://github.com/evennia/evennia/wiki/Getting-Started).
|
||||
|
||||
## Installing Python
|
||||
|
||||
First install [Python](https://www.python.org/). Linux users should
|
||||
have it in their repositories, Windows/Mac users can get it from the
|
||||
Python homepage. You need the 2.7.x version (Python 3 is not yet
|
||||
supported). Windows users, make sure to select the option to make
|
||||
Python available in your path - this is so you can call it everywhere
|
||||
as `python`. Python 2.7.9 and later also includes the
|
||||
[pip](https://pypi.python.org/pypi/pip/) installer out of the box,
|
||||
otherwise install this separately (in linux it's usually found as the
|
||||
`python-pip` package).
|
||||
|
||||
### installing virtualenv
|
||||
|
||||
This step is optional, but *highly* recommended. For installing
|
||||
up-to-date Python packages we recommend using
|
||||
[virtualenv](https://pypi.python.org/pypi/virtualenv), this makes it
|
||||
easy to keep your Python packages up-to-date without interfering with
|
||||
the defaults for your system.
|
||||
|
||||
```
|
||||
pip install virtualenv
|
||||
```
|
||||
|
||||
Go to the place where you want to make your virtual python library
|
||||
storage. This does not need to be near where you plan to install
|
||||
Evennia. Then do
|
||||
|
||||
```
|
||||
virtualenv vienv
|
||||
```
|
||||
|
||||
A new folder `vienv` will be created (you could also name it something
|
||||
else if you prefer). Activate the virtual environment like this:
|
||||
|
||||
```
|
||||
# for Linux/Unix/Mac:
|
||||
source vienv/bin/activate
|
||||
# for Windows:
|
||||
vienv\Scripts\activate.bat
|
||||
```
|
||||
|
||||
You should see `(vienv)` next to your prompt to show you the
|
||||
environment is active. You need to activate it whenever you open a new
|
||||
terminal, but you *don't* have to be inside the `vienv` folder henceforth.
|
||||
|
||||
|
||||
## Get the developer's version of Evennia
|
||||
|
||||
This is currently the only Evennia version available. First download
|
||||
and install [Git](http://git-scm.com/) from the homepage or via the
|
||||
package manager in Linux. Next, go to the place where you want the
|
||||
`evennia` folder to be created and run
|
||||
|
||||
```
|
||||
git clone https://github.com/evennia/evennia.git
|
||||
```
|
||||
|
||||
If you have a github account and have [set up SSH
|
||||
keys](https://help.github.com/articles/generating-ssh-keys/), you want
|
||||
to use this instead:
|
||||
|
||||
```
|
||||
git clone git@github.com:evennia/evennia.git
|
||||
```
|
||||
|
||||
In the future you just enter the new `evennia` folder and do
|
||||
|
||||
```
|
||||
git pull
|
||||
```
|
||||
|
||||
to get the latest Evennia updates.
|
||||
|
||||
## Evennia package install
|
||||
|
||||
Stand at the root of your new `evennia` directory and run
|
||||
|
||||
```
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
(note the period "." at the end, this tells pip to install from the
|
||||
current directory). This will install Evennia and all its dependencies
|
||||
(into your virtualenv if you are using that) and make the `evennia`
|
||||
command available on the command line. You can find Evennia's
|
||||
dependencies in `evennia/requirements.txt`.
|
||||
|
||||
## Creating your game project
|
||||
|
||||
To create your new game you need to initialize a new game project.
|
||||
This should be done somewhere *outside* of your `evennia` folder.
|
||||
|
||||
|
||||
```
|
||||
evennia --init mygame
|
||||
```
|
||||
|
||||
This will create a new game project named "mygame" in a folder of the
|
||||
same name. If you want to change the settings for your project, you
|
||||
will need to edit `mygame/server/conf/settings.py`.
|
||||
|
||||
|
||||
## Starting Evennia
|
||||
|
||||
Enter your new game directory and run
|
||||
|
||||
```
|
||||
evennia migrate
|
||||
evennia -i start
|
||||
```
|
||||
|
||||
Follow the instructions to create your superuser account. A lot of
|
||||
information will scroll past as the database is created and the server
|
||||
initializes. After this Evennia will be running. Use
|
||||
|
||||
```
|
||||
evennia -h
|
||||
```
|
||||
|
||||
for help with starting, stopping and other operations.
|
||||
|
||||
Start up your MUD client of choice and point it to your server and
|
||||
port *4000*. If you are just running locally the server name is
|
||||
*localhost*.
|
||||
|
||||
Alternatively, you can find the web interface and webclient by
|
||||
pointing your web browser to *http://localhost:8000*.
|
||||
|
||||
Finally, login with the superuser account and password you provided
|
||||
earlier. Welcome to Evennia!
|
||||
76
INSTALL.txt
76
INSTALL.txt
|
|
@ -1,76 +0,0 @@
|
|||
|
||||
-------------
|
||||
Evennia Setup
|
||||
-------------
|
||||
|
||||
You can find the updated and more detailed version of this page on
|
||||
https://github.com/evennia/evennia/wiki/Getting-Started. You don't
|
||||
need to make your server visible online during development, you only
|
||||
need an internet connection for downloading and updating.
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
* Make sure you have the prerequsites with minimum versions
|
||||
listed on https://github.com/evennia/evennia/wiki/Getting-Started:
|
||||
|
||||
- python
|
||||
- django
|
||||
- twisted + Pillow
|
||||
- GIT
|
||||
- django-south
|
||||
|
||||
* Go to a directory on your harddrive where you want the 'evennia'
|
||||
directory to be created, for example mud/.
|
||||
|
||||
cd mud/
|
||||
|
||||
* Get a copy of the Evennia source (you should have done this
|
||||
already if you are reading this file):
|
||||
|
||||
git clone https://github.com/evennia/evennia.git
|
||||
or
|
||||
git clone git@github.com:evennia/evennia.git
|
||||
|
||||
In the future, do 'git pull' to update the server.
|
||||
|
||||
* Change to the evennia/game directory and run the setup scripts.
|
||||
|
||||
cd evennia/game
|
||||
|
||||
python manage.py
|
||||
|
||||
* Edit the new game/settings.py if needed, then run
|
||||
|
||||
python manage.py syncdb
|
||||
|
||||
|
||||
Starting Evennia
|
||||
----------------
|
||||
|
||||
* Start the server with
|
||||
|
||||
python evennia.py -i start
|
||||
|
||||
or run without arguments for a menu of launch options. Make
|
||||
sure to create a superuser when asked. The email does not have
|
||||
to be a real one for testing (evennia does not check it)
|
||||
|
||||
* Start up your MUD client of choice and point it to your server and port 4000.
|
||||
If you are just running locally the server name is 'localhost'.
|
||||
|
||||
* Alternatively, you can find the web interface and webclient by
|
||||
pointing your web browser to http://localhost:8000.
|
||||
|
||||
* For superuser access, login with the account and password you provided earlier.
|
||||
|
||||
|
||||
Welcome to Evennia!
|
||||
-------------------
|
||||
|
||||
* See www.evennia.com for more information and help with how to
|
||||
proceed from here.
|
||||
|
||||
* For questions, see the discussion group or the chat. Report bugs or
|
||||
request features via the Issue Tracker.
|
||||
80
README.md
80
README.md
|
|
@ -1,29 +1,75 @@
|
|||
Evennia MUD/MU\* Creation System
|
||||
================================
|
||||
# Evennia MUD/MU\* Creation System ![evennia logo][logo]
|
||||
|
||||
*Evennia* is a Python-based MUD/MU\* server/codebase using modern technologies. It is made available as open source under the very friendly [BSD license](https://github.com/evennia/evennia/wiki/Licensing). Evennia allows creators to design and flesh out text-based massively-multiplayer online games with great freedom.
|
||||
*Evennia* is a modern library for creating [online multiplayer text
|
||||
games][wikimudpage] (MUD, MUSH, MUX, MOO etc) in pure Python. It allows game
|
||||
creators to design and flesh out their games with great freedom.
|
||||
Evennia is made available under the very friendly [BSD license][license].
|
||||
|
||||
http://www.evennia.com is the main hub tracking all things Evennia. The documentation wiki is found [here](https://github.com/evennia/evennia/wiki).
|
||||
http://www.evennia.com is the main hub tracking all things Evennia.
|
||||
|
||||
Features and Philosophy
|
||||
-----------------------
|
||||
|
||||
Evennia aims to supply a bare-bones MU\* codebase that allows vast flexibility for game designers while taking care of all the gritty networking and database-handling behind the scenes. Evennia offers an easy API for handling persistent objects, time-dependent scripting and all the other low-level features needed to create a MU\*. The idea is to allow the mud-coder to concentrate solely on designing the parts and systems of the mud that makes it uniquely fit their ideas.
|
||||
## Features and Philosophy
|
||||
|
||||
Coding in Evennia is primarily done by normal Python modules, making the codebase extremely flexible. The code is heavily documented and you use Python classes to represent your objects, scripts and players. The database layer is abstracted away.
|
||||
Evennia aims to supply a bare-bones MU\* codebase that allows vast
|
||||
flexibility for game designers while taking care of all the gritty
|
||||
networking and database-handling behind the scenes. Evennia offers an
|
||||
easy API for handling persistent objects, time-dependent scripting and
|
||||
all the other low-level features needed to create an online text-based
|
||||
game. The idea is to allow the mud-coder to concentrate solely on
|
||||
designing the parts and systems of the mud that makes it uniquely fit
|
||||
their ideas.
|
||||
|
||||
Evennia offers extensive connectivity options. A single server instance may offer connections over Telnet, SSH, SSL and HTTP. The latter is possible since Evennia is also its own web server: A default website as well as a browser-based comet-style mud client comes as part of the package ([screenshot](https://github.com/evennia/evennia/wiki/Screenshot)). Due to our Django and Twisted foundations, web integration is a snap since the same code that powers the game may also be used to run its web presence (you may use a third-party webserver too if you prefer though). Evennia in-game channels can also be interlinked with external IRC and IMC2 channels so players can chat with people "outside" the game.
|
||||
Coding in Evennia is primarily done by normal Python modules, making
|
||||
the codebase extremely flexible. The code is heavily documented and
|
||||
you use Python classes to represent your objects, scripts and players.
|
||||
The database layer is abstracted away.
|
||||
|
||||
Whereas Evennia is intended to be customized to almost any level you like, we do offer some defaults you can build from. The code base comes with basic classes for objects, exits, rooms and characters. There is also a default command set for handling administration, building, chat channels, poses and so on. This is enough to run a 'Talker' or some other social-style game out of the box. Stock Evennia is however deliberately void of any game-world-specific systems. So you won't find any AI codes, mobs, skill systems, races or combat stats in the default distribution (we might expand our contributions folder with optional plugins in the future though).
|
||||
![screenshot][screenshot]
|
||||
|
||||
If this piqued your interest, there is also a [lengthier introduction](https://github.com/evennia/evennia/wiki/Evennia-Introduction) to Evennia to read.
|
||||
Evennia offers extensive connectivity options. A single server
|
||||
instance may offer connections over Telnet, SSH, SSL and HTTP. The
|
||||
latter is possible since Evennia is also its own web server: A default
|
||||
website as well as a browser-based comet-style mud client comes as
|
||||
part of the package.
|
||||
|
||||
Current Status
|
||||
--------------
|
||||
Due to our Django and Twisted foundations, web integration is
|
||||
easy since the same code that powers the game may also be used to run
|
||||
its web presence.
|
||||
|
||||
The codebase is currently in **Beta**. While development continues, Evennia is already stable enough to be suitable for prototyping and development of your own games.
|
||||
Whereas Evennia is intended to be customized to almost any level you
|
||||
like, we do offer some defaults you can build from. The code base
|
||||
comes with basic classes for objects, exits, rooms and characters.
|
||||
There is also a default command set for handling administration,
|
||||
building, chat channels, poses and so on. This is enough to run a
|
||||
'Talker' or some other social-style game out of the box. Stock Evennia
|
||||
is however deliberately void of any game-world-specific systems. So
|
||||
you won't find any AI codes, mobs, skill systems, races or combat
|
||||
stats in the default distribution (we might expand our contributions
|
||||
folder with optional plugins in the future though).
|
||||
|
||||
More Information
|
||||
----------------
|
||||
## Current Status
|
||||
|
||||
To learn how to get your hands on the code base, the [Getting started](https://github.com/evennia/evennia/wiki/Getting-Started) page is the way to go. Otherwise you could browse the [Documentation wiki](https://github.com/evennia/evennia/wiki) or why not come join the [Evennia Community](http://www.evennia.com). Welcome!
|
||||
The codebase is currently in **Beta**. While development continues,
|
||||
Evennia is already stable enough to be suitable for prototyping and
|
||||
development of your own games.
|
||||
|
||||
## Where to go from here
|
||||
|
||||
If this piqued your interest, there is a [lengthier introduction][introduction] to read.
|
||||
|
||||
To learn how to get your hands on the code base, the [Getting started][gettingstarted] page
|
||||
is the way to go. Otherwise you could browse
|
||||
the [Documentation][wiki] or why not come join the [Evennia Community forum][group]
|
||||
or join us in our [development chat][chat]. Welcome!
|
||||
|
||||
|
||||
[homepage]: http://www.evennia.com
|
||||
[gettingstarted]: http://github.com/evennia/evennia/wiki/Getting-Started
|
||||
[wiki]: https://github.com/evennia/evennia/wiki
|
||||
[screenshot]: https://raw.githubusercontent.com/wiki/evennia/evennia/images/evennia_screenshot3.png
|
||||
[logo]: https://github.com/evennia/evennia/blob/devel/evennia/web/static/evennia_general/images/evennia_logo.png
|
||||
[introduction]: https://github.com/evennia/evennia/wiki/Evennia-Introduction
|
||||
[license]: https://github.com/evennia/evennia/wiki/Licensing
|
||||
[group]: https://groups.google.com/forum/#!forum/evennia
|
||||
[chat]: http://webchat.freenode.net/?channels=evennia&uio=MT1mYWxzZSY5PXRydWUmMTE9MTk1JjEyPXRydWUbb
|
||||
[wikimudpage]: http://en.wikipedia.org/wiki/MUD
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
Beta-GIT
|
||||
10
bin/unix/evennia
Executable file
10
bin/unix/evennia
Executable file
|
|
@ -0,0 +1,10 @@
|
|||
#! /usr/bin/python2.7
|
||||
"""
|
||||
Linux launcher
|
||||
|
||||
This is copied directly into the python bin directory and makes the
|
||||
'evennia' program available on $PATH.
|
||||
"""
|
||||
|
||||
from evennia.server.evennia_launcher import main
|
||||
main()
|
||||
16
bin/windows/evennia_launcher.py
Executable file
16
bin/windows/evennia_launcher.py
Executable file
|
|
@ -0,0 +1,16 @@
|
|||
#! /usr/bin/python
|
||||
"""
|
||||
Windows launcher. This is called by a dynamically created .bat file in
|
||||
the python bin directory and makes the 'evennia' program available on
|
||||
the command %path%.
|
||||
"""
|
||||
|
||||
import os, sys
|
||||
|
||||
# for pip install -e
|
||||
sys.path.insert(0, os.path.abspath(os.getcwd()))
|
||||
# main library path
|
||||
sys.path.insert(0, os.path.join(sys.prefix, "Lib", "site-packages"))
|
||||
|
||||
from evennia.server.evennia_launcher import main
|
||||
main()
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
|
||||
'Contrib' folder
|
||||
----------------
|
||||
|
||||
This folder contains 'contributions': extra snippets of code that are
|
||||
potentially very useful for the game coder but which are considered
|
||||
too game-specific to be a part of the main Evennia game server. These
|
||||
modules are not used unless you explicitly import them. See each file
|
||||
for more detailed instructions on how to install.
|
||||
|
||||
Modules in this folder is distributed under the same licence as
|
||||
Evennia unless noted differently in the individual module.
|
||||
|
||||
If you want to edit, tweak or expand on this code you should copy the
|
||||
things you want from here into game/gamesrc and change them there.
|
||||
|
||||
* Evennia MenuSystem (Griatch 2011) - A base set of classes and
|
||||
cmdsets for creating in-game multiple-choice menus in
|
||||
Evennia. The menu tree can be of any depth. Menu options can be
|
||||
numbered or given custom keys, and each option can execute
|
||||
code. Also contains a yes/no question generator function. This
|
||||
is intended to be used by commands and presents a y/n question
|
||||
to the user for accepting an action. Includes a simple new
|
||||
command 'menu' for testing and debugging.
|
||||
|
||||
* Evennia Line editor (Griatch 2011) - A powerful line-by-line editor
|
||||
for editing text in-game. Mimics the command names of the famous
|
||||
VI text editor. Supports undo/redo, search/replace,
|
||||
regex-searches, buffer formatting, indenting etc. It comes with
|
||||
its own help system. (Makes minute use of the MenuSystem module
|
||||
to show a y/n question if quitting without having
|
||||
saved). Includes a basic command '@edit' for activating the
|
||||
editor.
|
||||
|
||||
* Talking_NPC (Griatch 2011) - An example of a simple NPC object with
|
||||
which you can strike up a menu-driven converstaion. Uses the
|
||||
MenuSystem to allow conversation options. The npc object defines
|
||||
a command 'talk' for starting the (brief) conversation.
|
||||
|
||||
* Evennia Menu Login (Griatch 2011) - A menu-driven login screen that
|
||||
replaces the default command-based one. Uses the MenuSystem
|
||||
contrib. Does not require players to give their email and
|
||||
doesn't auto-create a Character object at first login like the
|
||||
default system does.
|
||||
|
||||
* CharGen (Griatch 2011) - A simple Character creator and selector for
|
||||
Evennia's ooc mode. Works well with the menu login contrib and
|
||||
is intended as a starting point for building a more full-featured
|
||||
character creation system.
|
||||
|
||||
* Evlang (Griatch 2012) - A heavily restricted version of Python for use
|
||||
as a "softcode" language by Players in-game. Contains a complete
|
||||
system with examples of objects and commands for coding.
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
|
||||
Battle for Evennia
|
||||
------------------
|
||||
|
||||
Evennia contrib - Griatch 2012 (WORK IN PROGRESS)
|
||||
|
||||
This is the beginnings of what will be a tutorial for building a
|
||||
simple yet still reasonably playable and not-quite-bog-standard
|
||||
starting game in Evennia. The tutorial text itself will eventually be
|
||||
found from the Dev blog and from the wiki.
|
||||
|
||||
|
||||
Ideas & Initial Brainstorm
|
||||
---------------------------
|
||||
|
||||
This is to be a hack&slash game. Characters fight mobiles and each
|
||||
other for random loot and better weapons. The highscore is based on
|
||||
most accumulated gold. They can sell loot to NPC merchants for gold,
|
||||
and also buy stuff others sold there (spending gold). Characters get
|
||||
better in the skills they use (no levels). They automatically collect
|
||||
loot when they kill things, and they cannot drop it (but they can give
|
||||
it away and, most importantly sell it). Death sends the player back to
|
||||
a starting position, but gives all but their weakest gear to their
|
||||
nemesis (they keep all their gold though).
|
||||
|
||||
Inventory of code we need:
|
||||
- Loot/Equipment lists of Weapons, Armour, Potions and Spells - maybe partly randomly generated.
|
||||
- Way to spawn in-game objects based on the loot lists
|
||||
- Character creation module (choose skills, attributes, assigns starting gear)
|
||||
- 3 Attributes, about 10 skills (some magic?)
|
||||
- Experience -> skill increase code
|
||||
- Skill success code - same between PCs as between NPC and PC
|
||||
- Combat code (twitch-based? Turn-based? Turn based seems easier to balance. Same for NPC vs PC and PC vs PC)
|
||||
- Mobile code (same for NPCs and enemies)
|
||||
- 'Give' mechanism (should require consent by receiver)
|
||||
- No quests, for simplicity. Use gold as a highscore.
|
||||
- Death respawn mechanism
|
||||
|
||||
|
||||
Elaboration based on Brainstorm
|
||||
-------------------------------
|
||||
|
||||
* Loot/Equipment lists and spawn - These could be global-level
|
||||
dictionaries in a module. Each dictionary gives info such as name,
|
||||
description and typeclass. Attributes could be set or
|
||||
randomized. The loot-spawner (probably a handler tied to a dead
|
||||
mob, treasure chest etc) would use utils.variable_from_module to
|
||||
extract a random item-template.
|
||||
|
||||
* Characters have 3 attributes: Wile, Strength and Agility. At
|
||||
creation, they distribute points between them. Wile is used for
|
||||
bartering with merchants, and using Magic. Strength determines
|
||||
hand-weapon damage and how heavy armour can be worn. Agility
|
||||
determines ability to dodge, initiative and using lighter weapons.
|
||||
Health is based on an average of all three attributes (i.e. all
|
||||
chars start with the same health).
|
||||
|
||||
* Skills are as follows (may change):
|
||||
- Long blades (str) ability to hit with swords and also axes.
|
||||
- Blunt (str) usage of blunt weapons like clubs. Good on armoured foes.
|
||||
- Spears (agi) usage of spears and hillebards. Bonus on first attack, minus on initiative.
|
||||
- Daggers (agi) usage of daggers and short blades. Bonus on initiative, bad on armour
|
||||
- Unarmoured (agi) usage of your fists and feet. Very fast. Bad on armour.
|
||||
- Dodge (agi) avoiding blows by swift footwork
|
||||
- Feint (agi) faign attacks to keep the enemy guessing
|
||||
- Shield (str) absorbing hits with a shielf
|
||||
- Platemail (str) utilizing heavy armour
|
||||
- Chainmail (max(str, agi)) utilizing medium armour
|
||||
- Leather (agi) utilizing light armour
|
||||
- Barter (wil) barter with merchants for a good price
|
||||
- Magic (wil) use of single-use magical scrolls to achieve various effects
|
||||
- Potions (wil) making the best of potions with various effects
|
||||
- Heal (wil) fixing yourself (or a friend) up between combat. Also judge opponent's wounds.
|
||||
|
||||
* Experience simply rises upon kills and is distributed between the
|
||||
skills used in the battle (so we need to log this). After N amount of
|
||||
XP in a skill, that skill automatically goes up one
|
||||
point. Increasing skills at least N points in 3+ different skills
|
||||
of a certain type (str, agi, wil) will increase the most trained
|
||||
Attribute by one point.
|
||||
|
||||
* Skill success is a comparison between the value of a random.gauss
|
||||
centered around the attacker's skill value vs the result of a
|
||||
random.gauss centered on the defender's skill. Certain
|
||||
weapons/defense combinations might be especially effective against
|
||||
one another (or not). The difference is the base damage, then
|
||||
adjusted by weapon and armour. In the case of bartering, skill
|
||||
challenge is between barter skill of both sides; difference
|
||||
influences the discount/higher price offered for selling/buying.
|
||||
|
||||
* Attacking another player or NPC will start a combat queue.
|
||||
Combat happens in turns. Each turn each player may enter two actions,
|
||||
picking among the following:
|
||||
- attack
|
||||
- parry (with weapon)
|
||||
- shield
|
||||
- feint
|
||||
- dodge
|
||||
- flee <direction>
|
||||
- block (anti-flee)
|
||||
- switch <weapon>
|
||||
Emoting is free in each round, but movement is forbidden unless one
|
||||
tries to flee (agi challenge, or cancelled by block action). All
|
||||
combattants involved in a fight submits their actions, then combat
|
||||
is resolved simultaneously by the code. Order of the two actions
|
||||
matter, so for example if both attack, neither is trying to parry,
|
||||
but may hit each other simultaneously. If both parry, shield or
|
||||
dodge, it means both are dancing about each other. If one feints
|
||||
and the other parries or dodges, they will have a disadvantage on
|
||||
the next defensive movement. A successful parry will give the
|
||||
parried attacker a disadvantage on their next attack. And so on.
|
||||
Another player may "join the queue" at any time by attacking one of
|
||||
the combatting PCs. They get to insert their actions together with
|
||||
the rest on the next round. A round should probably have a timeout
|
||||
to avoid a Character clogging the queue.
|
||||
|
||||
* Mobiles will use "a global ticker system" where they
|
||||
subscribe. They act the same way as PCs in combat, except with a
|
||||
semi- random selection of actions they take (they will probably be
|
||||
more predictable than PCs). Adding aggressive and passive mobiles
|
||||
should be straightforward, as well as un-killable ones (merchants).
|
||||
|
||||
* The inventory of a defeated enemy is automatically transferred to
|
||||
the winner's inventory. If there are many alternative pieces of
|
||||
equipment, they get to keep the weakest one, otherwise it's all
|
||||
transferred. There are no limits to carrying except the fear of
|
||||
losing gear. This should hopefully prevent hoarding of good items.
|
||||
One can give item(s) to another player - that player must then
|
||||
conceed to receiving it (use Y/N module in contrib.menusystem).
|
||||
There is no way of dropping items on the ground; one must either
|
||||
give them away or sell them for gold to a merchant.
|
||||
|
||||
* Gold is used for buying items from merchants, but is also the
|
||||
highscore. Whereas sell prices are fixed, buy prices are not fixed
|
||||
but is based on a percentage of the gold carried, adjusted by the
|
||||
barter skill (this should defeat inflation quite effectively). Items
|
||||
sold to merchants are made available for other players to buy.
|
||||
|
||||
* Death means loosing inventory (except weakest item, as mentioned),
|
||||
but no loss of gold. Otherwise death is cheap - one respawns at a
|
||||
random starting position (probably needing special-aliased rooms to
|
||||
use for this - maybe with one-way exits).
|
||||
|
||||
|
||||
Rough plan for order of implementation
|
||||
--------------------------------------
|
||||
|
||||
1) Conflict resolution system - work out how basic challenges should work, what format ingoing Skills
|
||||
should have and how generic bonuses from attributes and equipment affect things. Make a generic
|
||||
API for it. Try to list all supported plus/minues equipment may offer.
|
||||
2) Using skills - create XP and automatic skill improvement code
|
||||
3) Define new Character typeclass that stores skills/attributes in a way that the conflict
|
||||
system understands. Chars should also have the ability to "wear" things, so some
|
||||
sort of slot system is needed. Gold needs to be stored in a separate variable.
|
||||
4) Create "sell/buy" command stump, for testing the Skill resolution code with fixed on-character values.
|
||||
5) Create Combat queue code for turn-taking combat. Reiterate so that it works with the generic form of
|
||||
Skills and conflict resolution.
|
||||
6) Create all included skills and their associated commands.
|
||||
7) Test commands manually with two PC characters in the Combat queue and in other challenge situations.
|
||||
8) Create loot-creation mechanism based on equipment lists, for spawning semi-random items and gear.
|
||||
9) Create Death-respawn mechanism, including loss of equipment and transfer of same to the winner.
|
||||
10) Create NPC/mobile object runner Script. Use a copy of Character typeclass for mobiles, except some
|
||||
automation hooks and AI states. Tie loot creation to the death of NPCs.
|
||||
11) Test PC vs NPC combat and other challenges.
|
||||
12) Create merchants as interactive, immortal NPCs with the "barter" skill.
|
||||
13) Create Character creation module, for assigning attributes/skills when first starting.
|
||||
Add appropriate commands to ooccmdset.
|
||||
14) Add "give" command to command set. Remove unused commands like "drop" (or make it admin-only). Possibly
|
||||
expand "look" command to allow to look across exits into the next room. Also add "highscore" command for
|
||||
viewing game statistics.
|
||||
15) <starting building of game world>
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
|
||||
ProcPools
|
||||
---------
|
||||
|
||||
This contrib defines a process pool subsystem for Evennia.
|
||||
|
||||
A process pool handles a range of separately running processes that
|
||||
can accept information from the main Evennia process. The pool dynamically
|
||||
grows and shrinks depending on the need (and will queue requests if there
|
||||
are no free slots available).
|
||||
|
||||
The main use of this is to launch long-running, possibly blocking code
|
||||
in a way that will not freeze up the rest of the server. So you could
|
||||
execute time.sleep(10) on the process pool without anyone else on the
|
||||
server noticing anything.
|
||||
|
||||
This folder has the following contents:
|
||||
|
||||
ampoule/ - this is a separate library managing the process pool. You
|
||||
should not need to touch this.
|
||||
|
||||
Python Procpool
|
||||
---------------
|
||||
python_procpool.py - this implements a way to execute arbitrary python
|
||||
code on the procpool. Import run_async() from this
|
||||
module in order to use this functionality in-code
|
||||
(this is a replacement to the in-process run_async
|
||||
found in src.utils.utils).
|
||||
python_procpool_plugin.py - this is a plugin module for the python
|
||||
procpool, to start and add it to the server. Adding it
|
||||
is a single line in your settings file - see the header
|
||||
of the file for more info.
|
||||
|
||||
|
||||
|
||||
Adding other Procpools
|
||||
----------------------
|
||||
To add other types of procpools (such as for executing other remote languages
|
||||
than Python), you can pretty much mimic the layout of python_procpool.py
|
||||
and python_procpool_plugin.py.
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
Copyright (c) 2008
|
||||
Valentino Volonghi
|
||||
Matthew Lefkowitz
|
||||
Copyright (c) 2009 Canonical Ltd.
|
||||
|
||||
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.
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
|
||||
AMPOULE
|
||||
-------
|
||||
|
||||
https://launchpad.net/ampoule
|
||||
|
||||
AMPoule is a process management system using Twisted spawnProcess
|
||||
functionality. It uses AMP to pipe messages to a process Pool that it
|
||||
manages. The service is called ProcPool in Evennia settings.
|
||||
|
||||
AMPoule's very good, but unfortunately the source is very poorly
|
||||
documented. Hence the source in this directory does not comform to
|
||||
Evennia's normally rigid standards - for now we try to edit it as
|
||||
little as possible so as to make it easy to apply upstream updates
|
||||
down the line.
|
||||
|
||||
Changes made by Evennia are minor - it's mainly limiting spam to the
|
||||
log and an added ability to turn this on/off through settings. Most
|
||||
Evennia related code are found in src/server/procpool.py and
|
||||
src/server/server.py.
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
from pool import deferToAMPProcess, pp
|
||||
from commands import Shutdown, Ping, Echo
|
||||
from child import AMPChild
|
||||
__version__ = "0.2.1"
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
"""
|
||||
This defines the the parent for all subprocess children.
|
||||
|
||||
Inherit from this to define a new type of subprocess.
|
||||
|
||||
"""
|
||||
|
||||
from twisted.python import log
|
||||
from twisted.internet import error
|
||||
from twisted.protocols import amp
|
||||
from contrib.procpools.ampoule.commands import Echo, Shutdown, Ping
|
||||
|
||||
class AMPChild(amp.AMP):
|
||||
def __init__(self):
|
||||
super(AMPChild, self).__init__(self)
|
||||
self.shutdown = False
|
||||
|
||||
def connectionLost(self, reason):
|
||||
amp.AMP.connectionLost(self, reason)
|
||||
from twisted.internet import reactor
|
||||
try:
|
||||
reactor.stop()
|
||||
except error.ReactorNotRunning:
|
||||
# woa, this means that something bad happened,
|
||||
# most probably we received a SIGINT. Now this is only
|
||||
# a problem when you use Ctrl+C to stop the main process
|
||||
# because it would send the SIGINT to child processes too.
|
||||
# In all other cases receiving a SIGINT here would be an
|
||||
# error condition and correctly restarted. maybe we should
|
||||
# use sigprocmask?
|
||||
pass
|
||||
if not self.shutdown:
|
||||
# if the shutdown wasn't explicit we presume that it's an
|
||||
# error condition and thus we return a -1 error returncode.
|
||||
import os
|
||||
os._exit(-1)
|
||||
|
||||
def shutdown(self):
|
||||
"""
|
||||
This method is needed to shutdown the child gently without
|
||||
generating an exception.
|
||||
"""
|
||||
#log.msg("Shutdown message received, goodbye.")
|
||||
self.shutdown = True
|
||||
return {}
|
||||
Shutdown.responder(shutdown)
|
||||
|
||||
def ping(self):
|
||||
"""
|
||||
Ping the child and return an answer
|
||||
"""
|
||||
return {'response': "pong"}
|
||||
Ping.responder(ping)
|
||||
|
||||
def echo(self, data):
|
||||
"""
|
||||
Echo some data through the child.
|
||||
"""
|
||||
return {'response': data}
|
||||
Echo.responder(echo)
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
from twisted.protocols import amp
|
||||
|
||||
class Shutdown(amp.Command):
|
||||
responseType = amp.QuitBox
|
||||
|
||||
class Ping(amp.Command):
|
||||
response = [('response', amp.String())]
|
||||
|
||||
class Echo(amp.Command):
|
||||
arguments = [('data', amp.String())]
|
||||
response = [('response', amp.String())]
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
from zope.interface import Interface
|
||||
|
||||
class IStarter(Interface):
|
||||
def startAMPProcess(ampChild, ampParent=None):
|
||||
"""
|
||||
@param ampChild: The AMP protocol spoken by the created child.
|
||||
@type ampChild: L{twisted.protocols.amp.AMP}
|
||||
|
||||
@param ampParent: The AMP protocol spoken by the parent.
|
||||
@type ampParent: L{twisted.protocols.amp.AMP}
|
||||
"""
|
||||
|
||||
def startPythonProcess(prot, *args):
|
||||
"""
|
||||
@param prot: a L{protocol.ProcessProtocol} subclass
|
||||
@type prot: L{protocol.ProcessProtocol}
|
||||
|
||||
@param args: a tuple of arguments that will be passed to the
|
||||
child process.
|
||||
|
||||
@return: a tuple of the child process and the deferred finished.
|
||||
finished triggers when the subprocess dies for any reason.
|
||||
"""
|
||||
|
||||
|
|
@ -1,302 +0,0 @@
|
|||
import os
|
||||
import sys
|
||||
import imp
|
||||
import itertools
|
||||
|
||||
from zope.interface import implements
|
||||
|
||||
from twisted.internet import reactor, protocol, defer, error
|
||||
from twisted.python import log, util, reflect
|
||||
from twisted.protocols import amp
|
||||
from twisted.python import runtime
|
||||
from twisted.python.compat import set
|
||||
|
||||
from contrib.procpools.ampoule import iampoule
|
||||
|
||||
gen = itertools.count()
|
||||
|
||||
if runtime.platform.isWindows():
|
||||
IS_WINDOWS = True
|
||||
TO_CHILD = 0
|
||||
FROM_CHILD = 1
|
||||
else:
|
||||
IS_WINDOWS = False
|
||||
TO_CHILD = 3
|
||||
FROM_CHILD = 4
|
||||
|
||||
class AMPConnector(protocol.ProcessProtocol):
|
||||
"""
|
||||
A L{ProcessProtocol} subclass that can understand and speak AMP.
|
||||
|
||||
@ivar amp: the children AMP process
|
||||
@type amp: L{amp.AMP}
|
||||
|
||||
@ivar finished: a deferred triggered when the process dies.
|
||||
@type finished: L{defer.Deferred}
|
||||
|
||||
@ivar name: Unique name for the connector, much like a pid.
|
||||
@type name: int
|
||||
"""
|
||||
|
||||
def __init__(self, proto, name=None):
|
||||
"""
|
||||
@param proto: An instance or subclass of L{amp.AMP}
|
||||
@type proto: L{amp.AMP}
|
||||
|
||||
@param name: optional name of the subprocess.
|
||||
@type name: int
|
||||
"""
|
||||
self.finished = defer.Deferred()
|
||||
self.amp = proto
|
||||
self.name = name
|
||||
if name is None:
|
||||
self.name = gen.next()
|
||||
|
||||
def signalProcess(self, signalID):
|
||||
"""
|
||||
Send the signal signalID to the child process
|
||||
|
||||
@param signalID: The signal ID that you want to send to the
|
||||
corresponding child
|
||||
@type signalID: C{str} or C{int}
|
||||
"""
|
||||
return self.transport.signalProcess(signalID)
|
||||
|
||||
def connectionMade(self):
|
||||
#log.msg("Subprocess %s started." % (self.name,))
|
||||
self.amp.makeConnection(self)
|
||||
|
||||
# Transport
|
||||
disconnecting = False
|
||||
|
||||
def write(self, data):
|
||||
if IS_WINDOWS:
|
||||
self.transport.write(data)
|
||||
else:
|
||||
self.transport.writeToChild(TO_CHILD, data)
|
||||
|
||||
def loseConnection(self):
|
||||
self.transport.closeChildFD(TO_CHILD)
|
||||
self.transport.closeChildFD(FROM_CHILD)
|
||||
self.transport.loseConnection()
|
||||
|
||||
def getPeer(self):
|
||||
return ('subprocess %i' % self.name,)
|
||||
|
||||
def getHost(self):
|
||||
return ('Evennia Server',)
|
||||
|
||||
def childDataReceived(self, childFD, data):
|
||||
if childFD == FROM_CHILD:
|
||||
self.amp.dataReceived(data)
|
||||
return
|
||||
self.errReceived(data)
|
||||
|
||||
def errReceived(self, data):
|
||||
for line in data.strip().splitlines():
|
||||
log.msg("FROM %s: %s" % (self.name, line))
|
||||
|
||||
def processEnded(self, status):
|
||||
#log.msg("Process: %s ended" % (self.name,))
|
||||
self.amp.connectionLost(status)
|
||||
if status.check(error.ProcessDone):
|
||||
self.finished.callback('')
|
||||
return
|
||||
self.finished.errback(status)
|
||||
|
||||
BOOTSTRAP = """\
|
||||
import sys
|
||||
|
||||
def main(reactor, ampChildPath):
|
||||
from twisted.application import reactors
|
||||
reactors.installReactor(reactor)
|
||||
|
||||
from twisted.python import log
|
||||
%s
|
||||
|
||||
from twisted.internet import reactor, stdio
|
||||
from twisted.python import reflect, runtime
|
||||
|
||||
ampChild = reflect.namedAny(ampChildPath)
|
||||
ampChildInstance = ampChild(*sys.argv[1:-2])
|
||||
if runtime.platform.isWindows():
|
||||
stdio.StandardIO(ampChildInstance)
|
||||
else:
|
||||
stdio.StandardIO(ampChildInstance, %s, %s)
|
||||
enter = getattr(ampChildInstance, '__enter__', None)
|
||||
if enter is not None:
|
||||
enter()
|
||||
try:
|
||||
reactor.run()
|
||||
except:
|
||||
if enter is not None:
|
||||
info = sys.exc_info()
|
||||
if not ampChildInstance.__exit__(*info):
|
||||
raise
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
if enter is not None:
|
||||
ampChildInstance.__exit__(None, None, None)
|
||||
|
||||
main(sys.argv[-2], sys.argv[-1])
|
||||
""" % ('%s', TO_CHILD, FROM_CHILD)
|
||||
|
||||
# in the first spot above, either insert an empty string or
|
||||
# 'log.startLogging(sys.stderr)'
|
||||
# to start logging
|
||||
|
||||
class ProcessStarter(object):
|
||||
|
||||
implements(iampoule.IStarter)
|
||||
|
||||
connectorFactory = AMPConnector
|
||||
def __init__(self, bootstrap=BOOTSTRAP, args=(), env={},
|
||||
path=None, uid=None, gid=None, usePTY=0,
|
||||
packages=(), childReactor="select"):
|
||||
"""
|
||||
@param bootstrap: Startup code for the child process
|
||||
@type bootstrap: C{str}
|
||||
|
||||
@param args: Arguments that should be supplied to every child
|
||||
created.
|
||||
@type args: C{tuple} of C{str}
|
||||
|
||||
@param env: Environment variables that should be present in the
|
||||
child environment
|
||||
@type env: C{dict}
|
||||
|
||||
@param path: Path in which to run the child
|
||||
@type path: C{str}
|
||||
|
||||
@param uid: if defined, the uid used to run the new process.
|
||||
@type uid: C{int}
|
||||
|
||||
@param gid: if defined, the gid used to run the new process.
|
||||
@type gid: C{int}
|
||||
|
||||
@param usePTY: Should the child processes use PTY processes
|
||||
@type usePTY: 0 or 1
|
||||
|
||||
@param packages: A tuple of packages that should be guaranteed
|
||||
to be importable in the child processes
|
||||
@type packages: C{tuple} of C{str}
|
||||
|
||||
@param childReactor: a string that sets the reactor for child
|
||||
processes
|
||||
@type childReactor: C{str}
|
||||
"""
|
||||
self.bootstrap = bootstrap
|
||||
self.args = args
|
||||
self.env = env
|
||||
self.path = path
|
||||
self.uid = uid
|
||||
self.gid = gid
|
||||
self.usePTY = usePTY
|
||||
self.packages = ("ampoule",) + packages
|
||||
self.packages = packages
|
||||
self.childReactor = childReactor
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
Represent the ProcessStarter with a string.
|
||||
"""
|
||||
return """ProcessStarter(bootstrap=%r,
|
||||
args=%r,
|
||||
env=%r,
|
||||
path=%r,
|
||||
uid=%r,
|
||||
gid=%r,
|
||||
usePTY=%r,
|
||||
packages=%r,
|
||||
childReactor=%r)""" % (self.bootstrap,
|
||||
self.args,
|
||||
self.env,
|
||||
self.path,
|
||||
self.uid,
|
||||
self.gid,
|
||||
self.usePTY,
|
||||
self.packages,
|
||||
self.childReactor)
|
||||
|
||||
def _checkRoundTrip(self, obj):
|
||||
"""
|
||||
Make sure that an object will properly round-trip through 'qual' and
|
||||
'namedAny'.
|
||||
|
||||
Raise a L{RuntimeError} if they aren't.
|
||||
"""
|
||||
tripped = reflect.namedAny(reflect.qual(obj))
|
||||
if tripped is not obj:
|
||||
raise RuntimeError("importing %r is not the same as %r" %
|
||||
(reflect.qual(obj), obj))
|
||||
|
||||
def startAMPProcess(self, ampChild, ampParent=None, ampChildArgs=()):
|
||||
"""
|
||||
@param ampChild: a L{ampoule.child.AMPChild} subclass.
|
||||
@type ampChild: L{ampoule.child.AMPChild}
|
||||
|
||||
@param ampParent: an L{amp.AMP} subclass that implements the parent
|
||||
protocol for this process pool
|
||||
@type ampParent: L{amp.AMP}
|
||||
"""
|
||||
self._checkRoundTrip(ampChild)
|
||||
fullPath = reflect.qual(ampChild)
|
||||
if ampParent is None:
|
||||
ampParent = amp.AMP
|
||||
prot = self.connectorFactory(ampParent())
|
||||
args = ampChildArgs + (self.childReactor, fullPath)
|
||||
return self.startPythonProcess(prot, *args)
|
||||
|
||||
|
||||
def startPythonProcess(self, prot, *args):
|
||||
"""
|
||||
@param prot: a L{protocol.ProcessProtocol} subclass
|
||||
@type prot: L{protocol.ProcessProtocol}
|
||||
|
||||
@param args: a tuple of arguments that will be added after the
|
||||
ones in L{self.args} to start the child process.
|
||||
|
||||
@return: a tuple of the child process and the deferred finished.
|
||||
finished triggers when the subprocess dies for any reason.
|
||||
"""
|
||||
spawnProcess(prot, self.bootstrap, self.args+args, env=self.env,
|
||||
path=self.path, uid=self.uid, gid=self.gid,
|
||||
usePTY=self.usePTY, packages=self.packages)
|
||||
|
||||
# XXX: we could wait for startup here, but ... is there really any
|
||||
# reason to? the pipe should be ready for writing. The subprocess
|
||||
# might not start up properly, but then, a subprocess might shut down
|
||||
# at any point too. So we just return amp and have this piece to be
|
||||
# synchronous.
|
||||
return prot.amp, prot.finished
|
||||
|
||||
def spawnProcess(processProtocol, bootstrap, args=(), env={},
|
||||
path=None, uid=None, gid=None, usePTY=0,
|
||||
packages=()):
|
||||
env = env.copy()
|
||||
|
||||
pythonpath = []
|
||||
for pkg in packages:
|
||||
pkg_path, name = os.path.split(pkg)
|
||||
p = os.path.split(imp.find_module(name, [pkg_path] if pkg_path else None)[1])[0]
|
||||
if p.startswith(os.path.join(sys.prefix, 'lib')):
|
||||
continue
|
||||
pythonpath.append(p)
|
||||
pythonpath = list(set(pythonpath))
|
||||
pythonpath.extend(env.get('PYTHONPATH', '').split(os.pathsep))
|
||||
env['PYTHONPATH'] = os.pathsep.join(pythonpath)
|
||||
args = (sys.executable, '-c', bootstrap) + args
|
||||
# childFDs variable is needed because sometimes child processes
|
||||
# misbehave and use stdout to output stuff that should really go
|
||||
# to stderr. Of course child process might even use the wrong FDs
|
||||
# that I'm using here, 3 and 4, so we are going to fix all these
|
||||
# issues when I add support for the configuration object that can
|
||||
# fix this stuff in a more configurable way.
|
||||
if IS_WINDOWS:
|
||||
return reactor.spawnProcess(processProtocol, sys.executable, args,
|
||||
env, path, uid, gid, usePTY)
|
||||
else:
|
||||
return reactor.spawnProcess(processProtocol, sys.executable, args,
|
||||
env, path, uid, gid, usePTY,
|
||||
childFDs={0:"w", 1:"r", 2:"r", 3:"w", 4:"r"})
|
||||
|
|
@ -1,414 +0,0 @@
|
|||
import time
|
||||
import random
|
||||
import heapq
|
||||
import itertools
|
||||
import signal
|
||||
choice = random.choice
|
||||
now = time.time
|
||||
count = itertools.count().next
|
||||
pop = heapq.heappop
|
||||
|
||||
from twisted.internet import defer, task, error
|
||||
from twisted.python import log, failure
|
||||
|
||||
from contrib.procpools.ampoule import commands, main
|
||||
|
||||
try:
|
||||
DIE = signal.SIGKILL
|
||||
except AttributeError:
|
||||
# Windows doesn't have SIGKILL, let's just use SIGTERM then
|
||||
DIE = signal.SIGTERM
|
||||
|
||||
class ProcessPool(object):
|
||||
"""
|
||||
This class generalizes the functionality of a pool of
|
||||
processes to which work can be dispatched.
|
||||
|
||||
@ivar finished: Boolean flag, L{True} when the pool is finished.
|
||||
|
||||
@ivar started: Boolean flag, L{True} when the pool is started.
|
||||
|
||||
@ivar name: Optional name for the process pool
|
||||
|
||||
@ivar min: Minimum number of subprocesses to set up
|
||||
|
||||
@ivar max: Maximum number of subprocesses to set up
|
||||
|
||||
@ivar maxIdle: Maximum number of seconds of indleness in a child
|
||||
|
||||
@ivar starter: A process starter instance that provides
|
||||
L{iampoule.IStarter}.
|
||||
|
||||
@ivar recycleAfter: Maximum number of calls before restarting a
|
||||
subprocess, 0 to not recycle.
|
||||
|
||||
@ivar ampChild: The child AMP protocol subclass with the commands
|
||||
that the child should implement.
|
||||
|
||||
@ivar ampParent: The parent AMP protocol subclass with the commands
|
||||
that the parent should implement.
|
||||
|
||||
@ivar timeout: The general timeout (in seconds) for every child
|
||||
process call.
|
||||
"""
|
||||
|
||||
finished = False
|
||||
started = False
|
||||
name = None
|
||||
|
||||
def __init__(self, ampChild=None, ampParent=None, min=5, max=20,
|
||||
name=None, maxIdle=20, recycleAfter=500, starter=None,
|
||||
timeout=None, timeout_signal=DIE, ampChildArgs=()):
|
||||
self.starter = starter
|
||||
self.ampChildArgs = tuple(ampChildArgs)
|
||||
if starter is None:
|
||||
self.starter = main.ProcessStarter(packages=("twisted", "ampoule"))
|
||||
self.ampParent = ampParent
|
||||
self.ampChild = ampChild
|
||||
if ampChild is None:
|
||||
from contrib.procpools.ampoule.child import AMPChild
|
||||
self.ampChild = AMPChild
|
||||
self.min = min
|
||||
self.max = max
|
||||
self.name = name
|
||||
self.maxIdle = maxIdle
|
||||
self.recycleAfter = recycleAfter
|
||||
self.timeout = timeout
|
||||
self.timeout_signal = timeout_signal
|
||||
self._queue = []
|
||||
|
||||
self.processes = set()
|
||||
self.ready = set()
|
||||
self.busy = set()
|
||||
self._finishCallbacks = {}
|
||||
self._lastUsage = {}
|
||||
self._calls = {}
|
||||
self.looping = task.LoopingCall(self._pruneProcesses)
|
||||
self.looping.start(maxIdle, now=False)
|
||||
|
||||
def start(self, ampChild=None):
|
||||
"""
|
||||
Starts the ProcessPool with a given child protocol.
|
||||
|
||||
@param ampChild: a L{ampoule.child.AMPChild} subclass.
|
||||
@type ampChild: L{ampoule.child.AMPChild} subclass
|
||||
"""
|
||||
if ampChild is not None and not self.started:
|
||||
self.ampChild = ampChild
|
||||
self.finished = False
|
||||
self.started = True
|
||||
return self.adjustPoolSize()
|
||||
|
||||
def _pruneProcesses(self):
|
||||
"""
|
||||
Remove idle processes from the pool.
|
||||
"""
|
||||
n = now()
|
||||
d = []
|
||||
for child, lastUse in self._lastUsage.iteritems():
|
||||
if len(self.processes) > self.min and (n - lastUse) > self.maxIdle:
|
||||
# we are setting lastUse when processing finishes, it
|
||||
# might be processing right now
|
||||
if child not in self.busy:
|
||||
# we need to remove this child from the ready set
|
||||
# and the processes set because otherwise it might
|
||||
# get calls from doWork
|
||||
self.ready.discard(child)
|
||||
self.processes.discard(child)
|
||||
d.append(self.stopAWorker(child))
|
||||
return defer.DeferredList(d)
|
||||
|
||||
def _pruneProcess(self, child):
|
||||
"""
|
||||
Remove every trace of the process from this instance.
|
||||
"""
|
||||
self.processes.discard(child)
|
||||
self.ready.discard(child)
|
||||
self.busy.discard(child)
|
||||
self._lastUsage.pop(child, None)
|
||||
self._calls.pop(child, None)
|
||||
self._finishCallbacks.pop(child, None)
|
||||
|
||||
def _addProcess(self, child, finished):
|
||||
"""
|
||||
Adds the newly created child process to the pool.
|
||||
"""
|
||||
def restart(child, reason):
|
||||
#log.msg("FATAL: Restarting after %s" % (reason,))
|
||||
self._pruneProcess(child)
|
||||
return self.startAWorker()
|
||||
|
||||
def dieGently(data, child):
|
||||
#log.msg("STOPPING: '%s'" % (data,))
|
||||
self._pruneProcess(child)
|
||||
|
||||
self.processes.add(child)
|
||||
self.ready.add(child)
|
||||
finished.addCallback(dieGently, child
|
||||
).addErrback(lambda reason: restart(child, reason))
|
||||
self._finishCallbacks[child] = finished
|
||||
self._lastUsage[child] = now()
|
||||
self._calls[child] = 0
|
||||
self._catchUp()
|
||||
|
||||
def _catchUp(self):
|
||||
"""
|
||||
If there are queued items in the list then run them.
|
||||
"""
|
||||
if self._queue:
|
||||
_, (d, command, kwargs) = pop(self._queue)
|
||||
self._cb_doWork(command, **kwargs).chainDeferred(d)
|
||||
|
||||
def _handleTimeout(self, child):
|
||||
"""
|
||||
One of the children went timeout, we need to deal with it
|
||||
|
||||
@param child: The child process
|
||||
@type child: L{child.AMPChild}
|
||||
"""
|
||||
try:
|
||||
child.transport.signalProcess(self.timeout_signal)
|
||||
except error.ProcessExitedAlready:
|
||||
# don't do anything then... we are too late
|
||||
# or we were too early to call
|
||||
pass
|
||||
|
||||
def startAWorker(self):
|
||||
"""
|
||||
Start a worker and set it up in the system.
|
||||
"""
|
||||
if self.finished:
|
||||
# this is a race condition: basically if we call self.stop()
|
||||
# while a process is being recycled what happens is that the
|
||||
# process will be created anyway. By putting a check for
|
||||
# self.finished here we make sure that in no way we are creating
|
||||
# processes when the pool is stopped.
|
||||
# The race condition comes from the fact that:
|
||||
# stopAWorker() is asynchronous while stop() is synchronous.
|
||||
# so if you call:
|
||||
# pp.stopAWorker(child).addCallback(lambda _: pp.startAWorker())
|
||||
# pp.stop()
|
||||
# You might end up with a dirty reactor due to the stop()
|
||||
# returning before the new process is created.
|
||||
return
|
||||
startAMPProcess = self.starter.startAMPProcess
|
||||
child, finished = startAMPProcess(self.ampChild,
|
||||
ampParent=self.ampParent,
|
||||
ampChildArgs=self.ampChildArgs)
|
||||
return self._addProcess(child, finished)
|
||||
|
||||
def _cb_doWork(self, command, _timeout=None, _deadline=None,
|
||||
**kwargs):
|
||||
"""
|
||||
Go and call the command.
|
||||
|
||||
@param command: The L{amp.Command} to be executed in the child
|
||||
@type command: L{amp.Command}
|
||||
|
||||
@param _d: The deferred for the calling code.
|
||||
@type _d: L{defer.Deferred}
|
||||
|
||||
@param _timeout: The timeout for this call only
|
||||
@type _timeout: C{int}
|
||||
@param _deadline: The deadline for this call only
|
||||
@type _deadline: C{int}
|
||||
"""
|
||||
timeoutCall = None
|
||||
deadlineCall = None
|
||||
|
||||
def _returned(result, child, is_error=False):
|
||||
def cancelCall(call):
|
||||
if call is not None and call.active():
|
||||
call.cancel()
|
||||
cancelCall(timeoutCall)
|
||||
cancelCall(deadlineCall)
|
||||
self.busy.discard(child)
|
||||
if not die:
|
||||
# we are not marked to be removed, so add us back to
|
||||
# the ready set and let's see if there's some catching
|
||||
# up to do
|
||||
self.ready.add(child)
|
||||
self._catchUp()
|
||||
else:
|
||||
# We should die and we do, then we start a new worker
|
||||
# to pick up stuff from the queue otherwise we end up
|
||||
# without workers and the queue will remain there.
|
||||
self.stopAWorker(child).addCallback(lambda _: self.startAWorker())
|
||||
self._lastUsage[child] = now()
|
||||
# we can't do recycling here because it's too late and
|
||||
# the process might have received tons of calls already
|
||||
# which would make it run more calls than what is
|
||||
# configured to do.
|
||||
return result
|
||||
|
||||
die = False
|
||||
child = self.ready.pop()
|
||||
self.busy.add(child)
|
||||
self._calls[child] += 1
|
||||
|
||||
# Let's see if this call goes over the recycling barrier
|
||||
if self.recycleAfter and self._calls[child] >= self.recycleAfter:
|
||||
# it does so mark this child, using a closure, to be
|
||||
# removed at the end of the call.
|
||||
die = True
|
||||
|
||||
# If the command doesn't require a response then callRemote
|
||||
# returns nothing, so we prepare for that too.
|
||||
# We also need to guard against timeout errors for child
|
||||
# and local timeout parameter overrides the global one
|
||||
if _timeout == 0:
|
||||
timeout = _timeout
|
||||
else:
|
||||
timeout = _timeout or self.timeout
|
||||
|
||||
if timeout is not None:
|
||||
from twisted.internet import reactor
|
||||
timeoutCall = reactor.callLater(timeout, self._handleTimeout, child)
|
||||
|
||||
if _deadline is not None:
|
||||
from twisted.internet import reactor
|
||||
delay = max(0, _deadline - reactor.seconds())
|
||||
deadlineCall = reactor.callLater(delay, self._handleTimeout,
|
||||
child)
|
||||
|
||||
return defer.maybeDeferred(child.callRemote, command, **kwargs
|
||||
).addCallback(_returned, child
|
||||
).addErrback(_returned, child, is_error=True)
|
||||
|
||||
def callRemote(self, *args, **kwargs):
|
||||
"""
|
||||
Proxy call to keep the API homogeneous across twisted's RPCs
|
||||
"""
|
||||
return self.doWork(*args, **kwargs)
|
||||
|
||||
def doWork(self, command, **kwargs):
|
||||
"""
|
||||
Sends the command to one child.
|
||||
|
||||
@param command: an L{amp.Command} type object.
|
||||
@type command: L{amp.Command}
|
||||
|
||||
@param kwargs: dictionary containing the arguments for the command.
|
||||
"""
|
||||
if self.ready: # there are unused processes, let's use them
|
||||
return self._cb_doWork(command, **kwargs)
|
||||
else:
|
||||
if len(self.processes) < self.max:
|
||||
# no unused but we can start some new ones
|
||||
# since startAWorker is synchronous we won't have a
|
||||
# race condition here in case of multiple calls to
|
||||
# doWork, so we will end up in the else clause in case
|
||||
# of such calls:
|
||||
# Process pool with min=1, max=1, recycle_after=1
|
||||
# [call(Command) for x in xrange(BIG_NUMBER)]
|
||||
self.startAWorker()
|
||||
return self._cb_doWork(command, **kwargs)
|
||||
else:
|
||||
# No one is free... just queue up and wait for a process
|
||||
# to start and pick up the first item in the queue.
|
||||
d = defer.Deferred()
|
||||
self._queue.append((count(), (d, command, kwargs)))
|
||||
return d
|
||||
|
||||
def stopAWorker(self, child=None):
|
||||
"""
|
||||
Gently stop a child so that it's not restarted anymore
|
||||
|
||||
@param command: an L{ampoule.child.AmpChild} type object.
|
||||
@type command: L{ampoule.child.AmpChild} or None
|
||||
|
||||
"""
|
||||
if child is None:
|
||||
if self.ready:
|
||||
child = self.ready.pop()
|
||||
else:
|
||||
child = choice(list(self.processes))
|
||||
child.callRemote(commands.Shutdown
|
||||
# This is needed for timeout handling, the reason is pretty hard
|
||||
# to explain but I'll try to:
|
||||
# There's another small race condition in the system. If the
|
||||
# child process is shut down by a signal and you try to stop
|
||||
# the process pool immediately afterwards, like tests would do,
|
||||
# the child AMP object would still be in the system and trying
|
||||
# to call the command Shutdown on it would result in the same
|
||||
# errback that we got originally, for this reason we need to
|
||||
# trap it now so that it doesn't raise by not being handled.
|
||||
# Does this even make sense to you?
|
||||
).addErrback(lambda reason: reason.trap(error.ProcessTerminated))
|
||||
return self._finishCallbacks[child]
|
||||
|
||||
def _startSomeWorkers(self):
|
||||
"""
|
||||
Start a bunch of workers until we reach the max number of them.
|
||||
"""
|
||||
if len(self.processes) < self.max:
|
||||
self.startAWorker()
|
||||
|
||||
def adjustPoolSize(self, min=None, max=None):
|
||||
"""
|
||||
Change the pool size to be at least min and less than max,
|
||||
useful when you change the values of max and min in the instance
|
||||
and you want the pool to adapt to them.
|
||||
"""
|
||||
if min is None:
|
||||
min = self.min
|
||||
if max is None:
|
||||
max = self.max
|
||||
|
||||
assert min >= 0, 'minimum is negative'
|
||||
assert min <= max, 'minimum is greater than maximum'
|
||||
|
||||
self.min = min
|
||||
self.max = max
|
||||
|
||||
l = []
|
||||
if self.started:
|
||||
|
||||
for i in xrange(len(self.processes)-self.max):
|
||||
l.append(self.stopAWorker())
|
||||
while len(self.processes) < self.min:
|
||||
self.startAWorker()
|
||||
|
||||
return defer.DeferredList(l)#.addCallback(lambda _: self.dumpStats())
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stops the process protocol.
|
||||
"""
|
||||
self.finished = True
|
||||
l = [self.stopAWorker(process) for process in self.processes]
|
||||
def _cb(_):
|
||||
if self.looping.running:
|
||||
self.looping.stop()
|
||||
|
||||
return defer.DeferredList(l).addCallback(_cb)
|
||||
|
||||
def dumpStats(self):
|
||||
log.msg("ProcessPool stats:")
|
||||
log.msg('\tworkers: %s' % len(self.processes))
|
||||
log.msg('\ttimeout: %s' % (self.timeout))
|
||||
log.msg('\tparent: %r' % (self.ampParent,))
|
||||
log.msg('\tchild: %r' % (self.ampChild,))
|
||||
log.msg('\tmax idle: %r' % (self.maxIdle,))
|
||||
log.msg('\trecycle after: %r' % (self.recycleAfter,))
|
||||
log.msg('\tProcessStarter:')
|
||||
log.msg('\t\t%r' % (self.starter,))
|
||||
|
||||
pp = None
|
||||
|
||||
def deferToAMPProcess(command, **kwargs):
|
||||
"""
|
||||
Helper function that sends a command to the default process pool
|
||||
and returns a deferred that fires when the result of the
|
||||
subprocess computation is ready.
|
||||
|
||||
@param command: an L{amp.Command} subclass
|
||||
@param kwargs: dictionary containing the arguments for the command.
|
||||
|
||||
@return: a L{defer.Deferred} with the data from the subprocess.
|
||||
"""
|
||||
global pp
|
||||
if pp is None:
|
||||
pp = ProcessPool()
|
||||
return pp.start().addCallback(lambda _: pp.doWork(command, **kwargs))
|
||||
return pp.doWork(command, **kwargs)
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
"""
|
||||
This module implements a remote pool to use with AMP.
|
||||
"""
|
||||
|
||||
from twisted.protocols import amp
|
||||
|
||||
class AMPProxy(amp.AMP):
|
||||
"""
|
||||
A Proxy AMP protocol that forwards calls to a wrapped
|
||||
callRemote-like callable.
|
||||
"""
|
||||
def __init__(self, wrapped, child):
|
||||
"""
|
||||
@param wrapped: A callRemote-like callable that takes an
|
||||
L{amp.Command} as first argument and other
|
||||
optional keyword arguments afterwards.
|
||||
@type wrapped: L{callable}.
|
||||
|
||||
@param child: The protocol class of the process pool children.
|
||||
Used to forward only the methods that are actually
|
||||
understood correctly by them.
|
||||
@type child: L{amp.AMP}
|
||||
"""
|
||||
amp.AMP.__init__(self)
|
||||
self.wrapped = wrapped
|
||||
self.child = child
|
||||
|
||||
localCd = set(self._commandDispatch.keys())
|
||||
childCd = set(self.child._commandDispatch.keys())
|
||||
assert localCd.intersection(childCd) == set(["StartTLS"]), \
|
||||
"Illegal method overriding in Proxy"
|
||||
|
||||
def locateResponder(self, name):
|
||||
"""
|
||||
This is a custom locator to forward calls to the children
|
||||
processes while keeping the ProcessPool a transparent MITM.
|
||||
|
||||
This way of working has a few limitations, the first of which
|
||||
is the fact that children won't be able to take advantage of
|
||||
any dynamic locator except for the default L{CommandLocator}
|
||||
that is based on the _commandDispatch attribute added by the
|
||||
metaclass. This limitation might be lifted in the future.
|
||||
"""
|
||||
if name == "StartTLS":
|
||||
# This is a special case where the proxy takes precedence
|
||||
return amp.AMP.locateResponder(self, "StartTLS")
|
||||
|
||||
# Get the dict of commands from the child AMP implementation.
|
||||
cd = self.child._commandDispatch
|
||||
if name in cd:
|
||||
# If the command is there, then we forward stuff to it.
|
||||
commandClass, _responderFunc = cd[name]
|
||||
# We need to wrap the doWork function because the wrapping
|
||||
# call doesn't pass the command as first argument since it
|
||||
# thinks that we are the actual receivers and callable is
|
||||
# already the responder while it isn't.
|
||||
doWork = lambda **kw: self.wrapped(commandClass, **kw)
|
||||
# Now let's call the right function and wrap the result
|
||||
# dictionary.
|
||||
return self._wrapWithSerialization(doWork, commandClass)
|
||||
# of course if the name of the command is not in the child it
|
||||
# means that it might be in this class, so fallback to the
|
||||
# default behavior of this module.
|
||||
return amp.AMP.locateResponder(self, name)
|
||||
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
import os
|
||||
|
||||
from twisted.application import service
|
||||
from twisted.internet.protocol import ServerFactory
|
||||
|
||||
def makeService(options):
|
||||
"""
|
||||
Create the service for the application
|
||||
"""
|
||||
ms = service.MultiService()
|
||||
|
||||
from contrib.procpools.ampoule.pool import ProcessPool
|
||||
from contrib.procpools.ampoule.main import ProcessStarter
|
||||
name = options['name']
|
||||
ampport = options['ampport']
|
||||
ampinterface = options['ampinterface']
|
||||
child = options['child']
|
||||
parent = options['parent']
|
||||
min = options['min']
|
||||
max = options['max']
|
||||
maxIdle = options['max_idle']
|
||||
recycle = options['recycle']
|
||||
childReactor = options['reactor']
|
||||
timeout = options['timeout']
|
||||
|
||||
starter = ProcessStarter(packages=("twisted", "ampoule"), childReactor=childReactor)
|
||||
pp = ProcessPool(child, parent, min, max, name, maxIdle, recycle, starter, timeout)
|
||||
svc = AMPouleService(pp, child, ampport, ampinterface)
|
||||
svc.setServiceParent(ms)
|
||||
|
||||
return ms
|
||||
|
||||
class AMPouleService(service.Service):
|
||||
def __init__(self, pool, child, port, interface):
|
||||
self.pool = pool
|
||||
self.port = port
|
||||
self.child = child
|
||||
self.interface = interface
|
||||
self.server = None
|
||||
|
||||
def startService(self):
|
||||
"""
|
||||
Before reactor.run() is called we setup the system.
|
||||
"""
|
||||
service.Service.startService(self)
|
||||
from contrib.procpools.ampoule import rpool
|
||||
from twisted.internet import reactor
|
||||
|
||||
try:
|
||||
factory = ServerFactory()
|
||||
factory.protocol = lambda: rpool.AMPProxy(wrapped=self.pool.doWork,
|
||||
child=self.child)
|
||||
self.server = reactor.listenTCP(self.port,
|
||||
factory,
|
||||
interface=self.interface)
|
||||
# this is synchronous when it's the startup, even though
|
||||
# it returns a deferred. But we need to run it after the
|
||||
# first cycle in order to wait for signal handlers to be
|
||||
# installed.
|
||||
reactor.callLater(0, self.pool.start)
|
||||
except:
|
||||
import traceback
|
||||
print traceback.format_exc()
|
||||
|
||||
def stopService(self):
|
||||
service.Service.stopService(self)
|
||||
if self.server is not None:
|
||||
self.server.stopListening()
|
||||
return self.pool.stop()
|
||||
|
|
@ -1,867 +0,0 @@
|
|||
|
||||
from signal import SIGHUP
|
||||
import math
|
||||
import os
|
||||
import os.path
|
||||
from cStringIO import StringIO as sio
|
||||
import tempfile
|
||||
|
||||
from twisted.internet import error, defer, reactor
|
||||
from twisted.python import failure, reflect
|
||||
from twisted.trial import unittest
|
||||
from twisted.protocols import amp
|
||||
from contrib.procpools.ampoule import main, child, commands, pool
|
||||
|
||||
class ShouldntHaveBeenCalled(Exception):
|
||||
pass
|
||||
|
||||
def _raise(_):
|
||||
raise ShouldntHaveBeenCalled(_)
|
||||
|
||||
class _FakeT(object):
|
||||
closeStdinCalled = False
|
||||
def __init__(self, s):
|
||||
self.s = s
|
||||
|
||||
def closeStdin(self):
|
||||
self.closeStdinCalled = True
|
||||
|
||||
def write(self, data):
|
||||
self.s.write(data)
|
||||
|
||||
class FakeAMP(object):
|
||||
connector = None
|
||||
reason = None
|
||||
def __init__(self, s):
|
||||
self.s = s
|
||||
|
||||
def makeConnection(self, connector):
|
||||
if self.connector is not None:
|
||||
raise Exception("makeConnection called twice")
|
||||
self.connector = connector
|
||||
|
||||
def connectionLost(self, reason):
|
||||
if self.reason is not None:
|
||||
raise Exception("connectionLost called twice")
|
||||
self.reason = reason
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.s.write(data)
|
||||
|
||||
class Ping(amp.Command):
|
||||
arguments = [('data', amp.String())]
|
||||
response = [('response', amp.String())]
|
||||
|
||||
class Pong(amp.Command):
|
||||
arguments = [('data', amp.String())]
|
||||
response = [('response', amp.String())]
|
||||
|
||||
class Pid(amp.Command):
|
||||
response = [('pid', amp.Integer())]
|
||||
|
||||
class Reactor(amp.Command):
|
||||
response = [('classname', amp.String())]
|
||||
|
||||
class NoResponse(amp.Command):
|
||||
arguments = [('arg', amp.String())]
|
||||
requiresAnswer = False
|
||||
|
||||
class GetResponse(amp.Command):
|
||||
response = [("response", amp.String())]
|
||||
|
||||
class Child(child.AMPChild):
|
||||
def ping(self, data):
|
||||
return self.callRemote(Pong, data=data)
|
||||
Ping.responder(ping)
|
||||
|
||||
class PidChild(child.AMPChild):
|
||||
def pid(self):
|
||||
import os
|
||||
return {'pid': os.getpid()}
|
||||
Pid.responder(pid)
|
||||
|
||||
class NoResponseChild(child.AMPChild):
|
||||
_set = False
|
||||
def noresponse(self, arg):
|
||||
self._set = arg
|
||||
return {}
|
||||
NoResponse.responder(noresponse)
|
||||
|
||||
def getresponse(self):
|
||||
return {"response": self._set}
|
||||
GetResponse.responder(getresponse)
|
||||
|
||||
class ReactorChild(child.AMPChild):
|
||||
def reactor(self):
|
||||
from twisted.internet import reactor
|
||||
return {'classname': reactor.__class__.__name__}
|
||||
Reactor.responder(reactor)
|
||||
|
||||
class First(amp.Command):
|
||||
arguments = [('data', amp.String())]
|
||||
response = [('response', amp.String())]
|
||||
|
||||
class Second(amp.Command):
|
||||
pass
|
||||
|
||||
class WaitingChild(child.AMPChild):
|
||||
deferred = None
|
||||
def first(self, data):
|
||||
self.deferred = defer.Deferred()
|
||||
return self.deferred.addCallback(lambda _: {'response': data})
|
||||
First.responder(first)
|
||||
def second(self):
|
||||
self.deferred.callback('')
|
||||
return {}
|
||||
Second.responder(second)
|
||||
|
||||
class Die(amp.Command):
|
||||
pass
|
||||
|
||||
class BadChild(child.AMPChild):
|
||||
def die(self):
|
||||
self.shutdown = False
|
||||
self.transport.loseConnection()
|
||||
return {}
|
||||
Die.responder(die)
|
||||
|
||||
|
||||
class Write(amp.Command):
|
||||
response = [("response", amp.String())]
|
||||
pass
|
||||
|
||||
|
||||
class Writer(child.AMPChild):
|
||||
|
||||
def __init__(self, data='hello'):
|
||||
child.AMPChild.__init__(self)
|
||||
self.data = data
|
||||
|
||||
def write(self):
|
||||
return {'response': self.data}
|
||||
Write.responder(write)
|
||||
|
||||
|
||||
class GetCWD(amp.Command):
|
||||
|
||||
response = [("cwd", amp.String())]
|
||||
|
||||
|
||||
class TempDirChild(child.AMPChild):
|
||||
|
||||
def __init__(self, directory=None):
|
||||
child.AMPChild.__init__(self)
|
||||
self.directory = directory
|
||||
|
||||
def __enter__(self):
|
||||
directory = tempfile.mkdtemp()
|
||||
os.chdir(directory)
|
||||
if self.directory is not None:
|
||||
os.mkdir(self.directory)
|
||||
os.chdir(self.directory)
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
cwd = os.getcwd()
|
||||
os.chdir('..')
|
||||
os.rmdir(cwd)
|
||||
|
||||
def getcwd(self):
|
||||
return {'cwd': os.getcwd()}
|
||||
GetCWD.responder(getcwd)
|
||||
|
||||
|
||||
class TestAMPConnector(unittest.TestCase):
|
||||
def setUp(self):
|
||||
"""
|
||||
The only reason why this method exists is to let 'trial ampoule'
|
||||
to install the signal handlers (#3178 for reference).
|
||||
"""
|
||||
super(TestAMPConnector, self).setUp()
|
||||
d = defer.Deferred()
|
||||
reactor.callLater(0, d.callback, None)
|
||||
return d
|
||||
|
||||
def _makeConnector(self, s, sa):
|
||||
a = FakeAMP(sa)
|
||||
ac = main.AMPConnector(a)
|
||||
assert ac.name is not None
|
||||
ac.transport = _FakeT(s)
|
||||
return ac
|
||||
|
||||
def test_protocol(self):
|
||||
"""
|
||||
Test that outReceived writes to AMP and that it triggers the
|
||||
finished deferred once the process ended.
|
||||
"""
|
||||
s = sio()
|
||||
sa = sio()
|
||||
ac = self._makeConnector(s, sa)
|
||||
|
||||
for x in xrange(99):
|
||||
ac.childDataReceived(4, str(x))
|
||||
|
||||
ac.processEnded(failure.Failure(error.ProcessDone(0)))
|
||||
return ac.finished.addCallback(
|
||||
lambda _: self.assertEqual(sa.getvalue(), ''.join(str(x) for x in xrange(99)))
|
||||
)
|
||||
|
||||
def test_protocol_failing(self):
|
||||
"""
|
||||
Test that a failure in the process termination is correctly
|
||||
propagated to the finished deferred.
|
||||
"""
|
||||
s = sio()
|
||||
sa = sio()
|
||||
ac = self._makeConnector(s, sa)
|
||||
|
||||
ac.finished.addCallback(_raise)
|
||||
fail = failure.Failure(error.ProcessTerminated())
|
||||
self.assertFailure(ac.finished, error.ProcessTerminated)
|
||||
ac.processEnded(fail)
|
||||
|
||||
def test_startProcess(self):
|
||||
"""
|
||||
Test that startProcess actually starts a subprocess and that
|
||||
it receives data back from the process through AMP.
|
||||
"""
|
||||
s = sio()
|
||||
a = FakeAMP(s)
|
||||
STRING = "ciao"
|
||||
BOOT = """\
|
||||
import sys, os
|
||||
def main(arg):
|
||||
os.write(4, arg)
|
||||
main(sys.argv[1])
|
||||
"""
|
||||
starter = main.ProcessStarter(bootstrap=BOOT,
|
||||
args=(STRING,),
|
||||
packages=("twisted", "ampoule"))
|
||||
|
||||
amp, finished = starter.startPythonProcess(main.AMPConnector(a))
|
||||
def _eb(reason):
|
||||
print reason
|
||||
finished.addErrback(_eb)
|
||||
return finished.addCallback(lambda _: self.assertEquals(s.getvalue(), STRING))
|
||||
|
||||
def test_failing_deferToProcess(self):
|
||||
"""
|
||||
Test failing subprocesses and the way they terminate and preserve
|
||||
failing information.
|
||||
"""
|
||||
s = sio()
|
||||
a = FakeAMP(s)
|
||||
STRING = "ciao"
|
||||
BOOT = """\
|
||||
import sys
|
||||
def main(arg):
|
||||
raise Exception(arg)
|
||||
main(sys.argv[1])
|
||||
"""
|
||||
starter = main.ProcessStarter(bootstrap=BOOT, args=(STRING,), packages=("twisted", "ampoule"))
|
||||
ready, finished = starter.startPythonProcess(main.AMPConnector(a), "I'll be ignored")
|
||||
|
||||
self.assertFailure(finished, error.ProcessTerminated)
|
||||
finished.addErrback(lambda reason: self.assertEquals(reason.getMessage(), STRING))
|
||||
return finished
|
||||
|
||||
def test_env_setting(self):
|
||||
"""
|
||||
Test that and environment variable passed to the process starter
|
||||
is correctly passed to the child process.
|
||||
"""
|
||||
s = sio()
|
||||
a = FakeAMP(s)
|
||||
STRING = "ciao"
|
||||
BOOT = """\
|
||||
import sys, os
|
||||
def main():
|
||||
os.write(4, os.getenv("FOOBAR"))
|
||||
main()
|
||||
"""
|
||||
starter = main.ProcessStarter(bootstrap=BOOT,
|
||||
packages=("twisted", "ampoule"),
|
||||
env={"FOOBAR": STRING})
|
||||
amp, finished = starter.startPythonProcess(main.AMPConnector(a), "I'll be ignored")
|
||||
def _eb(reason):
|
||||
print reason
|
||||
finished.addErrback(_eb)
|
||||
return finished.addCallback(lambda _: self.assertEquals(s.getvalue(), STRING))
|
||||
|
||||
def test_startAMPProcess(self):
|
||||
"""
|
||||
Test that you can start an AMP subprocess and that it correctly
|
||||
accepts commands and correctly answers them.
|
||||
"""
|
||||
STRING = "ciao"
|
||||
|
||||
starter = main.ProcessStarter(packages=("twisted", "ampoule"))
|
||||
c, finished = starter.startAMPProcess(child.AMPChild)
|
||||
c.callRemote(commands.Echo, data=STRING
|
||||
).addCallback(lambda response:
|
||||
self.assertEquals(response['response'], STRING)
|
||||
).addCallback(lambda _: c.callRemote(commands.Shutdown))
|
||||
return finished
|
||||
|
||||
def test_BootstrapContext(self):
|
||||
starter = main.ProcessStarter(packages=('twisted', 'ampoule'))
|
||||
c, finished = starter.startAMPProcess(TempDirChild)
|
||||
cwd = []
|
||||
def checkBootstrap(response):
|
||||
cwd.append(response['cwd'])
|
||||
self.assertNotEquals(cwd, os.getcwd())
|
||||
d = c.callRemote(GetCWD)
|
||||
d.addCallback(checkBootstrap)
|
||||
d.addCallback(lambda _: c.callRemote(commands.Shutdown))
|
||||
finished.addCallback(lambda _: self.assertFalse(os.path.exists(cwd[0])))
|
||||
return finished
|
||||
|
||||
def test_BootstrapContextInstance(self):
|
||||
starter = main.ProcessStarter(packages=('twisted', 'ampoule'))
|
||||
c, finished = starter.startAMPProcess(TempDirChild,
|
||||
ampChildArgs=('foo',))
|
||||
cwd = []
|
||||
def checkBootstrap(response):
|
||||
cwd.append(response['cwd'])
|
||||
self.assertTrue(cwd[0].endswith('/foo'))
|
||||
d = c.callRemote(GetCWD)
|
||||
d.addCallback(checkBootstrap)
|
||||
d.addCallback(lambda _: c.callRemote(commands.Shutdown))
|
||||
finished.addCallback(lambda _: self.assertFalse(os.path.exists(cwd[0])))
|
||||
return finished
|
||||
|
||||
def test_startAMPAndParentProtocol(self):
|
||||
"""
|
||||
Test that you can start an AMP subprocess and the children can
|
||||
call methods on their parent.
|
||||
"""
|
||||
DATA = "CIAO"
|
||||
APPEND = "123"
|
||||
|
||||
class Parent(amp.AMP):
|
||||
def pong(self, data):
|
||||
return {'response': DATA+APPEND}
|
||||
Pong.responder(pong)
|
||||
|
||||
starter = main.ProcessStarter(packages=("twisted", "ampoule"))
|
||||
|
||||
subp, finished = starter.startAMPProcess(ampChild=Child, ampParent=Parent)
|
||||
subp.callRemote(Ping, data=DATA
|
||||
).addCallback(lambda response:
|
||||
self.assertEquals(response['response'], DATA+APPEND)
|
||||
).addCallback(lambda _: subp.callRemote(commands.Shutdown))
|
||||
return finished
|
||||
|
||||
def test_roundtripError(self):
|
||||
"""
|
||||
Test that invoking a child using an unreachable class raises
|
||||
a L{RunTimeError} .
|
||||
"""
|
||||
class Child(child.AMPChild):
|
||||
pass
|
||||
|
||||
starter = main.ProcessStarter(packages=("twisted", "ampoule"))
|
||||
|
||||
self.assertRaises(RuntimeError, starter.startAMPProcess, ampChild=Child)
|
||||
|
||||
class TestProcessPool(unittest.TestCase):
|
||||
|
||||
def test_startStopWorker(self):
|
||||
"""
|
||||
Test that starting and stopping a worker keeps the state of
|
||||
the process pool consistent.
|
||||
"""
|
||||
pp = pool.ProcessPool()
|
||||
self.assertEquals(pp.started, False)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(pp.processes, set())
|
||||
self.assertEquals(pp._finishCallbacks, {})
|
||||
|
||||
def _checks():
|
||||
self.assertEquals(pp.started, False)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(len(pp.processes), 1)
|
||||
self.assertEquals(len(pp._finishCallbacks), 1)
|
||||
return pp.stopAWorker()
|
||||
|
||||
def _closingUp(_):
|
||||
self.assertEquals(pp.started, False)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(len(pp.processes), 0)
|
||||
self.assertEquals(pp._finishCallbacks, {})
|
||||
pp.startAWorker()
|
||||
return _checks().addCallback(_closingUp).addCallback(lambda _: pp.stop())
|
||||
|
||||
def test_startAndStop(self):
|
||||
"""
|
||||
Test that a process pool's start and stop method create the
|
||||
expected number of workers and keep state consistent in the
|
||||
process pool.
|
||||
"""
|
||||
pp = pool.ProcessPool()
|
||||
self.assertEquals(pp.started, False)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(pp.processes, set())
|
||||
self.assertEquals(pp._finishCallbacks, {})
|
||||
|
||||
def _checks(_):
|
||||
self.assertEquals(pp.started, True)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(len(pp.processes), pp.min)
|
||||
self.assertEquals(len(pp._finishCallbacks), pp.min)
|
||||
return pp.stop()
|
||||
|
||||
def _closingUp(_):
|
||||
self.assertEquals(pp.started, True)
|
||||
self.assertEquals(pp.finished, True)
|
||||
self.assertEquals(len(pp.processes), 0)
|
||||
self.assertEquals(pp._finishCallbacks, {})
|
||||
return pp.start().addCallback(_checks).addCallback(_closingUp)
|
||||
|
||||
def test_adjustPoolSize(self):
|
||||
"""
|
||||
Test that calls to pool.adjustPoolSize are correctly handled.
|
||||
"""
|
||||
pp = pool.ProcessPool(min=10)
|
||||
self.assertEquals(pp.started, False)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(pp.processes, set())
|
||||
self.assertEquals(pp._finishCallbacks, {})
|
||||
|
||||
def _resize1(_):
|
||||
self.assertEquals(pp.started, True)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(len(pp.processes), pp.min)
|
||||
self.assertEquals(len(pp._finishCallbacks), pp.min)
|
||||
return pp.adjustPoolSize(min=2, max=3)
|
||||
|
||||
def _resize2(_):
|
||||
self.assertEquals(pp.started, True)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(pp.max, 3)
|
||||
self.assertEquals(pp.min, 2)
|
||||
self.assertEquals(len(pp.processes), pp.max)
|
||||
self.assertEquals(len(pp._finishCallbacks), pp.max)
|
||||
|
||||
def _resize3(_):
|
||||
self.assertRaises(AssertionError, pp.adjustPoolSize, min=-1, max=5)
|
||||
self.assertRaises(AssertionError, pp.adjustPoolSize, min=5, max=1)
|
||||
return pp.stop()
|
||||
|
||||
return pp.start(
|
||||
).addCallback(_resize1
|
||||
).addCallback(_resize2
|
||||
).addCallback(_resize3)
|
||||
|
||||
def test_childRestart(self):
|
||||
"""
|
||||
Test that a failing child process is immediately restarted.
|
||||
"""
|
||||
pp = pool.ProcessPool(ampChild=BadChild, min=1)
|
||||
STRING = "DATA"
|
||||
|
||||
def _checks(_):
|
||||
d = pp._finishCallbacks.values()[0]
|
||||
pp.doWork(Die).addErrback(lambda _: None)
|
||||
return d.addBoth(_checksAgain)
|
||||
|
||||
def _checksAgain(_):
|
||||
return pp.doWork(commands.Echo, data=STRING
|
||||
).addCallback(lambda result: self.assertEquals(result['response'], STRING))
|
||||
|
||||
return pp.start(
|
||||
).addCallback(_checks
|
||||
).addCallback(lambda _: pp.stop())
|
||||
|
||||
def test_parentProtocolChange(self):
|
||||
"""
|
||||
Test that the father can use an AMP protocol too.
|
||||
"""
|
||||
DATA = "CIAO"
|
||||
APPEND = "123"
|
||||
|
||||
class Parent(amp.AMP):
|
||||
def pong(self, data):
|
||||
return {'response': DATA+APPEND}
|
||||
Pong.responder(pong)
|
||||
|
||||
pp = pool.ProcessPool(ampChild=Child, ampParent=Parent)
|
||||
def _checks(_):
|
||||
return pp.doWork(Ping, data=DATA
|
||||
).addCallback(lambda response:
|
||||
self.assertEquals(response['response'], DATA+APPEND)
|
||||
)
|
||||
|
||||
return pp.start().addCallback(_checks).addCallback(lambda _: pp.stop())
|
||||
|
||||
|
||||
def test_deferToAMPProcess(self):
|
||||
"""
|
||||
Test that deferToAMPProcess works as expected.
|
||||
"""
|
||||
def cleanupGlobalPool():
|
||||
d = pool.pp.stop()
|
||||
pool.pp = None
|
||||
return d
|
||||
self.addCleanup(cleanupGlobalPool)
|
||||
|
||||
STRING = "CIAOOOO"
|
||||
d = pool.deferToAMPProcess(commands.Echo, data=STRING)
|
||||
d.addCallback(self.assertEquals, {"response": STRING})
|
||||
return d
|
||||
|
||||
def test_checkStateInPool(self):
|
||||
"""
|
||||
Test that busy and ready lists are correctly maintained.
|
||||
"""
|
||||
pp = pool.ProcessPool(ampChild=WaitingChild)
|
||||
|
||||
DATA = "foobar"
|
||||
|
||||
def _checks(_):
|
||||
d = pp.callRemote(First, data=DATA)
|
||||
self.assertEquals(pp.started, True)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(len(pp.processes), pp.min)
|
||||
self.assertEquals(len(pp._finishCallbacks), pp.min)
|
||||
self.assertEquals(len(pp.ready), pp.min-1)
|
||||
self.assertEquals(len(pp.busy), 1)
|
||||
child = pp.busy.pop()
|
||||
pp.busy.add(child)
|
||||
child.callRemote(Second)
|
||||
return d
|
||||
|
||||
return pp.start(
|
||||
).addCallback(_checks
|
||||
).addCallback(lambda _: pp.stop())
|
||||
|
||||
def test_growingToMax(self):
|
||||
"""
|
||||
Test that the pool grows over time until it reaches max processes.
|
||||
"""
|
||||
MAX = 5
|
||||
pp = pool.ProcessPool(ampChild=WaitingChild, min=1, max=MAX)
|
||||
|
||||
def _checks(_):
|
||||
self.assertEquals(pp.started, True)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(len(pp.processes), pp.min)
|
||||
self.assertEquals(len(pp._finishCallbacks), pp.min)
|
||||
|
||||
D = "DATA"
|
||||
d = [pp.doWork(First, data=D) for x in xrange(MAX)]
|
||||
|
||||
self.assertEquals(pp.started, True)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(len(pp.processes), pp.max)
|
||||
self.assertEquals(len(pp._finishCallbacks), pp.max)
|
||||
|
||||
[child.callRemote(Second) for child in pp.processes]
|
||||
return defer.DeferredList(d)
|
||||
|
||||
return pp.start(
|
||||
).addCallback(_checks
|
||||
).addCallback(lambda _: pp.stop())
|
||||
|
||||
def test_growingToMaxAndShrinking(self):
|
||||
"""
|
||||
Test that the pool grows but after 'idle' time the number of
|
||||
processes goes back to the minimum.
|
||||
"""
|
||||
|
||||
MAX = 5
|
||||
MIN = 1
|
||||
IDLE = 1
|
||||
pp = pool.ProcessPool(ampChild=WaitingChild, min=MIN, max=MAX, maxIdle=IDLE)
|
||||
|
||||
def _checks(_):
|
||||
self.assertEquals(pp.started, True)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(len(pp.processes), pp.min)
|
||||
self.assertEquals(len(pp._finishCallbacks), pp.min)
|
||||
|
||||
D = "DATA"
|
||||
d = [pp.doWork(First, data=D) for x in xrange(MAX)]
|
||||
|
||||
self.assertEquals(pp.started, True)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(len(pp.processes), pp.max)
|
||||
self.assertEquals(len(pp._finishCallbacks), pp.max)
|
||||
|
||||
[child.callRemote(Second) for child in pp.processes]
|
||||
return defer.DeferredList(d).addCallback(_realChecks)
|
||||
|
||||
def _realChecks(_):
|
||||
from twisted.internet import reactor
|
||||
d = defer.Deferred()
|
||||
def _cb():
|
||||
def __(_):
|
||||
try:
|
||||
self.assertEquals(pp.started, True)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(len(pp.processes), pp.min)
|
||||
self.assertEquals(len(pp._finishCallbacks), pp.min)
|
||||
d.callback(None)
|
||||
except Exception, e:
|
||||
d.errback(e)
|
||||
return pp._pruneProcesses().addCallback(__)
|
||||
# just to be shure we are called after the pruner
|
||||
pp.looping.stop() # stop the looping, we don't want it to
|
||||
# this right here
|
||||
reactor.callLater(IDLE, _cb)
|
||||
return d
|
||||
|
||||
return pp.start(
|
||||
).addCallback(_checks
|
||||
).addCallback(lambda _: pp.stop())
|
||||
|
||||
def test_recycling(self):
|
||||
"""
|
||||
Test that after a given number of calls subprocesses are
|
||||
recycled.
|
||||
"""
|
||||
MAX = 1
|
||||
MIN = 1
|
||||
RECYCLE_AFTER = 1
|
||||
pp = pool.ProcessPool(ampChild=PidChild, min=MIN, max=MAX, recycleAfter=RECYCLE_AFTER)
|
||||
self.addCleanup(pp.stop)
|
||||
|
||||
def _checks(_):
|
||||
self.assertEquals(pp.started, True)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(len(pp.processes), pp.min)
|
||||
self.assertEquals(len(pp._finishCallbacks), pp.min)
|
||||
return pp.doWork(Pid
|
||||
).addCallback(lambda response: response['pid'])
|
||||
|
||||
def _checks2(pid):
|
||||
return pp.doWork(Pid
|
||||
).addCallback(lambda response: response['pid']
|
||||
).addCallback(self.assertNotEquals, pid)
|
||||
|
||||
|
||||
d = pp.start()
|
||||
d.addCallback(_checks)
|
||||
d.addCallback(_checks2)
|
||||
return d
|
||||
|
||||
def test_recyclingWithQueueOverload(self):
|
||||
"""
|
||||
Test that we get the correct number of different results when
|
||||
we overload the pool of calls.
|
||||
"""
|
||||
MAX = 5
|
||||
MIN = 1
|
||||
RECYCLE_AFTER = 10
|
||||
CALLS = 60
|
||||
pp = pool.ProcessPool(ampChild=PidChild, min=MIN, max=MAX, recycleAfter=RECYCLE_AFTER)
|
||||
self.addCleanup(pp.stop)
|
||||
|
||||
def _check(results):
|
||||
s = set()
|
||||
for succeed, response in results:
|
||||
s.add(response['pid'])
|
||||
|
||||
# For the first C{MAX} calls, each is basically guaranteed to go to
|
||||
# a different child. After that, though, there are no guarantees.
|
||||
# All the rest might go to a single child, since the child to
|
||||
# perform a job is selected arbitrarily from the "ready" set. Fair
|
||||
# distribution of jobs needs to be implemented; right now it's "set
|
||||
# ordering" distribution of jobs.
|
||||
self.assertTrue(len(s) > MAX)
|
||||
|
||||
def _work(_):
|
||||
l = [pp.doWork(Pid) for x in xrange(CALLS)]
|
||||
d = defer.DeferredList(l)
|
||||
return d.addCallback(_check)
|
||||
d = pp.start()
|
||||
d.addCallback(_work)
|
||||
return d
|
||||
|
||||
|
||||
def test_disableProcessRecycling(self):
|
||||
"""
|
||||
Test that by setting 0 to recycleAfter we actually disable process recycling.
|
||||
"""
|
||||
MAX = 1
|
||||
MIN = 1
|
||||
RECYCLE_AFTER = 0
|
||||
pp = pool.ProcessPool(ampChild=PidChild, min=MIN, max=MAX, recycleAfter=RECYCLE_AFTER)
|
||||
|
||||
def _checks(_):
|
||||
self.assertEquals(pp.started, True)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(len(pp.processes), pp.min)
|
||||
self.assertEquals(len(pp._finishCallbacks), pp.min)
|
||||
return pp.doWork(Pid
|
||||
).addCallback(lambda response: response['pid'])
|
||||
|
||||
def _checks2(pid):
|
||||
return pp.doWork(Pid
|
||||
).addCallback(lambda response: response['pid']
|
||||
).addCallback(self.assertEquals, pid
|
||||
).addCallback(lambda _: pid)
|
||||
|
||||
def finish(reason):
|
||||
return pp.stop().addCallback(lambda _: reason)
|
||||
|
||||
return pp.start(
|
||||
).addCallback(_checks
|
||||
).addCallback(_checks2
|
||||
).addCallback(_checks2
|
||||
).addCallback(finish)
|
||||
|
||||
def test_changeChildrenReactor(self):
|
||||
"""
|
||||
Test that by passing the correct argument children change their
|
||||
reactor type.
|
||||
"""
|
||||
MAX = 1
|
||||
MIN = 1
|
||||
FIRST = "select"
|
||||
SECOND = "poll"
|
||||
|
||||
def checkDefault():
|
||||
pp = pool.ProcessPool(
|
||||
starter=main.ProcessStarter(
|
||||
childReactor=FIRST,
|
||||
packages=("twisted", "ampoule")),
|
||||
ampChild=ReactorChild, min=MIN, max=MAX)
|
||||
pp.start()
|
||||
return pp.doWork(Reactor
|
||||
).addCallback(self.assertEquals, {'classname': "SelectReactor"}
|
||||
).addCallback(lambda _: pp.stop())
|
||||
|
||||
def checkPool(_):
|
||||
pp = pool.ProcessPool(
|
||||
starter=main.ProcessStarter(
|
||||
childReactor=SECOND,
|
||||
packages=("twisted", "ampoule")),
|
||||
ampChild=ReactorChild, min=MIN, max=MAX)
|
||||
pp.start()
|
||||
return pp.doWork(Reactor
|
||||
).addCallback(self.assertEquals, {'classname': "PollReactor"}
|
||||
).addCallback(lambda _: pp.stop())
|
||||
|
||||
return checkDefault(
|
||||
).addCallback(checkPool)
|
||||
try:
|
||||
from select import poll
|
||||
except ImportError:
|
||||
test_changeChildrenReactor.skip = "This architecture doesn't support select.poll, I can't run this test"
|
||||
|
||||
def test_commandsWithoutResponse(self):
|
||||
"""
|
||||
Test that if we send a command without a required answer we
|
||||
actually don't have any problems.
|
||||
"""
|
||||
DATA = "hello"
|
||||
pp = pool.ProcessPool(ampChild=NoResponseChild, min=1, max=1)
|
||||
|
||||
def _check(_):
|
||||
return pp.doWork(GetResponse
|
||||
).addCallback(self.assertEquals, {"response": DATA})
|
||||
|
||||
def _work(_):
|
||||
return pp.doWork(NoResponse, arg=DATA)
|
||||
|
||||
return pp.start(
|
||||
).addCallback(_work
|
||||
).addCallback(_check
|
||||
).addCallback(lambda _: pp.stop())
|
||||
|
||||
def test_SupplyChildArgs(self):
|
||||
"""Ensure that arguments for the child constructor are passed in."""
|
||||
pp = pool.ProcessPool(Writer, ampChildArgs=['body'], min=0)
|
||||
def _check(result):
|
||||
return pp.doWork(Write).addCallback(
|
||||
self.assertEquals, {'response': 'body'})
|
||||
|
||||
return pp.start(
|
||||
).addCallback(_check
|
||||
).addCallback(lambda _: pp.stop())
|
||||
|
||||
def processTimeoutTest(self, timeout):
|
||||
pp = pool.ProcessPool(WaitingChild, min=1, max=1)
|
||||
|
||||
def _work(_):
|
||||
d = pp.callRemote(First, data="ciao", _timeout=timeout)
|
||||
self.assertFailure(d, error.ProcessTerminated)
|
||||
return d
|
||||
|
||||
return pp.start(
|
||||
).addCallback(_work
|
||||
).addCallback(lambda _: pp.stop())
|
||||
|
||||
def test_processTimeout(self):
|
||||
"""
|
||||
Test that a call that doesn't finish within the given timeout
|
||||
time is correctly handled.
|
||||
"""
|
||||
return self.processTimeoutTest(1)
|
||||
|
||||
def test_processTimeoutZero(self):
|
||||
"""
|
||||
Test that the process is correctly handled when the timeout is zero.
|
||||
"""
|
||||
return self.processTimeoutTest(0)
|
||||
|
||||
def test_processDeadline(self):
|
||||
pp = pool.ProcessPool(WaitingChild, min=1, max=1)
|
||||
|
||||
def _work(_):
|
||||
d = pp.callRemote(First, data="ciao", _deadline=reactor.seconds())
|
||||
self.assertFailure(d, error.ProcessTerminated)
|
||||
return d
|
||||
|
||||
return pp.start(
|
||||
).addCallback(_work
|
||||
).addCallback(lambda _: pp.stop())
|
||||
|
||||
def test_processBeforeDeadline(self):
|
||||
pp = pool.ProcessPool(PidChild, min=1, max=1)
|
||||
|
||||
def _work(_):
|
||||
d = pp.callRemote(Pid, _deadline=reactor.seconds() + 10)
|
||||
d.addCallback(lambda result: self.assertNotEqual(result['pid'], 0))
|
||||
return d
|
||||
|
||||
return pp.start(
|
||||
).addCallback(_work
|
||||
).addCallback(lambda _: pp.stop())
|
||||
|
||||
def test_processTimeoutSignal(self):
|
||||
"""
|
||||
Test that a call that doesn't finish within the given timeout
|
||||
time is correctly handled.
|
||||
"""
|
||||
pp = pool.ProcessPool(WaitingChild, min=1, max=1,
|
||||
timeout_signal=SIGHUP)
|
||||
|
||||
def _work(_):
|
||||
d = pp.callRemote(First, data="ciao", _timeout=1)
|
||||
d.addCallback(lambda d: self.fail())
|
||||
text = 'signal %d' % SIGHUP
|
||||
d.addErrback(
|
||||
lambda f: self.assertTrue(text in f.value[0],
|
||||
'"%s" not in "%s"' % (text, f.value[0])))
|
||||
return d
|
||||
|
||||
return pp.start(
|
||||
).addCallback(_work
|
||||
).addCallback(lambda _: pp.stop())
|
||||
|
||||
def test_processGlobalTimeout(self):
|
||||
"""
|
||||
Test that a call that doesn't finish within the given global
|
||||
timeout time is correctly handled.
|
||||
"""
|
||||
pp = pool.ProcessPool(WaitingChild, min=1, max=1, timeout=1)
|
||||
|
||||
def _work(_):
|
||||
d = pp.callRemote(First, data="ciao")
|
||||
self.assertFailure(d, error.ProcessTerminated)
|
||||
return d
|
||||
|
||||
return pp.start(
|
||||
).addCallback(_work
|
||||
).addCallback(lambda _: pp.stop())
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
from twisted.internet import defer, reactor
|
||||
from twisted.internet.protocol import ClientFactory
|
||||
from twisted.trial import unittest
|
||||
from twisted.protocols import amp
|
||||
|
||||
from contrib.procpools.ampoule import service, child, pool, main
|
||||
from contrib.procpools.ampoule.commands import Echo
|
||||
|
||||
class ClientAMP(amp.AMP):
|
||||
factory = None
|
||||
def connectionMade(self):
|
||||
if self.factory is not None:
|
||||
self.factory.theProto = self
|
||||
if hasattr(self.factory, 'onMade'):
|
||||
self.factory.onMade.callback(None)
|
||||
|
||||
class TestAMPProxy(unittest.TestCase):
|
||||
def setUp(self):
|
||||
"""
|
||||
Setup the proxy service and the client connection to the proxy
|
||||
service in order to run call through them.
|
||||
|
||||
Inspiration comes from twisted.test.test_amp
|
||||
"""
|
||||
self.pp = pool.ProcessPool()
|
||||
self.svc = service.AMPouleService(self.pp, child.AMPChild, 0, "")
|
||||
self.svc.startService()
|
||||
self.proxy_port = self.svc.server.getHost().port
|
||||
self.clientFactory = ClientFactory()
|
||||
self.clientFactory.protocol = ClientAMP
|
||||
d = self.clientFactory.onMade = defer.Deferred()
|
||||
self.clientConn = reactor.connectTCP("127.0.0.1",
|
||||
self.proxy_port,
|
||||
self.clientFactory)
|
||||
self.addCleanup(self.clientConn.disconnect)
|
||||
self.addCleanup(self.svc.stopService)
|
||||
def setClient(_):
|
||||
self.client = self.clientFactory.theProto
|
||||
return d.addCallback(setClient)
|
||||
|
||||
def test_forwardCall(self):
|
||||
"""
|
||||
Test that a call made from a client is correctly forwarded to
|
||||
the process pool and the result is correctly reported.
|
||||
"""
|
||||
DATA = "hello"
|
||||
return self.client.callRemote(Echo, data=DATA).addCallback(
|
||||
self.assertEquals, {'response': DATA}
|
||||
)
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
"""
|
||||
some utilities
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import __main__
|
||||
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.python.reflect import namedAny
|
||||
# from twisted.python.modules import theSystemPath
|
||||
|
||||
def findPackagePath(modulePath):
|
||||
"""
|
||||
Try to find the sys.path entry from a modulePath object, simultaneously
|
||||
computing the module name of the targetted file.
|
||||
"""
|
||||
p = modulePath
|
||||
l = [p.basename().split(".")[0]]
|
||||
while p.parent() != p:
|
||||
for extension in ['py', 'pyc', 'pyo', 'pyd', 'dll']:
|
||||
sib = p.sibling("__init__."+extension)
|
||||
if sib.exists():
|
||||
p = p.parent()
|
||||
l.insert(0, p.basename())
|
||||
break
|
||||
else:
|
||||
return p.parent(), '.'.join(l)
|
||||
|
||||
|
||||
def mainpoint(function):
|
||||
"""
|
||||
Decorator which declares a function to be an object's mainpoint.
|
||||
"""
|
||||
if function.__module__ == '__main__':
|
||||
# OK time to run a function
|
||||
p = FilePath(__main__.__file__)
|
||||
p, mn = findPackagePath(p)
|
||||
pname = p.path
|
||||
if pname not in map(os.path.abspath, sys.path):
|
||||
sys.path.insert(0, pname)
|
||||
# Maybe remove the module's path?
|
||||
exitcode = namedAny(mn+'.'+function.__name__)(sys.argv)
|
||||
if exitcode is None:
|
||||
exitcode = 0
|
||||
sys.exit(exitcode)
|
||||
return function
|
||||
|
|
@ -1,326 +0,0 @@
|
|||
"""
|
||||
Python ProcPool
|
||||
|
||||
Evennia Contribution - Griatch 2012
|
||||
|
||||
The ProcPool is used to execute code on a separate process. This allows for
|
||||
true asynchronous operation. Process communication happens over AMP and is
|
||||
thus fully asynchronous as far as Evennia is concerned.
|
||||
|
||||
The process pool is implemented using a slightly modified version of
|
||||
the Ampoule package (included).
|
||||
|
||||
The python_process pool is a service activated with the instructions
|
||||
in python_procpool_plugin.py.
|
||||
|
||||
To use, import run_async from this module and use instead of the
|
||||
in-process version found in src.utils.utils. Note that this is a much
|
||||
more complex function than the default run_async, so make sure to read
|
||||
the header carefully.
|
||||
|
||||
To test it works, make sure to activate the process pool, then try the
|
||||
following as superuser:
|
||||
|
||||
@py from contrib.procpools.python_procpool import run_async;run_async("_return('Wohoo!')", at_return=self.msg, at_err=self.msg)
|
||||
|
||||
You can also try to import time and do time.sleep(5) before the
|
||||
_return statement, to test it really is asynchronous.
|
||||
|
||||
"""
|
||||
|
||||
from twisted.protocols import amp
|
||||
from twisted.internet import threads
|
||||
from contrib.procpools.ampoule.child import AMPChild
|
||||
from src.utils.dbserialize import to_pickle, from_pickle, do_pickle, do_unpickle
|
||||
from src.utils.idmapper.base import PROC_MODIFIED_OBJS
|
||||
from src.utils.utils import clean_object_caches, to_str
|
||||
from src.utils import logger
|
||||
|
||||
|
||||
#
|
||||
# Multiprocess command for communication Server<->Client, relaying
|
||||
# data for remote Python execution
|
||||
#
|
||||
|
||||
class ExecuteCode(amp.Command):
|
||||
"""
|
||||
Executes python code in the python process,
|
||||
returning result when ready.
|
||||
|
||||
source - a compileable Python source code string
|
||||
environment - a pickled dictionary of Python
|
||||
data. Each key will become the name
|
||||
of a variable available to the source
|
||||
code. Database objects are stored on
|
||||
the form ((app, modelname), id) allowing
|
||||
the receiver to easily rebuild them on
|
||||
this side.
|
||||
errors - an all-encompassing error handler
|
||||
response - a string or a pickled string
|
||||
|
||||
"""
|
||||
arguments = [('source', amp.String()),
|
||||
('environment', amp.String())]
|
||||
errors = [(Exception, 'EXCEPTION')]
|
||||
response = [('response', amp.String()),
|
||||
('recached', amp.String())]
|
||||
|
||||
|
||||
#
|
||||
# Multiprocess AMP client-side factory, for executing remote Python code
|
||||
#
|
||||
|
||||
class PythonProcPoolChild(AMPChild):
|
||||
"""
|
||||
This is describing what happens on the subprocess side.
|
||||
|
||||
This already supports Echo, Shutdown and Ping.
|
||||
|
||||
Methods:
|
||||
executecode - a remote code execution environment
|
||||
|
||||
"""
|
||||
def executecode(self, source, environment):
|
||||
"""
|
||||
Remote code execution
|
||||
|
||||
source - Python code snippet
|
||||
environment - pickled dictionary of environment
|
||||
variables. They are stored in
|
||||
two keys "normal" and "objs" where
|
||||
normal holds a dictionary of
|
||||
normally pickled python objects
|
||||
wheras objs points to a dictionary
|
||||
of database represenations ((app,key),id).
|
||||
|
||||
The environment's entries will be made available as
|
||||
local variables during the execution. Normal eval
|
||||
results will be returned as-is. For more complex
|
||||
code snippets (run by exec), the _return function
|
||||
is available: All data sent to _return(retval) will
|
||||
be returned from this system whenever the system
|
||||
finishes. Multiple calls to _return will result in
|
||||
a list being return. The return value is pickled
|
||||
and thus allows for returning any pickleable data.
|
||||
|
||||
"""
|
||||
|
||||
class Ret(object):
|
||||
"Helper class for holding returns from exec"
|
||||
def __init__(self):
|
||||
self.returns = []
|
||||
def __call__(self, *args, **kwargs):
|
||||
self.returns.extend(list(args))
|
||||
def get_returns(self):
|
||||
lr = len(self.returns)
|
||||
val = lr and (lr == 1 and self.returns[0] or self.returns) or None
|
||||
if val not in (None, [], ()):
|
||||
return do_pickle(to_pickle(val))
|
||||
else:
|
||||
return ""
|
||||
_return = Ret()
|
||||
|
||||
available_vars = {'_return': _return}
|
||||
if environment:
|
||||
# load environment
|
||||
try:
|
||||
environment = from_pickle(do_unpickle(environment))
|
||||
available_vars.update(environment)
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
# try to execute with eval first
|
||||
try:
|
||||
ret = eval(source, {}, available_vars)
|
||||
if ret not in (None, [], ()):
|
||||
ret = _return.get_returns() or do_pickle(to_pickle(ret))
|
||||
else:
|
||||
ret = ""
|
||||
except Exception:
|
||||
# use exec instead
|
||||
exec source in available_vars
|
||||
ret = _return.get_returns()
|
||||
# get the list of affected objects to recache
|
||||
objs = PROC_MODIFIED_OBJS.values()
|
||||
# we need to include the locations too, to update their content caches
|
||||
objs = objs + list(set([o.location for o in objs
|
||||
if hasattr(o, "location") and o.location]))
|
||||
#print "objs:", objs
|
||||
#print "to_pickle", to_pickle(objs, emptypickle=False, do_pickle=False)
|
||||
if objs not in (None, [], ()):
|
||||
to_recache = do_pickle(to_pickle(objs))
|
||||
else:
|
||||
to_recache = ""
|
||||
# empty the list without loosing memory reference
|
||||
#PROC_MODIFIED_OBJS[:] = []
|
||||
PROC_MODIFIED_OBJS.clear() #TODO - is this not messing anything up?
|
||||
return {'response': ret,
|
||||
'recached': to_recache}
|
||||
ExecuteCode.responder(executecode)
|
||||
|
||||
|
||||
#
|
||||
# Procpool run_async - Server-side access function for executing
|
||||
# code in another process
|
||||
#
|
||||
|
||||
_PPOOL = None
|
||||
_SESSIONS = None
|
||||
_PROC_ERR = "A process has ended with a probable error condition: process ended by signal 9."
|
||||
|
||||
|
||||
def run_async(to_execute, *args, **kwargs):
|
||||
"""
|
||||
Runs a function or executes a code snippet asynchronously.
|
||||
|
||||
Inputs:
|
||||
to_execute (callable) - if this is a callable, it will
|
||||
be executed with *args and non-reserver *kwargs as
|
||||
arguments.
|
||||
The callable will be executed using ProcPool, or in
|
||||
a thread if ProcPool is not available.
|
||||
to_execute (string) - this is only available is ProcPool is
|
||||
running. If a string, to_execute this will be treated as a code
|
||||
snippet to execute asynchronously. *args are then not used
|
||||
and non-reserverd *kwargs are used to define the execution
|
||||
environment made available to the code.
|
||||
|
||||
reserved kwargs:
|
||||
'use_thread' (bool) - this only works with callables (not code).
|
||||
It forces the code to run in a thread instead
|
||||
of using the Process Pool, even if the latter
|
||||
is available. This could be useful if you want
|
||||
to make sure to not get out of sync with the
|
||||
main process (such as accessing in-memory global
|
||||
properties)
|
||||
'proc_timeout' (int) - only used if ProcPool is available. Sets a
|
||||
max time for execution. This alters the value set
|
||||
by settings.PROCPOOL_TIMEOUT
|
||||
'at_return' -should point to a callable with one argument.
|
||||
It will be called with the return value from
|
||||
to_execute.
|
||||
'at_return_kwargs' - this dictionary which be used as keyword
|
||||
arguments to the at_return callback.
|
||||
'at_err' - this will be called with a Failure instance if
|
||||
there is an error in to_execute.
|
||||
'at_err_kwargs' - this dictionary will be used as keyword
|
||||
arguments to the at_err errback.
|
||||
'procpool_name' - the Service name of the procpool to use.
|
||||
Default is PythonProcPool.
|
||||
|
||||
*args - if to_execute is a callable, these args will be used
|
||||
as arguments for that function. If to_execute is a string
|
||||
*args are not used.
|
||||
*kwargs - if to_execute is a callable, these kwargs will be used
|
||||
as keyword arguments in that function. If a string, they
|
||||
instead are used to define the executable environment
|
||||
that should be available to execute the code in to_execute.
|
||||
|
||||
run_async will either relay the code to a thread or to a processPool
|
||||
depending on input and what is available in the system. To activate
|
||||
Process pooling, settings.PROCPOOL_ENABLE must be set.
|
||||
|
||||
to_execute in string form should handle all imports needed. kwargs
|
||||
can be used to send objects and properties. Such properties will
|
||||
be pickled, except Database Objects which will be sent across
|
||||
on a special format and re-loaded on the other side.
|
||||
|
||||
To get a return value from your code snippet, Use the _return()
|
||||
function: Every call to this function from your snippet will
|
||||
append the argument to an internal list of returns. This return value
|
||||
(or a list) will be the first argument to the at_return callback.
|
||||
|
||||
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/processes 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 all global imports.
|
||||
global _PPOOL, _SESSIONS
|
||||
|
||||
# get the procpool name, if set in kwargs
|
||||
procpool_name = kwargs.get("procpool_name", "PythonProcPool")
|
||||
|
||||
if _PPOOL is None:
|
||||
# Try to load process Pool
|
||||
from src.server.sessionhandler import SESSIONS as _SESSIONS
|
||||
try:
|
||||
_PPOOL = _SESSIONS.server.services.namedServices.get(procpool_name).pool
|
||||
except AttributeError:
|
||||
_PPOOL = False
|
||||
|
||||
use_timeout = kwargs.pop("proc_timeout", _PPOOL.timeout)
|
||||
|
||||
# helper converters for callbacks/errbacks
|
||||
def convert_return(f):
|
||||
def func(ret, *args, **kwargs):
|
||||
rval = ret["response"] and from_pickle(do_unpickle(ret["response"]))
|
||||
reca = ret["recached"] and from_pickle(do_unpickle(ret["recached"]))
|
||||
# recache all indicated objects
|
||||
[clean_object_caches(obj) for obj in reca]
|
||||
if f:
|
||||
return f(rval, *args, **kwargs)
|
||||
else:
|
||||
return rval
|
||||
return func
|
||||
def convert_err(f):
|
||||
def func(err, *args, **kwargs):
|
||||
err.trap(Exception)
|
||||
err = err.getErrorMessage()
|
||||
if use_timeout and err == _PROC_ERR:
|
||||
err = "Process took longer than %ss and timed out." % use_timeout
|
||||
if f:
|
||||
return f(err, *args, **kwargs)
|
||||
else:
|
||||
err = "Error reported from subprocess: '%s'" % err
|
||||
logger.log_errmsg(err)
|
||||
return func
|
||||
|
||||
# handle special reserved input kwargs
|
||||
use_thread = kwargs.pop("use_thread", False)
|
||||
callback = convert_return(kwargs.pop("at_return", None))
|
||||
errback = convert_err(kwargs.pop("at_err", None))
|
||||
callback_kwargs = kwargs.pop("at_return_kwargs", {})
|
||||
errback_kwargs = kwargs.pop("at_err_kwargs", {})
|
||||
|
||||
if _PPOOL and not use_thread:
|
||||
# process pool is running
|
||||
if isinstance(to_execute, basestring):
|
||||
# run source code in process pool
|
||||
cmdargs = {"_timeout": use_timeout}
|
||||
cmdargs["source"] = to_str(to_execute)
|
||||
if kwargs:
|
||||
cmdargs["environment"] = do_pickle(to_pickle(kwargs))
|
||||
else:
|
||||
cmdargs["environment"] = ""
|
||||
# defer to process pool
|
||||
deferred = _PPOOL.doWork(ExecuteCode, **cmdargs)
|
||||
elif callable(to_execute):
|
||||
# execute callable in process
|
||||
callname = to_execute.__name__
|
||||
cmdargs = {"_timeout": use_timeout}
|
||||
cmdargs["source"] = "_return(%s(*args,**kwargs))" % callname
|
||||
cmdargs["environment"] = do_pickle(to_pickle({callname: to_execute,
|
||||
"args": args,
|
||||
"kwargs": kwargs}))
|
||||
deferred = _PPOOL.doWork(ExecuteCode, **cmdargs)
|
||||
else:
|
||||
raise RuntimeError("'%s' could not be handled by the process pool" % to_execute)
|
||||
elif callable(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
|
||||
raise RuntimeError("'%s' could not be handled by run_async - no valid input or no process pool." % to_execute)
|
||||
|
||||
# attach callbacks
|
||||
if callback:
|
||||
deferred.addCallback(callback, **callback_kwargs)
|
||||
deferred.addErrback(errback, **errback_kwargs)
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
"""
|
||||
Python ProcPool plugin
|
||||
|
||||
Evennia contribution - Griatch 2012
|
||||
|
||||
This is a plugin for the Evennia services. It will make the service
|
||||
and run_async in python_procpool.py available to the system.
|
||||
|
||||
To activate, add the following line to your settings file:
|
||||
|
||||
SERVER_SERVICES_PLUGIN_MODULES.append("contrib.procpools.python_procpool_plugin")
|
||||
|
||||
Next reboot the server and the new service will be available.
|
||||
|
||||
If you want to adjust the defaults, copy this file to
|
||||
game/gamesrc/conf/ and re-point
|
||||
settings.SERVER_SERVICES_PLUGINS_MODULES to that file instead. This
|
||||
is to avoid clashes with eventual upstream modifications to this file.
|
||||
|
||||
It is not recommended to use this with an SQLite3 database, at least
|
||||
if you plan to do many out-of-process database writes. SQLite3 does
|
||||
not work very well with a high frequency of off-process writes due to
|
||||
file locking clashes. Test what works with your mileage.
|
||||
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
# Process Pool setup
|
||||
|
||||
# convenient flag to turn off process pool without changing settings
|
||||
PROCPOOL_ENABLED = True
|
||||
# relay process stdout to log (debug mode, very spammy)
|
||||
PROCPOOL_DEBUG = False
|
||||
# max/min size of the process pool. Will expand up to max limit on demand.
|
||||
PROCPOOL_MIN_NPROC = 5
|
||||
PROCPOOL_MAX_NPROC = 20
|
||||
# maximum time (seconds) a process may idle before being pruned from
|
||||
# pool (if pool bigger than minsize)
|
||||
PROCPOOL_IDLETIME = 20
|
||||
# after sending a command, this is the maximum time in seconds the process
|
||||
# may run without returning. After this time the process will be killed. This
|
||||
# can be seen as a fallback; the run_async method takes a keyword proc_timeout
|
||||
# that will override this value on a per-case basis.
|
||||
PROCPOOL_TIMEOUT = 10
|
||||
# only change if the port clashes with something else on the system
|
||||
PROCPOOL_PORT = 5001
|
||||
# 0.0.0.0 means listening to all interfaces
|
||||
PROCPOOL_INTERFACE = '127.0.0.1'
|
||||
# user-id and group-id to run the processes as (for OS:es supporting this).
|
||||
# If you plan to run unsafe code one could experiment with setting this
|
||||
# to an unprivileged user.
|
||||
PROCPOOL_UID = None
|
||||
PROCPOOL_GID = None
|
||||
# real path to a directory where all processes will be run. If
|
||||
# not given, processes will be executed in game/.
|
||||
PROCPOOL_DIRECTORY = None
|
||||
|
||||
|
||||
# don't need to change normally
|
||||
SERVICE_NAME = "PythonProcPool"
|
||||
|
||||
|
||||
# plugin hook
|
||||
|
||||
def start_plugin_services(server):
|
||||
"""
|
||||
This will be called by the Evennia Server when starting up.
|
||||
|
||||
server - the main Evennia server application
|
||||
"""
|
||||
if not PROCPOOL_ENABLED:
|
||||
return
|
||||
|
||||
# terminal output
|
||||
print ' amp (Process Pool): %s' % PROCPOOL_PORT
|
||||
|
||||
from contrib.procpools.ampoule import main as ampoule_main
|
||||
from contrib.procpools.ampoule import service as ampoule_service
|
||||
from contrib.procpools.ampoule import pool as ampoule_pool
|
||||
from contrib.procpools.ampoule.main import BOOTSTRAP as _BOOTSTRAP
|
||||
from contrib.procpools.python_procpool import PythonProcPoolChild
|
||||
|
||||
# for some reason absolute paths don't work here, only relative ones.
|
||||
apackages = ("twisted",
|
||||
os.path.join(os.pardir, "contrib", "procpools", "ampoule"),
|
||||
os.path.join(os.pardir, "ev"),
|
||||
"settings")
|
||||
aenv = {"DJANGO_SETTINGS_MODULE": "settings",
|
||||
"DATABASE_NAME": settings.DATABASES.get("default", {}).get("NAME") or settings.DATABASE_NAME}
|
||||
if PROCPOOL_DEBUG:
|
||||
_BOOTSTRAP = _BOOTSTRAP % "log.startLogging(sys.stderr)"
|
||||
else:
|
||||
_BOOTSTRAP = _BOOTSTRAP % ""
|
||||
procpool_starter = ampoule_main.ProcessStarter(packages=apackages,
|
||||
env=aenv,
|
||||
path=PROCPOOL_DIRECTORY,
|
||||
uid=PROCPOOL_UID,
|
||||
gid=PROCPOOL_GID,
|
||||
bootstrap=_BOOTSTRAP,
|
||||
childReactor=sys.platform == 'linux2' and "epoll" or "default")
|
||||
procpool = ampoule_pool.ProcessPool(name=SERVICE_NAME,
|
||||
min=PROCPOOL_MIN_NPROC,
|
||||
max=PROCPOOL_MAX_NPROC,
|
||||
recycleAfter=500,
|
||||
timeout=PROCPOOL_TIMEOUT,
|
||||
maxIdle=PROCPOOL_IDLETIME,
|
||||
ampChild=PythonProcPoolChild,
|
||||
starter=procpool_starter)
|
||||
procpool_service = ampoule_service.AMPouleService(procpool,
|
||||
PythonProcPoolChild,
|
||||
PROCPOOL_PORT,
|
||||
PROCPOOL_INTERFACE)
|
||||
procpool_service.setName(SERVICE_NAME)
|
||||
# add the new services to the server
|
||||
server.services.addService(procpool_service)
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
|
||||
===============================================================
|
||||
Evennia Tutorial World
|
||||
|
||||
Griatch 2011
|
||||
===============================================================
|
||||
|
||||
This is a stand-alone tutorial area for an unmodified Evennia install.
|
||||
Think of it as a sort of single-player adventure rather than a
|
||||
full-fledged multi-player game world. The various rooms and objects
|
||||
herein are designed to show off features of the engine, not to be a
|
||||
very challenging (nor long) gaming experience. As such it's of course
|
||||
only skimming the surface of what is possible.
|
||||
|
||||
================================================================
|
||||
Install
|
||||
================================================================
|
||||
|
||||
Log in as superuser (#1), then run
|
||||
|
||||
@batchcommand contrib.tutorial_world.build
|
||||
|
||||
Wait a little while for building to complete. This should build the
|
||||
world and connect it to Limbo.
|
||||
|
||||
Log is as a non-superuser to play the game as intended. The
|
||||
tutorial area's systems mostly ignores the presence of a
|
||||
superuser (so use that to examine things "under the hood" later).
|
||||
|
||||
================================================================
|
||||
Comments
|
||||
================================================================
|
||||
|
||||
The tutorial world is intended for you playing around with the
|
||||
engine. It will help you learn how to accomplish some more advanced
|
||||
effects and might give some good ideas along the way.
|
||||
|
||||
It's suggested you play it through (as a normal user, NOT as
|
||||
Superuser!) and explore it a bit, then come back here and start
|
||||
looking into the (heavily documented) build/source code to find out
|
||||
how things tick - that's the "tutorial" in Tutorial world after all.
|
||||
|
||||
Please report bugs in the tutorial to the Evennia issue tracker.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
* Spoilers below - don't read on unless you already played the
|
||||
tutorial game. *
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
===============================================================
|
||||
Tutorial World Room map
|
||||
===============================================================
|
||||
|
||||
?
|
||||
|
|
||||
+---+----+ +-------------------+ +--------+ +--------+
|
||||
| | | | |gate | |corner |
|
||||
| cliff +----+ bridge +----+ +---+ |
|
||||
| | | | | | | |
|
||||
+---+---\+ +---------------+---+ +---+----+ +---+----+
|
||||
| \ | | castle |
|
||||
| \ +--------+ +----+---+ +---+----+ +---+----+
|
||||
| \ |under- | |ledge | |along | |court- |
|
||||
| \|ground +--+ | |wall +---+yard |
|
||||
| \ | | | | | | |
|
||||
| +------\-+ +--------+ +--------+ +---+----+
|
||||
| \ |
|
||||
++---------+ \ +--------+ +--------+ +---+----+
|
||||
|intro | \ |cell | |trap | |temple |
|
||||
o--+ | \| +----+ | | |
|
||||
L | | \ | /| | | |
|
||||
I +----+-----+ +--------+ / ---+-+-+-+ +---+----+
|
||||
M | / | | | |
|
||||
B +----+-----+ +--------+/ +--+-+-+---------+----+
|
||||
O |outro | |tomb | |antechamber |
|
||||
o--+ +----------+ | | |
|
||||
| | | | | |
|
||||
+----------+ +--------+ +---------------------+
|
||||
|
||||
Hints/Notes:
|
||||
|
||||
o-- connections to/from Limbo
|
||||
intro/outro areas are rooms that automatically sets/cleans the
|
||||
Character of any settings assigned to it during the
|
||||
tutorial game.
|
||||
The Cliff is a good place to get an overview of the surroundings.
|
||||
The Bridge may seem like a big room, but it is really only one
|
||||
room with custom move commands to make it take longer
|
||||
to cross. You can also fall off the bridge if you
|
||||
are unlucky or take your time to take in the view too
|
||||
long.
|
||||
In the Castle areas an aggressive mob is patrolling. It implements
|
||||
rudimentary AI but packs quite a punch unless you have
|
||||
found yourself a weapon that can harm it. Combat is only
|
||||
possible once you find a weapon.
|
||||
The Catacombs feature a puzzle for finding the correct Grave
|
||||
chamber.
|
||||
The Cell is your reward if you fail in various ways. Finding a
|
||||
way out of it is a small puzzle of its own.
|
||||
The Tomb is a nice place to find a weapon that can hurt the
|
||||
castle guardian. This is the goal of the tutorial.
|
||||
Explore on, or take the exit to finish the tutorial.
|
||||
? - look into the code if you cannot find this bonus area!
|
||||
|
|
@ -1,385 +0,0 @@
|
|||
"""
|
||||
This module implements a simple mobile object with
|
||||
a very rudimentary AI as well as an aggressive enemy
|
||||
object based on that mobile class.
|
||||
|
||||
"""
|
||||
|
||||
import random, time
|
||||
from django.conf import settings
|
||||
|
||||
from ev import search_object, utils, Script
|
||||
from contrib.tutorial_world import objects as tut_objects
|
||||
from contrib.tutorial_world import scripts as tut_scripts
|
||||
|
||||
BASE_CHARACTER_TYPECLASS = settings.BASE_CHARACTER_TYPECLASS
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Mob - mobile object
|
||||
#
|
||||
# This object utilizes exits and moves about randomly from
|
||||
# room to room.
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
class Mob(tut_objects.TutorialObject):
|
||||
"""
|
||||
This type of mobile will roam from exit to exit at
|
||||
random intervals. Simply lock exits against the is_mob attribute
|
||||
to block them from the mob (lockstring = "traverse:not attr(is_mob)").
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"This is called when the object is first created."
|
||||
self.db.tutorial_info = "This is a moving object. It moves randomly from room to room."
|
||||
|
||||
self.scripts.add(tut_scripts.IrregularEvent)
|
||||
# this is a good attribute for exits to look for, to block
|
||||
# a mob from entering certain exits.
|
||||
self.db.is_mob = True
|
||||
self.db.last_location = None
|
||||
# only when True will the mob move.
|
||||
self.db.roam_mode = True
|
||||
|
||||
def announce_move_from(self, destination):
|
||||
"Called just before moving"
|
||||
self.location.msg_contents("With a cold breeze, %s drifts in the direction of %s." % (self.key, destination.key))
|
||||
|
||||
def announce_move_to(self, source_location):
|
||||
"Called just after arriving"
|
||||
self.location.msg_contents("With a wailing sound, %s appears from the %s." % (self.key, source_location.key))
|
||||
|
||||
def update_irregular(self):
|
||||
"Called at irregular intervals. Moves the mob."
|
||||
if self.roam_mode:
|
||||
exits = [ex for ex in self.location.exits
|
||||
if ex.access(self, "traverse")]
|
||||
if exits:
|
||||
# Try to make it so the mob doesn't backtrack.
|
||||
new_exits = [ex for ex in exits
|
||||
if ex.destination != self.db.last_location]
|
||||
if new_exits:
|
||||
exits = new_exits
|
||||
self.db.last_location = self.location
|
||||
# execute_cmd() allows the mob to respect exit and
|
||||
# exit-command locks, but may pose a problem if there is more
|
||||
# than one exit with the same name.
|
||||
# - see Enemy example for another way to move
|
||||
self.execute_cmd("%s" % exits[random.randint(0, len(exits) - 1)].key)
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Enemy - mobile attacking object
|
||||
#
|
||||
# An enemy is a mobile that is aggressive against players
|
||||
# in its vicinity. An enemy will try to attack characters
|
||||
# in the same location. It will also pursue enemies through
|
||||
# exits if possible.
|
||||
#
|
||||
# An enemy needs to have a Weapon object in order to
|
||||
# attack.
|
||||
#
|
||||
# This particular tutorial enemy is a ghostly apparition that can only
|
||||
# be hurt by magical weapons. It will also not truly "die", but only
|
||||
# teleport to another room. Players defeated by the apparition will
|
||||
# conversely just be teleported to a holding room.
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
class AttackTimer(Script):
|
||||
"""
|
||||
This script is what makes an eneny "tick".
|
||||
"""
|
||||
def at_script_creation(self):
|
||||
"This sets up the script"
|
||||
self.key = "AttackTimer"
|
||||
self.desc = "Drives an Enemy's combat."
|
||||
self.interval = random.randint(2, 3) # how fast the Enemy acts
|
||||
self.start_delay = True # wait self.interval before first call
|
||||
self.persistent = True
|
||||
|
||||
def at_repeat(self):
|
||||
"Called every self.interval seconds."
|
||||
if self.obj.db.inactive:
|
||||
return
|
||||
#print "attack timer: at_repeat", self.dbobj.id, self.ndb.twisted_task,
|
||||
# id(self.ndb.twisted_task)
|
||||
if self.obj.db.roam_mode:
|
||||
self.obj.roam()
|
||||
#return
|
||||
elif self.obj.db.battle_mode:
|
||||
#print "attack"
|
||||
self.obj.attack()
|
||||
return
|
||||
elif self.obj.db.pursue_mode:
|
||||
#print "pursue"
|
||||
self.obj.pursue()
|
||||
#return
|
||||
else:
|
||||
#dead mode. Wait for respawn.
|
||||
if not self.obj.db.dead_at:
|
||||
self.obj.db.dead_at = time.time()
|
||||
if (time.time() - self.obj.db.dead_at) > self.obj.db.dead_timer:
|
||||
self.obj.reset()
|
||||
|
||||
|
||||
class Enemy(Mob):
|
||||
"""
|
||||
This is a ghostly enemy with health (hit points). Their chance to hit,
|
||||
damage etc is determined by the weapon they are wielding, same as
|
||||
characters.
|
||||
|
||||
An enemy can be in four modes:
|
||||
roam (inherited from Mob) - where it just moves around randomly
|
||||
battle - where it stands in one place and attacks players
|
||||
pursue - where it follows a player, trying to enter combat again
|
||||
dead - passive and invisible until it is respawned
|
||||
|
||||
Upon creation, the following attributes describe the enemy's actions
|
||||
desc - description
|
||||
full_health - integer number > 0
|
||||
defeat_location - unique name or #dbref to the location the player is
|
||||
taken when defeated. If not given, will remain in room.
|
||||
defeat_text - text to show player when they are defeated (just before
|
||||
being whisped away to defeat_location)
|
||||
defeat_text_room - text to show other players in room when a player
|
||||
is defeated
|
||||
win_text - text to show player when defeating the enemy
|
||||
win_text_room - text to show room when a player defeates the enemy
|
||||
respawn_text - text to echo to room when the mob is reset/respawn in
|
||||
that room.
|
||||
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"Called at object creation."
|
||||
super(Enemy, self).at_object_creation()
|
||||
|
||||
self.db.tutorial_info = "This moving object will attack players in the same room."
|
||||
|
||||
# state machine modes
|
||||
self.db.roam_mode = True
|
||||
self.db.battle_mode = False
|
||||
self.db.pursue_mode = False
|
||||
self.db.dead_mode = False
|
||||
# health (change this at creation time)
|
||||
self.db.full_health = 20
|
||||
self.db.health = 20
|
||||
self.db.dead_at = time.time()
|
||||
self.db.dead_timer = 100 # how long to stay dead
|
||||
# this is used during creation to make sure the mob doesn't move away
|
||||
self.db.inactive = True
|
||||
# store the last player to hit
|
||||
self.db.last_attacker = None
|
||||
# where to take defeated enemies
|
||||
self.db.defeat_location = "darkcell"
|
||||
self.scripts.add(AttackTimer)
|
||||
|
||||
def update_irregular(self):
|
||||
"the irregular event is inherited from Mob class"
|
||||
strings = self.db.irregular_echoes
|
||||
if strings:
|
||||
self.location.msg_contents(strings[random.randint(0, len(strings) - 1)])
|
||||
|
||||
def roam(self):
|
||||
"Called by Attack timer. Will move randomly as long as exits are open."
|
||||
|
||||
# in this mode, the mob is healed.
|
||||
self.db.health = self.db.full_health
|
||||
players = [obj for obj in self.location.contents
|
||||
if utils.inherits_from(obj, BASE_CHARACTER_TYPECLASS) and not obj.is_superuser]
|
||||
if players:
|
||||
# we found players in the room. Attack.
|
||||
self.db.roam_mode = False
|
||||
self.db.pursue_mode = False
|
||||
self.db.battle_mode = True
|
||||
|
||||
elif random.random() < 0.2:
|
||||
# no players to attack, move about randomly.
|
||||
exits = [ex.destination for ex in self.location.exits
|
||||
if ex.access(self, "traverse")]
|
||||
if exits:
|
||||
# Try to make it so the mob doesn't backtrack.
|
||||
new_exits = [ex for ex in exits
|
||||
if ex.destination != self.db.last_location]
|
||||
if new_exits:
|
||||
exits = new_exits
|
||||
self.db.last_location = self.location
|
||||
# locks should be checked here
|
||||
self.move_to(exits[random.randint(0, len(exits) - 1)])
|
||||
else:
|
||||
# no exits - a dead end room. Respawn back to start.
|
||||
self.move_to(self.home)
|
||||
|
||||
def attack(self):
|
||||
"""
|
||||
This is the main mode of combat. It will try to hit players in
|
||||
the location. If players are defeated, it will whisp them off
|
||||
to the defeat location.
|
||||
"""
|
||||
last_attacker = self.db.last_attacker
|
||||
players = [obj for obj in self.location.contents
|
||||
if utils.inherits_from(obj, BASE_CHARACTER_TYPECLASS) and not obj.is_superuser]
|
||||
if players:
|
||||
|
||||
# find a target
|
||||
if last_attacker in players:
|
||||
# prefer to attack the player last attacking.
|
||||
target = last_attacker
|
||||
else:
|
||||
# otherwise attack a random player in location
|
||||
target = players[random.randint(0, len(players) - 1)]
|
||||
|
||||
# try to use the weapon in hand
|
||||
attack_cmds = ("thrust", "pierce", "stab", "slash", "chop")
|
||||
cmd = attack_cmds[random.randint(0, len(attack_cmds) - 1)]
|
||||
self.execute_cmd("%s %s" % (cmd, target))
|
||||
|
||||
# analyze result.
|
||||
if target.db.health <= 0:
|
||||
# we reduced enemy to 0 health. Whisp them off to
|
||||
# the prison room.
|
||||
tloc = search_object(self.db.defeat_location)
|
||||
tstring = self.db.defeat_text
|
||||
if not tstring:
|
||||
tstring = "You feel your conciousness slip away ... you fall to the ground as "
|
||||
tstring += "the misty apparition envelopes you ...\n The world goes black ...\n"
|
||||
target.msg(tstring)
|
||||
ostring = self.db.defeat_text_room
|
||||
if tloc:
|
||||
if not ostring:
|
||||
ostring = "\n%s envelops the fallen ... and then their body is suddenly gone!" % self.key
|
||||
# silently move the player to defeat location
|
||||
# (we need to call hook manually)
|
||||
target.location = tloc[0]
|
||||
tloc[0].at_object_receive(target, self.location)
|
||||
elif not ostring:
|
||||
ostring = "%s falls to the ground!" % target.key
|
||||
self.location.msg_contents(ostring, exclude=[target])
|
||||
# Pursue any stragglers after the battle
|
||||
self.battle_mode = False
|
||||
self.roam_mode = False
|
||||
self.pursue_mode = True
|
||||
else:
|
||||
# no players found, this could mean they have fled.
|
||||
# Switch to pursue mode.
|
||||
self.battle_mode = False
|
||||
self.roam_mode = False
|
||||
self.pursue_mode = True
|
||||
|
||||
def pursue(self):
|
||||
"""
|
||||
In pursue mode, the enemy tries to find players in adjoining rooms, preferably
|
||||
those that previously attacked it.
|
||||
"""
|
||||
last_attacker = self.db.last_attacker
|
||||
players = [obj for obj in self.location.contents if utils.inherits_from(obj, BASE_CHARACTER_TYPECLASS) and not obj.is_superuser]
|
||||
if players:
|
||||
# we found players in the room. Maybe we caught up with some,
|
||||
# or some walked in on us before we had time to pursue them.
|
||||
# Switch to battle mode.
|
||||
self.battle_mode = True
|
||||
self.roam_mode = False
|
||||
self.pursue_mode = False
|
||||
else:
|
||||
# find all possible destinations.
|
||||
destinations = [ex.destination for ex in self.location.exits
|
||||
if ex.access(self, "traverse")]
|
||||
# find all players in the possible destinations. OBS-we cannot
|
||||
# just use the player's current position to move the Enemy; this
|
||||
# might have changed when the move is performed, causing the enemy
|
||||
# to teleport out of bounds.
|
||||
players = {}
|
||||
for dest in destinations:
|
||||
for obj in [o for o in dest.contents
|
||||
if utils.inherits_from(o, BASE_CHARACTER_TYPECLASS)]:
|
||||
players[obj] = dest
|
||||
if players:
|
||||
# we found targets. Move to intercept.
|
||||
if last_attacker in players:
|
||||
# preferably the one that last attacked us
|
||||
self.move_to(players[last_attacker])
|
||||
else:
|
||||
# otherwise randomly.
|
||||
key = players.keys()[random.randint(0, len(players) - 1)]
|
||||
self.move_to(players[key])
|
||||
else:
|
||||
# we found no players nearby. Return to roam mode.
|
||||
self.battle_mode = False
|
||||
self.roam_mode = True
|
||||
self.pursue_mode = False
|
||||
|
||||
def at_hit(self, weapon, attacker, damage):
|
||||
"""
|
||||
Called when this object is hit by an enemy's weapon
|
||||
Should return True if enemy is defeated, False otherwise.
|
||||
|
||||
In the case of players attacking, we handle all the events
|
||||
and information from here, so the return value is not used.
|
||||
"""
|
||||
|
||||
self.db.last_attacker = attacker
|
||||
if not self.db.battle_mode:
|
||||
# we were attacked, so switch to battle mode.
|
||||
self.db.roam_mode = False
|
||||
self.db.pursue_mode = False
|
||||
self.db.battle_mode = True
|
||||
#self.scripts.add(AttackTimer)
|
||||
|
||||
if not weapon.db.magic:
|
||||
# In the tutorial, the enemy is a ghostly apparition, so
|
||||
# only magical weapons can harm it.
|
||||
string = self.db.weapon_ineffective_text
|
||||
if not string:
|
||||
string = "Your weapon just passes through your enemy, causing no effect!"
|
||||
attacker.msg(string)
|
||||
return
|
||||
else:
|
||||
# an actual hit
|
||||
health = float(self.db.health)
|
||||
health -= damage
|
||||
self.db.health = health
|
||||
if health <= 0:
|
||||
string = self.db.win_text
|
||||
if not string:
|
||||
string = "After your last hit, %s folds in on itself, it seems to fade away into nothingness. " % self.key
|
||||
string += "In a moment there is nothing left but the echoes of its screams. But you have a "
|
||||
string += "feeling it is only temporarily weakened. "
|
||||
string += "You fear it's only a matter of time before it materializes somewhere again."
|
||||
attacker.msg(string)
|
||||
string = self.db.win_text_room
|
||||
if not string:
|
||||
string = "After %s's last hit, %s folds in on itself, it seems to fade away into nothingness. " % (attacker.name, self.key)
|
||||
string += "In a moment there is nothing left but the echoes of its screams. But you have a "
|
||||
string += "feeling it is only temporarily weakened. "
|
||||
string += "You fear it's only a matter of time before it materializes somewhere again."
|
||||
self.location.msg_contents(string, exclude=[attacker])
|
||||
|
||||
# put mob in dead mode and hide it from view.
|
||||
# AttackTimer will bring it back later.
|
||||
self.db.dead_at = time.time()
|
||||
self.db.roam_mode = False
|
||||
self.db.pursue_mode = False
|
||||
self.db.battle_mode = False
|
||||
self.db.dead_mode = True
|
||||
self.location = None
|
||||
else:
|
||||
self.location.msg_contents("%s wails, shudders and writhes." % self.key)
|
||||
return False
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
If the mob was 'dead', respawn it to its home position and reset
|
||||
all modes and damage."""
|
||||
if self.db.dead_mode:
|
||||
self.db.health = self.db.full_health
|
||||
self.db.roam_mode = True
|
||||
self.db.pursue_mode = False
|
||||
self.db.battle_mode = False
|
||||
self.db.dead_mode = False
|
||||
self.location = self.home
|
||||
string = self.db.respawn_text
|
||||
if not string:
|
||||
string = "%s fades into existence from out of thin air. It's looking pissed." % self.key
|
||||
self.location.msg_contents(string)
|
||||
|
|
@ -1,739 +0,0 @@
|
|||
"""
|
||||
|
||||
Room Typeclasses for the TutorialWorld.
|
||||
|
||||
"""
|
||||
|
||||
import random
|
||||
from ev import CmdSet, Script, Command, Room
|
||||
from ev import utils, create_object, search_object
|
||||
from contrib.tutorial_world import scripts as tut_scripts
|
||||
from contrib.tutorial_world.objects import LightSource, TutorialObject
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Tutorial room - parent room class
|
||||
#
|
||||
# This room is the parent of all rooms in the tutorial.
|
||||
# It defines a tutorial command on itself (available to
|
||||
# all who is in a tutorial room).
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
class CmdTutorial(Command):
|
||||
"""
|
||||
Get help during the tutorial
|
||||
|
||||
Usage:
|
||||
tutorial [obj]
|
||||
|
||||
This command allows you to get behind-the-scenes info
|
||||
about an object or the current location.
|
||||
|
||||
"""
|
||||
key = "tutorial"
|
||||
aliases = ["tut"]
|
||||
locks = "cmd:all()"
|
||||
help_category = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
All we do is to scan the current location for an attribute
|
||||
called `tutorial_info` and display that.
|
||||
"""
|
||||
|
||||
caller = self.caller
|
||||
|
||||
if not self.args:
|
||||
target = self.obj # this is the room the command is defined on
|
||||
else:
|
||||
target = caller.search(self.args.strip())
|
||||
if not target:
|
||||
return
|
||||
helptext = target.db.tutorial_info
|
||||
if helptext:
|
||||
caller.msg("{G%s{n" % helptext)
|
||||
else:
|
||||
caller.msg("{RSorry, there is no tutorial help available here.{n")
|
||||
|
||||
|
||||
class TutorialRoomCmdSet(CmdSet):
|
||||
"Implements the simple tutorial cmdset"
|
||||
key = "tutorial_cmdset"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"add the tutorial cmd"
|
||||
self.add(CmdTutorial())
|
||||
|
||||
|
||||
class TutorialRoom(Room):
|
||||
"""
|
||||
This is the base room type for all rooms in the tutorial world.
|
||||
It defines a cmdset on itself for reading tutorial info about the location.
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"Called when room is first created"
|
||||
self.db.tutorial_info = "This is a tutorial room. It allows you to use the 'tutorial' command."
|
||||
self.cmdset.add_default(TutorialRoomCmdSet)
|
||||
|
||||
def reset(self):
|
||||
"Can be called by the tutorial runner."
|
||||
pass
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Weather room - scripted room
|
||||
#
|
||||
# The weather room is called by a script at
|
||||
# irregular intervals. The script is generally useful
|
||||
# and so is split out into tutorialworld.scripts.
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
class WeatherRoom(TutorialRoom):
|
||||
"""
|
||||
This should probably better be called a rainy room...
|
||||
|
||||
This sets up an outdoor room typeclass. At irregular intervals,
|
||||
the effects of weather will show in the room. Outdoor rooms should
|
||||
inherit from this.
|
||||
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"Called when object is first created."
|
||||
super(WeatherRoom, self).at_object_creation()
|
||||
|
||||
# we use the imported IrregularEvent script
|
||||
self.scripts.add(tut_scripts.IrregularEvent)
|
||||
self.db.tutorial_info = \
|
||||
"This room has a Script running that has it echo a weather-related message at irregular intervals."
|
||||
|
||||
def update_irregular(self):
|
||||
"create a tuple of possible texts to return."
|
||||
strings = (
|
||||
"The rain coming down from the iron-grey sky intensifies.",
|
||||
"A gush of wind throws the rain right in your face. Despite your cloak you shiver.",
|
||||
"The rainfall eases a bit and the sky momentarily brightens.",
|
||||
"For a moment it looks like the rain is slowing, then it begins anew with renewed force.",
|
||||
"The rain pummels you with large, heavy drops. You hear the rumble of thunder in the distance.",
|
||||
"The wind is picking up, howling around you, throwing water droplets in your face. It's cold.",
|
||||
"Bright fingers of lightning flash over the sky, moments later followed by a deafening rumble.",
|
||||
"It rains so hard you can hardly see your hand in front of you. You'll soon be drenched to the bone.",
|
||||
"Lightning strikes in several thundering bolts, striking the trees in the forest to your west.",
|
||||
"You hear the distant howl of what sounds like some sort of dog or wolf.",
|
||||
"Large clouds rush across the sky, throwing their load of rain over the world.")
|
||||
|
||||
# get a random value so we can select one of the strings above.
|
||||
# Send this to the room.
|
||||
irand = random.randint(0, 15)
|
||||
if irand > 10:
|
||||
return # don't return anything, to add more randomness
|
||||
self.msg_contents("{w%s{n" % strings[irand])
|
||||
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
#
|
||||
# Dark Room - a scripted room
|
||||
#
|
||||
# This room limits the movemenets of its denizens unless they carry a and active
|
||||
# LightSource object (LightSource is defined in
|
||||
# tutorialworld.objects.LightSource)
|
||||
#
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
class CmdLookDark(Command):
|
||||
"""
|
||||
Look around in darkness
|
||||
|
||||
Usage:
|
||||
look
|
||||
|
||||
Looks in darkness
|
||||
"""
|
||||
key = "look"
|
||||
aliases = ["l", 'feel', 'feel around', 'fiddle']
|
||||
locks = "cmd:all()"
|
||||
help_category = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
"Implement the command."
|
||||
caller = self.caller
|
||||
# we don't have light, grasp around blindly.
|
||||
messages = ("It's pitch black. You fumble around but cannot find anything.",
|
||||
"You don't see a thing. You feel around, managing to bump your fingers hard against something. Ouch!",
|
||||
"You don't see a thing! Blindly grasping the air around you, you find nothing.",
|
||||
"It's totally dark here. You almost stumble over some un-evenness in the ground.",
|
||||
"You are completely blind. For a moment you think you hear someone breathing nearby ... \n ... surely you must be mistaken.",
|
||||
"Blind, you think you find some sort of object on the ground, but it turns out to be just a stone.",
|
||||
"Blind, you bump into a wall. The wall seems to be covered with some sort of vegetation, but its too damp to burn.",
|
||||
"You can't see anything, but the air is damp. It feels like you are far underground.")
|
||||
irand = random.randint(0, 10)
|
||||
if irand < len(messages):
|
||||
caller.msg(messages[irand])
|
||||
else:
|
||||
# check so we don't already carry a lightsource.
|
||||
carried_lights = [obj for obj in caller.contents
|
||||
if utils.inherits_from(obj, LightSource)]
|
||||
if carried_lights:
|
||||
string = "You don't want to stumble around in blindness anymore. You already found what you need. Let's get light already!"
|
||||
caller.msg(string)
|
||||
return
|
||||
#if we are lucky, we find the light source.
|
||||
lightsources = [obj for obj in self.obj.contents
|
||||
if utils.inherits_from(obj, LightSource)]
|
||||
if lightsources:
|
||||
lightsource = lightsources[0]
|
||||
else:
|
||||
# create the light source from scratch.
|
||||
lightsource = create_object(LightSource, key="splinter")
|
||||
lightsource.location = caller
|
||||
string = "Your fingers bump against a splinter of wood in a corner. It smells of resin and seems dry enough to burn!"
|
||||
string += "\nYou pick it up, holding it firmly. Now you just need to {wlight{n it using the flint and steel you carry with you."
|
||||
caller.msg(string)
|
||||
|
||||
|
||||
class CmdDarkHelp(Command):
|
||||
"""
|
||||
Help command for the dark state.
|
||||
"""
|
||||
key = "help"
|
||||
locks = "cmd:all()"
|
||||
help_category = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
"Implements the help command."
|
||||
string = "Can't help you until you find some light! Try feeling around for something to burn."
|
||||
string += " You cannot give up even if you don't find anything right away."
|
||||
self.caller.msg(string)
|
||||
|
||||
# the nomatch system command will give a suitable error when we cannot find
|
||||
# the normal commands.
|
||||
from src.commands.default.syscommands import CMD_NOMATCH
|
||||
from src.commands.default.general import CmdSay
|
||||
|
||||
|
||||
class CmdDarkNoMatch(Command):
|
||||
"This is called when there is no match"
|
||||
key = CMD_NOMATCH
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"Implements the command."
|
||||
self.caller.msg("Until you find some light, there's not much you can do. Try feeling around.")
|
||||
|
||||
|
||||
class DarkCmdSet(CmdSet):
|
||||
"Groups the commands."
|
||||
key = "darkroom_cmdset"
|
||||
mergetype = "Replace" # completely remove all other commands
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"populates the cmdset."
|
||||
self.add(CmdTutorial())
|
||||
self.add(CmdLookDark())
|
||||
self.add(CmdDarkHelp())
|
||||
self.add(CmdDarkNoMatch())
|
||||
self.add(CmdSay)
|
||||
|
||||
#
|
||||
# Darkness room two-state system
|
||||
#
|
||||
|
||||
class DarkState(Script):
|
||||
"""
|
||||
The darkness state is a script that keeps tabs on when
|
||||
a player in the room carries an active light source. It places
|
||||
a new, very restrictive cmdset (DarkCmdSet) on all the players
|
||||
in the room whenever there is no light in it. Upon turning on
|
||||
a light, the state switches off and moves to LightState.
|
||||
"""
|
||||
def at_script_creation(self):
|
||||
"This setups the script"
|
||||
self.key = "tutorial_darkness_state"
|
||||
self.desc = "A dark room"
|
||||
self.persistent = True
|
||||
|
||||
def at_start(self):
|
||||
"called when the script is first starting up."
|
||||
for char in [char for char in self.obj.contents if char.has_player]:
|
||||
if char.is_superuser:
|
||||
char.msg("You are Superuser, so you are not affected by the dark state.")
|
||||
else:
|
||||
char.cmdset.add(DarkCmdSet)
|
||||
char.msg("The room is pitch dark! You are likely to be eaten by a Grue.")
|
||||
|
||||
def is_valid(self):
|
||||
"is valid only as long as noone in the room has lit the lantern."
|
||||
return not self.obj.is_lit()
|
||||
|
||||
def at_stop(self):
|
||||
"Someone turned on a light. This state dies. Switch to LightState."
|
||||
for char in [char for char in self.obj.contents if char.has_player]:
|
||||
char.cmdset.delete(DarkCmdSet)
|
||||
self.obj.db.is_dark = False
|
||||
self.obj.scripts.add(LightState)
|
||||
|
||||
|
||||
class LightState(Script):
|
||||
"""
|
||||
This is the counterpart to the Darkness state. It is active when the
|
||||
lantern is on.
|
||||
"""
|
||||
def at_script_creation(self):
|
||||
"Called when script is first created."
|
||||
self.key = "tutorial_light_state"
|
||||
self.desc = "A room lit up"
|
||||
self.persistent = True
|
||||
|
||||
def is_valid(self):
|
||||
"""
|
||||
This state is only valid as long as there is an active light
|
||||
source in the room.
|
||||
"""
|
||||
return self.obj.is_lit()
|
||||
|
||||
def at_stop(self):
|
||||
"Light disappears. This state dies. Return to DarknessState."
|
||||
self.obj.db.is_dark = True
|
||||
self.obj.scripts.add(DarkState)
|
||||
|
||||
|
||||
class DarkRoom(TutorialRoom):
|
||||
"""
|
||||
A dark room. This tries to start the DarkState script on all
|
||||
objects entering. The script is responsible for making sure it is
|
||||
valid (that is, that there is no light source shining in the room).
|
||||
"""
|
||||
def is_lit(self):
|
||||
"""
|
||||
Helper method to check if the room is lit up. It checks all
|
||||
characters in room to see if they carry an active object of
|
||||
type LightSource.
|
||||
"""
|
||||
return any([any([True for obj in char.contents
|
||||
if utils.inherits_from(obj, LightSource) and obj.db.is_active])
|
||||
for char in self.contents if char.has_player])
|
||||
|
||||
def at_object_creation(self):
|
||||
"Called when object is first created."
|
||||
super(DarkRoom, self).at_object_creation()
|
||||
self.db.tutorial_info = "This is a room with custom command sets on itself."
|
||||
# this variable is set by the scripts. It makes for an easy flag to
|
||||
# look for by other game elements (such as the crumbling wall in
|
||||
# the tutorial)
|
||||
self.db.is_dark = True
|
||||
# the room starts dark.
|
||||
self.scripts.add(DarkState)
|
||||
|
||||
def at_object_receive(self, character, source_location):
|
||||
"""
|
||||
Called when an object enters the room. We crank the wheels to make
|
||||
sure scripts are synced.
|
||||
"""
|
||||
if character.has_player:
|
||||
if not self.is_lit() and not character.is_superuser:
|
||||
character.cmdset.add(DarkCmdSet)
|
||||
if character.db.health and character.db.health <= 0:
|
||||
# heal character coming here from being defeated by mob.
|
||||
health = character.db.health_max
|
||||
if not health:
|
||||
health = 20
|
||||
character.db.health = health
|
||||
self.scripts.validate()
|
||||
|
||||
def at_object_leave(self, character, target_location):
|
||||
"""
|
||||
In case people leave with the light, we make sure to update the
|
||||
states accordingly.
|
||||
"""
|
||||
character.cmdset.delete(DarkCmdSet) # in case we are teleported away
|
||||
self.scripts.validate()
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Teleport room - puzzle room
|
||||
#
|
||||
# This is a sort of puzzle room that requires a certain
|
||||
# attribute on the entering character to be the same as
|
||||
# an attribute of the room. If not, the character will
|
||||
# be teleported away to a target location. This is used
|
||||
# by the Obelisk - grave chamber puzzle, where one must
|
||||
# have looked at the obelisk to get an attribute set on
|
||||
# oneself, and then pick the grave chamber with the
|
||||
# matching imagery for this attribute.
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
class TeleportRoom(TutorialRoom):
|
||||
"""
|
||||
Teleporter - puzzle room.
|
||||
|
||||
Important attributes (set at creation):
|
||||
puzzle_key - which attr to look for on character
|
||||
puzzle_value - what char.db.puzzle_key must be set to
|
||||
teleport_to - where to teleport to in case of failure to match
|
||||
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"Called at first creation"
|
||||
super(TeleportRoom, self).at_object_creation()
|
||||
# what character.db.puzzle_clue must be set to, to avoid teleportation.
|
||||
self.db.puzzle_value = 1
|
||||
# target of successful teleportation. Can be a dbref or a
|
||||
# unique room name.
|
||||
self.db.success_teleport_to = "treasure room"
|
||||
# the target of the failure teleportation.
|
||||
self.db.failure_teleport_to = "dark cell"
|
||||
|
||||
def at_object_receive(self, character, source_location):
|
||||
"""
|
||||
This hook is called by the engine whenever the player is moved into
|
||||
this room.
|
||||
"""
|
||||
if not character.has_player:
|
||||
# only act on player characters.
|
||||
return
|
||||
#print character.db.puzzle_clue, self.db.puzzle_value
|
||||
if character.db.puzzle_clue != self.db.puzzle_value:
|
||||
# we didn't pass the puzzle. See if we can teleport.
|
||||
teleport_to = self.db.failure_teleport_to # this is a room name
|
||||
else:
|
||||
# passed the puzzle
|
||||
teleport_to = self.db.success_teleport_to # this is a room name
|
||||
|
||||
results = search_object(teleport_to)
|
||||
if not results or len(results) > 1:
|
||||
# we cannot move anywhere since no valid target was found.
|
||||
print "no valid teleport target for %s was found." % teleport_to
|
||||
return
|
||||
if character.player.is_superuser:
|
||||
# superusers don't get teleported
|
||||
character.msg("Superuser block: You would have been teleported to %s." % results[0])
|
||||
return
|
||||
# teleport
|
||||
character.execute_cmd("look")
|
||||
character.location = results[0] # stealth move
|
||||
character.location.at_object_receive(character, self)
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Bridge - unique room
|
||||
#
|
||||
# Defines a special west-eastward "bridge"-room, a large room it takes
|
||||
# several steps to cross. It is complete with custom commands and a
|
||||
# chance of falling off the bridge. This room has no regular exits,
|
||||
# instead the exiting are handled by custom commands set on the player
|
||||
# upon first entering the room.
|
||||
#
|
||||
# Since one can enter the bridge room from both ends, it is
|
||||
# divided into five steps:
|
||||
# westroom <- 0 1 2 3 4 -> eastroom
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
|
||||
class CmdEast(Command):
|
||||
"""
|
||||
Try to cross the bridge eastwards.
|
||||
"""
|
||||
key = "east"
|
||||
aliases = ["e"]
|
||||
locks = "cmd:all()"
|
||||
help_category = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
"move forward"
|
||||
caller = self.caller
|
||||
|
||||
bridge_step = min(5, caller.db.tutorial_bridge_position + 1)
|
||||
|
||||
if bridge_step > 4:
|
||||
# we have reached the far east end of the bridge.
|
||||
# Move to the east room.
|
||||
eexit = search_object(self.obj.db.east_exit)
|
||||
if eexit:
|
||||
caller.move_to(eexit[0])
|
||||
else:
|
||||
caller.msg("No east exit was found for this room. Contact an admin.")
|
||||
return
|
||||
caller.db.tutorial_bridge_position = bridge_step
|
||||
caller.location.msg_contents("%s steps eastwards across the bridge." % caller.name, exclude=caller)
|
||||
caller.execute_cmd("look")
|
||||
|
||||
|
||||
# go back across the bridge
|
||||
class CmdWest(Command):
|
||||
"""
|
||||
Go back across the bridge westwards.
|
||||
"""
|
||||
key = "west"
|
||||
aliases = ["w"]
|
||||
locks = "cmd:all()"
|
||||
help_category = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
"move forward"
|
||||
caller = self.caller
|
||||
|
||||
bridge_step = max(-1, caller.db.tutorial_bridge_position - 1)
|
||||
|
||||
if bridge_step < 0:
|
||||
# we have reached the far west end of the bridge.#
|
||||
# Move to the west room.
|
||||
wexit = search_object(self.obj.db.west_exit)
|
||||
if wexit:
|
||||
caller.move_to(wexit[0])
|
||||
else:
|
||||
caller.msg("No west exit was found for this room. Contact an admin.")
|
||||
return
|
||||
caller.db.tutorial_bridge_position = bridge_step
|
||||
caller.location.msg_contents("%s steps westwartswards across the bridge." % caller.name, exclude=caller)
|
||||
caller.execute_cmd("look")
|
||||
|
||||
|
||||
class CmdLookBridge(Command):
|
||||
"""
|
||||
looks around at the bridge.
|
||||
"""
|
||||
key = 'look'
|
||||
aliases = ["l"]
|
||||
locks = "cmd:all()"
|
||||
help_category = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
"Looking around, including a chance to fall."
|
||||
bridge_position = self.caller.db.tutorial_bridge_position
|
||||
|
||||
|
||||
messages =("You are standing {wvery close to the the bridge's western foundation{n. If you go west you will be back on solid ground ...",
|
||||
"The bridge slopes precariously where it extends eastwards towards the lowest point - the center point of the hang bridge.",
|
||||
"You are {whalfways{n out on the unstable bridge.",
|
||||
"The bridge slopes precariously where it extends westwards towards the lowest point - the center point of the hang bridge.",
|
||||
"You are standing {wvery close to the bridge's eastern foundation{n. If you go east you will be back on solid ground ...")
|
||||
moods = ("The bridge sways in the wind.", "The hanging bridge creaks dangerously.",
|
||||
"You clasp the ropes firmly as the bridge sways and creaks under you.",
|
||||
"From the castle you hear a distant howling sound, like that of a large dog or other beast.",
|
||||
"The bridge creaks under your feet. Those planks does not seem very sturdy.",
|
||||
"Far below you the ocean roars and throws its waves against the cliff, as if trying its best to reach you.",
|
||||
"Parts of the bridge come loose behind you, falling into the chasm far below!",
|
||||
"A gust of wind causes the bridge to sway precariously.",
|
||||
"Under your feet a plank comes loose, tumbling down. For a moment you dangle over the abyss ...",
|
||||
"The section of rope you hold onto crumble in your hands, parts of it breaking apart. You sway trying to regain balance.")
|
||||
message = "{c%s{n\n" % self.obj.key + messages[bridge_position] + "\n" + moods[random.randint(0, len(moods) - 1)]
|
||||
chars = [obj for obj in self.obj.contents if obj != self.caller and obj.has_player]
|
||||
if chars:
|
||||
message += "\n You see: %s" % ", ".join("{c%s{n" % char.key for char in chars)
|
||||
|
||||
self.caller.msg(message)
|
||||
|
||||
# there is a chance that we fall if we are on the western or central
|
||||
# part of the bridge.
|
||||
if bridge_position < 3 and random.random() < 0.05 and not self.caller.is_superuser:
|
||||
# we fall on 5% of the times.
|
||||
fexit = search_object(self.obj.db.fall_exit)
|
||||
if fexit:
|
||||
string = "\n Suddenly the plank you stand on gives way under your feet! You fall!"
|
||||
string += "\n You try to grab hold of an adjoining plank, but all you manage to do is to "
|
||||
string += "divert your fall westwards, towards the cliff face. This is going to hurt ... "
|
||||
string += "\n ... The world goes dark ...\n"
|
||||
# note that we move silently so as to not call look hooks (this is a little trick to leave
|
||||
# the player with the "world goes dark ..." message, giving them ample time to read it. They
|
||||
# have to manually call look to find out their new location). Thus we also call the
|
||||
# at_object_leave hook manually (otherwise this is done by move_to()).
|
||||
self.caller.msg("{r%s{n" % string)
|
||||
self.obj.at_object_leave(self.caller, fexit)
|
||||
self.caller.location = fexit[0] # stealth move, without any other hook calls.
|
||||
self.obj.msg_contents("A plank gives way under %s's feet and they fall from the bridge!" % self.caller.key)
|
||||
|
||||
|
||||
# custom help command
|
||||
class CmdBridgeHelp(Command):
|
||||
"""
|
||||
Overwritten help command
|
||||
"""
|
||||
key = "help"
|
||||
aliases = ["h"]
|
||||
locks = "cmd:all()"
|
||||
help_category = "Tutorial world"
|
||||
|
||||
def func(self):
|
||||
"Implements the command."
|
||||
string = "You are trying hard not to fall off the bridge ..."
|
||||
string += "\n\nWhat you can do is trying to cross the bridge {weast{n "
|
||||
string += "or try to get back to the mainland {wwest{n)."
|
||||
self.caller.msg(string)
|
||||
|
||||
|
||||
class BridgeCmdSet(CmdSet):
|
||||
"This groups the bridge commands. We will store it on the room."
|
||||
key = "Bridge commands"
|
||||
priority = 1 # this gives it precedence over the normal look/help commands.
|
||||
def at_cmdset_creation(self):
|
||||
"Called at first cmdset creation"
|
||||
self.add(CmdTutorial())
|
||||
self.add(CmdEast())
|
||||
self.add(CmdWest())
|
||||
self.add(CmdLookBridge())
|
||||
self.add(CmdBridgeHelp())
|
||||
|
||||
|
||||
class BridgeRoom(TutorialRoom):
|
||||
"""
|
||||
The bridge room implements an unsafe bridge. It also enters the player into
|
||||
a state where they get new commands so as to try to cross the bridge.
|
||||
|
||||
We want this to result in the player getting a special set of
|
||||
commands related to crossing the bridge. The result is that it will
|
||||
take several steps to cross it, despite it being represented by only a
|
||||
single room.
|
||||
|
||||
We divide the bridge into steps:
|
||||
|
||||
self.db.west_exit - - | - - self.db.east_exit
|
||||
0 1 2 3 4
|
||||
|
||||
The position is handled by a variable stored on the player when entering
|
||||
and giving special move commands will increase/decrease the counter
|
||||
until the bridge is crossed.
|
||||
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"Setups the room"
|
||||
super(BridgeRoom, self).at_object_creation()
|
||||
|
||||
# at irregular intervals, this will call self.update_irregular()
|
||||
self.scripts.add(tut_scripts.IrregularEvent)
|
||||
# this identifies the exits from the room (should be the command
|
||||
# needed to leave through that exit). These are defaults, but you
|
||||
# could of course also change them after the room has been created.
|
||||
self.db.west_exit = "cliff"
|
||||
self.db.east_exit = "gate"
|
||||
self.db.fall_exit = "cliffledge"
|
||||
# add the cmdset on the room.
|
||||
self.cmdset.add_default(BridgeCmdSet)
|
||||
|
||||
self.db.tutorial_info = \
|
||||
"""The bridge seem large but is actually only a single room that assigns custom west/east commands."""
|
||||
|
||||
def update_irregular(self):
|
||||
"""
|
||||
This is called at irregular intervals and makes the passage
|
||||
over the bridge a little more interesting.
|
||||
"""
|
||||
strings = (
|
||||
"The rain intensifies, making the planks of the bridge even more slippery.",
|
||||
"A gush of wind throws the rain right in your face.",
|
||||
"The rainfall eases a bit and the sky momentarily brightens.",
|
||||
"The bridge shakes under the thunder of a closeby thunder strike.",
|
||||
"The rain pummels you with large, heavy drops. You hear the distinct howl of a large hound in the distance.",
|
||||
"The wind is picking up, howling around you and causing the bridge to sway from side to side.",
|
||||
"Some sort of large bird sweeps by overhead, giving off an eery screech. Soon it has disappeared in the gloom.",
|
||||
"The bridge sways from side to side in the wind.")
|
||||
self.msg_contents("{w%s{n" % strings[random.randint(0, 7)])
|
||||
|
||||
def at_object_receive(self, character, source_location):
|
||||
"""
|
||||
This hook is called by the engine whenever the player is moved
|
||||
into this room.
|
||||
"""
|
||||
if character.has_player:
|
||||
# we only run this if the entered object is indeed a player object.
|
||||
# check so our east/west exits are correctly defined.
|
||||
wexit = search_object(self.db.west_exit)
|
||||
eexit = search_object(self.db.east_exit)
|
||||
fexit = search_object(self.db.fall_exit)
|
||||
if not wexit or not eexit or not fexit:
|
||||
character.msg("The bridge's exits are not properly configured. Contact an admin. Forcing west-end placement.")
|
||||
character.db.tutorial_bridge_position = 0
|
||||
return
|
||||
if source_location == eexit[0]:
|
||||
character.db.tutorial_bridge_position = 4
|
||||
else:
|
||||
character.db.tutorial_bridge_position = 0
|
||||
|
||||
def at_object_leave(self, character, target_location):
|
||||
"""
|
||||
This is triggered when the player leaves the bridge room.
|
||||
"""
|
||||
if character.has_player:
|
||||
# clean up the position attribute
|
||||
del character.db.tutorial_bridge_position
|
||||
|
||||
|
||||
#-----------------------------------------------------------
|
||||
#
|
||||
# Intro Room - unique room
|
||||
#
|
||||
# This room marks the start of the tutorial. It sets up properties on
|
||||
# the player char that is needed for the tutorial.
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
class IntroRoom(TutorialRoom):
|
||||
"""
|
||||
Intro room
|
||||
|
||||
properties to customize:
|
||||
char_health - integer > 0 (default 20)
|
||||
"""
|
||||
|
||||
def at_object_receive(self, character, source_location):
|
||||
"""
|
||||
Assign properties on characters
|
||||
"""
|
||||
|
||||
# setup
|
||||
health = self.db.char_health
|
||||
if not health:
|
||||
health = 20
|
||||
|
||||
if character.has_player:
|
||||
character.db.health = health
|
||||
character.db.health_max = health
|
||||
|
||||
if character.is_superuser:
|
||||
string = "-"*78
|
||||
string += "\nWARNING: YOU ARE PLAYING AS A SUPERUSER (%s). TO EXPLORE NORMALLY YOU NEED " % character.key
|
||||
string += "\nTO CREATE AND LOG IN AS A REGULAR USER INSTEAD. IF YOU CONTINUE, KNOW THAT "
|
||||
string += "\nMANY FUNCTIONS AND PUZZLES WILL IGNORE THE PRESENCE OF A SUPERUSER.\n"
|
||||
string += "-"*78
|
||||
character.msg("{r%s{n" % string)
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Outro room - unique room
|
||||
#
|
||||
# Cleans up the character from all tutorial-related properties.
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
class OutroRoom(TutorialRoom):
|
||||
"""
|
||||
Outro room.
|
||||
|
||||
One can set an attribute list "wracklist" with weapon-rack ids
|
||||
in order to clear all weapon rack ids from the character.
|
||||
|
||||
"""
|
||||
|
||||
def at_object_receive(self, character, source_location):
|
||||
"""
|
||||
Do cleanup.
|
||||
"""
|
||||
if character.has_player:
|
||||
if self.db.wracklist:
|
||||
for wrackid in self.db.wracklist:
|
||||
character.del_attribute(wrackid)
|
||||
del character.db.health_max
|
||||
del character.db.health
|
||||
del character.db.last_climbed
|
||||
del character.db.puzzle_clue
|
||||
del character.db.combat_parry_mode
|
||||
del character.db.tutorial_bridge_position
|
||||
for tut_obj in [obj for obj in character.contents
|
||||
if utils.inherits_from(obj, TutorialObject)]:
|
||||
tut_obj.reset()
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
"""
|
||||
This defines some generally useful scripts for the tutorial world.
|
||||
"""
|
||||
|
||||
import random
|
||||
from ev import Script
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# IrregularEvent - script firing at random intervals
|
||||
#
|
||||
# This is a generally useful script for updating
|
||||
# objects at irregular intervals. This is used by as diverse
|
||||
# entities as Weather rooms and mobs.
|
||||
#
|
||||
#
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
class IrregularEvent(Script):
|
||||
"""
|
||||
This script, which should be tied to a particular object upon
|
||||
instantiation, calls update_irregular on the object at random
|
||||
intervals.
|
||||
"""
|
||||
def at_script_creation(self):
|
||||
"This setups the script"
|
||||
|
||||
self.key = "update_irregular"
|
||||
self.desc = "Updates at irregular intervals"
|
||||
self.interval = random.randint(30, 70) # interval to call.
|
||||
self.start_delay = True # wait at least self.interval seconds before
|
||||
# calling at_repeat the first time
|
||||
self.persistent = True
|
||||
|
||||
# this attribute determines how likely it is the
|
||||
# 'update_irregular' method gets called on self.obj (value is
|
||||
# 0.0-1.0 with 1.0 meaning it being called every time.)
|
||||
self.db.random_chance = 0.2
|
||||
|
||||
def at_repeat(self):
|
||||
"This gets called every self.interval seconds."
|
||||
rand = random.random()
|
||||
if rand <= self.db.random_chance:
|
||||
try:
|
||||
#self.obj.msg_contents("irregular event for %s(#%i)" % (self.obj, self.obj.id))
|
||||
self.obj.update_irregular()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class FastIrregularEvent(IrregularEvent):
|
||||
"A faster updating irregular event"
|
||||
def at_script_creation(self):
|
||||
"Called at initial script creation"
|
||||
super(FastIrregularEvent, self).at_script_creation()
|
||||
self.interval = 5 # every 5 seconds, 1/5 chance of firing
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Tutorial world Runner - root reset timer for TutorialWorld
|
||||
#
|
||||
# This is a runner that resets the world
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
# #
|
||||
# # This sets up a reset system -- it resets the entire tutorial_world domain
|
||||
# # and all objects inheriting from it back to an initial state, MORPG style.
|
||||
# This is useful in order for different players to explore it without finding
|
||||
# # things missing.
|
||||
# #
|
||||
# # Note that this will of course allow a single player to end up with
|
||||
# # multiple versions of objects if they just wait around between resets;
|
||||
# # In a real game environment this would have to be resolved e.g.
|
||||
# # with custom versions of the 'get' command not accepting doublets.
|
||||
# #
|
||||
|
||||
# # setting up an event for reseting the world.
|
||||
|
||||
# UPDATE_INTERVAL = 60 * 10 # Measured in seconds
|
||||
|
||||
|
||||
# #This is a list of script parent objects that subscribe to the reset
|
||||
# functionality.
|
||||
# RESET_SUBSCRIBERS = ["examples.tutorial_world.p_weapon_rack",
|
||||
# "examples.tutorial_world.p_mob"]
|
||||
|
||||
# class EventResetTutorialWorld(Script):
|
||||
# """
|
||||
# This calls the reset function on all subscribed objects
|
||||
# """
|
||||
# def __init__(self):
|
||||
# super(EventResetTutorialWorld, self).__init__()
|
||||
# self.name = 'reset_tutorial_world'
|
||||
# #this you see when running @ps in game:
|
||||
# self.description = 'Reset the tutorial world .'
|
||||
# self.interval = UPDATE_INTERVAL
|
||||
# self.persistent = True
|
||||
|
||||
# def event_function(self):
|
||||
# """
|
||||
# This is called every self.interval seconds.
|
||||
# """
|
||||
# #find all objects inheriting the subscribing parents
|
||||
# for parent in RESET_SUBSCRIBERS:
|
||||
# objects = Object.objects.global_object_script_parent_search(parent)
|
||||
# for obj in objects:
|
||||
# try:
|
||||
# obj.scriptlink.reset()
|
||||
# except:
|
||||
# logger.log_errmsg(traceback.print_exc())
|
||||
118
docs/README.txt
118
docs/README.txt
|
|
@ -1,118 +0,0 @@
|
|||
|
||||
|
||||
EVENNIA DOCUMENTATION
|
||||
=====================
|
||||
|
||||
|
||||
- Evennia is extensively documented. Our manual is the
|
||||
continuously updating online wiki,
|
||||
|
||||
https://github.com/evennia/evennia/wiki
|
||||
|
||||
- Snapshots of the manual are also mirrored in reST
|
||||
form to ReadTheDocs:
|
||||
|
||||
http://evennia.readthedocs.org/en/latest/
|
||||
|
||||
- You can also ask for help from the evennia community,
|
||||
|
||||
http://groups.google.com/group/evennia
|
||||
|
||||
- Or by visiting our irc channel,
|
||||
|
||||
#evennia on the Freenode network
|
||||
|
||||
-------------------
|
||||
* Doxygen auto-docs
|
||||
-------------------
|
||||
|
||||
You can build the developer auto-docs
|
||||
(a fancy searchable index of the entire source tree).
|
||||
This makes use of doxygen, a doc generator that parses
|
||||
the source tree and creates docs on the fly.
|
||||
|
||||
- Install doxygen (v1.7+)
|
||||
|
||||
Doxygen is available for most platforms from
|
||||
|
||||
http://www.stack.nl/~dimitri/doxygen/
|
||||
|
||||
or through your package manager in Linux.
|
||||
|
||||
- Run
|
||||
|
||||
doxygen config.dox
|
||||
|
||||
This will create the auto-docs in a folder 'html'.
|
||||
|
||||
- Start your web browser and point it to
|
||||
|
||||
<evenniadir>/docs/html/index.html
|
||||
|
||||
- If you prefer a pdf version for printing, use LaTeX by
|
||||
activating the relevant section in config.dox. Run the
|
||||
doxygen command again as above and a new folder 'latex'
|
||||
will be created with the latex sources. You need the
|
||||
LaTeX processing system installed, then enter the new
|
||||
latex/ folder and run
|
||||
|
||||
make
|
||||
|
||||
This will create the pdf. Be warned however that the pdf
|
||||
docs are many hundreds of pages and the automatic formatting
|
||||
of doxygen is not always succeeding.
|
||||
|
||||
- Doxyfile is allows for plenty of configuration to get the
|
||||
docs to look the way you want. You can also output to other
|
||||
formats suitable for various developer environments, Windows
|
||||
help files etc.
|
||||
|
||||
------------------------
|
||||
* Sphinx Manuals
|
||||
------------------------
|
||||
|
||||
If you want to build the reST manuals yourself, you basically need to
|
||||
convert the wiki. First place yourself in a location where you want
|
||||
to clone the wiki repo to, then clone it:
|
||||
|
||||
git clone https://github.com/evennia/evennia.wiki.git
|
||||
|
||||
- Enter this directory and check out the sphinx branch:
|
||||
|
||||
git checkout sphinx
|
||||
|
||||
This branch has, apart from all the wiki pages (*.md files), also has a
|
||||
an extra directory sphinx/ that will hold the converted data.
|
||||
|
||||
- You need Pandoc for the markdown-to-reST conversion:
|
||||
|
||||
http://johnmacfarlane.net/pandoc/installing.html
|
||||
|
||||
You need a rather recent version. The versions coming with some linux
|
||||
repos are too old to support "github-flavoured markdown" conversion.
|
||||
See that page for getting the Haskill build environment in that case.
|
||||
|
||||
- You also need sphinx,
|
||||
|
||||
http://sphinx-doc.org/
|
||||
|
||||
You can most likely get it with 'pip install sphinx' under Linux.
|
||||
|
||||
- With all this in place, go to the pylib/ folder and run the
|
||||
converter script:
|
||||
|
||||
python update_rest_docs.py
|
||||
|
||||
If all goes well, you will see all the wiki pages getting converted.
|
||||
The converted *.rst files will end up in the sphinx/ directory.
|
||||
|
||||
- Finally, go to sphinx/ and run
|
||||
|
||||
make html
|
||||
|
||||
If sphinx is installed, this will create the html files in
|
||||
sphinx/.build. To look at them, point your browser to
|
||||
|
||||
<path-to-wiki-repo>/sphinx/.build/index.html
|
||||
|
||||
|
||||
288
docs/config.dox
288
docs/config.dox
|
|
@ -1,288 +0,0 @@
|
|||
# Doxyfile 1.8.1.2
|
||||
|
||||
#---------------------------------------------------------------------------
|
||||
# Project related configuration options
|
||||
#---------------------------------------------------------------------------
|
||||
DOXYFILE_ENCODING = UTF-8
|
||||
PROJECT_NAME = "Evennia"
|
||||
PROJECT_NUMBER = Git-Beta
|
||||
PROJECT_BRIEF = Python MUD development system
|
||||
PROJECT_LOGO = ../../src/web/media/images/evennia_logo.png
|
||||
OUTPUT_DIRECTORY =
|
||||
CREATE_SUBDIRS = NO
|
||||
OUTPUT_LANGUAGE = English
|
||||
BRIEF_MEMBER_DESC = YES
|
||||
REPEAT_BRIEF = YES
|
||||
ABBREVIATE_BRIEF =
|
||||
ALWAYS_DETAILED_SEC = NO
|
||||
INLINE_INHERITED_MEMB = NO
|
||||
FULL_PATH_NAMES = YES
|
||||
STRIP_FROM_PATH =
|
||||
STRIP_FROM_INC_PATH =
|
||||
SHORT_NAMES = NO
|
||||
JAVADOC_AUTOBRIEF = NO
|
||||
QT_AUTOBRIEF = NO
|
||||
MULTILINE_CPP_IS_BRIEF = NO
|
||||
INHERIT_DOCS = YES
|
||||
SEPARATE_MEMBER_PAGES = NO
|
||||
TAB_SIZE = 8
|
||||
ALIASES =
|
||||
TCL_SUBST =
|
||||
OPTIMIZE_OUTPUT_FOR_C = NO
|
||||
OPTIMIZE_OUTPUT_JAVA = NO
|
||||
OPTIMIZE_FOR_FORTRAN = NO
|
||||
OPTIMIZE_OUTPUT_VHDL = NO
|
||||
EXTENSION_MAPPING =
|
||||
MARKDOWN_SUPPORT = YES
|
||||
BUILTIN_STL_SUPPORT = NO
|
||||
CPP_CLI_SUPPORT = NO
|
||||
SIP_SUPPORT = NO
|
||||
IDL_PROPERTY_SUPPORT = YES
|
||||
DISTRIBUTE_GROUP_DOC = NO
|
||||
SUBGROUPING = YES
|
||||
INLINE_GROUPED_CLASSES = NO
|
||||
INLINE_SIMPLE_STRUCTS = NO
|
||||
TYPEDEF_HIDES_STRUCT = NO
|
||||
SYMBOL_CACHE_SIZE = 0
|
||||
LOOKUP_CACHE_SIZE = 0
|
||||
#---------------------------------------------------------------------------
|
||||
# Build related configuration options
|
||||
#---------------------------------------------------------------------------
|
||||
EXTRACT_ALL = YES
|
||||
EXTRACT_PRIVATE = NO
|
||||
EXTRACT_PACKAGE = NO
|
||||
EXTRACT_STATIC = YES
|
||||
EXTRACT_LOCAL_CLASSES = YES
|
||||
EXTRACT_LOCAL_METHODS = NO
|
||||
EXTRACT_ANON_NSPACES = NO
|
||||
HIDE_UNDOC_MEMBERS = NO
|
||||
HIDE_UNDOC_CLASSES = NO
|
||||
HIDE_FRIEND_COMPOUNDS = NO
|
||||
HIDE_IN_BODY_DOCS = NO
|
||||
INTERNAL_DOCS = NO
|
||||
CASE_SENSE_NAMES = YES
|
||||
HIDE_SCOPE_NAMES = NO
|
||||
SHOW_INCLUDE_FILES = YES
|
||||
FORCE_LOCAL_INCLUDES = NO
|
||||
INLINE_INFO = YES
|
||||
SORT_MEMBER_DOCS = YES
|
||||
SORT_BRIEF_DOCS = NO
|
||||
SORT_MEMBERS_CTORS_1ST = NO
|
||||
SORT_GROUP_NAMES = NO
|
||||
SORT_BY_SCOPE_NAME = NO
|
||||
STRICT_PROTO_MATCHING = NO
|
||||
GENERATE_TODOLIST = YES
|
||||
GENERATE_TESTLIST = YES
|
||||
GENERATE_BUGLIST = YES
|
||||
GENERATE_DEPRECATEDLIST= YES
|
||||
ENABLED_SECTIONS =
|
||||
MAX_INITIALIZER_LINES = 30
|
||||
SHOW_USED_FILES = YES
|
||||
SHOW_FILES = YES
|
||||
SHOW_NAMESPACES = YES
|
||||
FILE_VERSION_FILTER =
|
||||
LAYOUT_FILE =
|
||||
CITE_BIB_FILES =
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to warning and progress messages
|
||||
#---------------------------------------------------------------------------
|
||||
QUIET = NO
|
||||
WARNINGS = YES
|
||||
WARN_IF_UNDOCUMENTED = YES
|
||||
WARN_IF_DOC_ERROR = YES
|
||||
WARN_NO_PARAMDOC = NO
|
||||
WARN_FORMAT = "$file:$line: $text"
|
||||
WARN_LOGFILE =
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the input files
|
||||
#---------------------------------------------------------------------------
|
||||
INPUT = ../..
|
||||
INPUT_ENCODING = UTF-8
|
||||
FILE_PATTERNS = *.py
|
||||
RECURSIVE = YES
|
||||
EXCLUDE = ../../docs
|
||||
EXCLUDE_SYMLINKS = NO
|
||||
EXCLUDE_PATTERNS = 0*.py
|
||||
EXCLUDE_SYMBOLS =
|
||||
EXAMPLE_PATH =
|
||||
EXAMPLE_PATTERNS =
|
||||
EXAMPLE_RECURSIVE = NO
|
||||
IMAGE_PATH =
|
||||
INPUT_FILTER =
|
||||
FILTER_PATTERNS =
|
||||
FILTER_SOURCE_FILES = NO
|
||||
FILTER_SOURCE_PATTERNS =
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to source browsing
|
||||
#---------------------------------------------------------------------------
|
||||
SOURCE_BROWSER = YES
|
||||
INLINE_SOURCES = NO
|
||||
STRIP_CODE_COMMENTS = YES
|
||||
REFERENCED_BY_RELATION = NO
|
||||
REFERENCES_RELATION = NO
|
||||
REFERENCES_LINK_SOURCE = YES
|
||||
USE_HTAGS = NO
|
||||
VERBATIM_HEADERS = YES
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the alphabetical class index
|
||||
#---------------------------------------------------------------------------
|
||||
ALPHABETICAL_INDEX = YES
|
||||
COLS_IN_ALPHA_INDEX = 8
|
||||
IGNORE_PREFIX =
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the HTML output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_HTML = YES
|
||||
HTML_OUTPUT = html
|
||||
HTML_FILE_EXTENSION = .html
|
||||
HTML_HEADER =
|
||||
HTML_FOOTER =
|
||||
HTML_STYLESHEET =
|
||||
HTML_EXTRA_FILES =
|
||||
HTML_COLORSTYLE_HUE = 220
|
||||
HTML_COLORSTYLE_SAT = 100
|
||||
HTML_COLORSTYLE_GAMMA = 80
|
||||
HTML_TIMESTAMP = YES
|
||||
HTML_DYNAMIC_SECTIONS = NO
|
||||
HTML_INDEX_NUM_ENTRIES = 100
|
||||
GENERATE_DOCSET = NO
|
||||
DOCSET_FEEDNAME = "Doxygen generated docs"
|
||||
DOCSET_BUNDLE_ID = org.doxygen.Project
|
||||
DOCSET_PUBLISHER_ID = org.doxygen.Publisher
|
||||
DOCSET_PUBLISHER_NAME = Publisher
|
||||
GENERATE_HTMLHELP = NO
|
||||
CHM_FILE =
|
||||
HHC_LOCATION =
|
||||
GENERATE_CHI = NO
|
||||
CHM_INDEX_ENCODING =
|
||||
BINARY_TOC = NO
|
||||
TOC_EXPAND = YES
|
||||
GENERATE_QHP = NO
|
||||
QCH_FILE =
|
||||
QHP_NAMESPACE = org.doxygen.Project
|
||||
QHP_VIRTUAL_FOLDER = doc
|
||||
QHP_CUST_FILTER_NAME =
|
||||
QHP_CUST_FILTER_ATTRS =
|
||||
QHP_SECT_FILTER_ATTRS =
|
||||
QHG_LOCATION =
|
||||
GENERATE_ECLIPSEHELP = NO
|
||||
ECLIPSE_DOC_ID = org.doxygen.Project
|
||||
DISABLE_INDEX = YES
|
||||
GENERATE_TREEVIEW = YES
|
||||
ENUM_VALUES_PER_LINE = 4
|
||||
TREEVIEW_WIDTH = 250
|
||||
EXT_LINKS_IN_WINDOW = NO
|
||||
FORMULA_FONTSIZE = 10
|
||||
FORMULA_TRANSPARENT = YES
|
||||
USE_MATHJAX = NO
|
||||
MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest
|
||||
MATHJAX_EXTENSIONS =
|
||||
SEARCHENGINE = YES
|
||||
SERVER_BASED_SEARCH = NO
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the LaTeX output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_LATEX = NO
|
||||
LATEX_OUTPUT = latex
|
||||
LATEX_CMD_NAME = latex
|
||||
MAKEINDEX_CMD_NAME = makeindex
|
||||
COMPACT_LATEX = NO
|
||||
PAPER_TYPE = a4
|
||||
EXTRA_PACKAGES =
|
||||
LATEX_HEADER =
|
||||
LATEX_FOOTER =
|
||||
PDF_HYPERLINKS = YES
|
||||
USE_PDFLATEX = YES
|
||||
LATEX_BATCHMODE = NO
|
||||
LATEX_HIDE_INDICES = NO
|
||||
LATEX_SOURCE_CODE = NO
|
||||
LATEX_BIB_STYLE = plain
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the RTF output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_RTF = NO
|
||||
RTF_OUTPUT = rtf
|
||||
COMPACT_RTF = NO
|
||||
RTF_HYPERLINKS = NO
|
||||
RTF_STYLESHEET_FILE =
|
||||
RTF_EXTENSIONS_FILE =
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the man page output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_MAN = NO
|
||||
MAN_OUTPUT = man
|
||||
MAN_EXTENSION = .3
|
||||
MAN_LINKS = NO
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the XML output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_XML = NO
|
||||
XML_OUTPUT = xml
|
||||
XML_SCHEMA =
|
||||
XML_DTD =
|
||||
XML_PROGRAMLISTING = YES
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options for the AutoGen Definitions output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_AUTOGEN_DEF = NO
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the Perl module output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_PERLMOD = NO
|
||||
PERLMOD_LATEX = NO
|
||||
PERLMOD_PRETTY = YES
|
||||
PERLMOD_MAKEVAR_PREFIX =
|
||||
#---------------------------------------------------------------------------
|
||||
# Configuration options related to the preprocessor
|
||||
#---------------------------------------------------------------------------
|
||||
ENABLE_PREPROCESSING = YES
|
||||
MACRO_EXPANSION = NO
|
||||
EXPAND_ONLY_PREDEF = NO
|
||||
SEARCH_INCLUDES = YES
|
||||
INCLUDE_PATH =
|
||||
INCLUDE_FILE_PATTERNS =
|
||||
PREDEFINED =
|
||||
EXPAND_AS_DEFINED =
|
||||
SKIP_FUNCTION_MACROS = YES
|
||||
#---------------------------------------------------------------------------
|
||||
# Configuration::additions related to external references
|
||||
#---------------------------------------------------------------------------
|
||||
TAGFILES =
|
||||
GENERATE_TAGFILE =
|
||||
ALLEXTERNALS = NO
|
||||
EXTERNAL_GROUPS = YES
|
||||
PERL_PATH = /usr/bin/perl
|
||||
#---------------------------------------------------------------------------
|
||||
# Configuration options related to the dot tool
|
||||
#---------------------------------------------------------------------------
|
||||
CLASS_DIAGRAMS = YES
|
||||
MSCGEN_PATH =
|
||||
HIDE_UNDOC_RELATIONS = YES
|
||||
HAVE_DOT = NO
|
||||
DOT_NUM_THREADS = 0
|
||||
DOT_FONTNAME = Helvetica
|
||||
DOT_FONTSIZE = 10
|
||||
DOT_FONTPATH =
|
||||
CLASS_GRAPH = YES
|
||||
COLLABORATION_GRAPH = YES
|
||||
GROUP_GRAPHS = YES
|
||||
UML_LOOK = NO
|
||||
UML_LIMIT_NUM_FIELDS = 10
|
||||
TEMPLATE_RELATIONS = NO
|
||||
INCLUDE_GRAPH = YES
|
||||
INCLUDED_BY_GRAPH = YES
|
||||
CALL_GRAPH = NO
|
||||
CALLER_GRAPH = NO
|
||||
GRAPHICAL_HIERARCHY = YES
|
||||
DIRECTORY_GRAPH = YES
|
||||
DOT_IMAGE_FORMAT = png
|
||||
INTERACTIVE_SVG = NO
|
||||
DOT_PATH =
|
||||
DOTFILE_DIRS =
|
||||
MSCFILE_DIRS =
|
||||
DOT_GRAPH_MAX_NODES = 50
|
||||
MAX_DOT_GRAPH_DEPTH = 0
|
||||
DOT_TRANSPARENT = NO
|
||||
DOT_MULTI_TARGETS = YES
|
||||
GENERATE_LEGEND = YES
|
||||
DOT_CLEANUP = YES
|
||||
278
ev.py
278
ev.py
|
|
@ -1,278 +0,0 @@
|
|||
"""
|
||||
Central API for the Evennia MUD/MUX/MU* creation system.
|
||||
|
||||
This is a set of shortcuts for accessing common features in src/ with
|
||||
less boiler-plate. Import this from your code, use it with @py from
|
||||
in-game or explore it interactively from a python shell.
|
||||
|
||||
Notes:
|
||||
|
||||
0) Use ev.help, ev.managers.help, ev.default_cmds.help and
|
||||
syscmdkeys.help to view the API structure and explore which
|
||||
variables/methods are available.
|
||||
1) This module holds variables, not modules in their own right. This
|
||||
means you cannot use import dot-notation to import nested things via
|
||||
this API.
|
||||
2) "managers" is a container object that contains shortcuts to
|
||||
initiated versions of Evennia's django database managers (e.g.
|
||||
managers.objects is an alias for ObjectDB.objects). These allow for
|
||||
exploring the database in various ways.
|
||||
3) "syscmdkeys" is a container object holding the names of system
|
||||
commands. the syscmdkeys object.
|
||||
4) You -have- to use the create_* functions (shortcuts to
|
||||
src.utils.create) to create new typeclassed game entities (Objects,
|
||||
Scripts, Channels or Players).
|
||||
5) "settings" links to Evennia's game/settings file. "settings_full"
|
||||
shows all of django's available settings. Note that this is for
|
||||
viewing only - you cannot *change* settings from here in a meaningful
|
||||
way but have to update game/settings.py and restart the server.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
######################################################################
|
||||
# set Evennia version in __version__ property
|
||||
######################################################################
|
||||
|
||||
try:
|
||||
__version__ = "Evennia"
|
||||
with open(os.path.dirname(os.path.abspath(__file__)) + os.sep + "VERSION.txt", 'r') as f:
|
||||
__version__ += " %s" % f.read().strip()
|
||||
rev = (os.popen("git rev-parse --short HEAD").read().strip())
|
||||
__version__ += "-%s" % (rev or "(unknown revision)")
|
||||
except IOError:
|
||||
__version__ += " (unknown version)"
|
||||
|
||||
######################################################################
|
||||
# Stop erroneous direct run (would give a traceback since django is
|
||||
# not yet initialized)
|
||||
######################################################################
|
||||
|
||||
if __name__ == "__main__":
|
||||
print \
|
||||
"""
|
||||
Evennia MU* creation system (%s)
|
||||
|
||||
This module gives access to Evennia's API (Application Programming
|
||||
Interface). It should *not* be run on its own, but be imported and
|
||||
accessed from your code or explored interactively from a Python
|
||||
shell.
|
||||
|
||||
For help configuring and starting the Evennia server, see the
|
||||
INSTALL file. More help can be found at http://www.evennia.com.
|
||||
""" % __version__
|
||||
sys.exit()
|
||||
|
||||
######################################################################
|
||||
# make sure settings is available, also if starting this API stand-alone
|
||||
# make settings available, and also the full django settings
|
||||
######################################################################
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(1, os.path.dirname(os.path.abspath(__file__)) + os.path.sep + "game")
|
||||
os.environ["DJANGO_SETTINGS_MODULE"] = "game.settings"
|
||||
del sys, os
|
||||
|
||||
from game import settings
|
||||
from django.conf import settings as settings_full
|
||||
|
||||
try:
|
||||
# test this first import to make sure environment is set up correctly
|
||||
from src.help.models import HelpEntry
|
||||
except AttributeError, e:
|
||||
err = e.message
|
||||
err += "\nError initializing ev.py: Maybe the correct environment variables were not set."
|
||||
err += "\nUse \"python game/manage.py shell\" to start an interpreter"
|
||||
err += " with everything configured correctly."
|
||||
raise AttributeError(err)
|
||||
|
||||
|
||||
######################################################################
|
||||
# Start Evennia API
|
||||
# (easiest is to import this module interactively to explore it)
|
||||
######################################################################
|
||||
|
||||
README = __doc__
|
||||
|
||||
# help entries
|
||||
from src.help.models import HelpEntry
|
||||
|
||||
from src.typeclasses.models import Attribute
|
||||
# players
|
||||
from src.players.player import Player
|
||||
from src.players.models import PlayerDB
|
||||
|
||||
# commands
|
||||
from src.commands.command import Command
|
||||
from src.commands.cmdset import CmdSet
|
||||
# (default_cmds is created below)
|
||||
|
||||
# locks
|
||||
from src.locks import lockfuncs
|
||||
|
||||
# scripts
|
||||
from src.scripts.scripts import Script
|
||||
|
||||
# comms
|
||||
from src.comms.models import Msg, ChannelDB
|
||||
from src.comms.comms import Channel
|
||||
|
||||
# objects
|
||||
from src.objects.objects import Object, Character, Room, Exit
|
||||
|
||||
# utils
|
||||
|
||||
from src.utils.search import *
|
||||
from src.utils.create import *
|
||||
from src.scripts.tickerhandler import TICKER_HANDLER as tickerhandler
|
||||
from src.utils import logger
|
||||
from src.utils import utils
|
||||
from src.utils import gametime
|
||||
from src.utils import ansi
|
||||
from src.utils.spawner import spawn
|
||||
|
||||
|
||||
######################################################################
|
||||
# API containers and helper functions
|
||||
######################################################################
|
||||
|
||||
def help(header=False):
|
||||
"""
|
||||
Main Evennia API.
|
||||
ev.help() views API contents
|
||||
ev.help(True) or ev.README shows module instructions
|
||||
|
||||
See www.evennia.com for the full documentation.
|
||||
"""
|
||||
if header:
|
||||
return __doc__
|
||||
else:
|
||||
import ev
|
||||
names = [str(var) for var in ev.__dict__ if not var.startswith('_')]
|
||||
return ", ".join(names)
|
||||
|
||||
|
||||
class _EvContainer(object):
|
||||
"""
|
||||
Parent for other containers
|
||||
|
||||
"""
|
||||
def _help(self):
|
||||
"Returns list of contents"
|
||||
names = [name for name in self.__class__.__dict__ if not name.startswith('_')]
|
||||
names += [name for name in self.__dict__ if not name.startswith('_')]
|
||||
print self.__doc__ + "-" * 60 + "\n" + ", ".join(names)
|
||||
help = property(_help)
|
||||
|
||||
|
||||
class DBmanagers(_EvContainer):
|
||||
"""
|
||||
Links to instantiated database managers.
|
||||
|
||||
helpentry - HelpEntry.objects
|
||||
players - PlayerDB.objects
|
||||
scripts - ScriptDB.objects
|
||||
msgs - Msg.objects
|
||||
channels - Channel.objects
|
||||
objects - ObjectDB.objects
|
||||
serverconfigs = ServerConfig.objects
|
||||
tags - Tags.objects
|
||||
attributes - Attributes.objects
|
||||
|
||||
"""
|
||||
from src.help.models import HelpEntry
|
||||
from src.players.models import PlayerDB
|
||||
from src.scripts.models import ScriptDB
|
||||
from src.comms.models import Msg, ChannelDB
|
||||
from src.objects.models import ObjectDB
|
||||
from src.server.models import ServerConfig
|
||||
from src.typeclasses.models import Tag, Attribute
|
||||
|
||||
# create container's properties
|
||||
helpentries = HelpEntry.objects
|
||||
players = PlayerDB.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
|
||||
del HelpEntry, PlayerDB, ScriptDB, Msg, ChannelDB
|
||||
#del ExternalChannelConnection
|
||||
del ObjectDB, ServerConfig, Tag, Attribute
|
||||
|
||||
managers = DBmanagers()
|
||||
del DBmanagers
|
||||
|
||||
|
||||
class DefaultCmds(_EvContainer):
|
||||
"""
|
||||
This container holds direct shortcuts to all default commands in Evennia.
|
||||
|
||||
To access in code, do 'from ev import default_cmds' then
|
||||
access the properties on the imported default_cmds object.
|
||||
|
||||
"""
|
||||
|
||||
from src.commands.default.cmdset_character import CharacterCmdSet
|
||||
from src.commands.default.cmdset_player import PlayerCmdSet
|
||||
from src.commands.default.cmdset_unloggedin import UnloggedinCmdSet
|
||||
from src.commands.default.muxcommand import MuxCommand, MuxPlayerCommand
|
||||
|
||||
def __init__(self):
|
||||
"populate the object with commands"
|
||||
|
||||
def add_cmds(module):
|
||||
"helper method for populating this object with cmds"
|
||||
cmdlist = utils.variable_from_module(module, module.__all__)
|
||||
self.__dict__.update(dict([(c.__name__, c) for c in cmdlist]))
|
||||
|
||||
from src.commands.default import (admin, batchprocess,
|
||||
building, comms, general,
|
||||
player, help, system, unloggedin)
|
||||
add_cmds(admin)
|
||||
add_cmds(building)
|
||||
add_cmds(batchprocess)
|
||||
add_cmds(building)
|
||||
add_cmds(comms)
|
||||
add_cmds(general)
|
||||
add_cmds(player)
|
||||
add_cmds(help)
|
||||
add_cmds(system)
|
||||
add_cmds(unloggedin)
|
||||
default_cmds = DefaultCmds()
|
||||
del DefaultCmds
|
||||
|
||||
|
||||
class SystemCmds(_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_CHANNEL - the command name is a channel name
|
||||
CMD_LOGINSTART - this command will be called as the very
|
||||
first command when a player connects to
|
||||
the server.
|
||||
|
||||
To access in code, do 'from ev import syscmdkeys' then
|
||||
access the properties on the imported syscmdkeys object.
|
||||
|
||||
"""
|
||||
from src.commands import cmdhandler
|
||||
CMD_NOINPUT = cmdhandler.CMD_NOINPUT
|
||||
CMD_NOMATCH = cmdhandler.CMD_NOMATCH
|
||||
CMD_MULTIMATCH = cmdhandler.CMD_MULTIMATCH
|
||||
CMD_CHANNEL = cmdhandler.CMD_CHANNEL
|
||||
CMD_LOGINSTART = cmdhandler.CMD_LOGINSTART
|
||||
del cmdhandler
|
||||
syscmdkeys = SystemCmds()
|
||||
del SystemCmds
|
||||
del _EvContainer
|
||||
1
evennia/VERSION.txt
Normal file
1
evennia/VERSION.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
0.5.0
|
||||
291
evennia/__init__.py
Normal file
291
evennia/__init__.py
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
"""
|
||||
Evennia MUD/MUX/MU* creation system
|
||||
|
||||
This is the main top-level API for Evennia. You can also explore the
|
||||
evennia library by accessing evennia.<subpackage> directly.
|
||||
|
||||
For full functionality you need to explore this module via a django-
|
||||
aware shell. Go to your game directory and use the command 'evennia.py shell'
|
||||
to launch such a shell (using python or ipython depending on your install).
|
||||
|
||||
See www.evennia.com for full documentation.
|
||||
|
||||
"""
|
||||
|
||||
# Delayed loading of properties
|
||||
|
||||
# Typeclasses
|
||||
|
||||
DefaultPlayer = None
|
||||
DefaultGuest = None
|
||||
DefaultObject = None
|
||||
DefaultCharacter = None
|
||||
DefaultRoom = None
|
||||
DefaultExit = None
|
||||
DefaultChannel = None
|
||||
DefaultScript = None
|
||||
|
||||
# Database models
|
||||
ObjectDB = None
|
||||
PlayerDB = None
|
||||
ScriptDB = None
|
||||
ChannelDB = None
|
||||
Msg = None
|
||||
|
||||
# commands
|
||||
Command = None
|
||||
CmdSet = None
|
||||
default_cmds = None
|
||||
syscmdkeys = None
|
||||
|
||||
# search functions
|
||||
search_object = None
|
||||
search_script = None
|
||||
search_player = None
|
||||
search_channel = None
|
||||
search_help = None
|
||||
|
||||
# create functions
|
||||
create_object = None
|
||||
create_script = None
|
||||
create_player = None
|
||||
create_channel = None
|
||||
create_message = None
|
||||
|
||||
# utilities
|
||||
lockfuncs = None
|
||||
oobhandler = None
|
||||
logger = None
|
||||
gametime = None
|
||||
ansi = None
|
||||
spawn = None
|
||||
managers = None
|
||||
contrib = None
|
||||
|
||||
# Handlers
|
||||
SESSION_HANDLER = None
|
||||
TICKER_HANDLER = None
|
||||
OOB_HANDLER = None
|
||||
CHANNEL_HANDLER = None
|
||||
|
||||
|
||||
def _create_version():
|
||||
"""
|
||||
Helper function for building the version string
|
||||
"""
|
||||
import os
|
||||
from subprocess import check_output, CalledProcessError, STDOUT
|
||||
|
||||
version = "Unknown"
|
||||
root = os.path.dirname(os.path.abspath(__file__))
|
||||
try:
|
||||
with open(os.path.join(root, "VERSION.txt"), 'r') as f:
|
||||
version = f.read().strip()
|
||||
except IOError as err:
|
||||
print err
|
||||
try:
|
||||
version = "%s (rev %s)" % (version, check_output("git rev-parse --short HEAD", shell=True, cwd=root, stderr=STDOUT).strip())
|
||||
except (IOError, CalledProcessError):
|
||||
pass
|
||||
return version
|
||||
|
||||
__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.
|
||||
"""
|
||||
def imp(path, variable=True):
|
||||
"Helper function"
|
||||
mod, fromlist = path, "None"
|
||||
if variable:
|
||||
mod, fromlist = path.rsplit('.', 1)
|
||||
return __import__(mod, fromlist=[fromlist])
|
||||
|
||||
global DefaultPlayer, DefaultObject, DefaultGuest, DefaultCharacter
|
||||
global DefaultRoom, DefaultExit, DefaultChannel, DefaultScript
|
||||
global ObjectDB, PlayerDB, ScriptDB, ChannelDB, Msg
|
||||
global Command, CmdSet, default_cmds, syscmdkeys
|
||||
global search_object, search_script, search_player, search_channel, search_help
|
||||
global create_object, create_script, create_player, create_channel, create_message
|
||||
global lockfuncs, logger, utils, gametime, ansi, spawn, managers
|
||||
global contrib, TICKER_HANDLER, OOB_HANDLER, SESSION_HANDLER, CHANNEL_HANDLER
|
||||
|
||||
from players.players import DefaultPlayer
|
||||
from players.players import DefaultGuest
|
||||
from objects.objects import DefaultObject
|
||||
from objects.objects import DefaultCharacter
|
||||
from objects.objects import DefaultRoom
|
||||
from objects.objects import DefaultExit
|
||||
from comms.comms import DefaultChannel
|
||||
from scripts.scripts import DefaultScript
|
||||
|
||||
# Database models
|
||||
from objects.models import ObjectDB
|
||||
from players.models import PlayerDB
|
||||
from scripts.models import ScriptDB
|
||||
from comms.models import ChannelDB
|
||||
from comms.models import Msg
|
||||
|
||||
# commands
|
||||
from commands.command import Command
|
||||
from commands.cmdset import CmdSet
|
||||
|
||||
# search functions
|
||||
from utils.search import search_object
|
||||
from utils.search import search_script
|
||||
from utils.search import search_player
|
||||
from utils.search import search_channel
|
||||
from utils.search import search_help
|
||||
|
||||
# create functions
|
||||
from utils.create import create_object
|
||||
from utils.create import create_script
|
||||
from utils.create import create_player
|
||||
from utils.create import create_channel
|
||||
from utils.create import create_message
|
||||
|
||||
# utilities
|
||||
from locks import lockfuncs
|
||||
from utils import logger
|
||||
from utils import gametime
|
||||
from utils import ansi
|
||||
from utils.spawner import spawn
|
||||
import contrib
|
||||
|
||||
# handlers
|
||||
from scripts.tickerhandler import TICKER_HANDLER
|
||||
from server.oobhandler import OOB_HANDLER
|
||||
from server.sessionhandler import SESSION_HANDLER
|
||||
from comms.channelhandler import CHANNEL_HANDLER
|
||||
|
||||
# API containers
|
||||
|
||||
class _EvContainer(object):
|
||||
"""
|
||||
Parent for other containers
|
||||
|
||||
"""
|
||||
def _help(self):
|
||||
"Returns list of contents"
|
||||
names = [name for name in self.__class__.__dict__ if not name.startswith('_')]
|
||||
names += [name for name in self.__dict__ if not name.startswith('_')]
|
||||
print self.__doc__ + "-" * 60 + "\n" + ", ".join(names)
|
||||
help = property(_help)
|
||||
|
||||
|
||||
class DBmanagers(_EvContainer):
|
||||
"""
|
||||
Links to instantiated database managers.
|
||||
|
||||
helpentry - HelpEntry.objects
|
||||
players - PlayerDB.objects
|
||||
scripts - ScriptDB.objects
|
||||
msgs - Msg.objects
|
||||
channels - Channel.objects
|
||||
objects - ObjectDB.objects
|
||||
serverconfigs = ServerConfig.objects
|
||||
tags - Tags.objects
|
||||
attributes - Attributes.objects
|
||||
|
||||
"""
|
||||
from help.models import HelpEntry
|
||||
from players.models import PlayerDB
|
||||
from scripts.models import ScriptDB
|
||||
from comms.models import Msg, ChannelDB
|
||||
from objects.models import ObjectDB
|
||||
from server.models import ServerConfig
|
||||
from typeclasses.attributes import Attribute
|
||||
from typeclasses.tags import Tag
|
||||
|
||||
# create container's properties
|
||||
helpentries = HelpEntry.objects
|
||||
players = PlayerDB.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
|
||||
del HelpEntry, PlayerDB, ScriptDB, Msg, ChannelDB
|
||||
#del ExternalChannelConnection
|
||||
del ObjectDB, ServerConfig, Tag, Attribute
|
||||
|
||||
managers = DBmanagers()
|
||||
del DBmanagers
|
||||
|
||||
|
||||
class DefaultCmds(_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_character import CharacterCmdSet
|
||||
from commands.default.cmdset_player import PlayerCmdSet
|
||||
from commands.default.cmdset_unloggedin import UnloggedinCmdSet
|
||||
from commands.default.cmdset_session import SessionCmdSet
|
||||
from commands.default.muxcommand import MuxCommand, MuxPlayerCommand
|
||||
|
||||
def __init__(self):
|
||||
"populate the object with commands"
|
||||
|
||||
def add_cmds(module):
|
||||
"helper method for populating this object with cmds"
|
||||
cmdlist = utils.variable_from_module(module, module.__all__)
|
||||
self.__dict__.update(dict([(c.__name__, c) for c in cmdlist]))
|
||||
|
||||
from commands.default import (admin, batchprocess,
|
||||
building, comms, general,
|
||||
player, help, system, unloggedin)
|
||||
add_cmds(admin)
|
||||
add_cmds(building)
|
||||
add_cmds(batchprocess)
|
||||
add_cmds(building)
|
||||
add_cmds(comms)
|
||||
add_cmds(general)
|
||||
add_cmds(player)
|
||||
add_cmds(help)
|
||||
add_cmds(system)
|
||||
add_cmds(unloggedin)
|
||||
default_cmds = DefaultCmds()
|
||||
del DefaultCmds
|
||||
|
||||
|
||||
class SystemCmds(_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_CHANNEL - the command name is a channel name
|
||||
CMD_LOGINSTART - this command will be called as the very
|
||||
first command when a player connects to
|
||||
the server.
|
||||
|
||||
To access in code, do 'from evennia import syscmdkeys' then
|
||||
access the properties on the imported syscmdkeys object.
|
||||
|
||||
"""
|
||||
from commands import cmdhandler
|
||||
CMD_NOINPUT = cmdhandler.CMD_NOINPUT
|
||||
CMD_NOMATCH = cmdhandler.CMD_NOMATCH
|
||||
CMD_MULTIMATCH = cmdhandler.CMD_MULTIMATCH
|
||||
CMD_CHANNEL = cmdhandler.CMD_CHANNEL
|
||||
CMD_LOGINSTART = cmdhandler.CMD_LOGINSTART
|
||||
del cmdhandler
|
||||
syscmdkeys = SystemCmds()
|
||||
del SystemCmds
|
||||
del _EvContainer
|
||||
10
evennia/commands/__init__.py
Normal file
10
evennia/commands/__init__.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This sub-package contains Evennia's command system. It handles
|
||||
everything related to parsing input from the player, building cmdsets
|
||||
and executing the code associated with a found command class.
|
||||
|
||||
commands.default contains all the default "mux-like" commands of
|
||||
Evennia.
|
||||
|
||||
"""
|
||||
|
|
@ -40,10 +40,10 @@ from copy import copy
|
|||
from traceback import format_exc
|
||||
from twisted.internet.defer import inlineCallbacks, returnValue
|
||||
from django.conf import settings
|
||||
from src.comms.channelhandler import CHANNELHANDLER
|
||||
from src.utils import logger, utils
|
||||
from src.commands.cmdparser import at_multimatch_cmd
|
||||
from src.utils.utils import string_suggestions, to_unicode
|
||||
from evennia.comms.channelhandler import CHANNELHANDLER
|
||||
from evennia.utils import logger, utils
|
||||
from evennia.commands.cmdparser import at_multimatch_cmd
|
||||
from evennia.utils.utils import string_suggestions, to_unicode
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
|
|
@ -71,8 +71,39 @@ CMD_CHANNEL = "__send_to_channel_command"
|
|||
# (is expected to display the login screen)
|
||||
CMD_LOGINSTART = "__unloggedin_look_command"
|
||||
|
||||
# custom Exceptions
|
||||
# Output strings
|
||||
|
||||
_ERROR_UNTRAPPED = "{traceback}\n" \
|
||||
"Above traceback is from an untrapped error. " \
|
||||
"Please file a bug report."
|
||||
|
||||
_ERROR_CMDSETS = "{traceback}\n" \
|
||||
"Above traceback is from a cmdset merger error. " \
|
||||
"Please file a bug report."
|
||||
|
||||
_ERROR_NOCMDSETS = "No command sets found! This is a sign of a critical bug." \
|
||||
"\nThe error was logged. If disconnecting/reconnecting doesn't" \
|
||||
"\nsolve the problem, try to contact the server admin through" \
|
||||
"\nsome other means for assistance."
|
||||
|
||||
_ERROR_CMDHANDLER = "{traceback}\n"\
|
||||
"Above traceback is from a Command handler bug." \
|
||||
"Please file a bug report with the Evennia project."
|
||||
|
||||
|
||||
def _msg_err(receiver, string):
|
||||
"""
|
||||
Helper function for returning an error to the caller.
|
||||
|
||||
Args:
|
||||
receiver (Object): object to get the error message
|
||||
string (str): string with a {traceback} format marker inside it.
|
||||
|
||||
"""
|
||||
receiver.msg(string.format(traceback=format_exc(), _nomulti=True))
|
||||
|
||||
|
||||
# custom Exceptions
|
||||
|
||||
class NoCmdSets(Exception):
|
||||
"No cmdsets found. Critical error."
|
||||
|
|
@ -86,8 +117,10 @@ class ExecSystemCommand(Exception):
|
|||
self.syscmd = syscmd
|
||||
self.sysarg = sysarg
|
||||
|
||||
# Helper function
|
||||
class ErrorReported(Exception):
|
||||
"Re-raised when a subsructure already reported the error"
|
||||
|
||||
# Helper function
|
||||
|
||||
@inlineCallbacks
|
||||
def get_and_merge_cmdsets(caller, session, player, obj,
|
||||
|
|
@ -107,146 +140,165 @@ def get_and_merge_cmdsets(caller, session, player, obj,
|
|||
|
||||
Note that this function returns a deferred!
|
||||
"""
|
||||
local_obj_cmdsets = [None]
|
||||
|
||||
@inlineCallbacks
|
||||
def _get_channel_cmdsets(player, player_cmdset):
|
||||
"Channel-cmdsets"
|
||||
# Create cmdset for all player's available channels
|
||||
channel_cmdset = None
|
||||
if not player_cmdset.no_channels:
|
||||
channel_cmdset = yield CHANNELHANDLER.get_cmdset(player)
|
||||
returnValue(channel_cmdset)
|
||||
|
||||
@inlineCallbacks
|
||||
def _get_local_obj_cmdsets(obj, obj_cmdset):
|
||||
"Object-level cmdsets"
|
||||
# Gather cmdsets from location, objects in location or carried
|
||||
try:
|
||||
local_obj_cmdsets = [None]
|
||||
try:
|
||||
location = obj.location
|
||||
except Exception:
|
||||
location = None
|
||||
if location and not obj_cmdset.no_objs:
|
||||
# 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.dbobj) +
|
||||
obj.contents +
|
||||
[location])
|
||||
for lobj in local_objlist:
|
||||
|
||||
@inlineCallbacks
|
||||
def _get_channel_cmdsets(player, player_cmdset):
|
||||
"Channel-cmdsets"
|
||||
# Create cmdset for all player's available channels
|
||||
try:
|
||||
channel_cmdset = None
|
||||
if not player_cmdset.no_channels:
|
||||
channel_cmdset = yield CHANNELHANDLER.get_cmdset(player)
|
||||
returnValue(channel_cmdset)
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
_msg_err(caller, _ERROR_CMDSETS)
|
||||
raise ErrorReported
|
||||
|
||||
@inlineCallbacks
|
||||
def _get_local_obj_cmdsets(obj, obj_cmdset):
|
||||
"Object-level cmdsets"
|
||||
# Gather cmdsets from location, objects in location or carried
|
||||
try:
|
||||
local_obj_cmdsets = [None]
|
||||
try:
|
||||
# call hook in case we need to do dynamic changing to cmdset
|
||||
_GA(lobj, "at_cmdset_get")()
|
||||
location = obj.location
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
# the call-type lock is checked here, it makes sure a player
|
||||
# is not seeing e.g. the commands on a fellow player (which is why
|
||||
# the no_superuser_bypass must be True)
|
||||
local_obj_cmdsets = \
|
||||
yield [lobj.cmdset.current for lobj in local_objlist
|
||||
if (lobj.cmdset.current and
|
||||
lobj.locks.check(caller, 'call', no_superuser_bypass=True))]
|
||||
for cset in local_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.
|
||||
cset.old_duplicates = cset.duplicates
|
||||
cset.duplicates = True
|
||||
returnValue(local_obj_cmdsets)
|
||||
location = None
|
||||
if location and not obj_cmdset.no_objs:
|
||||
# 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 = [o for o in local_objlist if not o._is_deleted]
|
||||
for lobj in local_objlist:
|
||||
try:
|
||||
# call hook in case we need to do dynamic changing to cmdset
|
||||
_GA(lobj, "at_cmdset_get")()
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
# the call-type lock is checked here, it makes sure a player
|
||||
# is not seeing e.g. the commands on a fellow player (which is why
|
||||
# the no_superuser_bypass must be True)
|
||||
local_obj_cmdsets = \
|
||||
yield [lobj.cmdset.current for lobj in local_objlist
|
||||
if (lobj.cmdset.current and
|
||||
lobj.locks.check(caller, 'call', no_superuser_bypass=True))]
|
||||
for cset in local_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.
|
||||
cset.old_duplicates = cset.duplicates
|
||||
cset.duplicates = True
|
||||
returnValue(local_obj_cmdsets)
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
_msg_err(caller, _ERROR_CMDSETS)
|
||||
raise ErrorReported
|
||||
|
||||
@inlineCallbacks
|
||||
def _get_cmdset(obj):
|
||||
"Get cmdset, triggering all hooks"
|
||||
try:
|
||||
yield obj.at_cmdset_get()
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
try:
|
||||
returnValue(obj.cmdset.current)
|
||||
except AttributeError:
|
||||
returnValue(None)
|
||||
|
||||
if callertype == "session":
|
||||
# we are calling the command from the session level
|
||||
report_to = session
|
||||
session_cmdset = yield _get_cmdset(session)
|
||||
cmdsets = [session_cmdset]
|
||||
if player: # this automatically implies logged-in
|
||||
@inlineCallbacks
|
||||
def _get_cmdset(obj):
|
||||
"Get cmdset, triggering all hooks"
|
||||
try:
|
||||
yield obj.at_cmdset_get()
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
_msg_err(caller, _ERROR_CMDSETS)
|
||||
raise ErrorReported
|
||||
try:
|
||||
returnValue(obj.cmdset.current)
|
||||
except AttributeError:
|
||||
returnValue(None)
|
||||
|
||||
if callertype == "session":
|
||||
# we are calling the command from the session level
|
||||
report_to = session
|
||||
session_cmdset = yield _get_cmdset(session)
|
||||
cmdsets = [session_cmdset]
|
||||
if player: # this automatically implies logged-in
|
||||
player_cmdset = yield _get_cmdset(player)
|
||||
channel_cmdset = yield _get_channel_cmdsets(player, player_cmdset)
|
||||
cmdsets.extend([player_cmdset, channel_cmdset])
|
||||
if obj:
|
||||
obj_cmdset = yield _get_cmdset(obj)
|
||||
local_obj_cmdsets = yield _get_local_obj_cmdsets(obj, obj_cmdset)
|
||||
cmdsets.extend([obj_cmdset] + local_obj_cmdsets)
|
||||
elif callertype == "player":
|
||||
# we are calling the command from the player level
|
||||
report_to = player
|
||||
player_cmdset = yield _get_cmdset(player)
|
||||
channel_cmdset = yield _get_channel_cmdsets(player, player_cmdset)
|
||||
cmdsets.extend([player_cmdset, channel_cmdset])
|
||||
cmdsets = [player_cmdset, channel_cmdset]
|
||||
if obj:
|
||||
obj_cmdset = yield _get_cmdset(obj)
|
||||
local_obj_cmdsets = yield _get_local_obj_cmdsets(obj, obj_cmdset)
|
||||
cmdsets.extend([obj_cmdset] + local_obj_cmdsets)
|
||||
elif callertype == "player":
|
||||
# we are calling the command from the player level
|
||||
report_to = player
|
||||
player_cmdset = yield _get_cmdset(player)
|
||||
channel_cmdset = yield _get_channel_cmdsets(player, player_cmdset)
|
||||
cmdsets = [player_cmdset, channel_cmdset]
|
||||
if obj:
|
||||
elif callertype == "object":
|
||||
# we are calling the command from the object level
|
||||
report_to = obj
|
||||
obj_cmdset = yield _get_cmdset(obj)
|
||||
local_obj_cmdsets = yield _get_local_obj_cmdsets(obj, obj_cmdset)
|
||||
cmdsets.extend([obj_cmdset] + local_obj_cmdsets)
|
||||
elif callertype == "object":
|
||||
# we are calling the command from the object level
|
||||
report_to = obj
|
||||
obj_cmdset = yield _get_cmdset(obj)
|
||||
local_obj_cmdsets = yield _get_local_obj_cmdsets(obj, obj_cmdset)
|
||||
cmdsets = [obj_cmdset] + local_obj_cmdsets
|
||||
else:
|
||||
raise Exception("get_and_merge_cmdsets: callertype %s is not valid." % callertype)
|
||||
#cmdsets = yield [caller_cmdset] + [player_cmdset] +
|
||||
# [channel_cmdset] + local_obj_cmdsets
|
||||
|
||||
# weed out all non-found sets
|
||||
cmdsets = yield [cmdset for cmdset in cmdsets
|
||||
if cmdset and cmdset.key != "_EMPTY_CMDSET"]
|
||||
# report cmdset errors to user (these should already have been logged)
|
||||
yield [report_to.msg(cmdset.errmessage) for cmdset in cmdsets
|
||||
if cmdset.key == "_CMDSET_ERROR"]
|
||||
|
||||
if cmdsets:
|
||||
# faster to do tuple on list than to build tuple directly
|
||||
mergehash = tuple([id(cmdset) for cmdset in cmdsets])
|
||||
if mergehash in _CMDSET_MERGE_CACHE:
|
||||
# cached merge exist; use that
|
||||
cmdset = _CMDSET_MERGE_CACHE[mergehash]
|
||||
cmdsets = [obj_cmdset] + local_obj_cmdsets
|
||||
else:
|
||||
# we group and merge all same-prio cmdsets separately (this avoids
|
||||
# order-dependent clashes in certain cases, such as
|
||||
# when duplicates=True)
|
||||
tempmergers = {}
|
||||
for cmdset in cmdsets:
|
||||
prio = cmdset.priority
|
||||
#print cmdset.key, prio
|
||||
if prio in tempmergers:
|
||||
# merge same-prio cmdset together separately
|
||||
tempmergers[prio] = yield cmdset + tempmergers[prio]
|
||||
else:
|
||||
tempmergers[prio] = cmdset
|
||||
raise Exception("get_and_merge_cmdsets: callertype %s is not valid." % callertype)
|
||||
#cmdsets = yield [caller_cmdset] + [player_cmdset] +
|
||||
# [channel_cmdset] + local_obj_cmdsets
|
||||
|
||||
# sort cmdsets after reverse priority (highest prio are merged in last)
|
||||
cmdsets = yield sorted(tempmergers.values(), key=lambda x: x.priority)
|
||||
# weed out all non-found sets
|
||||
cmdsets = yield [cmdset for cmdset in cmdsets
|
||||
if cmdset and cmdset.key != "_EMPTY_CMDSET"]
|
||||
# report cmdset errors to user (these should already have been logged)
|
||||
yield [report_to.msg(cmdset.errmessage) for cmdset in cmdsets
|
||||
if cmdset.key == "_CMDSET_ERROR"]
|
||||
|
||||
# Merge all command sets into one, beginning with the lowest-prio one
|
||||
cmdset = cmdsets[0]
|
||||
for merging_cmdset in cmdsets[1:]:
|
||||
#print "<%s(%s,%s)> onto <%s(%s,%s)>" % (merging_cmdset.key, merging_cmdset.priority, merging_cmdset.mergetype,
|
||||
# cmdset.key, cmdset.priority, cmdset.mergetype)
|
||||
cmdset = yield merging_cmdset + cmdset
|
||||
# store the full sets for diagnosis
|
||||
cmdset.merged_from = cmdsets
|
||||
# cache
|
||||
_CMDSET_MERGE_CACHE[mergehash] = cmdset
|
||||
else:
|
||||
cmdset = None
|
||||
if cmdsets:
|
||||
# faster to do tuple on list than to build tuple directly
|
||||
mergehash = tuple([id(cmdset) for cmdset in cmdsets])
|
||||
if mergehash in _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 = {}
|
||||
for cmdset in cmdsets:
|
||||
prio = cmdset.priority
|
||||
#print cmdset.key, prio
|
||||
if prio in tempmergers:
|
||||
# merge same-prio cmdset together separately
|
||||
tempmergers[prio] = yield cmdset + tempmergers[prio]
|
||||
else:
|
||||
tempmergers[prio] = cmdset
|
||||
|
||||
for cset in (cset for cset in local_obj_cmdsets if cset):
|
||||
cset.duplicates = cset.old_duplicates
|
||||
#print "merged set:", cmdset.key
|
||||
returnValue(cmdset)
|
||||
# sort cmdsets after reverse priority (highest prio are merged in last)
|
||||
cmdsets = yield sorted(tempmergers.values(), key=lambda x: x.priority)
|
||||
|
||||
# Merge all command sets into one, beginning with the lowest-prio one
|
||||
cmdset = cmdsets[0]
|
||||
for merging_cmdset in cmdsets[1:]:
|
||||
#print "<%s(%s,%s)> onto <%s(%s,%s)>" % (merging_cmdset.key, merging_cmdset.priority, merging_cmdset.mergetype,
|
||||
# cmdset.key, cmdset.priority, cmdset.mergetype)
|
||||
cmdset = yield merging_cmdset + cmdset
|
||||
# store the full sets for diagnosis
|
||||
cmdset.merged_from = cmdsets
|
||||
# cache
|
||||
_CMDSET_MERGE_CACHE[mergehash] = cmdset
|
||||
else:
|
||||
cmdset = None
|
||||
|
||||
for cset in (cset for cset in local_obj_cmdsets if cset):
|
||||
cset.duplicates = cset.old_duplicates
|
||||
#print "merged set:", cmdset.key
|
||||
returnValue(cmdset)
|
||||
except ErrorReported:
|
||||
raise
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
_msg_err(caller, _ERROR_CMDSETS)
|
||||
raise ErrorReported
|
||||
|
||||
# Main command-handler function
|
||||
|
||||
|
|
@ -276,6 +328,78 @@ def cmdhandler(called_by, raw_string, _testing=False, callertype="session", sess
|
|||
Note that this function returns a deferred!
|
||||
"""
|
||||
|
||||
@inlineCallbacks
|
||||
def _run_command(cmd, cmdname, args):
|
||||
"""
|
||||
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
|
||||
Returns:
|
||||
deferred (Deferred): this will fire when the func() method
|
||||
returns.
|
||||
"""
|
||||
try:
|
||||
# Assign useful variables to the instance
|
||||
cmd.caller = caller
|
||||
cmd.cmdstring = cmdname
|
||||
cmd.args = args
|
||||
cmd.cmdset = cmdset
|
||||
cmd.sessid = session.sessid if session else sessid
|
||||
cmd.session = session
|
||||
cmd.player = player
|
||||
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 multuple cmdsets
|
||||
|
||||
if hasattr(cmd, 'obj') and hasattr(cmd.obj, 'scripts'):
|
||||
# cmd.obj is automatically made available by the cmdhandler.
|
||||
# we make sure to validate its scripts.
|
||||
yield cmd.obj.scripts.validate()
|
||||
|
||||
if _testing:
|
||||
# only return the command instance
|
||||
returnValue(cmd)
|
||||
|
||||
# assign custom kwargs to found cmd object
|
||||
for key, val in kwargs.items():
|
||||
setattr(cmd, key, val)
|
||||
|
||||
# pre-command hook
|
||||
abort = yield cmd.at_pre_cmd()
|
||||
if abort:
|
||||
# abort sequence
|
||||
returnValue(abort)
|
||||
|
||||
# Parse and execute
|
||||
yield cmd.parse()
|
||||
|
||||
# main command code
|
||||
# (return value is normally None)
|
||||
ret = yield cmd.func()
|
||||
|
||||
# post-command hook
|
||||
yield cmd.at_post_cmd()
|
||||
|
||||
if cmd.save_for_next:
|
||||
# store a reference to this command, possibly
|
||||
# accessible by the next command.
|
||||
caller.ndb.last_cmd = yield copy(cmd)
|
||||
else:
|
||||
caller.ndb.last_cmd = None
|
||||
# return result to the deferred
|
||||
returnValue(ret)
|
||||
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
_msg_err(caller, _ERROR_UNTRAPPED)
|
||||
raise ErrorReported
|
||||
|
||||
raw_string = to_unicode(raw_string, force_string=True)
|
||||
|
||||
session, player, obj = None, None, None
|
||||
|
|
@ -283,19 +407,22 @@ def cmdhandler(called_by, raw_string, _testing=False, callertype="session", sess
|
|||
session = called_by
|
||||
player = session.player
|
||||
if player:
|
||||
obj = yield _GA(player.dbobj, "get_puppet")(session.sessid)
|
||||
obj = yield player.get_puppet(session.sessid)
|
||||
elif callertype == "player":
|
||||
player = called_by
|
||||
if sessid:
|
||||
obj = yield _GA(player.dbobj, "get_puppet")(sessid)
|
||||
session = player.get_session(sessid)
|
||||
obj = yield player.get_puppet(sessid)
|
||||
elif callertype == "object":
|
||||
obj = called_by
|
||||
else:
|
||||
raise RuntimeError("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 = obj or player or session
|
||||
# The error_to is the default recipient for errors. Tries to make sure a player
|
||||
# does not get spammed for errors while preserving character mirroring.
|
||||
error_to = obj or session or player
|
||||
|
||||
try: # catch bugs in cmdhandler itself
|
||||
try: # catch special-type commands
|
||||
|
|
@ -346,7 +473,7 @@ def cmdhandler(called_by, raw_string, _testing=False, callertype="session", sess
|
|||
# No commands match our entered command
|
||||
syscmd = yield cmdset.get(CMD_NOMATCH)
|
||||
if syscmd:
|
||||
# use custom CMD_NOMATH command
|
||||
# use custom CMD_NOMATCH command
|
||||
sysarg = raw_string
|
||||
else:
|
||||
# fallback to default error text
|
||||
|
|
@ -373,107 +500,38 @@ def cmdhandler(called_by, raw_string, _testing=False, callertype="session", sess
|
|||
raise ExecSystemCommand(cmd, sysarg)
|
||||
|
||||
# A normal command.
|
||||
|
||||
# Assign useful variables to the instance
|
||||
cmd.caller = caller
|
||||
cmd.cmdstring = cmdname
|
||||
cmd.args = args
|
||||
cmd.cmdset = cmdset
|
||||
cmd.sessid = session.sessid if session else sessid
|
||||
cmd.session = session
|
||||
cmd.player = player
|
||||
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 multuple cmdsets
|
||||
|
||||
if hasattr(cmd, 'obj') and hasattr(cmd.obj, 'scripts'):
|
||||
# cmd.obj is automatically made available by the cmdhandler.
|
||||
# we make sure to validate its scripts.
|
||||
yield cmd.obj.scripts.validate()
|
||||
|
||||
if _testing:
|
||||
# only return the command instance
|
||||
returnValue(cmd)
|
||||
|
||||
# assign custom kwargs to found cmd object
|
||||
for key, val in kwargs.items():
|
||||
setattr(cmd, key, val)
|
||||
|
||||
# pre-command hook
|
||||
abort = yield cmd.at_pre_cmd()
|
||||
if abort:
|
||||
# abort sequence
|
||||
returnValue(abort)
|
||||
|
||||
# Parse and execute
|
||||
yield cmd.parse()
|
||||
# (return value is normally None)
|
||||
ret = yield cmd.func()
|
||||
|
||||
# post-command hook
|
||||
yield cmd.at_post_cmd()
|
||||
|
||||
if cmd.save_for_next:
|
||||
# store a reference to this command, possibly
|
||||
# accessible by the next command.
|
||||
caller.ndb.last_cmd = yield copy(cmd)
|
||||
else:
|
||||
caller.ndb.last_cmd = None
|
||||
|
||||
# Done! This returns a deferred. By default, Evennia does
|
||||
# not use this at all.
|
||||
ret = yield _run_command(cmd, cmdname, args)
|
||||
returnValue(ret)
|
||||
|
||||
except ErrorReported:
|
||||
# this error was already reported, so we
|
||||
# catch it here and don't pass it on.
|
||||
pass
|
||||
|
||||
except ExecSystemCommand, exc:
|
||||
# Not a normal command: run a system command, if available,
|
||||
# or fall back to a return string.
|
||||
syscmd = exc.syscmd
|
||||
sysarg = exc.sysarg
|
||||
|
||||
if syscmd:
|
||||
syscmd.caller = caller
|
||||
syscmd.cmdstring = syscmd.key
|
||||
syscmd.args = sysarg
|
||||
syscmd.cmdset = cmdset
|
||||
syscmd.sessid = session.sessid if session else None
|
||||
syscmd.raw_string = unformatted_raw_string
|
||||
|
||||
if hasattr(syscmd, 'obj') and hasattr(syscmd.obj, 'scripts'):
|
||||
# cmd.obj is automatically made available.
|
||||
# we make sure to validate its scripts.
|
||||
yield syscmd.obj.scripts.validate()
|
||||
|
||||
if _testing:
|
||||
# only return the command instance
|
||||
returnValue(syscmd)
|
||||
|
||||
# parse and run the command
|
||||
yield syscmd.parse()
|
||||
yield syscmd.func()
|
||||
ret = yield _run_command(syscmd, syscmd.key, sysarg)
|
||||
returnValue(ret)
|
||||
elif sysarg:
|
||||
# return system arg
|
||||
caller.msg(exc.sysarg)
|
||||
error_to.msg(exc.sysarg, _nomulti=True)
|
||||
|
||||
except NoCmdSets:
|
||||
# Critical error.
|
||||
string = "No command sets found! This is a sign of a critical bug.\n"
|
||||
string += "The error was logged.\n"
|
||||
string += "If logging out/in doesn't solve the problem, try to "
|
||||
string += "contact the server admin through some other means "
|
||||
string += "for assistance."
|
||||
caller.msg(_(string))
|
||||
logger.log_errmsg("No cmdsets found: %s" % caller)
|
||||
error_to.msg(_ERROR_NOCMDSETS, _nomulti=True)
|
||||
|
||||
except Exception:
|
||||
# We should not end up here. If we do, it's a programming bug.
|
||||
string = "%s\nAbove traceback is from an untrapped error."
|
||||
string += " Please file a bug report."
|
||||
logger.log_trace(_(string))
|
||||
caller.msg(string % format_exc())
|
||||
logger.log_trace()
|
||||
_msg_err(error_to, _ERROR_UNTRAPPED)
|
||||
|
||||
except Exception:
|
||||
# This catches exceptions in cmdhandler exceptions themselves
|
||||
string = "%s\nAbove traceback is from a Command handler bug."
|
||||
string += " Please contact an admin and/or file a bug report."
|
||||
logger.log_trace(_(string))
|
||||
caller.msg(string % format_exc())
|
||||
logger.log_trace()
|
||||
_msg_err(error_to, _ERROR_CMDHANDLER)
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
"""
|
||||
The default command parser. Use your own by assigning
|
||||
settings.ALTERNATE_PARSER to a Python path to a module containing the
|
||||
settings.COMMAND_PARSER to a Python path to a module containing the
|
||||
replacing cmdparser function. The replacement parser must matches
|
||||
on the sme form as the default cmdparser.
|
||||
"""
|
||||
|
||||
from src.utils.logger import log_trace
|
||||
from django.utils.translation import ugettext as _
|
||||
from evennia.utils.logger import log_trace
|
||||
|
||||
def cmdparser(raw_string, cmdset, caller, match_index=None):
|
||||
"""
|
||||
|
|
@ -16,7 +16,7 @@ together to create interesting in-game effects.
|
|||
|
||||
from weakref import WeakKeyDictionary
|
||||
from django.utils.translation import ugettext as _
|
||||
from src.utils.utils import inherits_from, is_iter
|
||||
from evennia.utils.utils import inherits_from, is_iter
|
||||
__all__ = ("CmdSet",)
|
||||
|
||||
|
||||
|
|
@ -345,7 +345,7 @@ class CmdSet(object):
|
|||
existing ones to make a unique set.
|
||||
"""
|
||||
|
||||
if inherits_from(cmd, "src.commands.cmdset.CmdSet"):
|
||||
if inherits_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)
|
||||
|
|
@ -64,9 +64,9 @@ example, you can have a 'On a boat' set, onto which you then tack on
|
|||
the 'Fishing' set. Fishing from a boat? No problem!
|
||||
"""
|
||||
from django.conf import settings
|
||||
from src.utils import logger, utils
|
||||
from src.commands.cmdset import CmdSet
|
||||
from src.server.models import ServerConfig
|
||||
from evennia.utils import logger, utils
|
||||
from evennia.commands.cmdset import CmdSet
|
||||
from evennia.server.models import ServerConfig
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
__all__ = ("import_cmdset", "CmdSetHandler")
|
||||
|
|
@ -125,16 +125,16 @@ def import_cmdset(path, cmdsetobj, emit_to_obj=None, no_logging=False):
|
|||
except ImportError, e:
|
||||
logger.log_trace()
|
||||
errstring += _("Error loading cmdset '%s': %s.")
|
||||
errstring = errstring % (modulepath, e)
|
||||
errstring = errstring % (python_path, e)
|
||||
except KeyError:
|
||||
logger.log_trace()
|
||||
errstring += _("Error in loading cmdset: No cmdset class '%(classname)s' in %(modulepath)s.")
|
||||
errstring = errstring % {"classname": classname,
|
||||
"modulepath": modulepath}
|
||||
"modulepath": python_path}
|
||||
except SyntaxError, e:
|
||||
logger.log_trace()
|
||||
errstring += _("SyntaxError encountered when loading cmdset '%s': %s.")
|
||||
errstring = errstring % (modulepath, e)
|
||||
errstring = errstring % (python_path, e)
|
||||
except Exception, e:
|
||||
logger.log_trace()
|
||||
errstring += _("Compile/Run error when loading cmdset '%s': %s.")
|
||||
|
|
@ -357,7 +357,7 @@ class CmdSetHandler(object):
|
|||
cmdset.permanent = False
|
||||
self.update()
|
||||
|
||||
def delete(self, cmdset=None):
|
||||
def remove(self, cmdset=None):
|
||||
"""
|
||||
Remove a cmdset from the handler.
|
||||
|
||||
|
|
@ -409,8 +409,10 @@ class CmdSetHandler(object):
|
|||
pass
|
||||
# re-sync the cmdsethandler.
|
||||
self.update()
|
||||
# legacy alias
|
||||
delete = remove
|
||||
|
||||
def delete_default(self):
|
||||
def remove_default(self):
|
||||
"""
|
||||
This explicitly deletes the default cmdset. It's the
|
||||
only command that can.
|
||||
|
|
@ -428,6 +430,8 @@ class CmdSetHandler(object):
|
|||
else:
|
||||
self.cmdset_stack = [_EmptyCmdSet(cmdsetobj=self.obj)]
|
||||
self.update()
|
||||
# legacy alias
|
||||
delete_default = remove_default
|
||||
|
||||
def all(self):
|
||||
"""
|
||||
|
|
@ -6,8 +6,8 @@ All commands in Evennia inherit from the 'Command' class in this module.
|
|||
"""
|
||||
|
||||
import re
|
||||
from src.locks.lockhandler import LockHandler
|
||||
from src.utils.utils import is_iter, fill, lazy_property
|
||||
from evennia.locks.lockhandler import LockHandler
|
||||
from evennia.utils.utils import is_iter, fill, lazy_property
|
||||
|
||||
|
||||
def _init_command(mcs, **kwargs):
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
#
|
||||
# This is Evennia's default connection screen. It is imported
|
||||
# and run from game/gamesrc/world/connection_screens.py.
|
||||
# and run from world/connection_screens.py.
|
||||
#
|
||||
|
||||
from django.conf import settings
|
||||
from src.utils import utils
|
||||
from evennia.utils import utils
|
||||
|
||||
DEFAULT_SCREEN = \
|
||||
"""{b=============================================================={n
|
||||
|
|
@ -7,10 +7,10 @@ Admin commands
|
|||
import time
|
||||
import re
|
||||
from django.conf import settings
|
||||
from src.server.sessionhandler import SESSIONS
|
||||
from src.server.models import ServerConfig
|
||||
from src.utils import prettytable, search
|
||||
from src.commands.default.muxcommand import MuxCommand
|
||||
from evennia.server.sessionhandler import SESSIONS
|
||||
from evennia.server.models import ServerConfig
|
||||
from evennia.utils import prettytable, search
|
||||
from evennia.commands.default.muxcommand import MuxCommand
|
||||
|
||||
PERMISSION_HIERARCHY = [p.lower() for p in settings.PERMISSION_HIERARCHY]
|
||||
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
Batch processors
|
||||
|
||||
These commands implements the 'batch-command' and 'batch-code'
|
||||
processors, using the functionality in src.utils.batchprocessors.
|
||||
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)
|
||||
|
|
@ -10,23 +10,19 @@ 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).
|
||||
|
||||
Example batch-command file: game/gamesrc/commands/examples/batch_cmds.ev
|
||||
|
||||
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.
|
||||
|
||||
Example batch-code file: game/gamesrc/commands/examples/batch_code.py
|
||||
|
||||
"""
|
||||
from traceback import format_exc
|
||||
from django.conf import settings
|
||||
from src.utils.batchprocessors import BATCHCMD, BATCHCODE
|
||||
from src.commands.cmdset import CmdSet
|
||||
from src.commands.default.muxcommand import MuxCommand
|
||||
from src.utils import utils
|
||||
from evennia.utils.batchprocessors import BATCHCMD, BATCHCODE
|
||||
from evennia.commands.cmdset import CmdSet
|
||||
from evennia.commands.default.muxcommand import MuxCommand
|
||||
from evennia.utils import utils
|
||||
|
||||
# limit symbols for API inclusion
|
||||
__all__ = ("CmdBatchCommands", "CmdBatchCode")
|
||||
|
|
@ -51,7 +47,7 @@ _UTF8_ERROR = \
|
|||
"""
|
||||
|
||||
_PROCPOOL_BATCHCMD_SOURCE = """
|
||||
from src.commands.default.batchprocess import batch_cmd_exec, step_pointer, BatchSafeCmdSet
|
||||
from evennia.commands.default.batchprocess import batch_cmd_exec, step_pointer, BatchSafeCmdSet
|
||||
caller.ndb.batch_stack = commands
|
||||
caller.ndb.batch_stackptr = 0
|
||||
caller.ndb.batch_batchmode = "batch_commands"
|
||||
|
|
@ -65,7 +61,7 @@ for inum in range(len(commands)):
|
|||
print "leaving run ..."
|
||||
"""
|
||||
_PROCPOOL_BATCHCODE_SOURCE = """
|
||||
from src.commands.default.batchprocess import batch_code_exec, step_pointer, BatchSafeCmdSet
|
||||
from evennia.commands.default.batchprocess import batch_code_exec, step_pointer, BatchSafeCmdSet
|
||||
caller.ndb.batch_stack = codes
|
||||
caller.ndb.batch_stackptr = 0
|
||||
caller.ndb.batch_batchmode = "batch_code"
|
||||
|
|
@ -246,7 +242,7 @@ class CmdBatchCommands(MuxCommand):
|
|||
except UnicodeDecodeError, err:
|
||||
caller.msg(_UTF8_ERROR % (python_path, err))
|
||||
return
|
||||
except IOError:
|
||||
except IOError as err:
|
||||
string = "'%s' not found.\nYou have to supply the python path "
|
||||
string += "of the file relative to \none of your batch-file directories (%s)."
|
||||
caller.msg(string % (python_path, ", ".join(settings.BASE_BATCHPROCESS_PATHS)))
|
||||
|
|
@ -698,11 +694,11 @@ class CmdStateCC(MuxCommand):
|
|||
|
||||
class CmdStateJJ(MuxCommand):
|
||||
"""
|
||||
j <command number>
|
||||
jj <command number>
|
||||
|
||||
Jump to specific command number
|
||||
"""
|
||||
key = "j"
|
||||
key = "jj"
|
||||
help_category = "BatchProcess"
|
||||
locks = "cmd:perm(batchcommands)"
|
||||
|
||||
|
|
@ -6,14 +6,14 @@ Building and world design commands
|
|||
"""
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
from src.objects.models import ObjectDB
|
||||
from src.locks.lockhandler import LockException
|
||||
from src.commands.default.muxcommand import MuxCommand
|
||||
from src.commands.cmdhandler import get_and_merge_cmdsets
|
||||
from src.utils import create, utils, search
|
||||
from src.utils.utils import inherits_from
|
||||
from src.utils.spawner import spawn
|
||||
from src.utils.ansi import raw
|
||||
from evennia.objects.models import ObjectDB
|
||||
from evennia.locks.lockhandler import LockException
|
||||
from evennia.commands.default.muxcommand import MuxCommand
|
||||
from evennia.commands.cmdhandler import get_and_merge_cmdsets
|
||||
from evennia.utils import create, utils, search
|
||||
from evennia.utils.utils import inherits_from
|
||||
from evennia.utils.spawner import spawn
|
||||
from evennia.utils.ansi import raw
|
||||
|
||||
# limit symbol import for API
|
||||
__all__ = ("ObjManipCommand", "CmdSetObjAlias", "CmdCopy",
|
||||
|
|
@ -150,7 +150,7 @@ class CmdSetObjAlias(MuxCommand):
|
|||
old_aliases = obj.aliases.all()
|
||||
if old_aliases:
|
||||
caller.msg("Cleared aliases from %s: %s" % (obj.key, ", ".join(old_aliases)))
|
||||
obj.dbobj.aliases.clear()
|
||||
obj.aliases.clear()
|
||||
else:
|
||||
caller.msg("No aliases to clear.")
|
||||
return
|
||||
|
|
@ -276,6 +276,45 @@ class CmdCpAttr(ObjManipCommand):
|
|||
locks = "cmd:perm(cpattr) or perm(Builders)"
|
||||
help_category = "Building"
|
||||
|
||||
def check_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.
|
||||
"""
|
||||
return True
|
||||
|
||||
def check_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.
|
||||
"""
|
||||
return True
|
||||
|
||||
def check_has_attr(self, obj, attr):
|
||||
"""
|
||||
Hook for overriding on subclassed commands. Do any preprocessing
|
||||
required and verify an object has an attribute.
|
||||
"""
|
||||
if not obj.attributes.has(attr):
|
||||
self.caller.msg(
|
||||
"%s doesn't have an attribute %s."
|
||||
% (obj.name, attr))
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_attr(self, obj, attr):
|
||||
"""
|
||||
Hook for overriding on subclassed commands. Do any preprocessing
|
||||
required and get the attribute from the object.
|
||||
"""
|
||||
return obj.attributes.get(attr)
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
Do the copying.
|
||||
|
|
@ -301,25 +340,28 @@ class CmdCpAttr(ObjManipCommand):
|
|||
# name on self.
|
||||
from_obj_attrs = [from_obj_name]
|
||||
from_obj = self.caller
|
||||
from_obj_name = self.caller.name
|
||||
else:
|
||||
from_obj = caller.search(from_obj_name)
|
||||
if not from_obj or not to_objs:
|
||||
caller.msg("You have to supply both source object and target(s).")
|
||||
return
|
||||
if not from_obj.attributes.has(from_obj_attrs[0]):
|
||||
caller.msg("%s doesn't have an attribute %s." % (from_obj_name,
|
||||
from_obj_attrs[0]))
|
||||
return
|
||||
srcvalue = from_obj.attributes.get(from_obj_attrs[0])
|
||||
|
||||
#copy to all to_obj:ects
|
||||
if "move" in self.switches:
|
||||
string = "Moving "
|
||||
clear = True
|
||||
else:
|
||||
string = "Copying "
|
||||
string += "%s/%s (with value %s) ..." % (from_obj_name,
|
||||
from_obj_attrs[0], srcvalue)
|
||||
clear = False
|
||||
if not self.check_from_attr(from_obj, from_obj_attrs[0], clear=clear):
|
||||
return
|
||||
|
||||
for attr in from_obj_attrs:
|
||||
if not self.check_has_attr(from_obj, attr):
|
||||
return
|
||||
|
||||
if (len(from_obj_attrs) != len(set(from_obj_attrs))) and clear:
|
||||
self.caller.msg("{RCannot have duplicate source names when moving!")
|
||||
return
|
||||
|
||||
string = ""
|
||||
|
||||
for to_obj in to_objs:
|
||||
to_obj_name = to_obj['name']
|
||||
|
|
@ -335,18 +377,24 @@ class CmdCpAttr(ObjManipCommand):
|
|||
# if there are too few attributes given
|
||||
# on the to_obj, we copy the original name instead.
|
||||
to_attr = from_attr
|
||||
to_obj.attributes.add(to_attr, srcvalue)
|
||||
if ("move" in self.switches and not (from_obj == to_obj and
|
||||
if not self.check_to_attr(to_obj, to_attr):
|
||||
continue
|
||||
value = self.get_attr(from_obj, from_attr)
|
||||
to_obj.attributes.add(to_attr, value)
|
||||
if (clear and not (from_obj == to_obj and
|
||||
from_attr == to_attr)):
|
||||
from_obj.attributes.remove(from_attr)
|
||||
string += "\nMoved %s.%s -> %s.%s." % (from_obj.name,
|
||||
from_attr,
|
||||
to_obj_name, to_attr)
|
||||
string += "\nMoved %s.%s -> %s.%s. (value: %s)" % (from_obj.name,
|
||||
from_attr,
|
||||
to_obj_name,
|
||||
to_attr,
|
||||
repr(value))
|
||||
else:
|
||||
string += "\nCopied %s.%s -> %s.%s." % (from_obj.name,
|
||||
string += "\nCopied %s.%s -> %s.%s. (value: %s)" % (from_obj.name,
|
||||
from_attr,
|
||||
to_obj_name,
|
||||
to_attr)
|
||||
to_attr,
|
||||
repr(value))
|
||||
caller.msg(string)
|
||||
|
||||
|
||||
|
|
@ -404,10 +452,10 @@ class CmdCreate(ObjManipCommand):
|
|||
|
||||
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 game/gamesrc/objects and any further
|
||||
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
|
||||
game/gamesrc/objects/examples/red_button.py, you could create a new
|
||||
types/examples/red_button.py, you could create a new
|
||||
object of this type like this:
|
||||
|
||||
@create/drop button;red : examples.red_button.RedButton
|
||||
|
|
@ -447,10 +495,10 @@ class CmdCreate(ObjManipCommand):
|
|||
continue
|
||||
if aliases:
|
||||
string = "You create a new %s: %s (aliases: %s)."
|
||||
string = string % (obj.typeclass.typename, obj.name, ", ".join(aliases))
|
||||
string = string % (obj.typename, obj.name, ", ".join(aliases))
|
||||
else:
|
||||
string = "You create a new %s: %s."
|
||||
string = string % (obj.typeclass.typename, obj.name)
|
||||
string = string % (obj.typename, obj.name)
|
||||
# set a default desc
|
||||
if not obj.db.desc:
|
||||
obj.db.desc = "You see nothing special."
|
||||
|
|
@ -1283,6 +1331,61 @@ class CmdSetAttribute(ObjManipCommand):
|
|||
locks = "cmd:perm(set) or perm(Builders)"
|
||||
help_category = "Building"
|
||||
|
||||
def check_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
|
||||
players.
|
||||
|
||||
This function is expected to display its own error message.
|
||||
|
||||
Returning False will abort the command.
|
||||
"""
|
||||
return True
|
||||
|
||||
def check_attr(self, obj, attr_name):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
return attr_name
|
||||
|
||||
def view_attr(self, obj, attr):
|
||||
"""
|
||||
Look up the value of an attribute and return a string displaying it.
|
||||
"""
|
||||
if obj.attributes.has(attr):
|
||||
return "\nAttribute %s/%s = %s" % (obj.name, attr,
|
||||
obj.attributes.get(attr))
|
||||
else:
|
||||
return "\n%s has no attribute '%s'." % (obj.name, attr)
|
||||
|
||||
def rm_attr(self, obj, attr):
|
||||
"""
|
||||
Remove an attribute from the object, and report back.
|
||||
"""
|
||||
if obj.attributes.has(attr):
|
||||
val = obj.attributes.has(attr)
|
||||
obj.attributes.remove(attr)
|
||||
return "\nDeleted attribute '%s' (= %s) from %s." % (attr, val, obj.name)
|
||||
else:
|
||||
return "\n%s has no attribute '%s'." % (obj.name, attr)
|
||||
|
||||
def set_attr(self, obj, attr, value):
|
||||
try:
|
||||
obj.attributes.add(attr, value)
|
||||
return "\nCreated attribute %s/%s = %s" % (obj.name, attr, repr(value))
|
||||
except SyntaxError:
|
||||
# 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")
|
||||
|
||||
def func(self):
|
||||
"Implement the set attribute - a limited form of @py."
|
||||
|
|
@ -1304,6 +1407,9 @@ class CmdSetAttribute(ObjManipCommand):
|
|||
if not obj:
|
||||
return
|
||||
|
||||
if not self.check_obj(obj):
|
||||
return
|
||||
|
||||
string = ""
|
||||
if not value:
|
||||
if self.rhs is None:
|
||||
|
|
@ -1311,36 +1417,25 @@ class CmdSetAttribute(ObjManipCommand):
|
|||
if not attrs:
|
||||
attrs = [attr.key for attr in obj.attributes.all()]
|
||||
for attr in attrs:
|
||||
if obj.attributes.has(attr):
|
||||
string += "\nAttribute %s/%s = %s" % (obj.name, attr,
|
||||
obj.attributes.get(attr))
|
||||
else:
|
||||
string += "\n%s has no attribute '%s'." % (obj.name, attr)
|
||||
# we view it without parsing markup.
|
||||
if not self.check_attr(obj, attr):
|
||||
continue
|
||||
string += self.view_attr(obj, attr)
|
||||
# we view it without parsing markup.
|
||||
self.caller.msg(string.strip(), raw=True)
|
||||
return
|
||||
else:
|
||||
# deleting the attribute(s)
|
||||
for attr in attrs:
|
||||
if obj.attributes.has(attr):
|
||||
val = obj.attributes.has(attr)
|
||||
obj.attributes.remove(attr)
|
||||
string += "\nDeleted attribute '%s' (= %s) from %s." % (attr, val, obj.name)
|
||||
else:
|
||||
string += "\n%s has no attribute '%s'." % (obj.name, attr)
|
||||
if not self.check_attr(obj, attr):
|
||||
continue
|
||||
string += self.rm_attr(obj, attr)
|
||||
else:
|
||||
# setting attribute(s). Make sure to convert to real Python type before saving.
|
||||
for attr in attrs:
|
||||
try:
|
||||
obj.attributes.add(attr, _convert_from_string(self, value))
|
||||
string += "\nCreated attribute %s/%s = %s" % (obj.name, attr, value)
|
||||
except SyntaxError:
|
||||
# this means literal_eval tried to parse a faulty string
|
||||
string = "{RCritical Python syntax error in your value. "
|
||||
string += "Only primitive Python structures are allowed. "
|
||||
string += "\nYou also need to use correct Python syntax. "
|
||||
string += "Remember especially to put quotes around all "
|
||||
string += "strings inside lists and dicts.{n"
|
||||
for attr in attrs:
|
||||
if not self.check_attr(obj, attr):
|
||||
continue
|
||||
value = _convert_from_string(self, value)
|
||||
string += self.set_attr(obj, attr, value)
|
||||
# send feedback
|
||||
caller.msg(string.strip('\n'))
|
||||
|
||||
|
|
@ -1353,6 +1448,7 @@ class CmdTypeclass(MuxCommand):
|
|||
@typclass[/switch] <object> [= <typeclass.path>]
|
||||
@type ''
|
||||
@parent ''
|
||||
@swap - this is a shorthand for using /force/reset flags.
|
||||
|
||||
Switch:
|
||||
reset - clean out *all* the attributes on the object -
|
||||
|
|
@ -1401,12 +1497,16 @@ class CmdTypeclass(MuxCommand):
|
|||
# current one instead.
|
||||
if hasattr(obj, "typeclass"):
|
||||
string = "%s's current typeclass is '%s' (%s)." % (obj.name,
|
||||
obj.typeclass.typename, obj.typeclass.path)
|
||||
obj.typename, obj.path)
|
||||
else:
|
||||
string = "%s is not a typed object." % obj.name
|
||||
caller.msg(string)
|
||||
return
|
||||
|
||||
if self.cmdstring == "@swap":
|
||||
self.switches.append("force")
|
||||
self.switches.append("reset")
|
||||
|
||||
# we have an =, a typeclass was supplied.
|
||||
typeclass = self.rhs
|
||||
|
||||
|
|
@ -1414,7 +1514,7 @@ class CmdTypeclass(MuxCommand):
|
|||
caller.msg("You are not allowed to do that.")
|
||||
return
|
||||
|
||||
if not hasattr(obj, 'swap_typeclass') or not hasattr(obj, 'typeclass'):
|
||||
if not hasattr(obj, 'swap_typeclass'):
|
||||
caller.msg("This object cannot have a type at all!")
|
||||
return
|
||||
|
||||
|
|
@ -1424,25 +1524,23 @@ class CmdTypeclass(MuxCommand):
|
|||
else:
|
||||
reset = "reset" in self.switches
|
||||
old_typeclass_path = obj.typeclass_path
|
||||
ok = obj.swap_typeclass(typeclass, clean_attributes=reset)
|
||||
if ok:
|
||||
if is_same:
|
||||
string = "%s updated its existing typeclass (%s).\n" % (obj.name, obj.typeclass.path)
|
||||
else:
|
||||
string = "%s changed typeclass from %s to %s.\n" % (obj.name,
|
||||
old_typeclass_path,
|
||||
obj.typeclass_path)
|
||||
string += "Creation hooks were run."
|
||||
if reset:
|
||||
string += " All old attributes where deleted before the swap."
|
||||
else:
|
||||
string += " Note that the typeclassed object could have ended up with a mixture of old"
|
||||
string += "\nand new attributes. Use /reset to remove old attributes if you don't want this."
|
||||
|
||||
# we let this raise exception if needed
|
||||
obj.swap_typeclass(typeclass, clean_attributes=reset)
|
||||
|
||||
if is_same:
|
||||
string = "%s updated its existing typeclass (%s).\n" % (obj.name, obj.path)
|
||||
else:
|
||||
string = obj.typeclass_last_errmsg
|
||||
string += "\nCould not swap '%s' (%s) to typeclass '%s'." % (obj.name,
|
||||
old_typeclass_path,
|
||||
typeclass)
|
||||
string = "%s changed typeclass from %s to %s.\n" % (obj.name,
|
||||
old_typeclass_path,
|
||||
obj.typeclass_path)
|
||||
string += "Creation hooks were run."
|
||||
if reset:
|
||||
string += " All old attributes where deleted before the swap."
|
||||
else:
|
||||
string += " Note that the typeclassed object could have ended up with a mixture of old"
|
||||
string += "\nand new attributes. Use /reset to remove old attributes if you don't want this."
|
||||
|
||||
caller.msg(string)
|
||||
|
||||
|
||||
|
|
@ -1616,9 +1714,12 @@ class CmdExamine(ObjManipCommand):
|
|||
"""
|
||||
Formats a single attribute line.
|
||||
"""
|
||||
if crop and isinstance(value, basestring):
|
||||
if crop:
|
||||
if not isinstance(value, basestring):
|
||||
value = utils.to_str(value, force_string=True)
|
||||
value = utils.crop(value)
|
||||
value = utils.to_unicode(value)
|
||||
|
||||
string = "\n %s = %s" % (attr, value)
|
||||
string = raw(string)
|
||||
return string
|
||||
|
|
@ -1677,7 +1778,7 @@ class CmdExamine(ObjManipCommand):
|
|||
string += "\n{wPlayer Perms{n: %s" % (", ".join(perms))
|
||||
if obj.player.attributes.has("_quell"):
|
||||
string += " {r(quelled){n"
|
||||
string += "\n{wTypeclass{n: %s (%s)" % (obj.typeclass.typename,
|
||||
string += "\n{wTypeclass{n: %s (%s)" % (obj.typename,
|
||||
obj.typeclass_path)
|
||||
if hasattr(obj, "location"):
|
||||
string += "\n{wLocation{n: %s" % obj.location
|
||||
|
|
@ -1811,7 +1912,7 @@ class CmdExamine(ObjManipCommand):
|
|||
obj_name = objdef['name']
|
||||
obj_attrs = objdef['attrs']
|
||||
|
||||
self.player_mode = utils.inherits_from(caller, "src.players.player.Player") or \
|
||||
self.player_mode = utils.inherits_from(caller, "evennia.players.players.Player") or \
|
||||
"player" in self.switches or obj_name.startswith('*')
|
||||
if self.player_mode:
|
||||
try:
|
||||
|
|
@ -1928,7 +2029,7 @@ class CmdFind(MuxCommand):
|
|||
else:
|
||||
result=result[0]
|
||||
string += "\n{g %s(%s) - %s{n" % (result.key, result.dbref,
|
||||
result.typeclass.path)
|
||||
result.path)
|
||||
else:
|
||||
# Not a player/dbref search but a wider search; build a queryset.
|
||||
# Searchs for key and aliases
|
||||
|
|
@ -1946,7 +2047,7 @@ class CmdFind(MuxCommand):
|
|||
|
||||
if nresults:
|
||||
# convert result to typeclasses.
|
||||
results = [result.typeclass for result in results]
|
||||
results = [result for result in results]
|
||||
if "room" in switches:
|
||||
results = [obj for obj in results if inherits_from(obj, ROOM_TYPECLASS)]
|
||||
if "exit" in switches:
|
||||
|
|
@ -2120,7 +2221,7 @@ class CmdScript(MuxCommand):
|
|||
string += "No scripts defined on %s." % obj.key
|
||||
elif not self.switches:
|
||||
# view all scripts
|
||||
from src.commands.default.system import format_script_list
|
||||
from evennia.commands.default.system import format_script_list
|
||||
string += format_script_list(scripts)
|
||||
elif "start" in self.switches:
|
||||
num = sum([obj.scripts.start(script.key) for script in scripts])
|
||||
|
|
@ -2274,11 +2375,8 @@ class CmdTag(MuxCommand):
|
|||
self.caller.msg(string)
|
||||
|
||||
#
|
||||
# To use the prototypes with the @spawn function, copy
|
||||
# game/gamesrc/world/examples/prototypes.py up one level
|
||||
# to game/gamesrc/world. Then add to game/settings.py the
|
||||
# line
|
||||
# PROTOTYPE_MODULES = ["game.gamesrc.commands.prototypes"]
|
||||
# To use the prototypes with the @spawn function set
|
||||
# PROTOTYPE_MODULES = ["commands.prototypes"]
|
||||
# Reload the server and the prototypes should be available.
|
||||
#
|
||||
|
||||
|
|
@ -4,10 +4,10 @@ available (i.e. IC commands). Note that some commands, such as
|
|||
communication-commands are instead put on the player level, in the
|
||||
Player cmdset. Player commands remain available also to Characters.
|
||||
"""
|
||||
from src.commands.cmdset import CmdSet
|
||||
from src.commands.default import general, help, admin, system
|
||||
from src.commands.default import building
|
||||
from src.commands.default import batchprocess
|
||||
from evennia.commands.cmdset import CmdSet
|
||||
from evennia.commands.default import general, help, admin, system
|
||||
from evennia.commands.default import building
|
||||
from evennia.commands.default import batchprocess
|
||||
|
||||
|
||||
class CharacterCmdSet(CmdSet):
|
||||
|
|
@ -26,6 +26,7 @@ class CharacterCmdSet(CmdSet):
|
|||
self.add(general.CmdInventory())
|
||||
self.add(general.CmdPose())
|
||||
self.add(general.CmdNick())
|
||||
self.add(general.CmdDesc())
|
||||
self.add(general.CmdGet())
|
||||
self.add(general.CmdDrop())
|
||||
self.add(general.CmdGive())
|
||||
|
|
@ -9,9 +9,9 @@ function, all commands in this cmdset should use the self.msg()
|
|||
command method rather than caller.msg().
|
||||
"""
|
||||
|
||||
from src.commands.cmdset import CmdSet
|
||||
from src.commands.default import help, comms, admin, system
|
||||
from src.commands.default import building, player
|
||||
from evennia.commands.cmdset import CmdSet
|
||||
from evennia.commands.default import help, comms, admin, system
|
||||
from evennia.commands.default import building, player
|
||||
|
||||
|
||||
class PlayerCmdSet(CmdSet):
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
"""
|
||||
This module stores session-level commands.
|
||||
"""
|
||||
from src.commands.cmdset import CmdSet
|
||||
from src.commands.default import player
|
||||
from evennia.commands.cmdset import CmdSet
|
||||
from evennia.commands.default import player
|
||||
|
||||
class SessionCmdSet(CmdSet):
|
||||
"""
|
||||
|
|
@ -3,8 +3,8 @@ 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.
|
||||
"""
|
||||
from src.commands.cmdset import CmdSet
|
||||
from src.commands.default import unloggedin
|
||||
from evennia.commands.cmdset import CmdSet
|
||||
from evennia.commands.default import unloggedin
|
||||
|
||||
|
||||
class UnloggedinCmdSet(CmdSet):
|
||||
|
|
@ -8,14 +8,14 @@ for easy handling.
|
|||
|
||||
"""
|
||||
from django.conf import settings
|
||||
from src.comms.models import ChannelDB, Msg
|
||||
#from src.comms import irc, imc2, rss
|
||||
from src.players.models import PlayerDB
|
||||
from src.players import bots
|
||||
from src.comms.channelhandler import CHANNELHANDLER
|
||||
from src.utils import create, utils, prettytable, evtable
|
||||
from src.utils.utils import make_iter
|
||||
from src.commands.default.muxcommand import MuxCommand, MuxPlayerCommand
|
||||
from evennia.comms.models import ChannelDB, Msg
|
||||
#from evennia.comms import irc, imc2, rss
|
||||
from evennia.players.models import PlayerDB
|
||||
from evennia.players import bots
|
||||
from evennia.comms.channelhandler import CHANNELHANDLER
|
||||
from evennia.utils import create, utils, prettytable, evtable
|
||||
from evennia.utils.utils import make_iter
|
||||
from evennia.commands.default.muxcommand import MuxCommand, MuxPlayerCommand
|
||||
|
||||
# limit symbol import for API
|
||||
__all__ = ("CmdAddCom", "CmdDelCom", "CmdAllCom",
|
||||
|
|
@ -130,7 +130,7 @@ class CmdDelCom(MuxPlayerCommand):
|
|||
"""
|
||||
|
||||
key = "delcom"
|
||||
aliases = ["delaliaschan, delchanalias"]
|
||||
aliases = ["delaliaschan", "delchanalias"]
|
||||
help_category = "Comms"
|
||||
locks = "cmd:not perm(channel_banned)"
|
||||
|
||||
|
|
@ -386,7 +386,7 @@ class CmdCBoot(MuxPlayerCommand):
|
|||
string = "You don't control this channel."
|
||||
self.msg(string)
|
||||
return
|
||||
if not player.dbobj in channel.db_subscriptions.all():
|
||||
if not player in channel.db_subscriptions.all():
|
||||
string = "Player %s is not connected to channel %s." % (player.key, channel.key)
|
||||
self.msg(string)
|
||||
return
|
||||
|
|
@ -533,6 +533,7 @@ class CmdChannelCreate(MuxPlayerCommand):
|
|||
description,
|
||||
locks=lockstring)
|
||||
new_chan.connect(caller)
|
||||
CHANNELHANDLER.update()
|
||||
self.msg("Created channel %s and connected to it." % new_chan.key)
|
||||
|
||||
|
||||
|
|
@ -792,9 +793,9 @@ class CmdIRC2Chan(MuxCommand):
|
|||
|
||||
if 'list' in self.switches:
|
||||
# show all connections
|
||||
ircbots = [bot.typeclass for bot in PlayerDB.objects.filter(db_is_bot=True, username__startswith="ircbot-")]
|
||||
ircbots = [bot for bot in PlayerDB.objects.filter(db_is_bot=True, username__startswith="ircbot-")]
|
||||
if ircbots:
|
||||
from src.utils.evtable import EvTable
|
||||
from evennia.utils.evtable import EvTable
|
||||
table = EvTable("{wdbid{n", "{wbotname{n", "{wev-channel{n", "{wirc-channel{n", maxwidth=78)
|
||||
for ircbot in ircbots:
|
||||
ircinfo = "%s (%s:%s)" % (ircbot.db.irc_channel, ircbot.db.irc_network, ircbot.db.irc_port)
|
||||
|
|
@ -842,7 +843,7 @@ class CmdIRC2Chan(MuxCommand):
|
|||
bot = PlayerDB.objects.filter(username__iexact=botname)
|
||||
if bot:
|
||||
# re-use an existing bot
|
||||
bot = bot[0].typeclass
|
||||
bot = bot[0]
|
||||
if not bot.is_bot:
|
||||
self.msg("Player '%s' already exists and is not a bot." % botname)
|
||||
return
|
||||
|
|
@ -901,9 +902,9 @@ class CmdRSS2Chan(MuxCommand):
|
|||
|
||||
if 'list' in self.switches:
|
||||
# show all connections
|
||||
rssbots = [bot.typeclass for bot in PlayerDB.objects.filter(db_is_bot=True, username__startswith="rssbot-")]
|
||||
rssbots = [bot for bot in PlayerDB.objects.filter(db_is_bot=True, username__startswith="rssbot-")]
|
||||
if rssbots:
|
||||
from src.utils.evtable import EvTable
|
||||
from evennia.utils.evtable import EvTable
|
||||
table = EvTable("{wdbid{n", "{wupdate rate{n", "{wev-channel", "{wRSS feed URL{n", border="cells", maxwidth=78)
|
||||
for rssbot in rssbots:
|
||||
table.add_row(rssbot.id, rssbot.db.rss_rate, rssbot.db.ev_channel, rssbot.db.rss_url)
|
||||
|
|
@ -938,7 +939,7 @@ class CmdRSS2Chan(MuxCommand):
|
|||
bot = PlayerDB.objects.filter(username__iexact=botname)
|
||||
if bot:
|
||||
# re-use existing bot
|
||||
bot = bot[0].typeclass
|
||||
bot = bot[0]
|
||||
if not bot.is_bot:
|
||||
self.msg("Player '%s' already exists and is not a bot." % botname)
|
||||
return
|
||||
|
|
@ -1061,8 +1062,8 @@ class CmdRSS2Chan(MuxCommand):
|
|||
# if "update" in self.switches:
|
||||
# # update the lists
|
||||
# import time
|
||||
# from src.comms.imc2lib import imc2_packets as pck
|
||||
# from src.comms.imc2 import IMC2_MUDLIST, IMC2_CHANLIST, IMC2_CLIENT
|
||||
# from evennia.comms.imc2lib import imc2_packets as pck
|
||||
# from evennia.comms.imc2 import IMC2_MUDLIST, IMC2_CHANLIST, IMC2_CLIENT
|
||||
# # update connected muds
|
||||
# IMC2_CLIENT.send_packet(pck.IMC2PacketKeepAliveRequest())
|
||||
# # prune inactive muds
|
||||
|
|
@ -1076,7 +1077,7 @@ class CmdRSS2Chan(MuxCommand):
|
|||
# elif("games" in self.switches or "muds" in self.switches
|
||||
# or self.cmdstring == "@imclist"):
|
||||
# # list muds
|
||||
# from src.comms.imc2 import IMC2_MUDLIST
|
||||
# from evennia.comms.imc2 import IMC2_MUDLIST
|
||||
#
|
||||
# muds = IMC2_MUDLIST.get_mud_list()
|
||||
# networks = set(mud.networkname for mud in muds)
|
||||
|
|
@ -1096,7 +1097,7 @@ class CmdRSS2Chan(MuxCommand):
|
|||
# if not self.args:
|
||||
# self.msg("Usage: @imcwhois <playername>")
|
||||
# return
|
||||
# from src.comms.imc2 import IMC2_CLIENT
|
||||
# from evennia.comms.imc2 import IMC2_CLIENT
|
||||
# self.msg("Sending IMC whois request. If you receive no response, no matches were found.")
|
||||
# IMC2_CLIENT.msg_imc2(None,
|
||||
# from_obj=self.caller,
|
||||
|
|
@ -1106,7 +1107,7 @@ class CmdRSS2Chan(MuxCommand):
|
|||
# elif(not self.switches or "channels" in self.switches or
|
||||
# self.cmdstring == "@imcchanlist"):
|
||||
# # show channels
|
||||
# from src.comms.imc2 import IMC2_CHANLIST, IMC2_CLIENT
|
||||
# from evennia.comms.imc2 import IMC2_CHANLIST, IMC2_CLIENT
|
||||
#
|
||||
# channels = IMC2_CHANLIST.get_channel_list()
|
||||
# string = ""
|
||||
|
|
@ -1151,7 +1152,7 @@ class CmdRSS2Chan(MuxCommand):
|
|||
# self.msg(string)
|
||||
# return
|
||||
#
|
||||
# from src.comms.imc2 import IMC2_CLIENT
|
||||
# from evennia.comms.imc2 import IMC2_CLIENT
|
||||
#
|
||||
# if not self.args or not '@' in self.lhs or not self.rhs:
|
||||
# string = "Usage: imctell User@Mud = <msg>"
|
||||
|
|
@ -2,8 +2,8 @@
|
|||
General Character commands usually availabe to all characters
|
||||
"""
|
||||
from django.conf import settings
|
||||
from src.utils import utils, prettytable
|
||||
from src.commands.default.muxcommand import MuxCommand
|
||||
from evennia.utils import utils, prettytable
|
||||
from evennia.commands.default.muxcommand import MuxCommand
|
||||
|
||||
|
||||
# limit symbol import for API
|
||||
|
|
@ -11,8 +11,6 @@ __all__ = ("CmdHome", "CmdLook", "CmdNick",
|
|||
"CmdInventory", "CmdGet", "CmdDrop", "CmdGive",
|
||||
"CmdSay", "CmdPose", "CmdAccess")
|
||||
|
||||
AT_SEARCH_RESULT = utils.variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1))
|
||||
|
||||
|
||||
class CmdHome(MuxCommand):
|
||||
"""
|
||||
|
|
@ -26,6 +24,7 @@ class CmdHome(MuxCommand):
|
|||
|
||||
key = "home"
|
||||
locks = "cmd:perm(home) or perm(Builders)"
|
||||
arg_regex = r"$"
|
||||
|
||||
def func(self):
|
||||
"Implement the command"
|
||||
|
|
@ -53,7 +52,7 @@ class CmdLook(MuxCommand):
|
|||
key = "look"
|
||||
aliases = ["l", "ls"]
|
||||
locks = "cmd:all()"
|
||||
arg_regex = r"\s.*?|$"
|
||||
arg_regex = r"\s|$"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
|
|
@ -155,7 +154,6 @@ class CmdNick(MuxCommand):
|
|||
string = ""
|
||||
for switch in switches:
|
||||
oldnick = caller.nicks.get(key=nick, category=switch)
|
||||
#oldnick = Nick.objects.filter(db_obj=caller.dbobj, db_nick__iexact=nick, db_type__iexact=switch)
|
||||
if not real:
|
||||
# removal of nick
|
||||
if oldnick:
|
||||
|
|
@ -187,6 +185,7 @@ class CmdInventory(MuxCommand):
|
|||
key = "inventory"
|
||||
aliases = ["inv", "i"]
|
||||
locks = "cmd:all()"
|
||||
arg_regex = r"$"
|
||||
|
||||
def func(self):
|
||||
"check inventory"
|
||||
|
|
@ -216,6 +215,7 @@ class CmdGet(MuxCommand):
|
|||
key = "get"
|
||||
aliases = "grab"
|
||||
locks = "cmd:all()"
|
||||
arg_regex = r"\s|$"
|
||||
|
||||
def func(self):
|
||||
"implements the command."
|
||||
|
|
@ -232,10 +232,6 @@ class CmdGet(MuxCommand):
|
|||
if caller == obj:
|
||||
caller.msg("You can't get yourself.")
|
||||
return
|
||||
#print obj, obj.location, caller, caller==obj.location
|
||||
if caller == obj.location:
|
||||
caller.msg("You already hold that.")
|
||||
return
|
||||
if not obj.access(caller, 'get'):
|
||||
if obj.db.get_err_msg:
|
||||
caller.msg(obj.db.get_err_msg)
|
||||
|
|
@ -248,7 +244,7 @@ class CmdGet(MuxCommand):
|
|||
caller.location.msg_contents("%s picks up %s." %
|
||||
(caller.name,
|
||||
obj.name),
|
||||
exclude=caller)
|
||||
exclude=caller)
|
||||
# calling hook method
|
||||
obj.at_get(caller)
|
||||
|
||||
|
|
@ -266,6 +262,7 @@ class CmdDrop(MuxCommand):
|
|||
|
||||
key = "drop"
|
||||
locks = "cmd:all()"
|
||||
arg_regex = r"\s|$"
|
||||
|
||||
def func(self):
|
||||
"Implement command"
|
||||
|
|
@ -277,13 +274,9 @@ class CmdDrop(MuxCommand):
|
|||
|
||||
# Because the DROP command by definition looks for items
|
||||
# in inventory, call the search function using location = caller
|
||||
results = caller.search(self.args, location=caller, quiet=True)
|
||||
|
||||
# now we send it into the error handler (this will output consistent
|
||||
# error messages if there are problems).
|
||||
obj = AT_SEARCH_RESULT(caller, self.args, results, False,
|
||||
nofound_string="You aren't carrying %s." % self.args,
|
||||
multimatch_string="You carry more than one %s:" % self.args)
|
||||
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)
|
||||
if not obj:
|
||||
return
|
||||
|
||||
|
|
@ -291,7 +284,7 @@ class CmdDrop(MuxCommand):
|
|||
caller.msg("You drop %s." % (obj.name,))
|
||||
caller.location.msg_contents("%s drops %s." %
|
||||
(caller.name, obj.name),
|
||||
exclude=caller)
|
||||
exclude=caller)
|
||||
# Call the object script's at_drop() method.
|
||||
obj.at_drop(caller)
|
||||
|
||||
|
|
@ -308,6 +301,7 @@ class CmdGive(MuxCommand):
|
|||
"""
|
||||
key = "give"
|
||||
locks = "cmd:all()"
|
||||
arg_regex = r"\s|$"
|
||||
|
||||
def func(self):
|
||||
"Implement give"
|
||||
|
|
@ -316,7 +310,9 @@ class CmdGive(MuxCommand):
|
|||
if not self.args or not self.rhs:
|
||||
caller.msg("Usage: give <inventory object> = <target>")
|
||||
return
|
||||
to_give = caller.search(self.lhs)
|
||||
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)
|
||||
if not (to_give and target):
|
||||
return
|
||||
|
|
@ -332,6 +328,31 @@ class CmdGive(MuxCommand):
|
|||
target.msg("%s gives you %s." % (caller.key, to_give.key))
|
||||
|
||||
|
||||
class CmdDesc(MuxCommand):
|
||||
"""
|
||||
describe yourself
|
||||
|
||||
Usage:
|
||||
desc <description>
|
||||
|
||||
Add a description to yourself. This
|
||||
will be visible to people when they
|
||||
look at you.
|
||||
"""
|
||||
key = "desc"
|
||||
locks = "cmd:all()"
|
||||
arg_regex = r"\s|$"
|
||||
|
||||
def func(self):
|
||||
"add the description"
|
||||
|
||||
if not self.args:
|
||||
self.caller.msg("You must add a description.")
|
||||
return
|
||||
|
||||
self.caller.db.desc = self.args.strip()
|
||||
self.caller.msg("You set your description.")
|
||||
|
||||
class CmdSay(MuxCommand):
|
||||
"""
|
||||
speak as your character
|
||||
|
|
@ -426,6 +447,7 @@ class CmdAccess(MuxCommand):
|
|||
key = "access"
|
||||
aliases = ["groups", "hierarchy"]
|
||||
locks = "cmd:all()"
|
||||
arg_regex = r"$"
|
||||
|
||||
def func(self):
|
||||
"Load the permission groups"
|
||||
|
|
@ -7,12 +7,12 @@ creation of other help topics such as RP help or game-world aides.
|
|||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
from src.utils.utils import fill, dedent
|
||||
from src.commands.command import Command
|
||||
from src.help.models import HelpEntry
|
||||
from src.utils import create
|
||||
from src.utils.utils import string_suggestions
|
||||
from src.commands.default.muxcommand import MuxCommand
|
||||
from evennia.utils.utils import fill, dedent
|
||||
from evennia.commands.command import Command
|
||||
from evennia.help.models import HelpEntry
|
||||
from evennia.utils import create
|
||||
from evennia.utils.utils import string_suggestions
|
||||
from evennia.commands.default.muxcommand import MuxCommand
|
||||
|
||||
# limit symbol import for API
|
||||
__all__ = ("CmdHelp", "CmdSetHelp")
|
||||
|
|
@ -3,8 +3,8 @@ The command template for the default MUX-style command set. There
|
|||
is also an Player/OOC version that makes sure caller is a Player object.
|
||||
"""
|
||||
|
||||
from src.utils import utils
|
||||
from src.commands.command import Command
|
||||
from evennia.utils import utils
|
||||
from evennia.commands.command import Command
|
||||
|
||||
# limit symbol import for API
|
||||
__all__ = ("MuxCommand", "MuxPlayerCommand")
|
||||
|
|
@ -12,7 +12,7 @@ __all__ = ("MuxCommand", "MuxPlayerCommand")
|
|||
class MuxCommand(Command):
|
||||
"""
|
||||
This sets up the basis for a MUX command. The idea
|
||||
is tkhat most other Mux-related commands should just
|
||||
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.
|
||||
|
|
@ -183,11 +183,11 @@ class MuxPlayerCommand(MuxCommand):
|
|||
"""
|
||||
super(MuxPlayerCommand, self).parse()
|
||||
|
||||
if utils.inherits_from(self.caller, "src.objects.objects.Object"):
|
||||
if utils.inherits_from(self.caller, "evennia.objects.objects.DefaultObject"):
|
||||
# caller is an Object/Character
|
||||
self.character = self.caller
|
||||
self.caller = self.caller.player
|
||||
elif utils.inherits_from(self.caller, "src.players.players.Player"):
|
||||
elif utils.inherits_from(self.caller, "evennia.players.players.DefaultPlayer"):
|
||||
# caller was already a Player
|
||||
self.character = self.caller.get_puppet(self.sessid)
|
||||
else:
|
||||
|
|
@ -14,18 +14,20 @@ 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, Player- commands should use
|
||||
Note that under MULTISESSION_MODE > 2, Player- 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.
|
||||
|
||||
"""
|
||||
import time
|
||||
from django.conf import settings
|
||||
from src.server.sessionhandler import SESSIONS
|
||||
from src.commands.default.muxcommand import MuxPlayerCommand
|
||||
from src.utils import utils, create, search, prettytable
|
||||
from evennia.server.sessionhandler import SESSIONS
|
||||
from evennia.commands.default.muxcommand import MuxPlayerCommand
|
||||
from evennia.utils import utils, create, search, prettytable
|
||||
|
||||
MAX_NR_CHARACTERS = settings.MAX_NR_CHARACTERS
|
||||
MULTISESSION_MODE = settings.MULTISESSION_MODE
|
||||
|
||||
from settings import MAX_NR_CHARACTERS, MULTISESSION_MODE
|
||||
# limit symbol import for API
|
||||
__all__ = ("CmdOOCLook", "CmdIC", "CmdOOC", "CmdPassword", "CmdQuit",
|
||||
"CmdCharCreate", "CmdEncoding", "CmdSessions", "CmdWho",
|
||||
|
|
@ -141,7 +143,7 @@ class CmdOOCLook(MuxPlayerCommand):
|
|||
string = "You are out-of-character (OOC).\nUse {w@ic{n to get back into the game."
|
||||
self.msg(string)
|
||||
return
|
||||
if utils.inherits_from(self.caller, "src.objects.objects.Object"):
|
||||
if utils.inherits_from(self.caller, "evennia.objects.objects.Object"):
|
||||
# An object of some type is calling. Use default look instead.
|
||||
super(CmdOOCLook, self).func()
|
||||
elif self.args:
|
||||
|
|
@ -180,7 +182,7 @@ class CmdCharCreate(MuxPlayerCommand):
|
|||
self.msg("You may only create a maximum of %i characters." % MAX_NR_CHARACTERS)
|
||||
return
|
||||
# create the character
|
||||
from src.objects.models import ObjectDB
|
||||
from evennia.objects.models import ObjectDB
|
||||
|
||||
start_location = ObjectDB.objects.get_id(settings.START_LOCATION)
|
||||
default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME)
|
||||
|
|
@ -221,7 +223,7 @@ class CmdIC(MuxPlayerCommand):
|
|||
"""
|
||||
|
||||
key = "@ic"
|
||||
# lockmust be all() for different puppeted objects to access it.
|
||||
# lock must be all() for different puppeted objects to access it.
|
||||
locks = "cmd:all()"
|
||||
aliases = "@puppet"
|
||||
help_category = "General"
|
||||
|
|
@ -247,34 +249,11 @@ class CmdIC(MuxPlayerCommand):
|
|||
else:
|
||||
self.msg("That is not a valid character choice.")
|
||||
return
|
||||
# permission checks
|
||||
if player.get_puppet(sessid) == new_character:
|
||||
self.msg("{RYou already act as {c%s{n." % new_character.name)
|
||||
return
|
||||
if new_character.player:
|
||||
# may not puppet an already puppeted character
|
||||
if new_character.sessid.count() and new_character.player == player:
|
||||
# as a safeguard we allow "taking over" chars from your own sessions.
|
||||
if MULTISESSION_MODE in (1, 3):
|
||||
txt = "{c%s{n{G is now shared from another of your sessions.{n"
|
||||
txt2 = "Sharing {c%s{n with another of your sessions."
|
||||
else:
|
||||
txt = "{c%s{n{R is now acted from another of your sessions.{n"
|
||||
txt2 = "Taking over {c%s{n from another of your sessions."
|
||||
player.unpuppet_object(new_character.sessid.get())
|
||||
player.msg(txt % (new_character.name), sessid=new_character.sessid.get())
|
||||
self.msg(txt2 % new_character.name)
|
||||
elif new_character.player != player and new_character.player.is_connected:
|
||||
self.msg("{c%s{r is already acted by another player.{n" % new_character.name)
|
||||
return
|
||||
if not new_character.access(player, "puppet"):
|
||||
# main acccess check
|
||||
self.msg("{rYou may not become %s.{n" % new_character.name)
|
||||
return
|
||||
if player.puppet_object(sessid, new_character):
|
||||
try:
|
||||
player.puppet_object(sessid, new_character)
|
||||
player.db._last_puppet = new_character
|
||||
else:
|
||||
self.msg("{rYou cannot become {C%s{n." % new_character.name)
|
||||
except RuntimeError, exc:
|
||||
self.msg("{rYou cannot become {C%s{n: %s" % (new_character.name, exc))
|
||||
|
||||
|
||||
class CmdOOC(MuxPlayerCommand):
|
||||
|
|
@ -290,7 +269,6 @@ class CmdOOC(MuxPlayerCommand):
|
|||
"""
|
||||
|
||||
key = "@ooc"
|
||||
# lock must be all(), for different puppeted objects to access it.
|
||||
locks = "cmd:pperm(Players)"
|
||||
aliases = "@unpuppet"
|
||||
help_category = "General"
|
||||
|
|
@ -310,11 +288,12 @@ class CmdOOC(MuxPlayerCommand):
|
|||
player.db._last_puppet = old_char
|
||||
|
||||
# disconnect
|
||||
if player.unpuppet_object(sessid):
|
||||
try:
|
||||
player.unpuppet_object(sessid)
|
||||
self.msg("\n{GYou go OOC.{n\n")
|
||||
player.execute_cmd("look", sessid=sessid)
|
||||
else:
|
||||
raise RuntimeError("Could not unpuppet!")
|
||||
except RuntimeError, exc:
|
||||
self.msg("{rCould not unpuppet from {c%s{n: %s" % (old_char, exc))
|
||||
|
||||
class CmdSessions(MuxPlayerCommand):
|
||||
"""
|
||||
|
|
@ -534,6 +513,7 @@ class CmdQuit(MuxPlayerCommand):
|
|||
game. Use the /all switch to disconnect from all sessions.
|
||||
"""
|
||||
key = "@quit"
|
||||
aliases = "quit"
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
|
|
@ -552,7 +532,7 @@ class CmdQuit(MuxPlayerCommand):
|
|||
player.msg("{RQuitting{n. %i session are still connected." % (nsess-1), sessid=self.sessid)
|
||||
else:
|
||||
# we are quitting the last available session
|
||||
player.msg("{RQuitting{n. Hope to see you soon again.", sessid=self.sessid)
|
||||
player.msg("{RQuitting{n. Hope to see you again, soon.", sessid=self.sessid)
|
||||
player.disconnect_session_from_player(self.sessid)
|
||||
|
||||
|
||||
|
|
@ -596,7 +576,7 @@ class CmdColorTest(MuxPlayerCommand):
|
|||
|
||||
if self.args.startswith("a"):
|
||||
# show ansi 16-color table
|
||||
from src.utils import ansi
|
||||
from evennia.utils import ansi
|
||||
ap = ansi.ANSI_PARSER
|
||||
# ansi colors
|
||||
# show all ansi color-related codes
|
||||
|
|
@ -18,17 +18,17 @@ line with a command (if there is no match to a known command,
|
|||
the line is just added to the editor buffer).
|
||||
"""
|
||||
|
||||
from src.comms.models import ChannelDB
|
||||
from src.utils import create
|
||||
from evennia.comms.models import ChannelDB
|
||||
from evennia.utils import create
|
||||
|
||||
# The command keys the engine is calling
|
||||
# (the actual names all start with __)
|
||||
from src.commands.cmdhandler import CMD_NOINPUT
|
||||
from src.commands.cmdhandler import CMD_NOMATCH
|
||||
from src.commands.cmdhandler import CMD_MULTIMATCH
|
||||
from src.commands.cmdhandler import CMD_CHANNEL
|
||||
from evennia.commands.cmdhandler import CMD_NOINPUT
|
||||
from evennia.commands.cmdhandler import CMD_NOMATCH
|
||||
from evennia.commands.cmdhandler import CMD_MULTIMATCH
|
||||
from evennia.commands.cmdhandler import CMD_CHANNEL
|
||||
|
||||
from src.commands.default.muxcommand import MuxCommand
|
||||
from evennia.commands.default.muxcommand import MuxCommand
|
||||
|
||||
# Command called when there is no input at line
|
||||
# (i.e. an lone return key)
|
||||
|
|
@ -61,7 +61,7 @@ class SystemNoMatch(MuxCommand):
|
|||
"""
|
||||
This is given the failed raw string as input.
|
||||
"""
|
||||
self.caller.msg("Huh?")
|
||||
self.msg("Huh?")
|
||||
|
||||
|
||||
#
|
||||
|
|
@ -76,7 +76,7 @@ class SystemMultimatch(MuxCommand):
|
|||
|
||||
matches = [(candidate, cmd) , (candidate, cmd), ...],
|
||||
|
||||
where candidate is an instance of src.commands.cmdparser.CommandCandidate
|
||||
where candidate is an instance of evennia.commands.cmdparser.CommandCandidate
|
||||
and cmd is an an instantiated Command object matching the candidate.
|
||||
"""
|
||||
key = CMD_MULTIMATCH
|
||||
|
|
@ -87,7 +87,7 @@ class SystemMultimatch(MuxCommand):
|
|||
Format multiple command matches to a useful error.
|
||||
|
||||
This is copied directly from the default method in
|
||||
src.commands.cmdhandler.
|
||||
evennia.commands.cmdhandler.
|
||||
|
||||
"""
|
||||
string = "There were multiple matches:"
|
||||
|
|
@ -124,7 +124,7 @@ class SystemMultimatch(MuxCommand):
|
|||
all the clashing matches.
|
||||
"""
|
||||
string = self.format_multimatches(self.caller, self.matches)
|
||||
self.caller.msg(string)
|
||||
self.msg(string)
|
||||
|
||||
|
||||
# Command called when the command given at the command line
|
||||
|
|
@ -170,4 +170,4 @@ class SystemSendToChannel(MuxCommand):
|
|||
return
|
||||
msg = "[%s] %s: %s" % (channel.key, caller.name, msg)
|
||||
msgobj = create.create_message(caller, msg, channels=[channel])
|
||||
channel.msg(msgobj)
|
||||
channel.msg(msgobj)
|
||||
|
|
@ -13,15 +13,14 @@ import twisted
|
|||
from time import time as timemeasure
|
||||
|
||||
from django.conf import settings
|
||||
#from src.server.caches import get_cache_sizes
|
||||
from src.server.sessionhandler import SESSIONS
|
||||
from src.scripts.models import ScriptDB
|
||||
from src.objects.models import ObjectDB
|
||||
from src.players.models import PlayerDB
|
||||
from src.utils import logger, utils, gametime, create, is_pypy, prettytable
|
||||
from src.utils.evtable import EvTable
|
||||
from src.utils.utils import crop
|
||||
from src.commands.default.muxcommand import MuxCommand
|
||||
from evennia.server.sessionhandler import SESSIONS
|
||||
from evennia.scripts.models import ScriptDB
|
||||
from evennia.objects.models import ObjectDB
|
||||
from evennia.players.models import PlayerDB
|
||||
from evennia.utils import logger, utils, gametime, create, is_pypy, prettytable
|
||||
from evennia.utils.evtable import EvTable
|
||||
from evennia.utils.utils import crop
|
||||
from evennia.commands.default.muxcommand import MuxCommand
|
||||
|
||||
# delayed imports
|
||||
_resource = None
|
||||
|
|
@ -68,7 +67,7 @@ class CmdReset(MuxCommand):
|
|||
@reset
|
||||
|
||||
A cold reboot. This works like a mixture of @reload and @shutdown,
|
||||
- all shutdown hooks will be called and non-persistent scrips will
|
||||
- all shutdown hooks will be called and non-persistent scripts will
|
||||
be purged. But the Portal will not be affected and the server will
|
||||
automatically restart again.
|
||||
"""
|
||||
|
|
@ -137,7 +136,7 @@ class CmdPy(MuxCommand):
|
|||
inherits_from(obj, parent) : check object inheritance
|
||||
|
||||
You can explore The evennia API from inside the game by calling
|
||||
ev.help(), ev.managers.help() etc.
|
||||
evennia.help(), evennia.managers.help() etc.
|
||||
|
||||
{rNote: In the wrong hands this command is a severe security risk.
|
||||
It should only be accessible by trusted server admins/superusers.{n
|
||||
|
|
@ -162,11 +161,12 @@ class CmdPy(MuxCommand):
|
|||
# check if caller is a player
|
||||
|
||||
# import useful variables
|
||||
import ev
|
||||
import evennia
|
||||
available_vars = {'self': caller,
|
||||
'me': caller,
|
||||
'here': hasattr(caller, "location") and caller.location or None,
|
||||
'ev': ev,
|
||||
'evennia': evennia,
|
||||
'ev': evennia,
|
||||
'inherits_from': utils.inherits_from}
|
||||
|
||||
try:
|
||||
|
|
@ -394,7 +394,7 @@ class CmdObjects(MuxCommand):
|
|||
latesttable.align = 'l'
|
||||
for obj in objs:
|
||||
latesttable.add_row(utils.datetime_format(obj.date_created),
|
||||
obj.dbref, obj.key, obj.typeclass.path)
|
||||
obj.dbref, obj.key, obj.path)
|
||||
|
||||
string = "\n{wObject subtype totals (out of %i Objects):{n\n%s" % (nobjs, totaltable)
|
||||
string += "\n{wObject typeclass distribution:{n\n%s" % typetable
|
||||
|
|
@ -416,6 +416,7 @@ class CmdPlayers(MuxCommand):
|
|||
key = "@players"
|
||||
aliases = ["@listplayers"]
|
||||
locks = "cmd:perm(listplayers) or perm(Wizards)"
|
||||
help_category = "System"
|
||||
|
||||
def func(self):
|
||||
"List the players"
|
||||
|
|
@ -437,7 +438,7 @@ class CmdPlayers(MuxCommand):
|
|||
plyrs = PlayerDB.objects.all().order_by("db_date_created")[max(0, nplayers - nlim):]
|
||||
latesttable = EvTable("{wcreated{n", "{wdbref{n", "{wname{n", "{wtypeclass{n", border="cells", align="l")
|
||||
for ply in plyrs:
|
||||
latesttable.add_row(utils.datetime_format(ply.date_created), ply.dbref, ply.key, ply.typeclass.path)
|
||||
latesttable.add_row(utils.datetime_format(ply.date_created), ply.dbref, ply.key, ply.path)
|
||||
|
||||
string = "\n{wPlayer typeclass distribution:{n\n%s" % typetable
|
||||
string += "\n{wLast %s Players created:{n\n%s" % (min(nplayers, nlim), latesttable)
|
||||
|
|
@ -665,7 +666,7 @@ class CmdServerLoad(MuxCommand):
|
|||
if not _resource:
|
||||
import resource as _resource
|
||||
if not _idmapper:
|
||||
from src.utils.idmapper import base as _idmapper
|
||||
from evennia.utils.idmapper import models as _idmapper
|
||||
|
||||
import resource
|
||||
loadavg = os.getloadavg()
|
||||
313
evennia/commands/default/tests.py
Normal file
313
evennia/commands/default/tests.py
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
# -*- 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.
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from mock import Mock
|
||||
|
||||
from evennia.commands.default.cmdset_character import CharacterCmdSet
|
||||
from evennia.utils.test_resources import EvenniaTest
|
||||
from evennia.commands.default import help, general, system, admin, player, building, batchprocess, comms
|
||||
from evennia.utils import ansi
|
||||
from evennia.server.sessionhandler import SESSIONS
|
||||
|
||||
|
||||
# set up signal here since we are not starting the server
|
||||
|
||||
_RE = re.compile(r"^\+|-+\+|\+-+|--*|\|", re.MULTILINE)
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Command testing
|
||||
# ------------------------------------------------------------
|
||||
|
||||
class CommandTest(EvenniaTest):
|
||||
"""
|
||||
Tests a command
|
||||
"""
|
||||
|
||||
def call(self, cmdobj, args, msg=None, cmdset=None, noansi=True, caller=None, receiver=None):
|
||||
"""
|
||||
Test a command by assigning all the needed
|
||||
properties to cmdobj and running
|
||||
cmdobj.at_pre_cmd()
|
||||
cmdobj.parse()
|
||||
cmdobj.func()
|
||||
cmdobj.at_post_cmd()
|
||||
The msgreturn value is compared to eventual
|
||||
output sent to caller.msg in the game
|
||||
"""
|
||||
caller = caller if caller else self.char1
|
||||
receiver = receiver if receiver else caller
|
||||
cmdobj.caller = caller
|
||||
cmdobj.cmdstring = cmdobj.key
|
||||
cmdobj.args = args
|
||||
cmdobj.cmdset = cmdset
|
||||
cmdobj.sessid = 1
|
||||
cmdobj.session = SESSIONS.session_from_sessid(1)
|
||||
cmdobj.player = self.player
|
||||
cmdobj.raw_string = cmdobj.key + " " + args
|
||||
cmdobj.obj = caller if caller else self.char1
|
||||
# test
|
||||
old_msg = receiver.msg
|
||||
try:
|
||||
receiver.msg = Mock()
|
||||
cmdobj.at_pre_cmd()
|
||||
cmdobj.parse()
|
||||
cmdobj.func()
|
||||
cmdobj.at_post_cmd()
|
||||
# clean out prettytable sugar
|
||||
stored_msg = [args[0] for name, args, kwargs in receiver.msg.mock_calls]
|
||||
returned_msg = "|".join(_RE.sub("", mess) for mess in stored_msg)
|
||||
returned_msg = ansi.parse_ansi(returned_msg, strip_ansi=noansi).strip()
|
||||
if msg is not None:
|
||||
if msg == "" and returned_msg or not returned_msg.startswith(msg.strip()):
|
||||
sep1 = "\n" + "="*30 + "Wanted message" + "="*34 + "\n"
|
||||
sep2 = "\n" + "="*30 + "Returned message" + "="*32 + "\n"
|
||||
sep3 = "\n" + "="*78
|
||||
retval = sep1 + msg.strip() + sep2 + returned_msg + sep3
|
||||
raise AssertionError(retval)
|
||||
finally:
|
||||
receiver.msg = old_msg
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Individual module Tests
|
||||
# ------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGeneral(CommandTest):
|
||||
def test_look(self):
|
||||
self.call(general.CmdLook(), "here", "Room\nroom_desc")
|
||||
|
||||
def test_home(self):
|
||||
self.call(general.CmdHome(), "", "You are already home")
|
||||
|
||||
def test_inventory(self):
|
||||
self.call(general.CmdInventory(), "", "You are not carrying anything.")
|
||||
|
||||
def test_pose(self):
|
||||
self.call(general.CmdPose(), "looks around", "Char looks around")
|
||||
|
||||
def test_nick(self):
|
||||
self.call(general.CmdNick(), "testalias = testaliasedstring1", "Nick set:")
|
||||
self.call(general.CmdNick(), "/player testalias = testaliasedstring2", "Nick set:")
|
||||
self.call(general.CmdNick(), "/object testalias = testaliasedstring3", "Nick set:")
|
||||
self.assertEqual(u"testaliasedstring1", self.char1.nicks.get("testalias"))
|
||||
self.assertEqual(u"testaliasedstring2", self.char1.nicks.get("testalias", category="player"))
|
||||
self.assertEqual(u"testaliasedstring3", self.char1.nicks.get("testalias", category="object"))
|
||||
|
||||
def test_get_and_drop(self):
|
||||
self.call(general.CmdGet(), "Obj", "You pick up Obj.")
|
||||
self.call(general.CmdDrop(), "Obj", "You drop Obj.")
|
||||
|
||||
def test_say(self):
|
||||
self.call(general.CmdSay(), "Testing", "You say, \"Testing\"")
|
||||
|
||||
def test_access(self):
|
||||
self.call(general.CmdAccess(), "", "Permission Hierarchy (climbing):")
|
||||
|
||||
|
||||
class TestHelp(CommandTest):
|
||||
def test_help(self):
|
||||
self.call(help.CmdHelp(), "", "Command help entries", cmdset=CharacterCmdSet())
|
||||
|
||||
def test_set_help(self):
|
||||
self.call(help.CmdSetHelp(), "testhelp, General = This is a test", "Topic 'testhelp' was successfully created.")
|
||||
self.call(help.CmdHelp(), "testhelp", "Help topic for testhelp", cmdset=CharacterCmdSet())
|
||||
|
||||
|
||||
class TestSystem(CommandTest):
|
||||
def test_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")
|
||||
|
||||
def test_scripts(self):
|
||||
self.call(system.CmdScripts(), "", "dbref ")
|
||||
|
||||
def test_objects(self):
|
||||
self.call(system.CmdObjects(), "", "Object subtype totals")
|
||||
|
||||
def test_about(self):
|
||||
self.call(system.CmdAbout(), "", None)
|
||||
|
||||
def test_server_load(self):
|
||||
self.call(system.CmdServerLoad(), "", "Server CPU and Memory load:")
|
||||
|
||||
|
||||
class TestAdmin(CommandTest):
|
||||
def test_emit(self):
|
||||
self.call(admin.CmdEmit(), "Char2 = Test", "Emitted to Char2:\nTest")
|
||||
|
||||
def test_perm(self):
|
||||
self.call(admin.CmdPerm(), "Obj = Builders", "Permission 'Builders' given to Obj (the Object/Character).")
|
||||
self.call(admin.CmdPerm(), "Char2 = Builders", "Permission 'Builders' given to Char2 (the Object/Character).")
|
||||
|
||||
def test_wall(self):
|
||||
self.call(admin.CmdWall(), "Test", "Announcing to all connected players ...")
|
||||
|
||||
def test_ban(self):
|
||||
self.call(admin.CmdBan(), "Char", "NameBan char was added.")
|
||||
|
||||
|
||||
class TestPlayer(CommandTest):
|
||||
|
||||
def test_ooc_look(self):
|
||||
if settings.MULTISESSION_MODE < 2:
|
||||
self.call(player.CmdOOCLook(), "", "You are outofcharacter (OOC).", caller=self.player)
|
||||
if settings.MULTISESSION_MODE == 2:
|
||||
self.call(player.CmdOOCLook(), "", "Account TestPlayer (you are OutofCharacter)", caller=self.player)
|
||||
|
||||
def test_ooc(self):
|
||||
self.call(player.CmdOOC(), "", "You go OOC.", caller=self.player)
|
||||
|
||||
def test_ic(self):
|
||||
self.player.unpuppet_object(self.session.sessid)
|
||||
self.call(player.CmdIC(), "Char", "You become Char.", caller=self.player, receiver=self.char1)
|
||||
|
||||
def test_password(self):
|
||||
self.call(player.CmdPassword(), "testpassword = testpassword", "Password changed.", caller=self.player)
|
||||
|
||||
def test_encoding(self):
|
||||
self.call(player.CmdEncoding(), "", "Default encoding:", caller=self.player)
|
||||
|
||||
def test_who(self):
|
||||
self.call(player.CmdWho(), "", "Players:", caller=self.player)
|
||||
|
||||
def test_quit(self):
|
||||
self.call(player.CmdQuit(), "", "Quitting. Hope to see you again, soon.", caller=self.player)
|
||||
|
||||
def test_sessions(self):
|
||||
self.call(player.CmdSessions(), "", "Your current session(s):", caller=self.player)
|
||||
|
||||
def test_color_test(self):
|
||||
self.call(player.CmdColorTest(), "ansi", "ANSI colors:", caller=self.player)
|
||||
|
||||
def test_char_create(self):
|
||||
self.call(player.CmdCharCreate(), "Test1=Test char", "Created new character Test1. Use @ic Test1 to enter the game", caller=self.player)
|
||||
|
||||
def test_quell(self):
|
||||
self.call(player.CmdQuell(), "", "Quelling to current puppet's permissions (immortals).", caller=self.player)
|
||||
|
||||
|
||||
class TestBuilding(CommandTest):
|
||||
def test_create(self):
|
||||
name = settings.BASE_OBJECT_TYPECLASS.rsplit('.', 1)[1]
|
||||
self.call(building.CmdCreate(), "/drop TestObj1", "You create a new %s: TestObj1." % name)
|
||||
|
||||
def test_examine(self):
|
||||
self.call(building.CmdExamine(), "Obj", "Name/key: Obj")
|
||||
|
||||
def test_set_obj_alias(self):
|
||||
self.call(building.CmdSetObjAlias(), "Obj = TestObj1b", "Alias(es) for 'Obj' set to testobj1b.")
|
||||
|
||||
def test_copy(self):
|
||||
self.call(building.CmdCopy(), "Obj = TestObj2;TestObj2b, TestObj3;TestObj3b", "Copied Obj to 'TestObj3' (aliases: ['TestObj3b']")
|
||||
|
||||
def test_attribute_commands(self):
|
||||
self.call(building.CmdSetAttribute(), "Obj/test1=\"value1\"", "Created attribute Obj/test1 = 'value1'")
|
||||
self.call(building.CmdSetAttribute(), "Obj2/test2=\"value2\"", "Created attribute Obj2/test2 = 'value2'")
|
||||
self.call(building.CmdMvAttr(), "Obj2/test2 = Obj/test3", "Moved Obj2.test2 > Obj.test3")
|
||||
self.call(building.CmdCpAttr(), "Obj/test1 = Obj2/test3", "Copied Obj.test1 > Obj2.test3")
|
||||
self.call(building.CmdWipe(), "Obj2/test2/test3", "Wiped attributes test2,test3 on Obj2.")
|
||||
|
||||
def test_name(self):
|
||||
self.call(building.CmdName(), "Obj2=Obj3", "Object's name changed to 'Obj3'.")
|
||||
|
||||
def test_desc(self):
|
||||
self.call(building.CmdDesc(), "Obj2=TestDesc", "The description was set on Obj2.")
|
||||
|
||||
def test_wipe(self):
|
||||
self.call(building.CmdDestroy(), "Obj", "Obj was destroyed.")
|
||||
|
||||
def test_dig(self):
|
||||
self.call(building.CmdDig(), "TestRoom1=testroom;tr,back;b", "Created room TestRoom1")
|
||||
|
||||
def test_tunnel(self):
|
||||
self.call(building.CmdTunnel(), "n = TestRoom2;test2", "Created room TestRoom2")
|
||||
|
||||
def test_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(), "TestExit1", "Former exit TestExit1 no longer links anywhere.")
|
||||
|
||||
def test_set_home(self):
|
||||
self.call(building.CmdSetHome(), "Obj = Room2", "Obj's home location was changed from Room")
|
||||
|
||||
def test_list_cmdsets(self):
|
||||
self.call(building.CmdListCmdSets(), "", "<DefaultCharacter (Union, prio 0, perm)>:")
|
||||
|
||||
def test_typeclass(self):
|
||||
self.call(building.CmdTypeclass(), "Obj = evennia.objects.objects.DefaultExit",
|
||||
"Obj changed typeclass from evennia.objects.objects.DefaultObject to evennia.objects.objects.DefaultExit.")
|
||||
|
||||
def test_lock(self):
|
||||
self.call(building.CmdLock(), "Obj = test:perm(Immortals)", "Added lock 'test:perm(Immortals)' to Obj.")
|
||||
|
||||
def test_find(self):
|
||||
self.call(building.CmdFind(), "Room2", "One Match")
|
||||
|
||||
def test_script(self):
|
||||
self.call(building.CmdScript(), "Obj = scripts.Script", "Script scripts.Script successfully added")
|
||||
|
||||
def test_teleport(self):
|
||||
self.call(building.CmdTeleport(), "Room2", "Room2\n|Teleported to Room2.")
|
||||
|
||||
|
||||
class TestComms(CommandTest):
|
||||
|
||||
def setUp(self):
|
||||
super(CommandTest, self).setUp()
|
||||
self.call(comms.CmdChannelCreate(), "testchan;test=Test Channel", "Created channel testchan and connected to it.", receiver=self.player)
|
||||
|
||||
def test_toggle_com(self):
|
||||
self.call(comms.CmdAddCom(), "tc = testchan", "You are already connected to channel testchan. You can now", receiver=self.player)
|
||||
self.call(comms.CmdDelCom(), "tc", "Your alias 'tc' for channel testchan was cleared.", receiver=self.player)
|
||||
|
||||
def test_channels(self):
|
||||
self.call(comms.CmdChannels(), "" ,"Available channels (use comlist,addcom and delcom to manage", receiver=self.player)
|
||||
|
||||
def test_all_com(self):
|
||||
self.call(comms.CmdAllCom(), "", "Available channels (use comlist,addcom and delcom to manage", receiver=self.player)
|
||||
|
||||
def test_clock(self):
|
||||
self.call(comms.CmdClock(), "testchan=send:all()", "Lock(s) applied. Current locks on testchan:", receiver=self.player)
|
||||
|
||||
def test_cdesc(self):
|
||||
self.call(comms.CmdCdesc(), "testchan = Test Channel", "Description of channel 'testchan' set to 'Test Channel'.", receiver=self.player)
|
||||
|
||||
def test_cemit(self):
|
||||
self.call(comms.CmdCemit(), "testchan = Test Message", "[testchan] Test Message|Sent to channel testchan: Test Message", receiver=self.player)
|
||||
|
||||
def test_cwho(self):
|
||||
self.call(comms.CmdCWho(), "testchan", "Channel subscriptions\ntestchan:\n TestPlayer", receiver=self.player)
|
||||
|
||||
def test_page(self):
|
||||
self.call(comms.CmdPage(), "TestPlayer2 = Test", "TestPlayer2 is offline. They will see your message if they list their pages later.|You paged TestPlayer2 with: 'Test'.", receiver=self.player)
|
||||
|
||||
def test_cboot(self):
|
||||
# No one else connected to boot
|
||||
self.call(comms.CmdCBoot(), "", "Usage: @cboot[/quiet] <channel> = <player> [:reason]", receiver=self.player)
|
||||
|
||||
def test_cdestroy(self):
|
||||
self.call(comms.CmdCdestroy(), "testchan" ,"[testchan] TestPlayer: testchan is being destroyed. Make sure to change your aliases.|Channel 'testchan' was destroyed.", receiver=self.player)
|
||||
|
||||
|
||||
class TestBatchProcess(CommandTest):
|
||||
def test_batch_commands(self):
|
||||
# cannot test batchcode here, it must run inside the server process
|
||||
self.call(batchprocess.CmdBatchCommands(), "example_batch_cmds", "Running Batchcommand processor Automatic mode for example_batch_cmds")
|
||||
#self.call(batchprocess.CmdBatchCode(), "examples.batch_code", "")
|
||||
|
||||
|
|
@ -5,14 +5,14 @@ import re
|
|||
from random import getrandbits
|
||||
import traceback
|
||||
from django.conf import settings
|
||||
from src.players.models import PlayerDB
|
||||
from src.objects.models import ObjectDB
|
||||
from src.server.models import ServerConfig
|
||||
from src.comms.models import ChannelDB
|
||||
from evennia.players.models import PlayerDB
|
||||
from evennia.objects.models import ObjectDB
|
||||
from evennia.server.models import ServerConfig
|
||||
from evennia.comms.models import ChannelDB
|
||||
|
||||
from src.utils import create, logger, utils, ansi
|
||||
from src.commands.default.muxcommand import MuxCommand
|
||||
from src.commands.cmdhandler import CMD_LOGINSTART
|
||||
from evennia.utils import create, logger, utils, ansi
|
||||
from evennia.commands.default.muxcommand import MuxCommand
|
||||
from evennia.commands.cmdhandler import CMD_LOGINSTART
|
||||
|
||||
# limit symbol import for API
|
||||
__all__ = ("CmdUnconnectedConnect", "CmdUnconnectedCreate",
|
||||
|
|
@ -20,14 +20,6 @@ __all__ = ("CmdUnconnectedConnect", "CmdUnconnectedCreate",
|
|||
|
||||
MULTISESSION_MODE = settings.MULTISESSION_MODE
|
||||
CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE
|
||||
CONNECTION_SCREEN = ""
|
||||
try:
|
||||
CONNECTION_SCREEN = ansi.parse_ansi(utils.random_string_from_module(CONNECTION_SCREEN_MODULE))
|
||||
except Exception:
|
||||
pass
|
||||
if not CONNECTION_SCREEN:
|
||||
CONNECTION_SCREEN = "\nEvennia: Error in CONNECTION_SCREEN MODULE (randomly picked connection screen variable is not a string). \nEnter 'help' for aid."
|
||||
|
||||
|
||||
class CmdUnconnectedConnect(MuxCommand):
|
||||
"""
|
||||
|
|
@ -280,7 +272,10 @@ class CmdUnconnectedLook(MuxCommand):
|
|||
|
||||
def func(self):
|
||||
"Show the connect screen."
|
||||
self.caller.msg(CONNECTION_SCREEN)
|
||||
connection_screen = ansi.parse_ansi(utils.random_string_from_module(CONNECTION_SCREEN_MODULE))
|
||||
if not connection_screen:
|
||||
connection_screen = "No connection screen found. Please contact an admin."
|
||||
self.caller.msg(connection_screen)
|
||||
|
||||
|
||||
class CmdUnconnectedHelp(MuxCommand):
|
||||
|
|
@ -420,12 +415,10 @@ def _create_player(session, playername, password,
|
|||
utils.init_new_player(new_player)
|
||||
|
||||
# join the new player to the public channel
|
||||
pchanneldef = settings.CHANNEL_PUBLIC
|
||||
if pchanneldef:
|
||||
pchannel = ChannelDB.objects.get_channel(pchanneldef[0])
|
||||
if not pchannel.connect(new_player):
|
||||
string = "New player '%s' could not connect to public channel!" % new_player.key
|
||||
logger.log_errmsg(string)
|
||||
pchannel = ChannelDB.objects.get_channel(settings.DEFAULT_CHANNELS[0]["key"])
|
||||
if not pchannel.connect(new_player):
|
||||
string = "New player '%s' could not connect to public channel!" % new_player.key
|
||||
logger.log_errmsg(string)
|
||||
return new_player
|
||||
|
||||
|
||||
6
evennia/comms/__init__.py
Normal file
6
evennia/comms/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
"""
|
||||
This sub-package contains Evennia's comms-system, a set of models and
|
||||
handlers for in-game communication via channels and messages as well
|
||||
as code related to external communication like IRC or RSS.
|
||||
|
||||
"""
|
||||
|
|
@ -4,8 +4,8 @@
|
|||
#
|
||||
|
||||
from django.contrib import admin
|
||||
from src.comms.models import ChannelDB
|
||||
from src.typeclasses.admin import AttributeInline, TagInline
|
||||
from evennia.comms.models import ChannelDB
|
||||
from evennia.typeclasses.admin import AttributeInline, TagInline
|
||||
|
||||
|
||||
class ChannelAttributeInline(AttributeInline):
|
||||
|
|
@ -23,8 +23,8 @@ update() on the channelhandler. Or use Channel.objects.delete() which
|
|||
does this for you.
|
||||
|
||||
"""
|
||||
from src.comms.models import ChannelDB
|
||||
from src.commands import cmdset, command
|
||||
from evennia.comms.models import ChannelDB
|
||||
from evennia.commands import cmdset, command
|
||||
|
||||
|
||||
class ChannelCommand(command.Command):
|
||||
|
|
@ -82,13 +82,25 @@ class ChannelCommand(command.Command):
|
|||
|
||||
class ChannelHandler(object):
|
||||
"""
|
||||
Handles the set of commands related to channels.
|
||||
The ChannelHandler manages all active in-game channels and
|
||||
dynamically creates channel commands for users so that they can
|
||||
just give the channek's key or alias to write to it. Whenever a
|
||||
new channel is created in the database, the update() method on
|
||||
this handler must be called to sync it with the database (this is
|
||||
done automatically if creating the channel with
|
||||
evennia.create_channel())
|
||||
|
||||
"""
|
||||
def __init__(self):
|
||||
"""
|
||||
Initializes the channel handler's internal state.
|
||||
|
||||
"""
|
||||
self.cached_channel_cmds = []
|
||||
self.cached_cmdsets = {}
|
||||
|
||||
def __str__(self):
|
||||
"Returns the string representation of the handler"
|
||||
return ", ".join(str(cmd) for cmd in self.cached_channel_cmds)
|
||||
|
||||
def clear(self):
|
||||
|
|
@ -133,7 +145,9 @@ class ChannelHandler(object):
|
|||
self.cached_cmdsets = {}
|
||||
|
||||
def update(self):
|
||||
"Updates the handler completely."
|
||||
"""
|
||||
Updates the handler completely.
|
||||
"""
|
||||
self.cached_channel_cmds = []
|
||||
self.cached_cmdsets = {}
|
||||
for channel in ChannelDB.objects.get_all_channels():
|
||||
|
|
@ -158,4 +172,5 @@ class ChannelHandler(object):
|
|||
self.cached_cmdsets[source_object] = chan_cmdset
|
||||
return chan_cmdset
|
||||
|
||||
CHANNELHANDLER = ChannelHandler()
|
||||
CHANNEL_HANDLER = ChannelHandler()
|
||||
CHANNELHANDLER = CHANNEL_HANDLER # legacy
|
||||
|
|
@ -3,21 +3,213 @@ Default Typeclass for Comms.
|
|||
|
||||
See objects.objects for more information on Typeclassing.
|
||||
"""
|
||||
from src.comms import Msg, TempMsg
|
||||
from src.typeclasses.typeclass import TypeClass
|
||||
from src.utils import logger
|
||||
from src.utils.utils import make_iter
|
||||
from evennia.typeclasses.models import TypeclassBase
|
||||
from evennia.comms.models import Msg, TempMsg, ChannelDB
|
||||
from evennia.comms.managers import ChannelManager
|
||||
from evennia.utils import logger
|
||||
from evennia.utils.utils import make_iter
|
||||
|
||||
|
||||
class Channel(TypeClass):
|
||||
class DefaultChannel(ChannelDB):
|
||||
"""
|
||||
This is the base class for all Comms. Inherit from this to create different
|
||||
types of communication channels.
|
||||
"""
|
||||
__metaclass__ = TypeclassBase
|
||||
objects = ChannelManager()
|
||||
|
||||
def at_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.at_channel_creation()
|
||||
|
||||
if hasattr(self, "_createdict"):
|
||||
# this is only set if the channel was created
|
||||
# with the utils.create.create_channel function.
|
||||
cdict = self._createdict
|
||||
if not cdict.get("key"):
|
||||
if not self.db_key:
|
||||
self.db_key = "#i" % self.dbid
|
||||
elif cdict["key"] and self.key != cdict["key"]:
|
||||
self.key = cdict["key"]
|
||||
if cdict.get("keep_log"):
|
||||
self.db_keep_log = cdict["keep_log"]
|
||||
if cdict.get("aliases"):
|
||||
self.aliases.add(cdict["aliases"])
|
||||
if cdict.get("locks"):
|
||||
self.locks.add(cdict["locks"])
|
||||
if cdict.get("keep_log"):
|
||||
self.attributes.add("keep_log", cdict["keep_log"])
|
||||
if cdict.get("desc"):
|
||||
self.attributes.add("desc", cdict["desc"])
|
||||
|
||||
def at_channel_creation(self):
|
||||
"""
|
||||
Called once, when the channel is first created.
|
||||
"""
|
||||
pass
|
||||
|
||||
# helper methods, for easy overloading
|
||||
|
||||
def has_connection(self, player):
|
||||
"""
|
||||
Checks so this player is actually listening
|
||||
to this channel.
|
||||
"""
|
||||
if hasattr(player, "player"):
|
||||
player = player.player
|
||||
return player in self.db_subscriptions.all()
|
||||
|
||||
def connect(self, player):
|
||||
"Connect the user to this channel. This checks access."
|
||||
if hasattr(player, "player"):
|
||||
player = player.player
|
||||
# check access
|
||||
if not self.access(player, 'listen'):
|
||||
return False
|
||||
# pre-join hook
|
||||
connect = self.pre_join_channel(player)
|
||||
if not connect:
|
||||
return False
|
||||
# subscribe
|
||||
self.db_subscriptions.add(player)
|
||||
# post-join hook
|
||||
self.post_join_channel(player)
|
||||
return True
|
||||
|
||||
def disconnect(self, player):
|
||||
"Disconnect user from this channel."
|
||||
if hasattr(player, "player"):
|
||||
player = player.player
|
||||
# pre-disconnect hook
|
||||
disconnect = self.pre_leave_channel(player)
|
||||
if not disconnect:
|
||||
return False
|
||||
# disconnect
|
||||
self.db_subscriptions.remove(player)
|
||||
# post-disconnect hook
|
||||
self.post_leave_channel(player)
|
||||
return True
|
||||
|
||||
def access(self, accessing_obj, access_type='listen', default=False):
|
||||
"""
|
||||
Determines if another object has permission to access.
|
||||
accessing_obj - object trying to access this one
|
||||
access_type - type of access sought
|
||||
default - what to return if no lock of access_type was found
|
||||
"""
|
||||
return self.locks.check(accessing_obj, access_type=access_type, default=default)
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Deletes channel while also cleaning up channelhandler
|
||||
"""
|
||||
self.attributes.clear()
|
||||
self.aliases.clear()
|
||||
super(DefaultChannel, self).delete()
|
||||
from evennia.comms.channelhandler import CHANNELHANDLER
|
||||
CHANNELHANDLER.update()
|
||||
|
||||
def message_transform(self, msg, emit=False, prefix=True,
|
||||
sender_strings=None, external=False):
|
||||
"""
|
||||
Generates the formatted string sent to listeners on a channel.
|
||||
"""
|
||||
if sender_strings or external:
|
||||
body = self.format_external(msg, sender_strings, emit=emit)
|
||||
else:
|
||||
body = self.format_message(msg, emit=emit)
|
||||
if prefix:
|
||||
body = "%s%s" % (self.channel_prefix(msg, emit=emit), body)
|
||||
msg.message = body
|
||||
return msg
|
||||
|
||||
def distribute_message(self, msg, online=False):
|
||||
"""
|
||||
Method for grabbing all listeners that a message should be sent to on
|
||||
this channel, and sending them a message.
|
||||
"""
|
||||
# get all players connected to this channel and send to them
|
||||
for player in self.db_subscriptions.all():
|
||||
try:
|
||||
# note our addition of the from_channel keyword here. This could be checked
|
||||
# by a custom player.msg() to treat channel-receives differently.
|
||||
player.msg(msg.message, from_obj=msg.senders, from_channel=self.id)
|
||||
except AttributeError, e:
|
||||
logger.log_trace("%s\nCannot send msg to player '%s'." % (e, player))
|
||||
|
||||
def msg(self, msgobj, header=None, senders=None, sender_strings=None,
|
||||
persistent=False, online=False, emit=False, external=False):
|
||||
"""
|
||||
Send the given message to all players connected to channel. Note that
|
||||
no permission-checking is done here; it is assumed to have been
|
||||
done before calling this method. The optional keywords are not used if
|
||||
persistent is False.
|
||||
|
||||
msgobj - a Msg/TempMsg instance or a message string. If one of the
|
||||
former, the remaining keywords will be ignored. If a string,
|
||||
this will either be sent as-is (if persistent=False) or it
|
||||
will be used together with header and senders keywords to
|
||||
create a Msg instance on the fly.
|
||||
senders - an object, player or a list of objects or players.
|
||||
Optional if persistent=False.
|
||||
sender_strings - Name strings of senders. Used for external
|
||||
connections where the sender is not a player or object. When
|
||||
this is defined, external will be assumed.
|
||||
external - Treat this message agnostic of its sender.
|
||||
persistent (default False) - ignored if msgobj is a Msg or TempMsg.
|
||||
If True, a Msg will be created, using header and senders
|
||||
keywords. If False, other keywords will be ignored.
|
||||
online (bool) - If this is set true, only messages people who are
|
||||
online. Otherwise, messages all players connected. This can
|
||||
make things faster, but may not trigger listeners on players
|
||||
that are offline.
|
||||
emit (bool) - Signals to the message formatter that this message is
|
||||
not to be directly associated with a name.
|
||||
"""
|
||||
if senders:
|
||||
senders = make_iter(senders)
|
||||
else:
|
||||
senders = []
|
||||
if isinstance(msgobj, basestring):
|
||||
# given msgobj is a string
|
||||
msg = msgobj
|
||||
if persistent and self.db.keep_log:
|
||||
msgobj = Msg()
|
||||
msgobj.save()
|
||||
else:
|
||||
# Use TempMsg, so this message is not stored.
|
||||
msgobj = TempMsg()
|
||||
msgobj.header = header
|
||||
msgobj.message = msg
|
||||
msgobj.channels = [self] # add this channel
|
||||
|
||||
if not msgobj.senders:
|
||||
msgobj.senders = senders
|
||||
msgobj = self.pre_send_message(msgobj)
|
||||
if not msgobj:
|
||||
return False
|
||||
msgobj = self.message_transform(msgobj, emit=emit,
|
||||
sender_strings=sender_strings,
|
||||
external=external)
|
||||
self.distribute_message(msgobj, online=online)
|
||||
self.post_send_message(msgobj)
|
||||
return True
|
||||
|
||||
def tempmsg(self, message, header=None, senders=None):
|
||||
"""
|
||||
A wrapper for sending non-persistent messages.
|
||||
"""
|
||||
self.msg(message, senders=senders, header=header, persistent=False)
|
||||
|
||||
|
||||
# hooks
|
||||
|
||||
def channel_prefix(self, msg=None, emit=False):
|
||||
|
||||
"""
|
||||
How the channel should prefix itself for users. Return a string.
|
||||
"""
|
||||
|
|
@ -89,26 +281,6 @@ class Channel(TypeClass):
|
|||
senders = ', '.join(senders)
|
||||
return self.pose_transform(msg, senders)
|
||||
|
||||
def message_transform(self, msg, emit=False, prefix=True,
|
||||
sender_strings=None, external=False):
|
||||
"""
|
||||
Generates the formatted string sent to listeners on a channel.
|
||||
"""
|
||||
if sender_strings or external:
|
||||
body = self.format_external(msg, sender_strings, emit=emit)
|
||||
else:
|
||||
body = self.format_message(msg, emit=emit)
|
||||
if prefix:
|
||||
body = "%s%s" % (self.channel_prefix(msg, emit=emit), body)
|
||||
msg.message = body
|
||||
return msg
|
||||
|
||||
def at_channel_create(self):
|
||||
"""
|
||||
Run at channel creation.
|
||||
"""
|
||||
pass
|
||||
|
||||
def pre_join_channel(self, joiner):
|
||||
"""
|
||||
Run right before a channel is joined. If this returns a false value,
|
||||
|
|
@ -160,82 +332,4 @@ class Channel(TypeClass):
|
|||
"""
|
||||
pass
|
||||
|
||||
def distribute_message(self, msg, online=False):
|
||||
"""
|
||||
Method for grabbing all listeners that a message should be sent to on
|
||||
this channel, and sending them a message.
|
||||
"""
|
||||
# get all players connected to this channel and send to them
|
||||
for player in self.dbobj.db_subscriptions.all():
|
||||
player = player.typeclass
|
||||
try:
|
||||
# note our addition of the from_channel keyword here. This could be checked
|
||||
# by a custom player.msg() to treat channel-receives differently.
|
||||
player.msg(msg.message, from_obj=msg.senders, from_channel=self.id)
|
||||
except AttributeError, e:
|
||||
logger.log_trace("%s\nCannot send msg to player '%s'." % (e, player))
|
||||
|
||||
def msg(self, msgobj, header=None, senders=None, sender_strings=None,
|
||||
persistent=False, online=False, emit=False, external=False):
|
||||
"""
|
||||
Send the given message to all players connected to channel. Note that
|
||||
no permission-checking is done here; it is assumed to have been
|
||||
done before calling this method. The optional keywords are not used if
|
||||
persistent is False.
|
||||
|
||||
msgobj - a Msg/TempMsg instance or a message string. If one of the
|
||||
former, the remaining keywords will be ignored. If a string,
|
||||
this will either be sent as-is (if persistent=False) or it
|
||||
will be used together with header and senders keywords to
|
||||
create a Msg instance on the fly.
|
||||
senders - an object, player or a list of objects or players.
|
||||
Optional if persistent=False.
|
||||
sender_strings - Name strings of senders. Used for external
|
||||
connections where the sender is not a player or object. When
|
||||
this is defined, external will be assumed.
|
||||
external - Treat this message agnostic of its sender.
|
||||
persistent (default False) - ignored if msgobj is a Msg or TempMsg.
|
||||
If True, a Msg will be created, using header and senders
|
||||
keywords. If False, other keywords will be ignored.
|
||||
online (bool) - If this is set true, only messages people who are
|
||||
online. Otherwise, messages all players connected. This can
|
||||
make things faster, but may not trigger listeners on players
|
||||
that are offline.
|
||||
emit (bool) - Signals to the message formatter that this message is
|
||||
not to be directly associated with a name.
|
||||
"""
|
||||
if senders:
|
||||
senders = make_iter(senders)
|
||||
else:
|
||||
senders = []
|
||||
if isinstance(msgobj, basestring):
|
||||
# given msgobj is a string
|
||||
msg = msgobj
|
||||
if persistent and self.db.keep_log:
|
||||
msgobj = Msg()
|
||||
msgobj.save()
|
||||
else:
|
||||
# Use TempMsg, so this message is not stored.
|
||||
msgobj = TempMsg()
|
||||
msgobj.header = header
|
||||
msgobj.message = msg
|
||||
msgobj.channels = [self.dbobj] # add this channel
|
||||
|
||||
if not msgobj.senders:
|
||||
msgobj.senders = senders
|
||||
msgobj = self.pre_send_message(msgobj)
|
||||
if not msgobj:
|
||||
return False
|
||||
msgobj = self.message_transform(msgobj, emit=emit,
|
||||
sender_strings=sender_strings,
|
||||
external=external)
|
||||
self.distribute_message(msgobj, online=online)
|
||||
self.post_send_message(msgobj)
|
||||
return True
|
||||
|
||||
def tempmsg(self, message, header=None, senders=None):
|
||||
"""
|
||||
A wrapper for sending non-persistent messages.
|
||||
"""
|
||||
self.msg(message, senders=senders, header=header, persistent=False)
|
||||
|
||||
|
|
@ -4,7 +4,8 @@ These managers handles the
|
|||
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from src.typeclasses.managers import TypedObjectManager, returns_typeclass_list, returns_typeclass
|
||||
from evennia.typeclasses.managers import (TypedObjectManager, TypeclassManager,
|
||||
returns_typeclass_list, returns_typeclass)
|
||||
|
||||
_GA = object.__getattribute__
|
||||
_PlayerDB = None
|
||||
|
|
@ -44,33 +45,20 @@ def dbref(dbref, reqhash=True):
|
|||
|
||||
def identify_object(inp):
|
||||
"identify if an object is a player or an object; return its database model"
|
||||
# load global stores
|
||||
global _PlayerDB, _ObjectDB, _ChannelDB
|
||||
if not _PlayerDB:
|
||||
from src.players.models import PlayerDB as _PlayerDB
|
||||
if not _ObjectDB:
|
||||
from src.objects.models import ObjectDB as _ObjectDB
|
||||
if not _ChannelDB:
|
||||
from src.comms.models import ChannelDB as _ChannelDB
|
||||
if not inp:
|
||||
if hasattr(inp, "__dbclass__"):
|
||||
clsname = inp.__dbclass__.__name__
|
||||
if clsname == "PlayerDB":
|
||||
return inp, "player"
|
||||
elif clsname == "ObjectDB":
|
||||
return inp ,"object"
|
||||
elif clsname == "ChannelDB":
|
||||
return inp, "channel"
|
||||
if isinstance(inp, basestring):
|
||||
return inp, "string"
|
||||
elif dbref(inp):
|
||||
return dbref(inp), "dbref"
|
||||
else:
|
||||
return inp, None
|
||||
# try to identify the type
|
||||
try:
|
||||
obj = _GA(inp, "dbobj") # this works for all typeclassed entities
|
||||
except AttributeError:
|
||||
obj = inp
|
||||
typ = type(obj)
|
||||
if typ == _PlayerDB:
|
||||
return obj, "player"
|
||||
elif typ == _ObjectDB:
|
||||
return obj, "object"
|
||||
elif typ == _ChannelDB:
|
||||
return obj, "channel"
|
||||
elif dbref(obj):
|
||||
return dbref(obj), "dbref"
|
||||
elif typ == basestring:
|
||||
return obj, "string"
|
||||
return obj, None # Something else
|
||||
|
||||
|
||||
def to_object(inp, objtype='player'):
|
||||
|
|
@ -133,7 +121,7 @@ class MsgManager(models.Manager):
|
|||
get_messages_by_receiver
|
||||
get_messages_by_channel
|
||||
text_search
|
||||
message_search (equivalent to ev.search_messages)
|
||||
message_search (equivalent to evennia.search_messages)
|
||||
"""
|
||||
|
||||
def identify_object(self, obj):
|
||||
|
|
@ -251,7 +239,7 @@ class MsgManager(models.Manager):
|
|||
# Channel manager
|
||||
#
|
||||
|
||||
class ChannelManager(TypedObjectManager):
|
||||
class ChannelDBManager(TypedObjectManager):
|
||||
"""
|
||||
This ChannelManager implements methods for searching
|
||||
and manipulating Channels directly from the database.
|
||||
|
|
@ -268,7 +256,7 @@ class ChannelManager(TypedObjectManager):
|
|||
get_all_channels
|
||||
get_channel(channel)
|
||||
get_subscriptions(player)
|
||||
channel_search (equivalent to ev.search_channel)
|
||||
channel_search (equivalent to evennia.search_channel)
|
||||
|
||||
"""
|
||||
@returns_typeclass_list
|
||||
|
|
@ -299,50 +287,7 @@ class ChannelManager(TypedObjectManager):
|
|||
"""
|
||||
Return all channels a given player is subscribed to
|
||||
"""
|
||||
return player.dbobj.subscription_set.all()
|
||||
|
||||
|
||||
# def del_channel(self, channelkey):
|
||||
# """
|
||||
# Delete channel matching channelkey.
|
||||
# Also cleans up channelhandler.
|
||||
# """
|
||||
# channels = self.filter(db_key__iexact=channelkey)
|
||||
# if not channels:
|
||||
# # no aliases allowed for deletion.
|
||||
# return False
|
||||
# for channel in channels:
|
||||
# channel.delete()
|
||||
# from src.comms.channelhandler import CHANNELHANDLER
|
||||
# CHANNELHANDLER.update()
|
||||
# return None
|
||||
|
||||
# def get_all_connections(self, channel, online=False):
|
||||
# """
|
||||
# Return the connections of all players listening
|
||||
# to this channel. If Online is true, it only returns
|
||||
# connected players.
|
||||
# """
|
||||
# global _SESSIONS
|
||||
# if not _SESSIONS:
|
||||
# from src.server.sessionhandler import SESSIONS as _SESSIONS
|
||||
#
|
||||
# PlayerChannelConnection = ContentType.objects.get(app_label="comms",
|
||||
# model="playerchannelconnection").model_class()
|
||||
# players = []
|
||||
# if online:
|
||||
# session_list = _SESSIONS.get_sessions()
|
||||
# unique_online_users = set(sess.uid for sess in session_list if sess.logged_in)
|
||||
# online_players = (sess.get_player() for sess in session_list if sess.uid in unique_online_users)
|
||||
# for player in online_players:
|
||||
# players.extend(PlayerChannelConnection.objects.filter(
|
||||
# db_player=player.dbobj, db_channel=channel.dbobj))
|
||||
# else:
|
||||
# players.extend(PlayerChannelConnection.objects.get_all_connections(channel))
|
||||
#
|
||||
# external_connections = ExternalChannelConnection.objects.get_all_connections(channel)
|
||||
#
|
||||
# return itertools.chain(players, external_connections)
|
||||
return player.subscription_set.all()
|
||||
|
||||
@returns_typeclass_list
|
||||
def channel_search(self, ostring, exact=True):
|
||||
|
|
@ -373,71 +318,7 @@ class ChannelManager(TypedObjectManager):
|
|||
for a in channel.aliases.all()]]
|
||||
return channels
|
||||
|
||||
|
||||
#
|
||||
# PlayerChannelConnection manager
|
||||
#
|
||||
class PlayerChannelConnectionManager(models.Manager):
|
||||
"""
|
||||
This PlayerChannelConnectionManager implements methods for searching
|
||||
and manipulating PlayerChannelConnections directly from the database.
|
||||
|
||||
These methods will all return database objects
|
||||
(or QuerySets) directly.
|
||||
|
||||
A PlayerChannelConnection defines a user's subscription to an in-game
|
||||
channel - deleting the connection object will disconnect the player
|
||||
from the channel.
|
||||
|
||||
Evennia-specific:
|
||||
get_all_player_connections
|
||||
has_connection
|
||||
get_all_connections
|
||||
create_connection
|
||||
break_connection
|
||||
|
||||
"""
|
||||
@returns_typeclass_list
|
||||
def get_all_player_connections(self, player):
|
||||
"Get all connections that the given player has."
|
||||
player = to_object(player)
|
||||
return self.filter(db_player=player)
|
||||
|
||||
def has_player_connection(self, player, channel):
|
||||
"Checks so a connection exists player<->channel"
|
||||
if player and channel:
|
||||
return self.filter(db_player=player.dbobj).filter(
|
||||
db_channel=channel.dbobj).count() > 0
|
||||
return False
|
||||
|
||||
def get_all_connections(self, channel):
|
||||
"""
|
||||
Get all connections for a channel
|
||||
"""
|
||||
channel = to_object(channel, objtype='channel')
|
||||
return self.filter(db_channel=channel)
|
||||
|
||||
def create_connection(self, player, channel):
|
||||
"""
|
||||
Connect a player to a channel. player and channel
|
||||
can be actual objects or keystrings.
|
||||
"""
|
||||
player = to_object(player)
|
||||
channel = to_object(channel, objtype='channel')
|
||||
if not player or not channel:
|
||||
raise CommError("NOTFOUND")
|
||||
new_connection = self.model(db_player=player, db_channel=channel)
|
||||
new_connection.save()
|
||||
return new_connection
|
||||
|
||||
def break_connection(self, player, channel):
|
||||
"Remove link between player and channel"
|
||||
player = to_object(player)
|
||||
channel = to_object(channel, objtype='channel')
|
||||
if not player or not channel:
|
||||
raise CommError("NOTFOUND")
|
||||
conns = self.filter(db_player=player).filter(db_channel=channel)
|
||||
for conn in conns:
|
||||
conn.delete()
|
||||
class ChannelManager(ChannelDBManager, TypeclassManager):
|
||||
pass
|
||||
|
||||
|
||||
20
evennia/comms/migrations/0004_auto_20150118_1631.py
Normal file
20
evennia/comms/migrations/0004_auto_20150118_1631.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
def convert_defaults(apps, schema_editor):
|
||||
ChannelDB = apps.get_model("comms", "ChannelDB")
|
||||
for channel in ChannelDB.objects.filter(db_typeclass_path="src.comms.comms.Channel"):
|
||||
channel.db_typeclass_path = "typeclasses.channels.Channel"
|
||||
channel.save()
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('comms', '0003_auto_20140917_0756'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(convert_defaults),
|
||||
]
|
||||
24
evennia/comms/migrations/0005_auto_20150223_1517.py
Normal file
24
evennia/comms/migrations/0005_auto_20150223_1517.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
def convert_channelnames(apps, schema_editor):
|
||||
ChannelDB = apps.get_model("comms", "ChannelDB")
|
||||
for chan in ChannelDB.objects.filter(db_key="MUDinfo"):
|
||||
# remove the old MUDinfo default channel
|
||||
chan.delete()
|
||||
for chan in ChannelDB.objects.filter(db_key__iexact="MUDconnections"):
|
||||
# change the old mudconnections to MudInfo instead
|
||||
chan.db_key = "MudInfo"
|
||||
chan.save()
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('comms', '0004_auto_20150118_1631'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(convert_channelnames),
|
||||
]
|
||||
|
|
@ -22,12 +22,11 @@ be able to delete connections on the fly).
|
|||
from datetime import datetime
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from src.typeclasses.models import TypedObject, TagHandler, AttributeHandler, AliasHandler
|
||||
from src.utils.idmapper.models import SharedMemoryModel
|
||||
from src.comms import managers
|
||||
from src.comms.managers import identify_object
|
||||
from src.locks.lockhandler import LockHandler
|
||||
from src.utils.utils import crop, make_iter, lazy_property
|
||||
from evennia.typeclasses.models import TypedObject
|
||||
from evennia.utils.idmapper.models import SharedMemoryModel
|
||||
from evennia.comms import managers
|
||||
from evennia.locks.lockhandler import LockHandler
|
||||
from evennia.utils.utils import crop, make_iter, lazy_property
|
||||
|
||||
__all__ = ("Msg", "TempMsg", "ChannelDB")
|
||||
|
||||
|
|
@ -70,16 +69,22 @@ class Msg(SharedMemoryModel):
|
|||
# Sender is either a player, an object or an external sender, like
|
||||
# an IRC channel; normally there is only one, but if co-modification of
|
||||
# a message is allowed, there may be more than one "author"
|
||||
db_sender_players = models.ManyToManyField("players.PlayerDB", related_name='sender_player_set', null=True, verbose_name='sender(player)', db_index=True)
|
||||
db_sender_objects = models.ManyToManyField("objects.ObjectDB", related_name='sender_object_set', null=True, verbose_name='sender(object)', db_index=True)
|
||||
db_sender_players = models.ManyToManyField("players.PlayerDB", related_name='sender_player_set',
|
||||
null=True, verbose_name='sender(player)', db_index=True)
|
||||
db_sender_objects = models.ManyToManyField("objects.ObjectDB", related_name='sender_object_set',
|
||||
null=True, verbose_name='sender(object)', db_index=True)
|
||||
db_sender_external = models.CharField('external sender', max_length=255, null=True, db_index=True,
|
||||
help_text="identifier for external sender, for example a sender over an IRC connection (i.e. someone who doesn't have an exixtence in-game).")
|
||||
help_text="identifier for external sender, for example a sender over an "
|
||||
"IRC connection (i.e. someone who doesn't have an exixtence in-game).")
|
||||
# The destination objects of this message. Stored as a
|
||||
# comma-separated string of object dbrefs. Can be defined along
|
||||
# with channels below.
|
||||
db_receivers_players = models.ManyToManyField('players.PlayerDB', related_name='receiver_player_set', null=True, help_text="player receivers")
|
||||
db_receivers_objects = models.ManyToManyField('objects.ObjectDB', related_name='receiver_object_set', null=True, help_text="object receivers")
|
||||
db_receivers_channels = models.ManyToManyField("ChannelDB", related_name='channel_set', null=True, help_text="channel recievers")
|
||||
db_receivers_players = models.ManyToManyField('players.PlayerDB', related_name='receiver_player_set',
|
||||
null=True, help_text="player receivers")
|
||||
db_receivers_objects = models.ManyToManyField('objects.ObjectDB', related_name='receiver_object_set',
|
||||
null=True, help_text="object receivers")
|
||||
db_receivers_channels = models.ManyToManyField("ChannelDB", related_name='channel_set',
|
||||
null=True, help_text="channel recievers")
|
||||
|
||||
# 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.
|
||||
|
|
@ -121,27 +126,28 @@ class Msg(SharedMemoryModel):
|
|||
#@property
|
||||
def __senders_get(self):
|
||||
"Getter. Allows for value = self.sender"
|
||||
return [hasattr(o, "typeclass") and o.typeclass or o for o in
|
||||
list(self.db_sender_players.all()) +
|
||||
list(self.db_sender_objects.all()) +
|
||||
self.extra_senders]
|
||||
return list(self.db_sender_players.all()) + \
|
||||
list(self.db_sender_objects.all()) + \
|
||||
self.extra_senders
|
||||
|
||||
#@sender.setter
|
||||
def __senders_set(self, value):
|
||||
def __senders_set(self, senders):
|
||||
"Setter. Allows for self.sender = value"
|
||||
for val in (v for v in make_iter(value) if v):
|
||||
obj, typ = identify_object(val)
|
||||
if typ == 'player':
|
||||
self.db_sender_players.add(obj)
|
||||
elif typ == 'object':
|
||||
self.db_sender_objects.add(obj)
|
||||
elif isinstance(typ, basestring):
|
||||
self.db_sender_external = obj
|
||||
elif not obj:
|
||||
return
|
||||
else:
|
||||
raise ValueError(obj)
|
||||
self.save()
|
||||
for sender in make_iter(senders):
|
||||
if not sender:
|
||||
continue
|
||||
if isinstance(sender, basestring):
|
||||
self.db_sender_external = sender
|
||||
self.extra_senders.append(sender)
|
||||
self.save(update_fields=["db_sender_external"])
|
||||
continue
|
||||
if not hasattr(sender, "__dbclass__"):
|
||||
raise ValueError("This is a not a typeclassed object!")
|
||||
clsname = sender.__dbclass__.__name__
|
||||
if clsname == "ObjectDB":
|
||||
self.db_sender_objects.add(sender)
|
||||
elif clsname == "PlayerDB":
|
||||
self.db_sender_players.add(sender)
|
||||
|
||||
#@sender.deleter
|
||||
def __senders_del(self):
|
||||
|
|
@ -153,19 +159,21 @@ class Msg(SharedMemoryModel):
|
|||
self.save()
|
||||
senders = property(__senders_get, __senders_set, __senders_del)
|
||||
|
||||
def remove_sender(self, value):
|
||||
def remove_sender(self, senders):
|
||||
"Remove a single sender or a list of senders"
|
||||
for val in make_iter(value):
|
||||
obj, typ = identify_object(val)
|
||||
if typ == 'player':
|
||||
self.db_sender_players.remove(obj)
|
||||
elif typ == 'object':
|
||||
self.db_sender_objects.remove(obj)
|
||||
elif isinstance(obj, basestring):
|
||||
self.db_sender_external = obj
|
||||
else:
|
||||
raise ValueError(obj)
|
||||
self.save()
|
||||
for sender in make_iter(senders):
|
||||
if not sender:
|
||||
continue
|
||||
if isinstance(sender, basestring):
|
||||
self.db_sender_external = ""
|
||||
self.save(update_fields=["db_sender_external"])
|
||||
if not hasattr(sender, "__dbclass__"):
|
||||
raise ValueError("This is a not a typeclassed object!")
|
||||
clsname = sender.__dbclass__.__name__
|
||||
if clsname == "ObjectDB":
|
||||
self.db_sender_objects.remove(sender)
|
||||
elif clsname == "PlayerDB":
|
||||
self.db_sender_players.remove(sender)
|
||||
|
||||
# receivers property
|
||||
#@property
|
||||
|
|
@ -174,46 +182,45 @@ class Msg(SharedMemoryModel):
|
|||
Getter. Allows for value = self.receivers.
|
||||
Returns three lists of receivers: players, objects and channels.
|
||||
"""
|
||||
return [hasattr(o, "typeclass") and o.typeclass or o for o in
|
||||
list(self.db_receivers_players.all()) + list(self.db_receivers_objects.all())]
|
||||
return list(self.db_receivers_players.all()) + list(self.db_receivers_objects.all())
|
||||
|
||||
#@receivers.setter
|
||||
def __receivers_set(self, value):
|
||||
def __receivers_set(self, receivers):
|
||||
"""
|
||||
Setter. Allows for self.receivers = value.
|
||||
This appends a new receiver to the message.
|
||||
"""
|
||||
for val in (v for v in make_iter(value) if v):
|
||||
obj, typ = identify_object(val)
|
||||
if typ == 'player':
|
||||
self.db_receivers_players.add(obj)
|
||||
elif typ == 'object':
|
||||
self.db_receivers_objects.add(obj)
|
||||
elif not obj:
|
||||
return
|
||||
else:
|
||||
raise ValueError
|
||||
self.save()
|
||||
for receiver in make_iter(receivers):
|
||||
if not receiver:
|
||||
continue
|
||||
if not hasattr(receiver, "__dbclass__"):
|
||||
raise ValueError("This is a not a typeclassed object!")
|
||||
clsname = receiver.__dbclass__.__name__
|
||||
if clsname == "ObjectDB":
|
||||
self.db_receivers_objects.add(receiver)
|
||||
elif clsname == "PlayerDB":
|
||||
self.db_receivers_players.add(receiver)
|
||||
|
||||
#@receivers.deleter
|
||||
def __receivers_del(self):
|
||||
"Deleter. Clears all receivers"
|
||||
self.db_receivers_players.clear()
|
||||
self.db_receivers_objects.clear()
|
||||
self.extra_senders = []
|
||||
self.save()
|
||||
receivers = property(__receivers_get, __receivers_set, __receivers_del)
|
||||
|
||||
def remove_receiver(self, obj):
|
||||
"Remove a single recevier"
|
||||
obj, typ = identify_object(obj)
|
||||
if typ == 'player':
|
||||
self.db_receivers_players.remove(obj)
|
||||
elif typ == 'object':
|
||||
self.db_receivers_objects.remove(obj)
|
||||
else:
|
||||
raise ValueError
|
||||
self.save()
|
||||
def remove_receiver(self, receivers):
|
||||
"Remove a single receiver or a list of receivers"
|
||||
for receiver in make_iter(receivers):
|
||||
if not receiver:
|
||||
continue
|
||||
if not hasattr(receiver, "__dbclass__"):
|
||||
raise ValueError("This is a not a typeclassed object!")
|
||||
clsname = receiver.__dbclass__.__name__
|
||||
if clsname == "ObjectDB":
|
||||
self.db_receivers_objects.remove(receiver)
|
||||
elif clsname == "PlayerDB":
|
||||
self.db_receivers_players.remove(receiver)
|
||||
|
||||
# channels property
|
||||
#@property
|
||||
|
|
@ -225,8 +232,9 @@ class Msg(SharedMemoryModel):
|
|||
def __channels_set(self, value):
|
||||
"""
|
||||
Setter. Allows for self.channels = value.
|
||||
Requires a channel to be added."""
|
||||
for val in (v.dbobj for v in make_iter(value) if v):
|
||||
Requires a channel to be added.
|
||||
"""
|
||||
for val in (v for v in make_iter(value) if v):
|
||||
self.db_receivers_channels.add(val)
|
||||
|
||||
#@channels.deleter
|
||||
|
|
@ -244,18 +252,20 @@ class Msg(SharedMemoryModel):
|
|||
return self.db_hide_from_players.all(), self.db_hide_from_objects.all(), self.db_hide_from_channels.all()
|
||||
|
||||
#@hide_from_sender.setter
|
||||
def __hide_from_set(self, value):
|
||||
def __hide_from_set(self, hiders):
|
||||
"Setter. Allows for self.hide_from = value. Will append to hiders"
|
||||
obj, typ = identify_object(value)
|
||||
if typ == "player":
|
||||
self.db_hide_from_players.add(obj)
|
||||
elif typ == "object":
|
||||
self.db_hide_from_objects.add(obj)
|
||||
elif typ == "channel":
|
||||
self.db_hide_from_channels.add(obj)
|
||||
else:
|
||||
raise ValueError
|
||||
self.save()
|
||||
for hider in make_iter(hiders):
|
||||
if not hider:
|
||||
continue
|
||||
if not hasattr(hider, "__dbclass__"):
|
||||
raise ValueError("This is a not a typeclassed object!")
|
||||
clsname = hider.__dbclass__.__name__
|
||||
if clsname == "PlayerDB":
|
||||
self.db_hide_from_players.add(hider.__dbclass__)
|
||||
elif clsname == "ObjectDB":
|
||||
self.db_hide_from_objects.add(hider.__dbclass__)
|
||||
elif clsname == "ChannelDB":
|
||||
self.db_hide_from_channels.add(hider.__dbclass__)
|
||||
|
||||
#@hide_from_sender.deleter
|
||||
def __hide_from_del(self):
|
||||
|
|
@ -348,7 +358,6 @@ class ChannelDB(TypedObject):
|
|||
key - main name for channel
|
||||
desc - optional description of channel
|
||||
aliases - alternative names for the channel
|
||||
keep_log - bool if the channel should remember messages
|
||||
permissions - perm strings
|
||||
|
||||
"""
|
||||
|
|
@ -356,81 +365,15 @@ class ChannelDB(TypedObject):
|
|||
related_name="subscription_set", null=True, verbose_name='subscriptions', db_index=True)
|
||||
|
||||
# Database manager
|
||||
objects = managers.ChannelManager()
|
||||
objects = managers.ChannelDBManager()
|
||||
|
||||
_typeclass_paths = settings.CHANNEL_TYPECLASS_PATHS
|
||||
_default_typeclass_path = settings.BASE_CHANNEL_TYPECLASS or "src.comms.comms.Channel"
|
||||
_default_typeclass_path = settings.BASE_CHANNEL_TYPECLASS or "evennia.comms.comms.Channel"
|
||||
|
||||
class Meta:
|
||||
"Define Django meta options"
|
||||
verbose_name = "Channel"
|
||||
verbose_name_plural = "Channels"
|
||||
|
||||
#
|
||||
# Channel class methods
|
||||
#
|
||||
|
||||
def __str__(self):
|
||||
return "Channel '%s' (%s)" % (self.key, self.typeclass.db.desc)
|
||||
|
||||
def has_connection(self, player):
|
||||
"""
|
||||
Checks so this player is actually listening
|
||||
to this channel.
|
||||
"""
|
||||
if hasattr(player, "player"):
|
||||
player = player.player
|
||||
player = player.dbobj
|
||||
return player in self.db_subscriptions.all()
|
||||
|
||||
def connect(self, player):
|
||||
"Connect the user to this channel. This checks access."
|
||||
if hasattr(player, "player"):
|
||||
player = player.player
|
||||
player = player.typeclass
|
||||
# check access
|
||||
if not self.access(player, 'listen'):
|
||||
return False
|
||||
# pre-join hook
|
||||
connect = self.typeclass.pre_join_channel(player)
|
||||
if not connect:
|
||||
return False
|
||||
# subscribe
|
||||
self.db_subscriptions.add(player.dbobj)
|
||||
# post-join hook
|
||||
self.typeclass.post_join_channel(player)
|
||||
return True
|
||||
|
||||
def disconnect(self, player):
|
||||
"Disconnect user from this channel."
|
||||
if hasattr(player, "player"):
|
||||
player = player.player
|
||||
player = player.typeclass
|
||||
# pre-disconnect hook
|
||||
disconnect = self.typeclass.pre_leave_channel(player)
|
||||
if not disconnect:
|
||||
return False
|
||||
# disconnect
|
||||
self.db_subscriptions.remove(player.dbobj)
|
||||
# post-disconnect hook
|
||||
self.typeclass.post_leave_channel(player.dbobj)
|
||||
return True
|
||||
|
||||
def access(self, accessing_obj, access_type='listen', default=False):
|
||||
"""
|
||||
Determines if another object has permission to access.
|
||||
accessing_obj - object trying to access this one
|
||||
access_type - type of access sought
|
||||
default - what to return if no lock of access_type was found
|
||||
"""
|
||||
return self.locks.check(accessing_obj, access_type=access_type, default=default)
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Deletes channel while also cleaning up channelhandler
|
||||
"""
|
||||
_GA(self, "attributes").clear()
|
||||
_GA(self, "aliases").clear()
|
||||
super(ChannelDB, self).delete()
|
||||
from src.comms.channelhandler import CHANNELHANDLER
|
||||
CHANNELHANDLER.update()
|
||||
return "Channel '%s' (%s)" % (self.key, self.db.desc)
|
||||
42
evennia/contrib/README.md
Normal file
42
evennia/contrib/README.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
|
||||
# Contrib folder
|
||||
|
||||
This folder contains 'contributions': extra snippets of code that are
|
||||
potentially very useful for the game coder but which are considered
|
||||
too game-specific to be a part of the main Evennia game server. These
|
||||
modules are not used unless you explicitly import them. See each file
|
||||
for more detailed instructions on how to install.
|
||||
|
||||
Modules in this folder are distributed under the same licence as
|
||||
Evennia unless noted differently in the individual module.
|
||||
|
||||
If you want to edit, tweak or expand on this code you should copy the
|
||||
things you want from here into your game folder and change them there.
|
||||
|
||||
* Barter system (Griatch 2012) - A safe and effective barter-system
|
||||
for any game. Allows safe trading of any godds (including coin)
|
||||
* CharGen (Griatch 2011) - A simple Character creator and selector for
|
||||
Evennia's ooc mode. Works well with the menu login contrib and
|
||||
is intended as a starting point for building a more full-featured
|
||||
character creation system.
|
||||
* Dice (Griatch 2012) - A fully featured dice rolling system.
|
||||
* Email-login (Griatch 2012) - A variant of the standard login system
|
||||
that requires an email to login rather then just name+password.
|
||||
* Extended Room (Griatch 2012) - An expanded Room typeclass with
|
||||
multiple descriptions for time and season as well as details.
|
||||
* Line Editor (Griatch 2011) - A fully-featured in-game line-editor
|
||||
with undo/redo/search/replace/format and other modern features.
|
||||
* Menu login (Griatch 2011) - A login system using menus asking
|
||||
for name/password rather than giving them as one command
|
||||
* Menu System (Griatch 2011) - A general menu system with multiple-
|
||||
choice options and nodes able to trigger commands and code snippets.
|
||||
* ProcPool (Griatch 2012) - Process pool mechanisms to offload heavy
|
||||
operations to a separate computer process asynchronously.
|
||||
* Slow exit (Griatch 2014) - Custom Exit class that takes different
|
||||
time to pass depending on if you are walking/running etc.
|
||||
* Talking NPC (Griatch 2011) - A talking NPC object that offers a
|
||||
menu-driven conversation tree.
|
||||
* Tutorial examples (Griatch 2011, 2015) - A folder of basic
|
||||
example objects, commands and scripts.
|
||||
* Tutorial world (Griatch 2011, 2015) - A folder containing the
|
||||
rooms, objects and commands for building the Tutorial world.
|
||||
7
evennia/contrib/__init__.py
Normal file
7
evennia/contrib/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This sub-package holds Evennia's contributions - code that may be
|
||||
useful but are deemed too game-specific to go into the core library.
|
||||
|
||||
See README.md for more info.
|
||||
"""
|
||||
|
|
@ -94,7 +94,7 @@ in-game.
|
|||
|
||||
"""
|
||||
|
||||
from ev import Command, Script, CmdSet
|
||||
from evennia import Command, Script, CmdSet
|
||||
|
||||
TRADE_TIMEOUT = 60 # timeout for B to accept trade
|
||||
|
||||
|
|
@ -18,7 +18,7 @@ while puppeting a Character already before.
|
|||
|
||||
Installation:
|
||||
|
||||
Read the instructions in game/gamesrc/commands/examples/cmdset.py in
|
||||
Read the instructions in contrib/examples/cmdset.py in
|
||||
order to create a new default cmdset module for Evennia to use (copy
|
||||
the template up one level, and change the settings file's relevant
|
||||
variables to point to the cmdsets inside). If you already have such
|
||||
|
|
@ -32,8 +32,8 @@ following line to the end of OOCCmdSet's at_cmdset_creation():
|
|||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from ev import Command, create_object, utils
|
||||
from ev import default_cmds, managers
|
||||
from evennia import Command, create_object, utils
|
||||
from evennia import default_cmds, managers
|
||||
|
||||
CHARACTER_TYPECLASS = settings.BASE_CHARACTER_TYPECLASS
|
||||
|
||||
|
|
@ -72,7 +72,7 @@ class CmdOOCLook(default_cmds.CmdLook):
|
|||
|
||||
# making sure caller is really a player
|
||||
self.character = None
|
||||
if utils.inherits_from(self.caller, "src.objects.objects.Object"):
|
||||
if utils.inherits_from(self.caller, "evennia.objects.objects.Object"):
|
||||
# An object of some type is calling. Convert to player.
|
||||
#print self.caller, self.caller.__class__
|
||||
self.character = self.caller
|
||||
|
|
@ -149,7 +149,7 @@ class CmdOOCCharacterCreate(Command):
|
|||
|
||||
# making sure caller is really a player
|
||||
self.character = None
|
||||
if utils.inherits_from(self.caller, "src.objects.objects.Object"):
|
||||
if utils.inherits_from(self.caller, "evennia.objects.objects.Object"):
|
||||
# An object of some type is calling. Convert to player.
|
||||
#print self.caller, self.caller.__class__
|
||||
self.character = self.caller
|
||||
|
|
@ -32,7 +32,7 @@ After a reload the dice (or roll) command will be available in-game.
|
|||
|
||||
import re
|
||||
from random import randint
|
||||
from ev import default_cmds, CmdSet
|
||||
from evennia import default_cmds, CmdSet
|
||||
|
||||
|
||||
def roll_dice(dicenum, dicetype, modifier=None, conditional=None, return_tuple=False):
|
||||
|
|
@ -24,24 +24,21 @@ That's it. Reload the server and try to log in to see it.
|
|||
|
||||
The initial login "graphic" will still not mention email addresses
|
||||
after this change. The login splash screen is taken from strings in
|
||||
the module given by settings.CONNECTION_SCREEN_MODULE. You will want
|
||||
to copy the template file in game/gamesrc/conf/examples up one level
|
||||
and re-point the settings file to this custom module. The "MUX_SCREEN"
|
||||
example in that file is the recommended one to use with this module.
|
||||
the module given by settings.CONNECTION_SCREEN_MODULE.
|
||||
|
||||
"""
|
||||
import re
|
||||
import traceback
|
||||
from django.conf import settings
|
||||
from src.players.models import PlayerDB
|
||||
from src.objects.models import ObjectDB
|
||||
from src.server.models import ServerConfig
|
||||
from src.comms.models import ChannelDB
|
||||
from evennia.players.models import PlayerDB
|
||||
from evennia.objects.models import ObjectDB
|
||||
from evennia.server.models import ServerConfig
|
||||
from evennia.comms.models import ChannelDB
|
||||
|
||||
from src.commands.cmdset import CmdSet
|
||||
from src.utils import create, logger, utils, ansi
|
||||
from src.commands.default.muxcommand import MuxCommand
|
||||
from src.commands.cmdhandler import CMD_LOGINSTART
|
||||
from evennia.commands.cmdset import CmdSet
|
||||
from evennia.utils import create, logger, utils, ansi
|
||||
from evennia.commands.default.muxcommand import MuxCommand
|
||||
from evennia.commands.cmdhandler import CMD_LOGINSTART
|
||||
|
||||
# limit symbol import for API
|
||||
__all__ = ("CmdUnconnectedConnect", "CmdUnconnectedCreate",
|
||||
|
|
@ -23,7 +23,7 @@ time comes.
|
|||
|
||||
An updated @desc command allows for setting seasonal descriptions.
|
||||
|
||||
The room uses the src.utils.gametime.GameTime global script. This is
|
||||
The room uses the evennia.utils.gametime.GameTime global script. This is
|
||||
started by default, but if you have deactivated it, you need to
|
||||
supply your own time keeping mechanism.
|
||||
|
||||
|
|
@ -69,10 +69,10 @@ Installation/testing:
|
|||
|
||||
import re
|
||||
from django.conf import settings
|
||||
from ev import Room
|
||||
from ev import gametime
|
||||
from ev import default_cmds
|
||||
from ev import utils
|
||||
from evennia import Room
|
||||
from evennia import gametime
|
||||
from evennia import default_cmds
|
||||
from evennia import utils
|
||||
|
||||
# error return function, needed by Extended Look command
|
||||
_AT_SEARCH_RESULT = utils.variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1))
|
||||
|
|
@ -24,9 +24,9 @@ module. To use it just import and add it to your default cmdset.
|
|||
"""
|
||||
|
||||
import re
|
||||
from ev import Command, CmdSet, utils
|
||||
from ev import syscmdkeys
|
||||
from contrib.menusystem import prompt_yesno
|
||||
from evennia import Command, CmdSet, utils
|
||||
from evennia import syscmdkeys
|
||||
from evennia.contrib.menusystem import prompt_yesno
|
||||
|
||||
CMD_NOMATCH = syscmdkeys.CMD_NOMATCH
|
||||
CMD_NOINPUT = syscmdkeys.CMD_NOINPUT
|
||||
|
|
@ -20,25 +20,20 @@ CMDSET_UNLOGGEDIN = "contrib.menu_login.UnloggedInCmdSet"
|
|||
That's it. Reload the server and try to log in to see it.
|
||||
|
||||
The initial login "graphic" is taken from strings in the module given
|
||||
by settings.CONNECTION_SCREEN_MODULE. You will want to copy the
|
||||
template file in game/gamesrc/conf/examples up one level and re-point
|
||||
the settings file to this custom module. you can then edit the string
|
||||
in that module (at least comment out the default string that mentions
|
||||
commands that are not available) and add something more suitable for
|
||||
the initial splash screen.
|
||||
by settings.CONNECTION_SCREEN_MODULE.
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
import traceback
|
||||
from django.conf import settings
|
||||
from ev import managers
|
||||
from ev import utils, logger, create_player
|
||||
from ev import Command, CmdSet
|
||||
from ev import syscmdkeys
|
||||
from src.server.models import ServerConfig
|
||||
from evennia import managers
|
||||
from evennia import utils, logger, create_player
|
||||
from evennia import Command, CmdSet
|
||||
from evennia import syscmdkeys
|
||||
from evennia.server.models import ServerConfig
|
||||
|
||||
from contrib.menusystem import MenuNode, MenuTree
|
||||
from evennia.contrib.menusystem import MenuNode, MenuTree
|
||||
|
||||
CMD_LOGINSTART = syscmdkeys.CMD_LOGINSTART
|
||||
CMD_NOINPUT = syscmdkeys.CMD_NOINPUT
|
||||
|
|
@ -18,23 +18,17 @@ There is also a simple Yes/No function supplied. This will create a
|
|||
one-off Yes/No question and executes a given code depending on which
|
||||
choice was made.
|
||||
|
||||
To test, make sure to follow the instructions in
|
||||
game/gamesrc/commands/examples/cmdset.py (copy the template up one level
|
||||
and change settings to point to the relevant cmdsets within). If you
|
||||
already have such a module, you can of course use that. Next you
|
||||
import and add the CmdTestMenu command to the end of the default cmdset in
|
||||
this custom module.
|
||||
The test command is also a good example of how to use this module in code.
|
||||
To test, add this to the default cmdset
|
||||
|
||||
"""
|
||||
from types import MethodType
|
||||
from ev import syscmdkeys
|
||||
from evennia import syscmdkeys
|
||||
|
||||
from ev import Command, CmdSet, utils
|
||||
from ev import default_cmds, logger
|
||||
from evennia import Command, CmdSet, utils
|
||||
from evennia import default_cmds, logger
|
||||
|
||||
# imported only to make it available during execution of code blocks
|
||||
import ev
|
||||
import evennia
|
||||
|
||||
CMD_NOMATCH = syscmdkeys.CMD_NOMATCH
|
||||
CMD_NOINPUT = syscmdkeys.CMD_NOINPUT
|
||||
|
|
@ -67,7 +61,7 @@ class CmdMenuNode(Command):
|
|||
except Exception, e:
|
||||
self.caller.msg("%s\n{rThere was an error with this selection.{n" % e)
|
||||
elif self.code:
|
||||
ev.logger.log_depmsg("menusystem.code is deprecated. Use menusystem.func.")
|
||||
evennia.logger.log_depmsg("menusystem.code is deprecated. Use menusystem.func.")
|
||||
try:
|
||||
exec(self.code)
|
||||
except Exception, e:
|
||||
|
|
@ -230,7 +224,7 @@ class MenuTree(object):
|
|||
self.caller.msg("{rNode callback could not be executed for node %s. Continuing anyway.{n" % key)
|
||||
if node.code:
|
||||
# Execute eventual code active on this node. self.caller is available at this point.
|
||||
ev.logger.log_depmsg("menusystem.code is deprecated. Use menusystem.callback.")
|
||||
evennia.logger.log_depmsg("menusystem.code is deprecated. Use menusystem.callback.")
|
||||
try:
|
||||
exec(node.code)
|
||||
except Exception:
|
||||
|
|
@ -287,7 +281,7 @@ class MenuNode(object):
|
|||
code - functional code. Deprecated. This will be executed just before this
|
||||
node is loaded (i.e. as soon after it's been selected from
|
||||
another node). self.caller is available to call from this
|
||||
code block, as well as ev.
|
||||
code block, as well as the evennia flat API.
|
||||
callback - function callback. This will be called as callback(currentnode) just
|
||||
before this node is loaded (i.e. as soon as possible as it's
|
||||
been selected from another node). currentnode.caller is available.
|
||||
|
|
@ -309,7 +303,7 @@ class MenuNode(object):
|
|||
Nlinks = len(self.links)
|
||||
|
||||
if code:
|
||||
ev.logger.log_depmsg("menusystem.code is deprecated. Use menusystem.callback.")
|
||||
evennia.logger.log_depmsg("menusystem.code is deprecated. Use menusystem.callback.")
|
||||
|
||||
# validate the input
|
||||
if not self.links:
|
||||
|
|
@ -449,10 +443,10 @@ def prompt_yesno(caller, question="", yesfunc=None, nofunc=None, yescode="", noc
|
|||
|
||||
# code exec is deprecated:
|
||||
if yescode:
|
||||
ev.logger.log_depmsg("yesnosystem.code is deprecated. Use yesnosystem.callback.")
|
||||
evennia.logger.log_depmsg("yesnosystem.code is deprecated. Use yesnosystem.callback.")
|
||||
cmdyes.code = yescode + "\nself.caller.cmdset.delete('menucmdset')\ndel self.caller.db._menu_data"
|
||||
if nocode:
|
||||
ev.logger.log_depmsg("yesnosystem.code is deprecated. Use yesnosystem.callback.")
|
||||
evennia.logger.log_depmsg("yesnosystem.code is deprecated. Use yesnosystem.callback.")
|
||||
cmdno.code = nocode + "\nself.caller.cmdset.delete('menucmdset')\ndel self.caller.db._menu_data"
|
||||
|
||||
# creating cmdset (this will already have look/help commands)
|
||||
|
|
@ -503,7 +497,7 @@ def prompt_choice(caller, question="", prompts=None, choicefunc=None, force_choo
|
|||
count = 0
|
||||
choices = ""
|
||||
commands = []
|
||||
for choice in utils.makeiter(prompts):
|
||||
for choice in utils.make_iter(prompts):
|
||||
count += 1
|
||||
choices += "\n{lc%d{lt[%d]{le %s" % (count, count, choice)
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ TickerHandler might be better.
|
|||
|
||||
"""
|
||||
|
||||
from ev import Exit, utils, Command
|
||||
from evennia import Exit, utils, Command
|
||||
|
||||
MOVE_DELAY = {"stroll": 6,
|
||||
"walk": 4,
|
||||
|
|
@ -23,8 +23,8 @@ mob implementation.
|
|||
|
||||
"""
|
||||
|
||||
from ev import Object, CmdSet, default_cmds
|
||||
from contrib import menusystem
|
||||
from evennia import DefaultObject, CmdSet, default_cmds
|
||||
from evennia.contrib import menusystem
|
||||
|
||||
|
||||
#
|
||||
|
|
@ -110,7 +110,7 @@ CONV = {"START": {"text": "Hello there, how can I help you?",
|
|||
}
|
||||
|
||||
|
||||
class TalkingNPC(Object):
|
||||
class TalkingNPC(DefaultObject):
|
||||
"""
|
||||
This implements a simple Object using the talk command and using the
|
||||
conversation defined above. .
|
||||
|
|
@ -122,4 +122,4 @@ class TalkingNPC(Object):
|
|||
self.db.conversation = CONV
|
||||
self.db.desc = "This is a talkative NPC."
|
||||
# assign the talk command to npc
|
||||
self.cmdset.add_default(TalkingCmdSet, permanent=True)
|
||||
self.cmdset.add_default(TalkingCmdSet, permanent=True)
|
||||
|
|
@ -12,9 +12,9 @@ or you won't see any messages!
|
|||
|
||||
"""
|
||||
import random
|
||||
from ev import Script
|
||||
from evennia import DefaultScript
|
||||
|
||||
class BodyFunctions(Script):
|
||||
class BodyFunctions(DefaultScript):
|
||||
"""
|
||||
This class defines the script itself
|
||||
"""
|
||||
|
|
@ -8,7 +8,7 @@ cmdset - this way you can often re-use the commands too.
|
|||
"""
|
||||
|
||||
import random
|
||||
from ev import Command, CmdSet
|
||||
from evennia import Command, CmdSet
|
||||
|
||||
# Some simple commands for the red button
|
||||
|
||||
|
|
@ -312,8 +312,8 @@ class BlindCmdSet(CmdSet):
|
|||
|
||||
def at_cmdset_creation(self):
|
||||
"Setup the blind cmdset"
|
||||
from src.commands.default.general import CmdSay
|
||||
from src.commands.default.general import CmdPose
|
||||
from evennia.commands.default.general import CmdSay
|
||||
from evennia.commands.default.general import CmdPose
|
||||
self.add(CmdSay())
|
||||
self.add(CmdPose())
|
||||
self.add(CmdBlindLook())
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
# This creates a red button
|
||||
|
||||
@create button:examples.red_button.RedButton
|
||||
@create button:tutorial_examples.red_button.RedButton
|
||||
|
||||
# This comment ends input for @create
|
||||
# Next command:
|
||||
|
|
@ -45,9 +45,9 @@
|
|||
# everything in this block will be appended to the beginning of
|
||||
# all other #CODE blocks when they are executed.
|
||||
|
||||
from ev import create_object, search_object
|
||||
from game.gamesrc.objects.examples import red_button
|
||||
from ev import Object
|
||||
from evennia import create_object, search_object
|
||||
from evennia.contrib.tutorial_examples import red_button
|
||||
from evennia import DefaultObject
|
||||
|
||||
limbo = search_object('Limbo')[0]
|
||||
|
||||
|
|
@ -62,7 +62,7 @@ limbo = search_object('Limbo')[0]
|
|||
|
||||
# create a red button in limbo
|
||||
red_button = create_object(red_button.RedButton, key="Red button",
|
||||
location=limbo, aliases=["button"])
|
||||
location=limbo, aliases=["button"])
|
||||
|
||||
# we take a look at what we created
|
||||
caller.msg("A %s was created." % red_button.key)
|
||||
|
|
@ -78,8 +78,8 @@ caller.msg("A %s was created." % red_button.key)
|
|||
# the python variables we assign to must match the ones given in the
|
||||
# header for the system to be able to delete them afterwards during a
|
||||
# debugging run.
|
||||
table = create_object(Object, key="Table", location=limbo)
|
||||
chair = create_object(Object, key="Chair", location=limbo)
|
||||
table = create_object(DefaultObject, key="Table", location=limbo)
|
||||
chair = create_object(DefaultObject, key="Chair", location=limbo)
|
||||
|
||||
string = "A %s and %s were created. If debug was active, they were deleted again."
|
||||
caller.msg(string % (table, chair))
|
||||
|
|
@ -11,19 +11,19 @@ Create this button with
|
|||
Note that you must drop the button before you can see its messages!
|
||||
"""
|
||||
import random
|
||||
from ev import Object
|
||||
from game.gamesrc.scripts.examples import red_button_scripts as scriptexamples
|
||||
from game.gamesrc.commands.examples import cmdset_red_button as cmdsetexamples
|
||||
from evennia import DefaultObject
|
||||
from evennia.contrib.tutorial_examples import red_button_scripts as scriptexamples
|
||||
from evennia.contrib.tutorial_examples import cmdset_red_button as cmdsetexamples
|
||||
|
||||
#
|
||||
# Definition of the object itself
|
||||
#
|
||||
|
||||
|
||||
class RedButton(Object):
|
||||
class RedButton(DefaultObject):
|
||||
"""
|
||||
This class describes an evil red button. It will use the script
|
||||
definition in game/gamesrc/events/example.py to blink at regular
|
||||
definition in contrib/examples/red_button_scripts to blink at regular
|
||||
intervals. It also uses a series of script and commands to handle
|
||||
pushing the button and causing effects when doing so.
|
||||
|
||||
|
|
@ -2,12 +2,12 @@
|
|||
Example of scripts.
|
||||
|
||||
These are scripts intended for a particular object - the
|
||||
red_button object type in gamesrc/types/examples. A few variations
|
||||
red_button object type in contrib/examples. A few variations
|
||||
on uses of scripts are included.
|
||||
|
||||
"""
|
||||
from ev import Script
|
||||
from game.gamesrc.commands.examples import cmdset_red_button as cmdsetexamples
|
||||
from evennia import DefaultScript
|
||||
from evennia.contrib.tutorial_examples import cmdset_red_button as cmdsetexamples
|
||||
|
||||
#
|
||||
# Scripts as state-managers
|
||||
|
|
@ -26,7 +26,7 @@ from game.gamesrc.commands.examples import cmdset_red_button as cmdsetexamples
|
|||
# a bright light. The last one also has a timer component that allows it
|
||||
# to remove itself after a while (and the player recovers their eyesight).
|
||||
|
||||
class ClosedLidState(Script):
|
||||
class ClosedLidState(DefaultScript):
|
||||
"""
|
||||
This manages the cmdset for the "closed" button state. What this
|
||||
means is that while this script is valid, we add the RedButtonClosed
|
||||
|
|
@ -62,7 +62,7 @@ class ClosedLidState(Script):
|
|||
self.obj.cmdset.delete(cmdsetexamples.LidClosedCmdSet)
|
||||
|
||||
|
||||
class OpenLidState(Script):
|
||||
class OpenLidState(DefaultScript):
|
||||
"""
|
||||
This manages the cmdset for the "open" button state. This will add
|
||||
the RedButtonOpen
|
||||
|
|
@ -97,7 +97,7 @@ class OpenLidState(Script):
|
|||
self.obj.cmdset.delete(cmdsetexamples.LidOpenCmdSet)
|
||||
|
||||
|
||||
class BlindedState(Script):
|
||||
class BlindedState(DefaultScript):
|
||||
"""
|
||||
This is a timed state.
|
||||
|
||||
|
|
@ -152,7 +152,7 @@ class BlindedState(Script):
|
|||
# that makes the lid covering the button slide back after a while.
|
||||
#
|
||||
|
||||
class CloseLidEvent(Script):
|
||||
class CloseLidEvent(DefaultScript):
|
||||
"""
|
||||
This event closes the glass lid over the button
|
||||
some time after it was opened. It's a one-off
|
||||
|
|
@ -195,7 +195,7 @@ class CloseLidEvent(Script):
|
|||
"""
|
||||
self.obj.close_lid()
|
||||
|
||||
class BlinkButtonEvent(Script):
|
||||
class BlinkButtonEvent(DefaultScript):
|
||||
"""
|
||||
This timed script lets the button flash at regular intervals.
|
||||
"""
|
||||
|
|
@ -225,7 +225,7 @@ class BlinkButtonEvent(Script):
|
|||
"""
|
||||
self.obj.blink()
|
||||
|
||||
class DeactivateButtonEvent(Script):
|
||||
class DeactivateButtonEvent(DefaultScript):
|
||||
"""
|
||||
This deactivates the button for a short while (it won't blink, won't
|
||||
close its lid etc). It is meant to be called when the button is pushed
|
||||
104
evennia/contrib/tutorial_world/README.md
Normal file
104
evennia/contrib/tutorial_world/README.md
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
|
||||
# Evennia Tutorial World
|
||||
|
||||
Griatch 2011, 2015
|
||||
|
||||
This is a stand-alone tutorial area for an unmodified Evennia install.
|
||||
Think of it as a sort of single-player adventure rather than a
|
||||
full-fledged multi-player game world. The various rooms and objects
|
||||
herein are designed to show off features of the engine, not to be a
|
||||
very challenging (nor long) gaming experience. As such it's of course
|
||||
only skimming the surface of what is possible.
|
||||
|
||||
|
||||
## Install
|
||||
|
||||
Log in as superuser (#1), then run
|
||||
|
||||
@batchcommand tutorial_world.build
|
||||
|
||||
Wait a little while for building to complete and don't run the command
|
||||
again. This should build the world and connect it to Limbo.
|
||||
|
||||
If you are a superuser (User `#1`), use the `@quell` command to play
|
||||
the tutorial as intended.
|
||||
|
||||
|
||||
## Comments
|
||||
|
||||
The tutorial world is intended for you playing around with the engine.
|
||||
It will help you learn how to accomplish some more advanced effects
|
||||
and might give some good ideas along the way.
|
||||
|
||||
It's suggested you play it through (as a normal user, NOT as
|
||||
Superuser!) and explore it a bit, then come back here and start
|
||||
looking into the (heavily documented) build/source code to find out
|
||||
how things tick - that's the "tutorial" in Tutorial world after all.
|
||||
|
||||
Please report bugs in the tutorial to the Evennia issue tracker.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
**Spoilers below - don't read on unless you already played the
|
||||
tutorial game**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Tutorial World Room map
|
||||
|
||||
?
|
||||
|
|
||||
+---+----+ +-------------------+ +--------+ +--------+
|
||||
| | | | |gate | |corner |
|
||||
| cliff +----+ bridge +----+ +---+ |
|
||||
| | | | | | | |
|
||||
+---+---\+ +---------------+---+ +---+----+ +---+----+
|
||||
| \ | | castle |
|
||||
| \ +--------+ +----+---+ +---+----+ +---+----+
|
||||
| \ |under- | |ledge | |along | |court- |
|
||||
| \|ground +--+ | |wall +---+yard |
|
||||
| \ | | | | | | |
|
||||
| +------\-+ +--------+ +--------+ +---+----+
|
||||
| \ |
|
||||
++---------+ \ +--------+ +--------+ +---+----+
|
||||
|intro | \ |cell | |trap | |temple |
|
||||
o--+ | \| +----+ | | |
|
||||
L | | \ | /| | | |
|
||||
I +----+-----+ +--------+ / ---+-+-+-+ +---+----+
|
||||
M | / | | | |
|
||||
B +----+-----+ +--------+/ +--+-+-+---------+----+
|
||||
O |outro | |tomb | |antechamber |
|
||||
o--+ +----------+ | | |
|
||||
| | | | | |
|
||||
+----------+ +--------+ +---------------------+
|
||||
|
||||
|
||||
## Hints/Notes:
|
||||
|
||||
* o-- connections to/from Limbo
|
||||
* intro/outro areas are rooms that automatically sets/cleans the
|
||||
Character of any settings assigned to it during the
|
||||
tutorial game.
|
||||
* The Cliff is a good place to get an overview of the surroundings.
|
||||
* The Bridge may seem like a big room, but it is really only one room
|
||||
with custom move commands to make it take longer to cross. You can
|
||||
also fall off the bridge if youare unlucky or take your time to
|
||||
take in the view too long.
|
||||
* In the Castle areas an aggressive mob is patrolling. It implements
|
||||
rudimentary AI but packs quite a punch unless you have
|
||||
found yourself a weapon that can harm it. Combat is only
|
||||
possible once you find a weapon.
|
||||
* The Antechamber feature a puzzle for finding the correct Grave
|
||||
chamber.
|
||||
* The Cell is your reward if you fail in various ways. Finding a
|
||||
way out of it is a small puzzle of its own.
|
||||
* The Tomb is a nice place to find a weapon that can hurt the
|
||||
castle guardian. This is the goal of the tutorial.
|
||||
Explore on, or take the exit to finish the tutorial.
|
||||
* ? - look into the code if you cannot find this bonus area!
|
||||
File diff suppressed because it is too large
Load diff
416
evennia/contrib/tutorial_world/mob.py
Normal file
416
evennia/contrib/tutorial_world/mob.py
Normal file
|
|
@ -0,0 +1,416 @@
|
|||
"""
|
||||
This module implements a simple mobile object with
|
||||
a very rudimentary AI as well as an aggressive enemy
|
||||
object based on that mobile class.
|
||||
|
||||
"""
|
||||
|
||||
import random
|
||||
|
||||
from evennia import TICKER_HANDLER
|
||||
from evennia import search_object
|
||||
from evennia import Command, CmdSet
|
||||
from evennia import logger
|
||||
from evennia.contrib.tutorial_world import objects as tut_objects
|
||||
|
||||
class CmdMobOnOff(Command):
|
||||
"""
|
||||
Activates/deactivates Mob
|
||||
|
||||
Usage:
|
||||
mobon <mob>
|
||||
moboff <mob>
|
||||
|
||||
This turns the mob from active (alive) mode
|
||||
to inactive (dead) mode. It is used during
|
||||
building to activate the mob once it's
|
||||
prepared.
|
||||
"""
|
||||
key = "mobon"
|
||||
aliases = "moboff"
|
||||
locks = "cmd:superuser()"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
Uses the mob's set_alive/set_dead methods
|
||||
to turn on/off the mob."
|
||||
"""
|
||||
if not self.args:
|
||||
self.caller.msg("Usage: mobon|moboff <mob>")
|
||||
return
|
||||
mob = self.caller.search(self.args)
|
||||
if not mob:
|
||||
return
|
||||
if self.cmdstring == "mobon":
|
||||
mob.set_alive()
|
||||
else:
|
||||
mob.set_dead()
|
||||
|
||||
|
||||
class MobCmdSet(CmdSet):
|
||||
"""
|
||||
Holds the admin command controlling the mob
|
||||
"""
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdMobOnOff())
|
||||
|
||||
class Mob(tut_objects.TutorialObject):
|
||||
"""
|
||||
This is a state-machine AI mobile. It has several states which are
|
||||
controlled from setting various Attributes. All default to True:
|
||||
|
||||
patrolling: if set, the mob will move randomly
|
||||
from room to room, but preferring to not return
|
||||
the way it came. If unset, the mob will remain
|
||||
stationary (idling) until attacked.
|
||||
aggressive: if set, will attack Characters in
|
||||
the same room using whatever Weapon it
|
||||
carries (see tutorial_world.objects.Weapon).
|
||||
if unset, the mob will never engage in combat
|
||||
no matter what.
|
||||
hunting: if set, the mob will pursue enemies trying
|
||||
to flee from it, so it can enter combat. If unset,
|
||||
it will return to patrolling/idling if fled from.
|
||||
immortal: If set, the mob cannot take any damage.
|
||||
irregular_echoes: list of strings the mob generates at irregular intervals.
|
||||
desc_alive: the physical description while alive
|
||||
desc_dead: the physical descripion while dead
|
||||
send_defeated_to: unique key/alias for location to send defeated enemies to
|
||||
defeat_msg: message to echo to defeated opponent
|
||||
defeat_msg_room: message to echo to room. Accepts %s as the name of the defeated.
|
||||
hit_msg: message to echo when this mob is hit. Accepts %s for the mob's key.
|
||||
weapon_ineffective_msg: message to echo for useless attacks
|
||||
death_msg: message to echo to room when this mob dies.
|
||||
patrolling_pace: how many seconds per tick, when patrolling
|
||||
aggressive_pace: -"- attacking
|
||||
hunting_pace: -"- hunting
|
||||
death_pace: -"- returning to life when dead
|
||||
|
||||
field 'home' - the home location should set to someplace inside
|
||||
the patrolling area. The mob will use this if it should
|
||||
happen to roam into a room with no exits.
|
||||
|
||||
"""
|
||||
def at_init(self):
|
||||
"""
|
||||
When initialized from cache (after a server reboot), set up
|
||||
the AI state.
|
||||
"""
|
||||
# The AI state machine (not persistent).
|
||||
self.ndb.is_patrolling = self.db.patrolling and not self.db.is_dead
|
||||
self.ndb.is_attacking = False
|
||||
self.ndb.is_hunting = False
|
||||
self.ndb.is_immortal = self.db.immortal or self.db.is_dead
|
||||
|
||||
def at_object_creation(self):
|
||||
"""
|
||||
Called the first time the object is created.
|
||||
We set up the base properties and flags here.
|
||||
"""
|
||||
self.cmdset.add(MobCmdSet, permanent=True)
|
||||
# Main AI flags. We start in dead mode so we don't have to
|
||||
# chase the mob around when building.
|
||||
self.db.patrolling = True
|
||||
self.db.aggressive = True
|
||||
self.db.immortal = False
|
||||
# db-store if it is dead or not
|
||||
self.db.is_dead = True
|
||||
# specifies how much damage we divide away from non-magic weapons
|
||||
self.db.damage_resistance = 100.0
|
||||
# pace (number of seconds between ticks) for
|
||||
# the respective modes.
|
||||
self.db.patrolling_pace = 6
|
||||
self.db.aggressive_pace = 2
|
||||
self.db.hunting_pace = 1
|
||||
self.db.death_pace = 100 # stay dead for 100 seconds
|
||||
|
||||
# we store the call to the tickerhandler
|
||||
# so we can easily deactivate the last
|
||||
# ticker subscription when we switch.
|
||||
# since we will use the same idstring
|
||||
# throughout we only need to save the
|
||||
# previous interval we used.
|
||||
self.db.last_ticker_interval = None
|
||||
|
||||
# store two separate descriptions, one for alive and
|
||||
# one for dead (corpse)
|
||||
self.db.desc_alive = "This is a moving object."
|
||||
self.db.desc_dead = "A dead body."
|
||||
|
||||
# health stats
|
||||
self.db.full_health = 20
|
||||
self.db.health = 20
|
||||
|
||||
# when this mob defeats someone, we move the character off to
|
||||
# some other place (Dark Cell in the tutorial).
|
||||
self.db.send_defeated_to = "dark cell"
|
||||
# text to echo to the defeated foe.
|
||||
self.db.defeat_msg = "You fall to the ground."
|
||||
self.db.defeat_msg_room = "%s falls to the ground."
|
||||
self.db.weapon_ineffective_msg = "Your weapon just passes through your enemy, causing almost no effect!"
|
||||
|
||||
self.db.death_msg = "After the last hit %s evaporates." % self.key
|
||||
self.db.hit_msg = "%s wails, shudders and writhes." % self.key
|
||||
self.db.irregular_msgs = ["the enemy looks about.", "the enemy changes stance."]
|
||||
|
||||
self.db.tutorial_info = "This is an object with simple state AI, using a ticker to move."
|
||||
|
||||
def _set_ticker(self, interval, hook_key, stop=False):
|
||||
"""
|
||||
Set how often the given hook key should
|
||||
be "ticked".
|
||||
|
||||
Args:
|
||||
interval (int): The number of seconds
|
||||
between ticks
|
||||
hook_key (str): The name of the method
|
||||
(on this mob) to call every interval
|
||||
seconds.
|
||||
stop (bool, optional): Just stop the
|
||||
last ticker without starting a new one.
|
||||
With this set, the interval and hook_key
|
||||
arguments are unused.
|
||||
|
||||
In order to only have one ticker
|
||||
running at a time, we make sure to store the
|
||||
previous ticker subscription so that we can
|
||||
easily find and stop it before setting a
|
||||
new one. The tickerhandler is persistent so
|
||||
we need to remember this across reloads.
|
||||
|
||||
"""
|
||||
idstring = "tutorial_mob" # this doesn't change
|
||||
last_interval = self.db.last_ticker_interval
|
||||
if last_interval:
|
||||
# we have a previous subscription, kill this first.
|
||||
TICKER_HANDLER.remove(self, last_interval, idstring)
|
||||
self.db.last_ticker_interval = interval
|
||||
if not stop:
|
||||
# set the new ticker
|
||||
TICKER_HANDLER.add(self, interval, idstring, hook_key)
|
||||
|
||||
def _find_target(self, location):
|
||||
"""
|
||||
Scan the given location for suitable targets (this is defined
|
||||
as Characters) to attack. Will ignore superusers.
|
||||
|
||||
Args:
|
||||
location (Object): the room to scan.
|
||||
|
||||
Returns:
|
||||
The first suitable target found.
|
||||
|
||||
"""
|
||||
targets = [obj for obj in location.contents_get(exclude=self)
|
||||
if obj.has_player and not obj.is_superuser]
|
||||
return targets[0] if targets else None
|
||||
|
||||
def set_alive(self, *args, **kwargs):
|
||||
"""
|
||||
Set the mob to "alive" mode. This effectively
|
||||
resurrects it from the dead state.
|
||||
"""
|
||||
self.db.health = self.db.full_health
|
||||
self.db.is_dead = False
|
||||
self.db.desc = self.db.desc_alive
|
||||
self.ndb.is_immortal = self.db.immortal
|
||||
self.ndb.is_patrolling = self.db.patrolling
|
||||
if not self.location:
|
||||
self.move_to(self.home)
|
||||
if self.db.patrolling:
|
||||
self.start_patrolling()
|
||||
|
||||
def set_dead(self):
|
||||
"""
|
||||
Set the mob to "dead" mode. This turns it off
|
||||
and makes sure it can take no more damage.
|
||||
It also starts a ticker for when it will return.
|
||||
"""
|
||||
self.db.is_dead = True
|
||||
self.location = None
|
||||
self.ndb.is_patrolling = False
|
||||
self.ndb.is_attacking = False
|
||||
self.ndb.is_hunting = False
|
||||
self.ndb.is_immortal = True
|
||||
# we shall return after some time
|
||||
self._set_ticker(self.db.death_pace, "set_alive")
|
||||
|
||||
def start_idle(self):
|
||||
"""
|
||||
Starts just standing around. This will kill
|
||||
the ticker and do nothing more.
|
||||
"""
|
||||
self._set_ticker(None, None, stop=True)
|
||||
|
||||
def start_patrolling(self):
|
||||
"""
|
||||
Start the patrolling state by
|
||||
registering us with the ticker-handler
|
||||
at a leasurely pace.
|
||||
"""
|
||||
if not self.db.patrolling:
|
||||
self.start_idle()
|
||||
return
|
||||
self._set_ticker(self.db.patrolling_pace, "do_patrol")
|
||||
self.ndb.is_patrolling = True
|
||||
self.ndb.is_hunting = False
|
||||
self.ndb.is_attacking = False
|
||||
# for the tutorial, we also heal the mob in this mode
|
||||
self.db.health = self.db.full_health
|
||||
|
||||
def start_hunting(self):
|
||||
"""
|
||||
Start the hunting state
|
||||
"""
|
||||
if not self.db.hunting:
|
||||
self.start_patrolling()
|
||||
return
|
||||
self._set_ticker(self.db.hunting_pace, "do_hunt")
|
||||
self.ndb.is_patrolling = False
|
||||
self.ndb.is_hunting = True
|
||||
self.ndb.is_attacking = False
|
||||
|
||||
def start_attacking(self):
|
||||
"""
|
||||
Start the attacking state
|
||||
"""
|
||||
if not self.db.aggressive:
|
||||
self.start_hunting()
|
||||
return
|
||||
self._set_ticker(self.db.aggressive_pace, "do_attack")
|
||||
self.ndb.is_patrolling = False
|
||||
self.ndb.is_hunting = False
|
||||
self.ndb.is_attacking = True
|
||||
|
||||
def do_patrol(self, *args, **kwargs):
|
||||
"""
|
||||
Called repeatedly during patrolling mode. In this mode, the
|
||||
mob scans its surroundings and randomly chooses a viable exit.
|
||||
One should lock exits with the traverse:has_player() lock in
|
||||
order to block the mob from moving outside its area while
|
||||
allowing player-controlled characters to move normally.
|
||||
"""
|
||||
if random.random() < 0.01 and self.db.irregular_msgs:
|
||||
self.location.msg_contents(random.choice(self.db.irregular_msgs))
|
||||
if self.db.aggressive:
|
||||
# first check if there are any targets in the room.
|
||||
target = self._find_target(self.location)
|
||||
if target:
|
||||
self.start_attacking()
|
||||
return
|
||||
# no target found, look for an exit.
|
||||
exits = [exi for exi in self.location.exits
|
||||
if exi.access(self, "traverse")]
|
||||
if exits:
|
||||
# randomly pick an exit
|
||||
exit = random.choice(exits)
|
||||
# move there.
|
||||
self.move_to(exit.destination)
|
||||
else:
|
||||
# no exits! teleport to home to get away.
|
||||
self.move_to(self.home)
|
||||
|
||||
def do_hunting(self, *args, **kwargs):
|
||||
"""
|
||||
Called regularly when in hunting mode. In hunting mode the mob
|
||||
scans adjacent rooms for enemies and moves towards them to
|
||||
attack if possible.
|
||||
"""
|
||||
if random.random() < 0.01 and self.db.irregular_msgs:
|
||||
self.location.msg_contents(random.choice(self.db.irregular_msgs))
|
||||
if self.db.aggressive:
|
||||
# first check if there are any targets in the room.
|
||||
target = self._find_target(self.location)
|
||||
if target:
|
||||
self.start_attacking()
|
||||
return
|
||||
# no targets found, scan surrounding rooms
|
||||
exits = [exi for exi in self.location.exits
|
||||
if exi.access(self, "traverse")]
|
||||
if exits:
|
||||
# scan the exits destination for targets
|
||||
for exit in exits:
|
||||
target = self._find_target(exit.destination)
|
||||
if target:
|
||||
# a target found. Move there.
|
||||
self.move_to(exit.destination)
|
||||
return
|
||||
# if we get to this point we lost our
|
||||
# prey. Resume patrolling.
|
||||
self.start_patrolling()
|
||||
else:
|
||||
# no exits! teleport to home to get away.
|
||||
self.move_to(self.home)
|
||||
|
||||
def do_attack(self, *args, **kwargs):
|
||||
"""
|
||||
Called regularly when in attacking mode. In attacking mode
|
||||
the mob will bring its weapons to bear on any targets
|
||||
in the room.
|
||||
"""
|
||||
if random.random() < 0.01 and self.db.irregular_msgs:
|
||||
self.location.msg_contents(random.choice(self.db.irregular_msgs))
|
||||
# first make sure we have a target
|
||||
target = self._find_target(self.location)
|
||||
if not target:
|
||||
# no target, start looking for one
|
||||
self.start_hunting()
|
||||
return
|
||||
|
||||
# we use the same attack commands as defined in
|
||||
# tutorial_world.objects.Weapon, assuming that
|
||||
# the mob is given a Weapon to attack with.
|
||||
attack_cmd = random.choice(("thrust", "pierce", "stab", "slash", "chop"))
|
||||
self.execute_cmd("%s %s" % (attack_cmd, target))
|
||||
|
||||
# analyze the current state
|
||||
if target.db.health <= 0:
|
||||
# we reduced the target to <= 0 health. Move them to the
|
||||
# defeated room
|
||||
target.msg(self.db.defeat_msg)
|
||||
self.location.msg_contents(self.db.defeat_msg_room % target.key, exclude=target)
|
||||
send_defeated_to = search_object(self.db.send_defeated_to)
|
||||
if send_defeated_to:
|
||||
target.move_to(send_defeated_to[0], quiet=True)
|
||||
else:
|
||||
logger.log_err("Mob: mob.db.send_defeated_to not found: %s" % self.db.send_defeated_to)
|
||||
|
||||
|
||||
# response methods - called by other objects
|
||||
|
||||
def at_hit(self, weapon, attacker, damage):
|
||||
"""
|
||||
Someone landed a hit on us. Check our status
|
||||
and start attacking if not already doing so.
|
||||
"""
|
||||
if not self.ndb.is_immortal:
|
||||
if not weapon.db.magic:
|
||||
# not a magic weapon - divide away magic resistance
|
||||
damage /= self.db.damage_resistance
|
||||
attacker.msg(self.db.weapon_ineffective_msg)
|
||||
else:
|
||||
self.location.msg_contents(self.db.hit_msg)
|
||||
self.db.health -= damage
|
||||
|
||||
# analyze the result
|
||||
if self.db.health <= 0:
|
||||
# we are dead!
|
||||
attacker.msg(self.db.death_msg)
|
||||
self.set_dead()
|
||||
else:
|
||||
# still alive, start attack if not already attacking
|
||||
if self.db.aggressive and not self.ndb.is_attacking:
|
||||
self.start_attacking()
|
||||
|
||||
def at_new_arrival(self, new_character):
|
||||
"""
|
||||
This is triggered whenever a new character enters the room.
|
||||
This is called by the TutorialRoom the mob stands in and
|
||||
allows it to be aware of changes immediately without needing
|
||||
to poll for them all the time. For example, the mob can react
|
||||
right away, also when patrolling on a very slow ticker.
|
||||
"""
|
||||
# the room actually already checked all we need, so
|
||||
# we know it is a valid target.
|
||||
if self.db.aggressive and not self.ndb.is_attacking:
|
||||
self.start_attacking()
|
||||
File diff suppressed because it is too large
Load diff
998
evennia/contrib/tutorial_world/rooms.py
Normal file
998
evennia/contrib/tutorial_world/rooms.py
Normal file
|
|
@ -0,0 +1,998 @@
|
|||
"""
|
||||
|
||||
Room Typeclasses for the TutorialWorld.
|
||||
|
||||
This defines special types of Rooms available in the tutorial. To keep
|
||||
everything in one place we define them together with the custom
|
||||
commands needed to control them. Those commands could also have been
|
||||
in a separate module (e.g. if they could have been re-used elsewhere.)
|
||||
|
||||
"""
|
||||
|
||||
import random
|
||||
from evennia import TICKER_HANDLER
|
||||
from evennia import CmdSet, Command, DefaultRoom
|
||||
from evennia import utils, create_object, search_object
|
||||
from evennia import syscmdkeys, default_cmds
|
||||
from evennia.contrib.tutorial_world.objects import LightSource, TutorialObject
|
||||
|
||||
# the system error-handling module is defined in the settings. We load the
|
||||
# given setting here using utils.object_from_module. This way we can use
|
||||
# it regardless of if we change settings later.
|
||||
from django.conf import settings
|
||||
_SEARCH_AT_RESULT = utils.object_from_module(settings.SEARCH_AT_RESULT)
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Tutorial room - parent room class
|
||||
#
|
||||
# This room is the parent of all rooms in the tutorial.
|
||||
# It defines a tutorial command on itself (available to
|
||||
# all who is in a tutorial room).
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
#
|
||||
# Special command avaiable in all tutorial rooms
|
||||
#
|
||||
|
||||
class CmdTutorial(Command):
|
||||
"""
|
||||
Get help during the tutorial
|
||||
|
||||
Usage:
|
||||
tutorial [obj]
|
||||
|
||||
This command allows you to get behind-the-scenes info
|
||||
about an object or the current location.
|
||||
|
||||
"""
|
||||
key = "tutorial"
|
||||
aliases = ["tut"]
|
||||
locks = "cmd:all()"
|
||||
help_category = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
All we do is to scan the current location for an Attribute
|
||||
called `tutorial_info` and display that.
|
||||
"""
|
||||
|
||||
caller = self.caller
|
||||
|
||||
if not self.args:
|
||||
target = self.obj # this is the room the command is defined on
|
||||
else:
|
||||
target = caller.search(self.args.strip())
|
||||
if not target:
|
||||
return
|
||||
helptext = target.db.tutorial_info
|
||||
if helptext:
|
||||
caller.msg("{G%s{n" % helptext)
|
||||
else:
|
||||
caller.msg("{RSorry, there is no tutorial help available here.{n")
|
||||
|
||||
|
||||
# for the @detail command we inherit from MuxCommand, since
|
||||
# we want to make use of MuxCommand's pre-parsing of '=' in the
|
||||
# argument.
|
||||
class CmdTutorialSetDetail(default_cmds.MuxCommand):
|
||||
"""
|
||||
sets a detail on a room
|
||||
|
||||
Usage:
|
||||
@detail <key> = <description>
|
||||
@detail <key>;<alias>;... = description
|
||||
|
||||
Example:
|
||||
@detail walls = The walls are covered in ...
|
||||
@detail castle;ruin;tower = The distant ruin ...
|
||||
|
||||
This sets a "detail" on the object this command is defined on
|
||||
(TutorialRoom for this tutorial). This detail can be accessed with
|
||||
the TutorialRoomLook command sitting on TutorialRoom objects (details
|
||||
are set as a simple dictionary on the room). This is a Builder command.
|
||||
|
||||
We custom parse the key for the ;-separator in order to create
|
||||
multiple aliases to the detail all at once.
|
||||
"""
|
||||
key = "@detail"
|
||||
locks = "cmd:perm(Builders)"
|
||||
help_category = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
All this does is to check if the object has
|
||||
the set_detail method and uses it.
|
||||
"""
|
||||
if not self.args or not self.rhs:
|
||||
self.caller.msg("Usage: @detail key = description")
|
||||
return
|
||||
if not hasattr(self.obj, "set_detail"):
|
||||
self.caller.msg("Details cannot be set on %s." % self.obj)
|
||||
return
|
||||
for key in self.lhs.split(";"):
|
||||
# loop over all aliases, if any (if not, this will just be
|
||||
# the one key to loop over)
|
||||
self.obj.set_detail(key, self.rhs)
|
||||
self.caller.msg("Detail set: '%s': '%s'" % (self.lhs, self.rhs))
|
||||
|
||||
|
||||
class CmdTutorialLook(default_cmds.CmdLook):
|
||||
"""
|
||||
looks at the room and on details
|
||||
|
||||
Usage:
|
||||
look <obj>
|
||||
look <room detail>
|
||||
look *<player>
|
||||
|
||||
Observes your location, details at your location or objects
|
||||
in your vicinity.
|
||||
|
||||
Tutorial: This is a child of the default Look command, that also
|
||||
allows us to look at "details" in the room. These details are
|
||||
things to examine and offers some extra description without
|
||||
actually having to be actual database objects. It uses the
|
||||
return_detail() hook on TutorialRooms for this.
|
||||
"""
|
||||
# we don't need to specify key/locks etc, this is already
|
||||
# set by the parent.
|
||||
help_category = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
Handle the looking. This is a copy of the default look
|
||||
code except for adding in the details.
|
||||
"""
|
||||
caller = self.caller
|
||||
args = self.args
|
||||
if args:
|
||||
# we use quiet=True to turn off automatic error reporting.
|
||||
# This tells search that we want to handle error messages
|
||||
# ourself. This also means the search function will always
|
||||
# return a list (with 0, 1 or more elements) rather than
|
||||
# result/None.
|
||||
looking_at_obj = caller.search(args, use_nicks=True, quiet=True)
|
||||
if len(looking_at_obj) != 1:
|
||||
# no target found or more than one target found (multimatch)
|
||||
# look for a detail that may match
|
||||
detail = self.obj.return_detail(args)
|
||||
if detail:
|
||||
self.caller.msg(detail)
|
||||
return
|
||||
else:
|
||||
# no detail found, delegate our result to the normal
|
||||
# error message handler.
|
||||
_SEARCH_AT_RESULT(caller, args, looking_at_obj)
|
||||
return
|
||||
else:
|
||||
# we found a match, extract it from the list and carry on
|
||||
# normally with the look handling.
|
||||
looking_at_obj = looking_at_obj[0]
|
||||
|
||||
else:
|
||||
looking_at_obj = caller.location
|
||||
if not looking_at_obj:
|
||||
caller.msg("You have no location to look at!")
|
||||
return
|
||||
|
||||
if not hasattr(looking_at_obj, 'return_appearance'):
|
||||
# this is likely due to us having a player instead
|
||||
looking_at_obj = looking_at_obj.character
|
||||
if not looking_at_obj.access(caller, "view"):
|
||||
caller.msg("Could not find '%s'." % args)
|
||||
return
|
||||
# get object's appearance
|
||||
caller.msg(looking_at_obj.return_appearance(caller))
|
||||
# the object's at_desc() method.
|
||||
looking_at_obj.at_desc(looker=caller)
|
||||
|
||||
|
||||
|
||||
class TutorialRoomCmdSet(CmdSet):
|
||||
"""
|
||||
Implements the simple tutorial cmdset. This will overload the look
|
||||
command in the default CharacterCmdSet since it has a higher
|
||||
priority (ChracterCmdSet has prio 0)
|
||||
"""
|
||||
key = "tutorial_cmdset"
|
||||
priority = 1
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"add the tutorial-room commands"
|
||||
self.add(CmdTutorial())
|
||||
self.add(CmdTutorialSetDetail())
|
||||
self.add(CmdTutorialLook())
|
||||
|
||||
|
||||
class TutorialRoom(DefaultRoom):
|
||||
"""
|
||||
This is the base room type for all rooms in the tutorial world.
|
||||
It defines a cmdset on itself for reading tutorial info about the location.
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"Called when room is first created"
|
||||
self.db.tutorial_info = "This is a tutorial room. It allows you to use the 'tutorial' command."
|
||||
self.cmdset.add_default(TutorialRoomCmdSet)
|
||||
|
||||
def at_object_receive(self, new_arrival, source_location):
|
||||
"""
|
||||
When an object enter a tutorial room we tell other objects in
|
||||
the room about it by trying to call a hook on them. The Mob object
|
||||
uses this to cheaply get notified of enemies without having
|
||||
to constantly scan for them.
|
||||
|
||||
Args:
|
||||
new_arrival (Object): the object that just entered this room.
|
||||
source_location (Object): the previous location of new_arrival.
|
||||
|
||||
"""
|
||||
if new_arrival.has_player and not new_arrival.is_superuser:
|
||||
# this is a character
|
||||
for obj in self.contents_get(exclude=new_arrival):
|
||||
if hasattr(obj, "at_new_arrival"):
|
||||
obj.at_new_arrival(new_arrival)
|
||||
|
||||
def return_detail(self, detailkey):
|
||||
"""
|
||||
This looks for an Attribute "obj_details" and possibly
|
||||
returns the value of it.
|
||||
|
||||
Args:
|
||||
detailkey (str): The detail being looked at. This is
|
||||
case-insensitive.
|
||||
|
||||
"""
|
||||
details = self.db.details
|
||||
if details:
|
||||
return details.get(detailkey.lower(), None)
|
||||
|
||||
def set_detail(self, detailkey, description):
|
||||
"""
|
||||
This sets a new detail, using an Attribute "details".
|
||||
|
||||
Args:
|
||||
detailkey (str): The detail identifier to add (for
|
||||
aliases you need to add multiple keys to the
|
||||
same description). Case-insensitive.
|
||||
description (str): The text to return when looking
|
||||
at the given detailkey.
|
||||
|
||||
"""
|
||||
if self.db.details:
|
||||
self.db.details[detailkey.lower()] = description
|
||||
else:
|
||||
self.db.details = {detailkey.lower(): description}
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Weather room - room with a ticker
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
# These are rainy weather strings
|
||||
WEATHER_STRINGS = (
|
||||
"The rain coming down from the iron-grey sky intensifies.",
|
||||
"A gush of wind throws the rain right in your face. Despite your cloak you shiver.",
|
||||
"The rainfall eases a bit and the sky momentarily brightens.",
|
||||
"For a moment it looks like the rain is slowing, then it begins anew with renewed force.",
|
||||
"The rain pummels you with large, heavy drops. You hear the rumble of thunder in the distance.",
|
||||
"The wind is picking up, howling around you, throwing water droplets in your face. It's cold.",
|
||||
"Bright fingers of lightning flash over the sky, moments later followed by a deafening rumble.",
|
||||
"It rains so hard you can hardly see your hand in front of you. You'll soon be drenched to the bone.",
|
||||
"Lightning strikes in several thundering bolts, striking the trees in the forest to your west.",
|
||||
"You hear the distant howl of what sounds like some sort of dog or wolf.",
|
||||
"Large clouds rush across the sky, throwing their load of rain over the world.")
|
||||
|
||||
class WeatherRoom(TutorialRoom):
|
||||
"""
|
||||
This should probably better be called a rainy room...
|
||||
|
||||
This sets up an outdoor room typeclass. At irregular intervals,
|
||||
the effects of weather will show in the room. Outdoor rooms should
|
||||
inherit from this.
|
||||
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"""
|
||||
Called when object is first created.
|
||||
We set up a ticker to update this room regularly.
|
||||
|
||||
Note that we could in principle also use a Script to manage
|
||||
the ticking of the room, the TickerHandler is works fine for
|
||||
simple things like this though.
|
||||
"""
|
||||
super(WeatherRoom, self).at_object_creation()
|
||||
# subscribe ourselves to a ticker to repeatedly call the hook
|
||||
# "update_weather" on this object. The interval is randomized
|
||||
# so as to not have all weather rooms update at the same time.
|
||||
interval = random.randint(50, 70)
|
||||
TICKER_HANDLER.add(self, interval, idstring="tutorial", hook_key="update_weather")
|
||||
# this is parsed by the 'tutorial' command on TutorialRooms.
|
||||
self.db.tutorial_info = \
|
||||
"This room has a Script running that has it echo a weather-related message at irregular intervals."
|
||||
|
||||
def update_weather(self, *args, **kwargs):
|
||||
"""
|
||||
Called by the tickerhandler at regular intervals. Even so, we
|
||||
only update 20% of the time, picking a random weather message
|
||||
when we do. The tickerhandler requires that this hook accepts
|
||||
any arguments and keyword arguments (hence the *args, **kwargs
|
||||
even though we don't actually use them in this example)
|
||||
"""
|
||||
if random.random() < 0.2:
|
||||
# only update 20 % of the time
|
||||
self.msg_contents("{w%s{n" % random.choice(WEATHER_STRINGS))
|
||||
|
||||
|
||||
|
||||
SUPERUSER_WARNING = "\nWARNING: You are playing as a superuser ({name}). Use the {quell} command to\n" \
|
||||
"play without superuser privileges (many functions and puzzles ignore the \n" \
|
||||
"presence of a superuser, making this mode useful for exploring things behind \n" \
|
||||
"the scenes later).\n" \
|
||||
|
||||
|
||||
#-----------------------------------------------------------
|
||||
#
|
||||
# Intro Room - unique room
|
||||
#
|
||||
# This room marks the start of the tutorial. It sets up properties on
|
||||
# the player char that is needed for the tutorial.
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
class IntroRoom(TutorialRoom):
|
||||
"""
|
||||
Intro room
|
||||
|
||||
properties to customize:
|
||||
char_health - integer > 0 (default 20)
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"""
|
||||
Called when the room is first created.
|
||||
"""
|
||||
super(IntroRoom, self).at_object_creation()
|
||||
self.db_tutorial_info = "The first room of the tutorial. " \
|
||||
"This assigns the health Attribute to "\
|
||||
"the player."
|
||||
|
||||
def at_object_receive(self, character, source_location):
|
||||
"""
|
||||
Assign properties on characters
|
||||
"""
|
||||
|
||||
# setup character for the tutorial
|
||||
health = self.db.char_health or 20
|
||||
|
||||
if character.has_player:
|
||||
character.db.health = health
|
||||
character.db.health_max = health
|
||||
|
||||
if character.is_superuser:
|
||||
string = "-"*78 + SUPERUSER_WARNING + "-"*78
|
||||
character.msg("{r%s{n" % string.format(name=character.key, quell="{w@quell{r"))
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Bridge - unique room
|
||||
#
|
||||
# Defines a special west-eastward "bridge"-room, a large room it takes
|
||||
# several steps to cross. It is complete with custom commands and a
|
||||
# chance of falling off the bridge. This room has no regular exits,
|
||||
# instead the exiting are handled by custom commands set on the player
|
||||
# upon first entering the room.
|
||||
#
|
||||
# Since one can enter the bridge room from both ends, it is
|
||||
# divided into five steps:
|
||||
# westroom <- 0 1 2 3 4 -> eastroom
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
|
||||
class CmdEast(Command):
|
||||
"""
|
||||
Go eastwards across the bridge.
|
||||
|
||||
Tutorial info:
|
||||
This command relies on the caller having two Attributes
|
||||
(assigned by the room when entering):
|
||||
- east_exit: a unique name or dbref to the room to go to
|
||||
when exiting east.
|
||||
- west_exit: a unique name or dbref to the room to go to
|
||||
when exiting west.
|
||||
The room must also have the following Attributes
|
||||
- tutorial_bridge_posistion: the current position on
|
||||
on the bridge, 0 - 4.
|
||||
|
||||
"""
|
||||
key = "east"
|
||||
aliases = ["e"]
|
||||
locks = "cmd:all()"
|
||||
help_category = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
"move one step eastwards"
|
||||
caller = self.caller
|
||||
|
||||
bridge_step = min(5, caller.db.tutorial_bridge_position + 1)
|
||||
|
||||
if bridge_step > 4:
|
||||
# we have reached the far east end of the bridge.
|
||||
# Move to the east room.
|
||||
eexit = search_object(self.obj.db.east_exit)
|
||||
if eexit:
|
||||
caller.move_to(eexit[0])
|
||||
else:
|
||||
caller.msg("No east exit was found for this room. Contact an admin.")
|
||||
return
|
||||
caller.db.tutorial_bridge_position = bridge_step
|
||||
# since we are really in one room, we have to notify others
|
||||
# in the room when we move.
|
||||
caller.location.msg_contents("%s steps eastwards across the bridge." % caller.name, exclude=caller)
|
||||
caller.execute_cmd("look")
|
||||
|
||||
|
||||
# go back across the bridge
|
||||
class CmdWest(Command):
|
||||
"""
|
||||
Go westwards across the bridge.
|
||||
|
||||
Tutorial info:
|
||||
This command relies on the caller having two Attributes
|
||||
(assigned by the room when entering):
|
||||
- east_exit: a unique name or dbref to the room to go to
|
||||
when exiting east.
|
||||
- west_exit: a unique name or dbref to the room to go to
|
||||
when exiting west.
|
||||
The room must also have the following property:
|
||||
- tutorial_bridge_posistion: the current position on
|
||||
on the bridge, 0 - 4.
|
||||
|
||||
"""
|
||||
key = "west"
|
||||
aliases = ["w"]
|
||||
locks = "cmd:all()"
|
||||
help_category = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
"move one step westwards"
|
||||
caller = self.caller
|
||||
|
||||
bridge_step = max(-1, caller.db.tutorial_bridge_position - 1)
|
||||
|
||||
if bridge_step < 0:
|
||||
# we have reached the far west end of the bridge.
|
||||
# Move to the west room.
|
||||
wexit = search_object(self.obj.db.west_exit)
|
||||
if wexit:
|
||||
caller.move_to(wexit[0])
|
||||
else:
|
||||
caller.msg("No west exit was found for this room. Contact an admin.")
|
||||
return
|
||||
caller.db.tutorial_bridge_position = bridge_step
|
||||
# since we are really in one room, we have to notify others
|
||||
# in the room when we move.
|
||||
caller.location.msg_contents("%s steps westwards across the bridge." % caller.name, exclude=caller)
|
||||
caller.execute_cmd("look")
|
||||
|
||||
|
||||
BRIDGE_POS_MESSAGES = ("You are standing {wvery close to the the bridge's western foundation{n. If you go west you will be back on solid ground ...",
|
||||
"The bridge slopes precariously where it extends eastwards towards the lowest point - the center point of the hang bridge.",
|
||||
"You are {whalfways{n out on the unstable bridge.",
|
||||
"The bridge slopes precariously where it extends westwards towards the lowest point - the center point of the hang bridge.",
|
||||
"You are standing {wvery close to the bridge's eastern foundation{n. If you go east you will be back on solid ground ...")
|
||||
BRIDGE_MOODS = ("The bridge sways in the wind.", "The hanging bridge creaks dangerously.",
|
||||
"You clasp the ropes firmly as the bridge sways and creaks under you.",
|
||||
"From the castle you hear a distant howling sound, like that of a large dog or other beast.",
|
||||
"The bridge creaks under your feet. Those planks does not seem very sturdy.",
|
||||
"Far below you the ocean roars and throws its waves against the cliff, as if trying its best to reach you.",
|
||||
"Parts of the bridge come loose behind you, falling into the chasm far below!",
|
||||
"A gust of wind causes the bridge to sway precariously.",
|
||||
"Under your feet a plank comes loose, tumbling down. For a moment you dangle over the abyss ...",
|
||||
"The section of rope you hold onto crumble in your hands, parts of it breaking apart. You sway trying to regain balance.")
|
||||
|
||||
FALL_MESSAGE = "Suddenly the plank you stand on gives way under your feet! You fall!" \
|
||||
"\nYou try to grab hold of an adjoining plank, but all you manage to do is to " \
|
||||
"divert your fall westwards, towards the cliff face. This is going to hurt ... " \
|
||||
"\n ... The world goes dark ...\n\n" \
|
||||
|
||||
|
||||
class CmdLookBridge(Command):
|
||||
"""
|
||||
looks around at the bridge.
|
||||
|
||||
Tutorial info:
|
||||
This command assumes that the room has an Attribute
|
||||
"fall_exit", a unique name or dbref to the place they end upp
|
||||
if they fall off the bridge.
|
||||
"""
|
||||
key = 'look'
|
||||
aliases = ["l"]
|
||||
locks = "cmd:all()"
|
||||
help_category = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
"Looking around, including a chance to fall."
|
||||
caller = self.caller
|
||||
bridge_position = self.caller.db.tutorial_bridge_position
|
||||
# this command is defined on the room, so we get it through self.obj
|
||||
location = self.obj
|
||||
# randomize the look-echo
|
||||
message = "{c%s{n\n%s\n%s" % (location.key,
|
||||
BRIDGE_POS_MESSAGES[bridge_position],
|
||||
random.choice(BRIDGE_MOODS))
|
||||
|
||||
chars = [obj for obj in self.obj.contents_get(exclude=caller) if obj.has_player]
|
||||
if chars:
|
||||
# we create the You see: message manually here
|
||||
message += "\n You see: %s" % ", ".join("{c%s{n" % char.key for char in chars)
|
||||
self.caller.msg(message)
|
||||
|
||||
# there is a chance that we fall if we are on the western or central
|
||||
# part of the bridge.
|
||||
if bridge_position < 3 and random.random() < 0.05 and not self.caller.is_superuser:
|
||||
# we fall 5% of time.
|
||||
fall_exit = search_object(self.obj.db.fall_exit)
|
||||
if fall_exit:
|
||||
self.caller.msg("{r%s{n" % FALL_MESSAGE)
|
||||
self.caller.move_to(fall_exit[0], quiet=True)
|
||||
# inform others on the bridge
|
||||
self.obj.msg_contents("A plank gives way under %s's feet and " \
|
||||
"they fall from the bridge!" % self.caller.key)
|
||||
|
||||
|
||||
# custom help command
|
||||
class CmdBridgeHelp(Command):
|
||||
"""
|
||||
Overwritten help command while on the bridge.
|
||||
"""
|
||||
key = "help"
|
||||
aliases = ["h"]
|
||||
locks = "cmd:all()"
|
||||
help_category = "Tutorial world"
|
||||
|
||||
def func(self):
|
||||
"Implements the command."
|
||||
string = "You are trying hard not to fall off the bridge ..."
|
||||
string += "\n\nWhat you can do is trying to cross the bridge {weast{n "
|
||||
string += "or try to get back to the mainland {wwest{n)."
|
||||
self.caller.msg(string)
|
||||
|
||||
|
||||
class BridgeCmdSet(CmdSet):
|
||||
"This groups the bridge commands. We will store it on the room."
|
||||
key = "Bridge commands"
|
||||
priority = 1 # this gives it precedence over the normal look/help commands.
|
||||
def at_cmdset_creation(self):
|
||||
"Called at first cmdset creation"
|
||||
self.add(CmdTutorial())
|
||||
self.add(CmdEast())
|
||||
self.add(CmdWest())
|
||||
self.add(CmdLookBridge())
|
||||
self.add(CmdBridgeHelp())
|
||||
|
||||
|
||||
BRIDGE_WEATHER = (
|
||||
"The rain intensifies, making the planks of the bridge even more slippery.",
|
||||
"A gush of wind throws the rain right in your face.",
|
||||
"The rainfall eases a bit and the sky momentarily brightens.",
|
||||
"The bridge shakes under the thunder of a closeby thunder strike.",
|
||||
"The rain pummels you with large, heavy drops. You hear the distinct howl of a large hound in the distance.",
|
||||
"The wind is picking up, howling around you and causing the bridge to sway from side to side.",
|
||||
"Some sort of large bird sweeps by overhead, giving off an eery screech. Soon it has disappeared in the gloom.",
|
||||
"The bridge sways from side to side in the wind.",
|
||||
"Below you a particularly large wave crashes into the rocks.",
|
||||
"From the ruin you hear a distant, otherwordly howl. Or maybe it was just the wind.")
|
||||
|
||||
|
||||
class BridgeRoom(WeatherRoom):
|
||||
"""
|
||||
The bridge room implements an unsafe bridge. It also enters the player into
|
||||
a state where they get new commands so as to try to cross the bridge.
|
||||
|
||||
We want this to result in the player getting a special set of
|
||||
commands related to crossing the bridge. The result is that it
|
||||
will take several steps to cross it, despite it being represented
|
||||
by only a single room.
|
||||
|
||||
We divide the bridge into steps:
|
||||
|
||||
self.db.west_exit - - | - - self.db.east_exit
|
||||
0 1 2 3 4
|
||||
|
||||
The position is handled by a variable stored on the character
|
||||
when entering and giving special move commands will
|
||||
increase/decrease the counter until the bridge is crossed.
|
||||
|
||||
We also has self.db.fall_exit, which points to a gathering
|
||||
location to end up if we happen to fall off the bridge (used by
|
||||
the CmdLookBridge command).
|
||||
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"Setups the room"
|
||||
# this will start the weather room's ticker and tell
|
||||
# it to call update_weather regularly.
|
||||
super(BridgeRoom, self).at_object_creation()
|
||||
# this identifies the exits from the room (should be the command
|
||||
# needed to leave through that exit). These are defaults, but you
|
||||
# could of course also change them after the room has been created.
|
||||
self.db.west_exit = "cliff"
|
||||
self.db.east_exit = "gate"
|
||||
self.db.fall_exit = "cliffledge"
|
||||
# add the cmdset on the room.
|
||||
self.cmdset.add_default(BridgeCmdSet)
|
||||
|
||||
def update_weather(self, *args, **kwargs):
|
||||
"""
|
||||
This is called at irregular intervals and makes the passage
|
||||
over the bridge a little more interesting.
|
||||
"""
|
||||
if random.random() < 80:
|
||||
# send a message most of the time
|
||||
self.msg_contents("{w%s{n" % random.choice(BRIDGE_WEATHER))
|
||||
|
||||
def at_object_receive(self, character, source_location):
|
||||
"""
|
||||
This hook is called by the engine whenever the player is moved
|
||||
into this room.
|
||||
"""
|
||||
if character.has_player:
|
||||
# we only run this if the entered object is indeed a player object.
|
||||
# check so our east/west exits are correctly defined.
|
||||
wexit = search_object(self.db.west_exit)
|
||||
eexit = search_object(self.db.east_exit)
|
||||
fexit = search_object(self.db.fall_exit)
|
||||
if not (wexit and eexit and fexit):
|
||||
character.msg("The bridge's exits are not properly configured. "\
|
||||
"Contact an admin. Forcing west-end placement.")
|
||||
character.db.tutorial_bridge_position = 0
|
||||
return
|
||||
if source_location == eexit[0]:
|
||||
# we assume we enter from the same room we will exit to
|
||||
character.db.tutorial_bridge_position = 4
|
||||
else:
|
||||
# if not from the east, then from the west!
|
||||
character.db.tutorial_bridge_position = 0
|
||||
|
||||
def at_object_leave(self, character, target_location):
|
||||
"""
|
||||
This is triggered when the player leaves the bridge room.
|
||||
"""
|
||||
if character.has_player:
|
||||
# clean up the position attribute
|
||||
del character.db.tutorial_bridge_position
|
||||
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
#
|
||||
# Dark Room - a room with states
|
||||
#
|
||||
# This room limits the movemenets of its denizens unless they carry an active
|
||||
# LightSource object (LightSource is defined in
|
||||
# tutorialworld.objects.LightSource)
|
||||
#
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
|
||||
DARK_MESSAGES = ("It is pitch black. You are likely to be eaten by a grue.",
|
||||
"It's pitch black. You fumble around but cannot find anything.",
|
||||
"You don't see a thing. You feel around, managing to bump your fingers hard against something. Ouch!",
|
||||
"You don't see a thing! Blindly grasping the air around you, you find nothing.",
|
||||
"It's totally dark here. You almost stumble over some un-evenness in the ground.",
|
||||
"You are completely blind. For a moment you think you hear someone breathing nearby ... \n ... surely you must be mistaken.",
|
||||
"Blind, you think you find some sort of object on the ground, but it turns out to be just a stone.",
|
||||
"Blind, you bump into a wall. The wall seems to be covered with some sort of vegetation, but its too damp to burn.",
|
||||
"You can't see anything, but the air is damp. It feels like you are far underground.")
|
||||
|
||||
ALREADY_LIGHTSOURCE = "You don't want to stumble around in blindness anymore. You already " \
|
||||
"found what you need. Let's get light already!"
|
||||
|
||||
FOUND_LIGHTSOURCE = "Your fingers bump against a splinter of wood in a corner. It smells of resin and seems dry enough to burn! " \
|
||||
"You pick it up, holding it firmly. Now you just need to {wlight{n it using the flint and steel you carry with you."
|
||||
|
||||
|
||||
class CmdLookDark(Command):
|
||||
"""
|
||||
Look around in darkness
|
||||
|
||||
Usage:
|
||||
look
|
||||
|
||||
Look around in the darkness, trying
|
||||
to find something.
|
||||
"""
|
||||
key = "look"
|
||||
aliases = ["l", 'feel', 'search', 'feel around', 'fiddle']
|
||||
locks = "cmd:all()"
|
||||
help_category = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
Implement the command.
|
||||
|
||||
This works both as a look and a search command; there is a
|
||||
random chance of eventually finding a light source.
|
||||
"""
|
||||
caller = self.caller
|
||||
|
||||
if random.random() < 0.8:
|
||||
# we don't find anything
|
||||
caller.msg(random.choice(DARK_MESSAGES))
|
||||
else:
|
||||
# we could have found something!
|
||||
if any(obj for obj in caller.contents if utils.inherits_from(obj, LightSource)):
|
||||
# we already carry a LightSource object.
|
||||
caller.msg(ALREADY_LIGHTSOURCE)
|
||||
else:
|
||||
# don't have a light source, create a new one.
|
||||
create_object(LightSource, key="splinter", location=caller)
|
||||
caller.msg(FOUND_LIGHTSOURCE)
|
||||
|
||||
|
||||
class CmdDarkHelp(Command):
|
||||
"""
|
||||
Help command for the dark state.
|
||||
"""
|
||||
key = "help"
|
||||
locks = "cmd:all()"
|
||||
help_category = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
Replace the the help command with a not-so-useful help
|
||||
"""
|
||||
string = "Can't help you until you find some light! Try looking/feeling around for something to burn. " \
|
||||
"You shouldn't give up even if you don't find anything right away."
|
||||
self.caller.msg(string)
|
||||
|
||||
|
||||
class CmdDarkNoMatch(Command):
|
||||
"""
|
||||
This is a system command. Commands with special keys are used to
|
||||
override special sitations in the game. The CMD_NOMATCH is used
|
||||
when the given command is not found in the current command set (it
|
||||
replaces Evennia's default behavior or offering command
|
||||
suggestions)
|
||||
"""
|
||||
key = syscmdkeys.CMD_NOMATCH
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"Implements the command."
|
||||
self.caller.msg("Until you find some light, there's not much you can do. Try feeling around.")
|
||||
|
||||
|
||||
class DarkCmdSet(CmdSet):
|
||||
"""
|
||||
Groups the commands of the dark room together. We also import the
|
||||
default say command here so that players can still talk in the
|
||||
darkness.
|
||||
|
||||
We give the cmdset the mergetype "Replace" to make sure it
|
||||
completely replaces whichever command set it is merged onto
|
||||
(usually the default cmdset)
|
||||
"""
|
||||
key = "darkroom_cmdset"
|
||||
mergetype = "Replace"
|
||||
priority = 2
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"populate the cmdset."
|
||||
self.add(CmdTutorial())
|
||||
self.add(CmdLookDark())
|
||||
self.add(CmdDarkHelp())
|
||||
self.add(CmdDarkNoMatch())
|
||||
self.add(default_cmds.CmdSay)
|
||||
|
||||
|
||||
class DarkRoom(TutorialRoom):
|
||||
"""
|
||||
A dark room. This tries to start the DarkState script on all
|
||||
objects entering. The script is responsible for making sure it is
|
||||
valid (that is, that there is no light source shining in the room).
|
||||
|
||||
The is_lit Attribute is used to define if the room is currently lit
|
||||
or not, so as to properly echo state changes.
|
||||
|
||||
Since this room (in the tutorial) is meant as a sort of catch-all,
|
||||
we also make sure to heal characters ending up here, since they
|
||||
may have been beaten up by the ghostly apparition at this point.
|
||||
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"""
|
||||
Called when object is first created.
|
||||
"""
|
||||
super(DarkRoom, self).at_object_creation()
|
||||
self.db.tutorial_info = "This is a room with custom command sets on itself."
|
||||
# the room starts dark.
|
||||
self.db.is_lit = False
|
||||
self.cmdset.add(DarkCmdSet, permanent=True)
|
||||
|
||||
def at_init(self):
|
||||
"""
|
||||
Called when room is first recached (such as after a reload)
|
||||
"""
|
||||
self.check_light_state()
|
||||
|
||||
def _carries_light(self, obj):
|
||||
"""
|
||||
Checks if the given object carries anything that gives light.
|
||||
|
||||
Note that we do NOT look for a specific LightSource typeclass,
|
||||
but for the Attribute is_giving_light - this makes it easy to
|
||||
later add other types of light-giving items. We also accept
|
||||
if there is a light-giving object in the room overall (like if
|
||||
a splinter was dropped in the room)
|
||||
"""
|
||||
return obj.is_superuser or obj.db.is_giving_light or obj.is_superuser or any(o for o in obj.contents if o.db.is_giving_light)
|
||||
|
||||
def _heal(self, character):
|
||||
"""
|
||||
Heal a character.
|
||||
"""
|
||||
health = character.db.health_max or 20
|
||||
character.db.health = health
|
||||
|
||||
def check_light_state(self):
|
||||
"""
|
||||
This method checks if there are any light sources in the room.
|
||||
If there isn't it makes sure to add the dark cmdset to all
|
||||
characters in the room. It is called whenever characters enter
|
||||
the room and also by the Light sources when they turn on.
|
||||
"""
|
||||
if any(self._carries_light(obj) for obj in self.contents):
|
||||
self.cmdset.remove(DarkCmdSet)
|
||||
self.db.is_lit = True
|
||||
for char in (obj for obj in self.contents if obj.has_player):
|
||||
# this won't do anything if it is already removed
|
||||
char.msg("The room is lit up.")
|
||||
else:
|
||||
# noone is carrying light - darken the room
|
||||
self.db.is_lit = False
|
||||
self.cmdset.add(DarkCmdSet, permanent=True)
|
||||
for char in (obj for obj in self.contents if obj.has_player):
|
||||
if char.is_superuser:
|
||||
char.msg("You are Superuser, so you are not affected by the dark state.")
|
||||
else:
|
||||
# put players in darkness
|
||||
char.msg("The room is completely dark.")
|
||||
|
||||
def at_object_receive(self, obj, source_location):
|
||||
"""
|
||||
Called when an object enters the room.
|
||||
"""
|
||||
if obj.has_player:
|
||||
# a puppeted object, that is, a Character
|
||||
self._heal(obj)
|
||||
# in case the new guy carries light with them
|
||||
self.check_light_state()
|
||||
|
||||
def at_object_leave(self, obj, target_location):
|
||||
"""
|
||||
In case people leave with the light, we make sure to clear the
|
||||
DarkCmdSet if necessary. This also works if they are
|
||||
teleported away.
|
||||
"""
|
||||
self.check_light_state()
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Teleport room - puzzles solution
|
||||
#
|
||||
# This is a sort of puzzle room that requires a certain
|
||||
# attribute on the entering character to be the same as
|
||||
# an attribute of the room. If not, the character will
|
||||
# be teleported away to a target location. This is used
|
||||
# by the Obelisk - grave chamber puzzle, where one must
|
||||
# have looked at the obelisk to get an attribute set on
|
||||
# oneself, and then pick the grave chamber with the
|
||||
# matching imagery for this attribute.
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
|
||||
class TeleportRoom(TutorialRoom):
|
||||
"""
|
||||
Teleporter - puzzle room.
|
||||
|
||||
Important attributes (set at creation):
|
||||
puzzle_key - which attr to look for on character
|
||||
puzzle_value - what char.db.puzzle_key must be set to
|
||||
success_teleport_to - where to teleport in case if success
|
||||
success_teleport_msg - message to echo while teleporting to success
|
||||
failure_teleport_to - where to teleport to in case of failure
|
||||
failure_teleport_msg - message to echo while teleporting to failure
|
||||
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"Called at first creation"
|
||||
super(TeleportRoom, self).at_object_creation()
|
||||
# what character.db.puzzle_clue must be set to, to avoid teleportation.
|
||||
self.db.puzzle_value = 1
|
||||
# target of successful teleportation. Can be a dbref or a
|
||||
# unique room name.
|
||||
self.db.success_teleport_msg = "You are successful!"
|
||||
self.db.success_teleport_to = "treasure room"
|
||||
# the target of the failure teleportation.
|
||||
self.db.failure_teleport_msg = "You fail!"
|
||||
self.db.failure_teleport_to = "dark cell"
|
||||
|
||||
def at_object_receive(self, character, source_location):
|
||||
"""
|
||||
This hook is called by the engine whenever the player is moved into
|
||||
this room.
|
||||
"""
|
||||
if not character.has_player:
|
||||
# only act on player characters.
|
||||
return
|
||||
# determine if the puzzle is a success or not
|
||||
is_success = str(character.db.puzzle_clue) == str(self.db.puzzle_value)
|
||||
teleport_to = self.db.success_teleport_to if is_success else self.db.failure_teleport_to
|
||||
# note that this returns a list
|
||||
results = search_object(teleport_to)
|
||||
if not results or len(results) > 1:
|
||||
# we cannot move anywhere since no valid target was found.
|
||||
print "no valid teleport target for %s was found." % teleport_to
|
||||
return
|
||||
if character.is_superuser:
|
||||
# superusers don't get teleported
|
||||
character.msg("Superuser block: You would have been teleported to %s." % results[0])
|
||||
return
|
||||
# perform the teleport
|
||||
if is_success:
|
||||
character.msg(self.db.success_teleport_msg)
|
||||
else:
|
||||
character.msg(self.db.failure_teleport_msg)
|
||||
# teleport quietly to the new place
|
||||
character.move_to(results[0], quiet=True, move_hooks=False)
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Outro room - unique exit room
|
||||
#
|
||||
# Cleans up the character from all tutorial-related properties.
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
class OutroRoom(TutorialRoom):
|
||||
"""
|
||||
Outro room.
|
||||
|
||||
Called when exiting the tutorial, cleans the
|
||||
character of tutorial-related attributes.
|
||||
|
||||
"""
|
||||
|
||||
def at_object_creation(self):
|
||||
"""
|
||||
Called when the room is first created.
|
||||
"""
|
||||
super(OutroRoom, self).at_object_creation()
|
||||
self.db_tutorial_info = "The last room of the tutorial. " \
|
||||
"This cleans up all temporary Attributes " \
|
||||
"the tutorial may have assigned to the "\
|
||||
"character."
|
||||
|
||||
def at_object_receive(self, character, source_location):
|
||||
"""
|
||||
Do cleanup.
|
||||
"""
|
||||
if character.has_player:
|
||||
if self.db.wracklist:
|
||||
for wrackid in self.db.wracklist:
|
||||
character.del_attribute(wrackid)
|
||||
del character.db.health_max
|
||||
del character.db.health
|
||||
del character.db.last_climbed
|
||||
del character.db.puzzle_clue
|
||||
del character.db.combat_parry_mode
|
||||
del character.db.tutorial_bridge_position
|
||||
character.tags.clear(category="tutorial_world")
|
||||
51
evennia/game_template/.gitignore
vendored
Normal file
51
evennia/game_template/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
*.py[cod]
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Packages
|
||||
*.egg
|
||||
*.egg-info
|
||||
dist
|
||||
build
|
||||
eggs
|
||||
parts
|
||||
var
|
||||
sdist
|
||||
develop-eggs
|
||||
.installed.cfg
|
||||
lib
|
||||
lib64
|
||||
__pycache__
|
||||
|
||||
# Other
|
||||
*.swp
|
||||
*.log
|
||||
*.pid
|
||||
*.restart
|
||||
*.db3
|
||||
|
||||
# Installation-specific
|
||||
server/conf/settings.py
|
||||
server/logs/*.log.*
|
||||
web/static/*
|
||||
web/media/*
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
.coverage
|
||||
.tox
|
||||
nosetests.xml
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
||||
# Mr Developer
|
||||
.mr.developer.cfg
|
||||
.project
|
||||
.pydevproject
|
||||
|
||||
# PyCharm config
|
||||
.idea
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue