Merged the devel-branch.

This commit is contained in:
Griatch 2015-03-01 13:06:48 +01:00
commit 96ac3f967b
413 changed files with 15558 additions and 20456 deletions

7
.gitignore vendored
View file

@ -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
View file

@ -0,0 +1,10 @@
language: python
python:
- "2.7"
install: pip install .
script:
- evennia --init dummy
- cd dummy
- evennia migrate
- evennia test evennia

View file

@ -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
View 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.

View file

@ -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
View 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!

View file

@ -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.

View file

@ -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

View file

@ -1 +0,0 @@
Beta-GIT

10
bin/unix/evennia Executable file
View 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
View 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()

View file

@ -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.

View file

@ -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>

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -1,4 +0,0 @@
from pool import deferToAMPProcess, pp
from commands import Shutdown, Ping, Echo
from child import AMPChild
__version__ = "0.2.1"

View file

@ -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)

View file

@ -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())]

View file

@ -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.
"""

View file

@ -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"})

View file

@ -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)

View file

@ -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)

View file

@ -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()

View file

@ -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())

View file

@ -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}
)

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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!

View file

@ -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)

View file

@ -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()

View file

@ -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())

View file

@ -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

View file

@ -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
View file

@ -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
View file

@ -0,0 +1 @@
0.5.0

291
evennia/__init__.py Normal file
View 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

View 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.
"""

View file

@ -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)

View file

@ -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):
"""

View file

@ -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)

View file

@ -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):
"""

View file

@ -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):

View file

@ -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

View file

@ -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]

View file

@ -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)"

View file

@ -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.
#

View file

@ -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())

View file

@ -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):

View file

@ -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):
"""

View file

@ -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):

View file

@ -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>"

View file

@ -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"

View file

@ -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")

View file

@ -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:

View file

@ -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

View file

@ -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)

View file

@ -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()

View 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", "")

View file

@ -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

View 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.
"""

View file

@ -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):

View file

@ -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

View file

@ -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)

View file

@ -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

View 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),
]

View 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),
]

View file

@ -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
View 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.

View 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.
"""

View file

@ -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

View file

@ -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

View file

@ -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):

View file

@ -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",

View file

@ -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))

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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,

View file

@ -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)

View file

@ -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
"""

View file

@ -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())

View file

@ -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:

View file

@ -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))

View file

@ -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.

View file

@ -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

View 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!

View 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()

View 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
View 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