diff --git a/.gitignore b/.gitignore index 93408a842..7f6c06873 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..86306cf74 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: python +python: + - "2.7" +install: pip install . +script: + - evennia --init dummy + - cd dummy + - evennia migrate + - evennia test evennia + diff --git a/CHANGELOG.txt b/CHANGELOG.md similarity index 64% rename from CHANGELOG.txt rename to CHANGELOG.md index 098aa3e8a..c961ff219 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.md @@ -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 diff --git a/CODING_STYLE.md b/CODING_STYLE.md new file mode 100644 index 000000000..af196a3ad --- /dev/null +++ b/CODING_STYLE.md @@ -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. diff --git a/CODING_STYLE.txt b/CODING_STYLE.txt deleted file mode 100644 index 5557ab379..000000000 --- a/CODING_STYLE.txt +++ /dev/null @@ -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. diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 000000000..a3daa2b70 --- /dev/null +++ b/INSTALL.md @@ -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! diff --git a/INSTALL.txt b/INSTALL.txt deleted file mode 100644 index 2464c6f5d..000000000 --- a/INSTALL.txt +++ /dev/null @@ -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. diff --git a/README.md b/README.md index 260e57aca..25e341173 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,75 @@ -Evennia MUD/MU\* Creation System -================================ +# Evennia MUD/MU\* Creation System ![evennia logo][logo] -*Evennia* is a Python-based MUD/MU\* server/codebase using modern technologies. It is made available as open source under the very friendly [BSD license](https://github.com/evennia/evennia/wiki/Licensing). Evennia allows creators to design and flesh out text-based massively-multiplayer online games with great freedom. +*Evennia* is a modern library for creating [online multiplayer text +games][wikimudpage] (MUD, MUSH, MUX, MOO etc) in pure Python. It allows game +creators to design and flesh out their games with great freedom. +Evennia is made available under the very friendly [BSD license][license]. -http://www.evennia.com is the main hub tracking all things Evennia. The documentation wiki is found [here](https://github.com/evennia/evennia/wiki). +http://www.evennia.com is the main hub tracking all things Evennia. -Features and Philosophy ------------------------ -Evennia aims to supply a bare-bones MU\* codebase that allows vast flexibility for game designers while taking care of all the gritty networking and database-handling behind the scenes. Evennia offers an easy API for handling persistent objects, time-dependent scripting and all the other low-level features needed to create a MU\*. The idea is to allow the mud-coder to concentrate solely on designing the parts and systems of the mud that makes it uniquely fit their ideas. +## Features and Philosophy -Coding in Evennia is primarily done by normal Python modules, making the codebase extremely flexible. The code is heavily documented and you use Python classes to represent your objects, scripts and players. The database layer is abstracted away. +Evennia aims to supply a bare-bones MU\* codebase that allows vast +flexibility for game designers while taking care of all the gritty +networking and database-handling behind the scenes. Evennia offers an +easy API for handling persistent objects, time-dependent scripting and +all the other low-level features needed to create an online text-based +game. The idea is to allow the mud-coder to concentrate solely on +designing the parts and systems of the mud that makes it uniquely fit +their ideas. -Evennia offers extensive connectivity options. A single server instance may offer connections over Telnet, SSH, SSL and HTTP. The latter is possible since Evennia is also its own web server: A default website as well as a browser-based comet-style mud client comes as part of the package ([screenshot](https://github.com/evennia/evennia/wiki/Screenshot)). Due to our Django and Twisted foundations, web integration is a snap since the same code that powers the game may also be used to run its web presence (you may use a third-party webserver too if you prefer though). Evennia in-game channels can also be interlinked with external IRC and IMC2 channels so players can chat with people "outside" the game. +Coding in Evennia is primarily done by normal Python modules, making +the codebase extremely flexible. The code is heavily documented and +you use Python classes to represent your objects, scripts and players. +The database layer is abstracted away. -Whereas Evennia is intended to be customized to almost any level you like, we do offer some defaults you can build from. The code base comes with basic classes for objects, exits, rooms and characters. There is also a default command set for handling administration, building, chat channels, poses and so on. This is enough to run a 'Talker' or some other social-style game out of the box. Stock Evennia is however deliberately void of any game-world-specific systems. So you won't find any AI codes, mobs, skill systems, races or combat stats in the default distribution (we might expand our contributions folder with optional plugins in the future though). +![screenshot][screenshot] -If this piqued your interest, there is also a [lengthier introduction](https://github.com/evennia/evennia/wiki/Evennia-Introduction) to Evennia to read. +Evennia offers extensive connectivity options. A single server +instance may offer connections over Telnet, SSH, SSL and HTTP. The +latter is possible since Evennia is also its own web server: A default +website as well as a browser-based comet-style mud client comes as +part of the package. -Current Status --------------- +Due to our Django and Twisted foundations, web integration is +easy since the same code that powers the game may also be used to run +its web presence. -The codebase is currently in **Beta**. While development continues, Evennia is already stable enough to be suitable for prototyping and development of your own games. +Whereas Evennia is intended to be customized to almost any level you +like, we do offer some defaults you can build from. The code base +comes with basic classes for objects, exits, rooms and characters. +There is also a default command set for handling administration, +building, chat channels, poses and so on. This is enough to run a +'Talker' or some other social-style game out of the box. Stock Evennia +is however deliberately void of any game-world-specific systems. So +you won't find any AI codes, mobs, skill systems, races or combat +stats in the default distribution (we might expand our contributions +folder with optional plugins in the future though). -More Information ----------------- +## Current Status -To learn how to get your hands on the code base, the [Getting started](https://github.com/evennia/evennia/wiki/Getting-Started) page is the way to go. Otherwise you could browse the [Documentation wiki](https://github.com/evennia/evennia/wiki) or why not come join the [Evennia Community](http://www.evennia.com). Welcome! +The codebase is currently in **Beta**. While development continues, +Evennia is already stable enough to be suitable for prototyping and +development of your own games. + +## Where to go from here + +If this piqued your interest, there is a [lengthier introduction][introduction] to read. + +To learn how to get your hands on the code base, the [Getting started][gettingstarted] page +is the way to go. Otherwise you could browse +the [Documentation][wiki] or why not come join the [Evennia Community forum][group] +or join us in our [development chat][chat]. Welcome! + + +[homepage]: http://www.evennia.com +[gettingstarted]: http://github.com/evennia/evennia/wiki/Getting-Started +[wiki]: https://github.com/evennia/evennia/wiki +[screenshot]: https://raw.githubusercontent.com/wiki/evennia/evennia/images/evennia_screenshot3.png +[logo]: https://github.com/evennia/evennia/blob/devel/evennia/web/static/evennia_general/images/evennia_logo.png +[introduction]: https://github.com/evennia/evennia/wiki/Evennia-Introduction +[license]: https://github.com/evennia/evennia/wiki/Licensing +[group]: https://groups.google.com/forum/#!forum/evennia +[chat]: http://webchat.freenode.net/?channels=evennia&uio=MT1mYWxzZSY5PXRydWUmMTE9MTk1JjEyPXRydWUbb +[wikimudpage]: http://en.wikipedia.org/wiki/MUD diff --git a/VERSION.txt b/VERSION.txt deleted file mode 100644 index 4cc8a67cd..000000000 --- a/VERSION.txt +++ /dev/null @@ -1 +0,0 @@ -Beta-GIT diff --git a/bin/unix/evennia b/bin/unix/evennia new file mode 100755 index 000000000..fbb3f9a73 --- /dev/null +++ b/bin/unix/evennia @@ -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() diff --git a/bin/windows/evennia_launcher.py b/bin/windows/evennia_launcher.py new file mode 100755 index 000000000..1784b05fe --- /dev/null +++ b/bin/windows/evennia_launcher.py @@ -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() diff --git a/contrib/README b/contrib/README deleted file mode 100644 index 57bed4319..000000000 --- a/contrib/README +++ /dev/null @@ -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. diff --git a/contrib/battle_for_evennia/README b/contrib/battle_for_evennia/README deleted file mode 100644 index 5e43939e4..000000000 --- a/contrib/battle_for_evennia/README +++ /dev/null @@ -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 - - block (anti-flee) - - switch - 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) \ No newline at end of file diff --git a/contrib/procpools/README.txt b/contrib/procpools/README.txt deleted file mode 100644 index 0b1b91bc4..000000000 --- a/contrib/procpools/README.txt +++ /dev/null @@ -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. diff --git a/contrib/procpools/ampoule/COPYING.txt b/contrib/procpools/ampoule/COPYING.txt deleted file mode 100644 index 89d86c8ac..000000000 --- a/contrib/procpools/ampoule/COPYING.txt +++ /dev/null @@ -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. diff --git a/contrib/procpools/ampoule/EVENNIA.txt b/contrib/procpools/ampoule/EVENNIA.txt deleted file mode 100644 index 09cc95ef0..000000000 --- a/contrib/procpools/ampoule/EVENNIA.txt +++ /dev/null @@ -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. diff --git a/contrib/procpools/ampoule/__init__.py b/contrib/procpools/ampoule/__init__.py deleted file mode 100644 index ed99c5ef2..000000000 --- a/contrib/procpools/ampoule/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from pool import deferToAMPProcess, pp -from commands import Shutdown, Ping, Echo -from child import AMPChild -__version__ = "0.2.1" diff --git a/contrib/procpools/ampoule/child.py b/contrib/procpools/ampoule/child.py deleted file mode 100644 index d1fade960..000000000 --- a/contrib/procpools/ampoule/child.py +++ /dev/null @@ -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) diff --git a/contrib/procpools/ampoule/commands.py b/contrib/procpools/ampoule/commands.py deleted file mode 100644 index 2ac61e7d0..000000000 --- a/contrib/procpools/ampoule/commands.py +++ /dev/null @@ -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())] diff --git a/contrib/procpools/ampoule/iampoule.py b/contrib/procpools/ampoule/iampoule.py deleted file mode 100644 index caa3e0142..000000000 --- a/contrib/procpools/ampoule/iampoule.py +++ /dev/null @@ -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. - """ - diff --git a/contrib/procpools/ampoule/main.py b/contrib/procpools/ampoule/main.py deleted file mode 100644 index 1f46deb3f..000000000 --- a/contrib/procpools/ampoule/main.py +++ /dev/null @@ -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"}) diff --git a/contrib/procpools/ampoule/pool.py b/contrib/procpools/ampoule/pool.py deleted file mode 100644 index d90921dad..000000000 --- a/contrib/procpools/ampoule/pool.py +++ /dev/null @@ -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) diff --git a/contrib/procpools/ampoule/rpool.py b/contrib/procpools/ampoule/rpool.py deleted file mode 100644 index 642b98835..000000000 --- a/contrib/procpools/ampoule/rpool.py +++ /dev/null @@ -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) - diff --git a/contrib/procpools/ampoule/service.py b/contrib/procpools/ampoule/service.py deleted file mode 100644 index bded0ac99..000000000 --- a/contrib/procpools/ampoule/service.py +++ /dev/null @@ -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() diff --git a/contrib/procpools/ampoule/test/test_process.py b/contrib/procpools/ampoule/test/test_process.py deleted file mode 100644 index b35de9981..000000000 --- a/contrib/procpools/ampoule/test/test_process.py +++ /dev/null @@ -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()) diff --git a/contrib/procpools/ampoule/test/test_proxy.py b/contrib/procpools/ampoule/test/test_proxy.py deleted file mode 100644 index 0ba09b2db..000000000 --- a/contrib/procpools/ampoule/test/test_proxy.py +++ /dev/null @@ -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} - ) diff --git a/contrib/procpools/ampoule/util.py b/contrib/procpools/ampoule/util.py deleted file mode 100644 index 738e5f2ed..000000000 --- a/contrib/procpools/ampoule/util.py +++ /dev/null @@ -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 diff --git a/contrib/procpools/python_procpool.py b/contrib/procpools/python_procpool.py deleted file mode 100644 index 1a1e483fb..000000000 --- a/contrib/procpools/python_procpool.py +++ /dev/null @@ -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) diff --git a/contrib/procpools/python_procpool_plugin.py b/contrib/procpools/python_procpool_plugin.py deleted file mode 100644 index 1ddc1f476..000000000 --- a/contrib/procpools/python_procpool_plugin.py +++ /dev/null @@ -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) - - - diff --git a/contrib/tutorial_world/README b/contrib/tutorial_world/README deleted file mode 100644 index 97042a576..000000000 --- a/contrib/tutorial_world/README +++ /dev/null @@ -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! \ No newline at end of file diff --git a/contrib/tutorial_world/mob.py b/contrib/tutorial_world/mob.py deleted file mode 100644 index bbdb4c561..000000000 --- a/contrib/tutorial_world/mob.py +++ /dev/null @@ -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) \ No newline at end of file diff --git a/contrib/tutorial_world/rooms.py b/contrib/tutorial_world/rooms.py deleted file mode 100644 index 514124a47..000000000 --- a/contrib/tutorial_world/rooms.py +++ /dev/null @@ -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() diff --git a/contrib/tutorial_world/scripts.py b/contrib/tutorial_world/scripts.py deleted file mode 100644 index 42c28cf41..000000000 --- a/contrib/tutorial_world/scripts.py +++ /dev/null @@ -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()) \ No newline at end of file diff --git a/docs/README.txt b/docs/README.txt deleted file mode 100644 index cc09c8563..000000000 --- a/docs/README.txt +++ /dev/null @@ -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 - - /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 - - /sphinx/.build/index.html - - diff --git a/docs/config.dox b/docs/config.dox deleted file mode 100644 index fb73095d6..000000000 --- a/docs/config.dox +++ /dev/null @@ -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 diff --git a/ev.py b/ev.py deleted file mode 100644 index af05cb09c..000000000 --- a/ev.py +++ /dev/null @@ -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 diff --git a/evennia/VERSION.txt b/evennia/VERSION.txt new file mode 100644 index 000000000..8f0916f76 --- /dev/null +++ b/evennia/VERSION.txt @@ -0,0 +1 @@ +0.5.0 diff --git a/evennia/__init__.py b/evennia/__init__.py new file mode 100644 index 000000000..53e6a8b30 --- /dev/null +++ b/evennia/__init__.py @@ -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. 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 diff --git a/evennia/commands/__init__.py b/evennia/commands/__init__.py new file mode 100644 index 000000000..5f77fa3a3 --- /dev/null +++ b/evennia/commands/__init__.py @@ -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. + +""" diff --git a/src/commands/cmdhandler.py b/evennia/commands/cmdhandler.py similarity index 57% rename from src/commands/cmdhandler.py rename to evennia/commands/cmdhandler.py index de21c3c1c..2e6468239 100644 --- a/src/commands/cmdhandler.py +++ b/evennia/commands/cmdhandler.py @@ -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) diff --git a/src/commands/cmdparser.py b/evennia/commands/cmdparser.py similarity index 98% rename from src/commands/cmdparser.py rename to evennia/commands/cmdparser.py index 2bcc98254..7efbb1645 100644 --- a/src/commands/cmdparser.py +++ b/evennia/commands/cmdparser.py @@ -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): """ diff --git a/src/commands/cmdset.py b/evennia/commands/cmdset.py similarity index 99% rename from src/commands/cmdset.py rename to evennia/commands/cmdset.py index d1d745e75..3257c649d 100644 --- a/src/commands/cmdset.py +++ b/evennia/commands/cmdset.py @@ -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) diff --git a/src/commands/cmdsethandler.py b/evennia/commands/cmdsethandler.py similarity index 97% rename from src/commands/cmdsethandler.py rename to evennia/commands/cmdsethandler.py index c8b0c7cf7..b10e15dd1 100644 --- a/src/commands/cmdsethandler.py +++ b/evennia/commands/cmdsethandler.py @@ -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): """ diff --git a/src/commands/command.py b/evennia/commands/command.py similarity index 99% rename from src/commands/command.py rename to evennia/commands/command.py index bec477b9b..2eae1bb08 100644 --- a/src/commands/command.py +++ b/evennia/commands/command.py @@ -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): diff --git a/src/commands/connection_screen.py b/evennia/commands/connection_screen.py similarity index 88% rename from src/commands/connection_screen.py rename to evennia/commands/connection_screen.py index 4c0d26809..c164fe62f 100644 --- a/src/commands/connection_screen.py +++ b/evennia/commands/connection_screen.py @@ -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 diff --git a/src/commands/default/__init__.py b/evennia/commands/default/__init__.py similarity index 100% rename from src/commands/default/__init__.py rename to evennia/commands/default/__init__.py diff --git a/src/commands/default/admin.py b/evennia/commands/default/admin.py similarity index 98% rename from src/commands/default/admin.py rename to evennia/commands/default/admin.py index 9d0230f4b..3301c7f6e 100644 --- a/src/commands/default/admin.py +++ b/evennia/commands/default/admin.py @@ -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] diff --git a/src/commands/default/batchprocess.py b/evennia/commands/default/batchprocess.py similarity index 97% rename from src/commands/default/batchprocess.py rename to evennia/commands/default/batchprocess.py index 08a865bec..1e2a45238 100644 --- a/src/commands/default/batchprocess.py +++ b/evennia/commands/default/batchprocess.py @@ -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 + jj Jump to specific command number """ - key = "j" + key = "jj" help_category = "BatchProcess" locks = "cmd:perm(batchcommands)" diff --git a/src/commands/default/building.py b/evennia/commands/default/building.py similarity index 91% rename from src/commands/default/building.py rename to evennia/commands/default/building.py index b888d3344..f634b492a 100644 --- a/src/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -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] [= ] @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. # diff --git a/src/commands/default/cmdset_character.py b/evennia/commands/default/cmdset_character.py similarity index 91% rename from src/commands/default/cmdset_character.py rename to evennia/commands/default/cmdset_character.py index b7d533c13..14a2c09f7 100644 --- a/src/commands/default/cmdset_character.py +++ b/evennia/commands/default/cmdset_character.py @@ -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()) diff --git a/src/commands/default/cmdset_player.py b/evennia/commands/default/cmdset_player.py similarity index 92% rename from src/commands/default/cmdset_player.py rename to evennia/commands/default/cmdset_player.py index 81e1485f3..e1dfd7642 100644 --- a/src/commands/default/cmdset_player.py +++ b/evennia/commands/default/cmdset_player.py @@ -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): diff --git a/src/commands/default/cmdset_session.py b/evennia/commands/default/cmdset_session.py similarity index 76% rename from src/commands/default/cmdset_session.py rename to evennia/commands/default/cmdset_session.py index fe33962d9..8095d3119 100644 --- a/src/commands/default/cmdset_session.py +++ b/evennia/commands/default/cmdset_session.py @@ -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): """ diff --git a/src/commands/default/cmdset_unloggedin.py b/evennia/commands/default/cmdset_unloggedin.py similarity index 88% rename from src/commands/default/cmdset_unloggedin.py rename to evennia/commands/default/cmdset_unloggedin.py index 06b0d40c5..6038fa5f3 100644 --- a/src/commands/default/cmdset_unloggedin.py +++ b/evennia/commands/default/cmdset_unloggedin.py @@ -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): diff --git a/src/commands/default/comms.py b/evennia/commands/default/comms.py similarity index 96% rename from src/commands/default/comms.py rename to evennia/commands/default/comms.py index a507d236f..5d7e3afef 100644 --- a/src/commands/default/comms.py +++ b/evennia/commands/default/comms.py @@ -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 ") # 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 = " diff --git a/src/commands/default/general.py b/evennia/commands/default/general.py similarity index 90% rename from src/commands/default/general.py rename to evennia/commands/default/general.py index 74303bb22..7fbfc9de8 100644 --- a/src/commands/default/general.py +++ b/evennia/commands/default/general.py @@ -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 = ") 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 + + 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" diff --git a/src/commands/default/help.py b/evennia/commands/default/help.py similarity index 97% rename from src/commands/default/help.py rename to evennia/commands/default/help.py index 02a11cada..54e5fb117 100644 --- a/src/commands/default/help.py +++ b/evennia/commands/default/help.py @@ -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") diff --git a/src/commands/default/muxcommand.py b/evennia/commands/default/muxcommand.py similarity index 96% rename from src/commands/default/muxcommand.py rename to evennia/commands/default/muxcommand.py index 965189d2a..1de7ac866 100644 --- a/src/commands/default/muxcommand.py +++ b/evennia/commands/default/muxcommand.py @@ -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: diff --git a/src/commands/default/player.py b/evennia/commands/default/player.py similarity index 91% rename from src/commands/default/player.py rename to evennia/commands/default/player.py index fe966246c..ba9bcbaad 100644 --- a/src/commands/default/player.py +++ b/evennia/commands/default/player.py @@ -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 diff --git a/src/commands/default/syscommands.py b/evennia/commands/default/syscommands.py similarity index 89% rename from src/commands/default/syscommands.py rename to evennia/commands/default/syscommands.py index 639d82142..29953f7d7 100644 --- a/src/commands/default/syscommands.py +++ b/evennia/commands/default/syscommands.py @@ -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) \ No newline at end of file + channel.msg(msgobj) diff --git a/src/commands/default/system.py b/evennia/commands/default/system.py similarity index 97% rename from src/commands/default/system.py rename to evennia/commands/default/system.py index 596177e24..7c9f8d815 100644 --- a/src/commands/default/system.py +++ b/evennia/commands/default/system.py @@ -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() diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py new file mode 100644 index 000000000..03afb59fe --- /dev/null +++ b/evennia/commands/default/tests.py @@ -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(), "", ":") + + 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] = [: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", "") + diff --git a/src/commands/default/unloggedin.py b/evennia/commands/default/unloggedin.py similarity index 94% rename from src/commands/default/unloggedin.py rename to evennia/commands/default/unloggedin.py index 4e25912bb..e6af6679c 100644 --- a/src/commands/default/unloggedin.py +++ b/evennia/commands/default/unloggedin.py @@ -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 diff --git a/evennia/comms/__init__.py b/evennia/comms/__init__.py new file mode 100644 index 000000000..e8f2bc844 --- /dev/null +++ b/evennia/comms/__init__.py @@ -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. + +""" diff --git a/src/comms/admin.py b/evennia/comms/admin.py similarity index 93% rename from src/comms/admin.py rename to evennia/comms/admin.py index 5a8c790c9..b972ed3da 100644 --- a/src/comms/admin.py +++ b/evennia/comms/admin.py @@ -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): diff --git a/src/comms/channelhandler.py b/evennia/comms/channelhandler.py similarity index 86% rename from src/comms/channelhandler.py rename to evennia/comms/channelhandler.py index 9bc0e00fd..97cf80ef0 100644 --- a/src/comms/channelhandler.py +++ b/evennia/comms/channelhandler.py @@ -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 diff --git a/src/comms/comms.py b/evennia/comms/comms.py similarity index 69% rename from src/comms/comms.py rename to evennia/comms/comms.py index 5e0aead3e..b9ad1bc0f 100644 --- a/src/comms/comms.py +++ b/evennia/comms/comms.py @@ -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) diff --git a/src/comms/managers.py b/evennia/comms/managers.py similarity index 66% rename from src/comms/managers.py rename to evennia/comms/managers.py index b2cbce4e5..1b73c3d37 100644 --- a/src/comms/managers.py +++ b/evennia/comms/managers.py @@ -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 diff --git a/src/comms/migrations/0001_initial.py b/evennia/comms/migrations/0001_initial.py similarity index 100% rename from src/comms/migrations/0001_initial.py rename to evennia/comms/migrations/0001_initial.py diff --git a/src/comms/migrations/0002_msg_db_hide_from_objects.py b/evennia/comms/migrations/0002_msg_db_hide_from_objects.py similarity index 100% rename from src/comms/migrations/0002_msg_db_hide_from_objects.py rename to evennia/comms/migrations/0002_msg_db_hide_from_objects.py diff --git a/src/comms/migrations/0003_auto_20140917_0756.py b/evennia/comms/migrations/0003_auto_20140917_0756.py similarity index 100% rename from src/comms/migrations/0003_auto_20140917_0756.py rename to evennia/comms/migrations/0003_auto_20140917_0756.py diff --git a/evennia/comms/migrations/0004_auto_20150118_1631.py b/evennia/comms/migrations/0004_auto_20150118_1631.py new file mode 100644 index 000000000..2b525c9b2 --- /dev/null +++ b/evennia/comms/migrations/0004_auto_20150118_1631.py @@ -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), + ] diff --git a/evennia/comms/migrations/0005_auto_20150223_1517.py b/evennia/comms/migrations/0005_auto_20150223_1517.py new file mode 100644 index 000000000..5fcdfcd66 --- /dev/null +++ b/evennia/comms/migrations/0005_auto_20150223_1517.py @@ -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), + ] diff --git a/__init__.py b/evennia/comms/migrations/__init__.py similarity index 100% rename from __init__.py rename to evennia/comms/migrations/__init__.py diff --git a/src/comms/models.py b/evennia/comms/models.py similarity index 64% rename from src/comms/models.py rename to evennia/comms/models.py index a56c665ad..3dc6ea4b9 100644 --- a/src/comms/models.py +++ b/evennia/comms/models.py @@ -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) diff --git a/evennia/contrib/README.md b/evennia/contrib/README.md new file mode 100644 index 000000000..e674187c0 --- /dev/null +++ b/evennia/contrib/README.md @@ -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. diff --git a/evennia/contrib/__init__.py b/evennia/contrib/__init__.py new file mode 100644 index 000000000..3a4073ffe --- /dev/null +++ b/evennia/contrib/__init__.py @@ -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. +""" diff --git a/contrib/barter.py b/evennia/contrib/barter.py similarity index 99% rename from contrib/barter.py rename to evennia/contrib/barter.py index b593e7ee0..5e4feb862 100644 --- a/contrib/barter.py +++ b/evennia/contrib/barter.py @@ -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 diff --git a/contrib/chargen.py b/evennia/contrib/chargen.py similarity index 95% rename from contrib/chargen.py rename to evennia/contrib/chargen.py index 519c610e3..98a647112 100644 --- a/contrib/chargen.py +++ b/evennia/contrib/chargen.py @@ -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 diff --git a/contrib/dice.py b/evennia/contrib/dice.py similarity index 99% rename from contrib/dice.py rename to evennia/contrib/dice.py index e3ca9a535..9845e5f96 100644 --- a/contrib/dice.py +++ b/evennia/contrib/dice.py @@ -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): diff --git a/contrib/email-login.py b/evennia/contrib/email-login.py similarity index 95% rename from contrib/email-login.py rename to evennia/contrib/email-login.py index e55dd17f9..0a581ad82 100644 --- a/contrib/email-login.py +++ b/evennia/contrib/email-login.py @@ -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", diff --git a/contrib/extended_room.py b/evennia/contrib/extended_room.py similarity index 98% rename from contrib/extended_room.py rename to evennia/contrib/extended_room.py index d0bed9f97..e2321709b 100644 --- a/contrib/extended_room.py +++ b/evennia/contrib/extended_room.py @@ -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)) diff --git a/contrib/lineeditor.py b/evennia/contrib/lineeditor.py similarity index 99% rename from contrib/lineeditor.py rename to evennia/contrib/lineeditor.py index 3f9e34124..75fc9859a 100644 --- a/contrib/lineeditor.py +++ b/evennia/contrib/lineeditor.py @@ -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 diff --git a/contrib/menu_login.py b/evennia/contrib/menu_login.py similarity index 95% rename from contrib/menu_login.py rename to evennia/contrib/menu_login.py index a49bc079f..d9dc29fa7 100644 --- a/contrib/menu_login.py +++ b/evennia/contrib/menu_login.py @@ -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 diff --git a/contrib/menusystem.py b/evennia/contrib/menusystem.py similarity index 95% rename from contrib/menusystem.py rename to evennia/contrib/menusystem.py index 70f79ea06..0a5b6dd7b 100644 --- a/contrib/menusystem.py +++ b/evennia/contrib/menusystem.py @@ -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) diff --git a/contrib/slow_exit.py b/evennia/contrib/slow_exit.py similarity index 99% rename from contrib/slow_exit.py rename to evennia/contrib/slow_exit.py index 02cf3b03b..19067df87 100644 --- a/contrib/slow_exit.py +++ b/evennia/contrib/slow_exit.py @@ -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, diff --git a/contrib/talking_npc.py b/evennia/contrib/talking_npc.py similarity index 95% rename from contrib/talking_npc.py rename to evennia/contrib/talking_npc.py index 58b7ed985..71858df04 100644 --- a/contrib/talking_npc.py +++ b/evennia/contrib/talking_npc.py @@ -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) \ No newline at end of file + self.cmdset.add_default(TalkingCmdSet, permanent=True) diff --git a/contrib/procpools/ampoule/test/__init__.py b/evennia/contrib/tutorial_examples/__init__.py similarity index 100% rename from contrib/procpools/ampoule/test/__init__.py rename to evennia/contrib/tutorial_examples/__init__.py diff --git a/game/gamesrc/scripts/examples/bodyfunctions.py b/evennia/contrib/tutorial_examples/bodyfunctions.py similarity index 96% rename from game/gamesrc/scripts/examples/bodyfunctions.py rename to evennia/contrib/tutorial_examples/bodyfunctions.py index 56534d7be..b566400b2 100644 --- a/game/gamesrc/scripts/examples/bodyfunctions.py +++ b/evennia/contrib/tutorial_examples/bodyfunctions.py @@ -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 """ diff --git a/game/gamesrc/commands/examples/cmdset_red_button.py b/evennia/contrib/tutorial_examples/cmdset_red_button.py similarity index 98% rename from game/gamesrc/commands/examples/cmdset_red_button.py rename to evennia/contrib/tutorial_examples/cmdset_red_button.py index 583d15e25..5ce0ba37a 100644 --- a/game/gamesrc/commands/examples/cmdset_red_button.py +++ b/evennia/contrib/tutorial_examples/cmdset_red_button.py @@ -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()) diff --git a/game/gamesrc/world/examples/batch_cmds.ev b/evennia/contrib/tutorial_examples/example_batch_cmds.ev similarity index 96% rename from game/gamesrc/world/examples/batch_cmds.ev rename to evennia/contrib/tutorial_examples/example_batch_cmds.ev index fe8d4dd9f..943bf8a1c 100644 --- a/game/gamesrc/world/examples/batch_cmds.ev +++ b/evennia/contrib/tutorial_examples/example_batch_cmds.ev @@ -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: diff --git a/game/gamesrc/world/examples/batch_code.py b/evennia/contrib/tutorial_examples/example_batch_code.py similarity index 90% rename from game/gamesrc/world/examples/batch_code.py rename to evennia/contrib/tutorial_examples/example_batch_code.py index fe43209a4..85c4597eb 100644 --- a/game/gamesrc/world/examples/batch_code.py +++ b/evennia/contrib/tutorial_examples/example_batch_code.py @@ -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)) diff --git a/game/gamesrc/objects/examples/red_button.py b/evennia/contrib/tutorial_examples/red_button.py similarity index 94% rename from game/gamesrc/objects/examples/red_button.py rename to evennia/contrib/tutorial_examples/red_button.py index 7b827c468..2f3d961fd 100644 --- a/game/gamesrc/objects/examples/red_button.py +++ b/evennia/contrib/tutorial_examples/red_button.py @@ -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. diff --git a/game/gamesrc/scripts/examples/red_button_scripts.py b/evennia/contrib/tutorial_examples/red_button_scripts.py similarity index 96% rename from game/gamesrc/scripts/examples/red_button_scripts.py rename to evennia/contrib/tutorial_examples/red_button_scripts.py index a3cf8da06..4146d2f41 100644 --- a/game/gamesrc/scripts/examples/red_button_scripts.py +++ b/evennia/contrib/tutorial_examples/red_button_scripts.py @@ -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 diff --git a/evennia/contrib/tutorial_world/README.md b/evennia/contrib/tutorial_world/README.md new file mode 100644 index 000000000..9a535b6c9 --- /dev/null +++ b/evennia/contrib/tutorial_world/README.md @@ -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! diff --git a/contrib/__init__.py b/evennia/contrib/tutorial_world/__init__.py similarity index 100% rename from contrib/__init__.py rename to evennia/contrib/tutorial_world/__init__.py diff --git a/contrib/tutorial_world/build.ev b/evennia/contrib/tutorial_world/build.ev similarity index 63% rename from contrib/tutorial_world/build.ev rename to evennia/contrib/tutorial_world/build.ev index bcfc301ca..642d996d3 100644 --- a/contrib/tutorial_world/build.ev +++ b/evennia/contrib/tutorial_world/build.ev @@ -38,14 +38,14 @@ # | +--------+ +--------+ +--------+ +---+----+ # | \ | # ++---------+ \ +--------+ +--------+ +---+----+ -# |intro | \ |cell | |trap/ |temple | -# o--+ 01 | \| 08 +----+ fall | | 13 | -# | | | | /| 15 | | | +# |intro | \ |cell | | | |temple | +# o--+ 01 | \| 08 +----+ trap | | 13 | +# | | | | /| | | | # +----+-----+ +--------+ / +--+-+-+-+ +---+----+ # | / | | | | # +----+-----+ +--------+/ +--+-+-+---------+----+ # |outro | |tomb | |antechamber | -# o--+ 17 +----------+ 16 | | 14 | +# o--+ 16 +----------+ 15 | | 14 | # | | | | | | # +----------+ +--------+ +---------------------+ # @@ -75,10 +75,12 @@ # # Build the intro room (don't forget to also connect the outro room to this later) # -# Note the unique id tut#XX we give each room. One empty line results in a -# line-break in the game, whereas two lines create a new -# paragraph. The length of the lines in the batchfile does not matter, -# in-game they will fill the lines to the width as defined by the +# Note the unique alias tut#XX we give each room. This is used to +# easily reference this object from other objects in the build script +# without knowing the dbref. One empty line results in a line-break in +# the game, whereas two lines create a new paragraph. The length of the +# lines in the batchfile does not matter, in-game they will fill the +# lines to the width as defined by the # player's client. # @dig Intro;tut#01 @@ -125,20 +127,21 @@ tutorial along a high, rocky coast ... - {g(write 'start' or 'begin' to start the tutorial){n + {g(write 'start' or 'begin' to start the tutorial. Try 'tutorial' + to get behind-the-scenes help anywhere.){n # # Show that the tutorial command works ... # @set here/tutorial_info = - This is the tutorial command. Use it in various rooms to see what's - technically going on and what you could try in each room. The intro - room assigns some properties to your character, like a simple - "health" property used when fighting. Other rooms and puzzles might - do the same. Leaving the tutorial world through any of the normal - exit rooms will clean away all such temporary properties. + You just tried the tutorial command. Use it in various rooms to see +what's technically going on and what you could try in each room. The +intro room assigns some properties to your character, like a simple +"health" property used when fighting. Other rooms and puzzles might do +the same. Leaving the tutorial world through any of the normal exit +rooms will clean away all such temporary properties. - If you play this scenario as superuser, you will see a big red - warning. This warning is generated in the intro-rooms Typeclass. +If you play this scenario as superuser, you will see a big red +warning. This warning is generated in the intro-rooms Typeclass. #------------------------------------------------------------ # @@ -179,34 +182,60 @@ start # exits. Note the alias tut#02: this unique identifier can be used # later in the script to always find the way back to this room (for # example by teleporting and similar). This is necessary since there -# is no way of knowing what dbref a given room will get in the +# is no way of knowing beforehand what dbref a given room will get in the # database. # -@dig/teleport Cliff by the sea;cliff;tut#02 +@dig/teleport Cliff by the coast;cliff;tut#02 : tutorial_world.rooms.WeatherRoom = begin adventure;begin;start # -# We define the tutorial message seen when the using the tutorial command +# We define the tutorial message seen when using the tutorial command # @set here/tutorial_info = Weather room - This room inherits from a parent called WeatherRoom. It runs on a - timer-Script that allows various weather-related messages to appear - at irregular intervals. + This room inherits from a parent called WeatherRoom. It uses the + tickerhandler to regularly 'tick and randomly display various + weather-related messages. + + The room also has 'details' set on it (such as the ruin in the distance), those + are snippets of text stored on the room that the custom look command + used for all tutorial rooms can display. # @desc - You stand on the high coast line overlooking a stormy sea far + You stand on the high coast line overlooking a stormy {wsea{n far below. Around you the ground is covered in low gray-green grass, pushed flat by wind and rain. Inland, the vast dark moors begin, only here and there covered in patches of low trees and brushes. - - To the east, you glimpse the ragged outline of a castle ruin. It sits + To the east, you glimpse the ragged outline of a castle {wruin{n. It sits perched on a sheer cliff out into the water, isolated from the shore. The only way to reach it seems by way of an old hanging bridge, anchored not far east from here. # +# Mood-setting details to look at. This makes use of the custom look +# command in use on tutorial rooms to display extra text strings. It +# adds the detail as a dictionary Attribute on the room. +# +@detail ruin;ruins;castle = + A fair bit out from the rocky shores you can make out the foggy + outlines of a ruined castle. The once mighty towers have crumbled and + it presents a jagged shape against the rainy sky. The ruin is perched + on its own cliff, only connected to the mainland by means of an old + hanging bridge starting not far east from you. +# +@detail sea;ocean;waves = + The gray sea stretches as far as the eye can see to the east. Far + below you its waves crash against the foot of the cliff. The vast + inland moor meets the ocean along a high and uninviting coastline of + ragged vertical stone. + + Once this part of the world might have been beautiful, but now the + eternal winds and storms have washed it all down into a gray and + barren wasteland. +# +@detail +# # This is the well you will come back up from if you end up in the underground. # @create/drop Old well;well @@ -231,9 +260,10 @@ start all those stones? Start your own quarry?). # @set well/tutorial_info = - This is a normal object, locked with get:false() so that Players - cannot pick it up. Since the get_err property is set, you get a - customized error message when trying to pick it up. + This is a normal object, locked with the lock get:false() so that + Characters can't pick it up. Since the get_err Attribute is also set, + you get a customized error message when trying to pick it up (that + is checked and echoed by the 'get' command). # @create/drop Wooden sign;sign : tutorial_world.objects.Readable # @@ -248,62 +278,22 @@ start @set sign/get_err_msg = The sign is securely anchored to the ground. # @set sign/readable_text = - WARNING - Bridge is not safe! + + + WARNING - The bridge is not safe! + + + Below this official warning, someone has carved some sprawling + letters into the wood. It reads: "The guardian will not bleed to + mortal blade." # @set sign/tutorial_info = This is a readable object, of the Typeclass - contrib.tutorial_world.objects.Readable. The sign has a cmdset + evennia.contrib.tutorial_world.objects.Readable. The sign has a cmdset defined on itself, containing only one command, namely 'read'. This command is what allows you to 'read sign'. Doing so returns the - contents of an attribute containing the information on the sign. -# -# Mood-setting objects to look at -# -@create/drop ruin (in the distance);castle;ruin -# -@desc ruin = - A fair bit out from the rocky shores you can make out the foggy - outlines of a ruined castle. The once mighty towers have crumbled and - it presents a jagged shape against the rainy sky. The ruin is perched - on its own cliff, only connected to the mainland by means of an old - hanging bridge starting not far east from you. -# -@lock ruin = get:false() -# -@set ruin/get_err_msg = - Small as it may appear from a distance, you still cannot reach over and - pick up the castle to put in your pocket. -# -@set ruin/tutorial_info = - This is just a normal object, dropped in the room and setting the - mood. This is an easy solution, but in a real game one would probably - want to modify the look command to be able to see various 'scenery'- - like property on the room itself rather than creating faux - game-objects like this. -# -@create/drop The sea (in the distance);sea;ocean -# -@desc sea = - The gray sea stretches as far as the eye can see to the east. Far - below you its waves crash against the foot of the cliff. The vast - inland moor meets the ocean along a high and uninviting coastline of - ragged vertical stone. - - Once this part of the world might have been beautiful, but now the - eternal winds and storms have washed it all down into a gray and - barren wasteland. -# -@lock sea = get:false() -# -@set sea/get_err_msg = No one gets the sea. The sea gets you. -# -@set sea/tutorial_info = - This is just a normal object, dropped in the room and setting the - mood. This is an easy solution, but in a real game one would probably - want to modify the look command to be able to see various 'scenery'- - like property on the room itself rather than creating faux - game-objects like this. - + contents of the Attribute 'readable_sign', containing the information + on the sign. # Set a climbable object for discovering a hidden exit # @create/drop gnarled old trees;tree;trees;gnarled : tutorial_world.objects.Climbable @@ -317,15 +307,20 @@ start # @set trees/get_err_msg = The group of old trees have withstood the eternal wind for hundreds - of years. You will not uproot them any time soon. + of years. You will not uproot them any time soon. # @set trees/tutorial_info = These are climbable objects; they make for a small puzzle for accessing a hidden exit. Climbing the trees allows the - Climbable typeclass to assign a attribute on the character + Climbable typeclass to assign an Attribute on the character that an exit is then looking for. # -# The text to echo to player if trying 'climb tree' +# The text to echo to player if trying 'climb tree'. What +# happens when we do this is that the climb command assigns +# a Tag 'tutorial_climbed_tree' on the climber. The footpath +# exit (created below) is locked with this tag, meaning that +# it can only be seen/traversed by someone first having +# climbed. # @set tree/climb_text = With some effort you climb one of the old trees. @@ -334,10 +329,11 @@ start The branches are wet and slippery but can easily carry your weight. From this high vantage point you can see far and wide. - In fact, you notice {Ya faint yellowish light{n not far to the north, - beyond the trees. It looks like some sort of building. From this - angle you can make out a {wfaint footpath{n leading in that - direction, all but impossible to make out from ground level. + ... In fact, you notice {Ya faint yellowish light{n not far to the north, + beyond the trees. It looks like some sort of building. From this angle + you can make out a {wfaint footpath{n leading in that direction, all + but impossible to make out from ground level. You mentally register + where the footpath starts and will now be able to find it again. You climb down again. @@ -356,34 +352,48 @@ start = northern path;north;n;path,back to cliff;back;cliff;south;s # # Lock exit from view/traverse until we climbed that tree (which is -# when last_climbed get assigned). +# when tutorial_climbed_tree Tag gets assigned to us). # -@lock north = view:attr(last_climbed) ; traverse:attr(last_climbed) +@lock north = view:tag(tutorial_climbed_tree) ; traverse:tag(tutorial_climbed_tree) # @desc north = This is a hardly visible footpath leading off through the rain-beaten grass. It seems to circle the trees northward. You would never had - noticed it had you not seen it from above. + noticed it had you not spotted it from up in the tree. # @set north/tutorial_info = This exit is locked with a lock string that looks like this: - view:attr(last_climbed); traverse:attr(last_climbed) + view:tag(tutorial_climbed_tree) ; traverse:tag(tutorial_climbed_tree) - This checks if Character has an attribute last_climbed assigned in - order to be displayed and traversed. This attribute is set by the - trees when they are climbed. + This checks if Character has a Tag tutorial_climbed_tree set before it + allows itself to be displayed. This Tag is set by the tree object when + the 'climb' command is used. # -# Now that the exit is prepared, move to outside inn +# Now that the exit is prepared, move to outside inn to continue building. # north # @desc You stand outside a one-story sturdy wooden building. Light flickers behind closed storm shutters. Over the door a sign creaks in the wind - - the writing says {cEvennia Inn{n and is surrounded by a painted - image of some sort of snake. From inside you hear the sound of - laughter, singing and loud conversation. + - the writing says {cEvennia Inn{n and the curly letters are + surrounded by a painted image of some sort of snake. From inside you + hear the sound of laughter, singing and loud conversation. +# +# Some details to look at +# +@detail shutters;storm = + The shutters are closed. +# +@detail inn;sign = + You think you might have heard of this name before, + but at the moment you can recall where from. +# +@detail snake;letters;writing = + The snake is cartoonish with big googly eyes. It looks somewhat + like one of those big snakes from the distant jungles - the kind + squeezes their victims. #------------------------------------------------------------ # @@ -408,46 +418,43 @@ north sticking out. There is a sign next to it: {wFree to take{n. A patron tells you cheerfully that it's the leftovers from those foolish adventurers that challenged the old ruin before you ... + + (to get a weapon from the barrel, use {wget weapon{n) +# +@desc barrel = + This barrel has the air of leftovers - it contains an assorted + mess of random weaponry in various states and qualities. +# +@detail barkeep;man;landlord = + The landlord is a cheerful fellow, always ready to supply you with + more beer. He mentions doing some sort of arcane magic known as + "software development" when not running this place. Whatever that + means. # @set here/tutorial_info = Nothing special about this room, only a bonus place to potentially go for chatting with other online players. Oh, and don't forget to grab - a blade if you don't already have one. The weapons are locked so that - you cannot take more than one. (only three blades are available in - this location and they won't get refilled until a player goes to the - outro room, so the barrel might be empty - if it's any comfort, the - weapons in there won't help much against what is waiting in the ruin - anyway ...) + a blade if you don't already have one. # -# Create the weapons to be held by the barrel. +# Create the weapon rack (the barrel) # -@create/drop rusty old sword;rusty;sword;weapon : tutorial_world.objects.Weapon +@create/drop barrel: tutorial_world.objects.WeaponRack # -@desc rusty = - This is a rusty old broadsword. It has seen better days but the hilt is in good shape. +@lock barrel = get:false() # -# Only allow to pick up if we don't already has something called weapon -@lock rusty = get:not holds(weapon) +# This id makes sure that we cannot pick more than one weapon from this rack # -@set rusty/get_err_msg = "The barkeep smiles and says: 'Now, don't be greedy, friend! You already have a weapon.'" +@set barrel/rack_id = "rack_barrel" # -@create/drop blunt axe;blunt;axe;weapon : tutorial_world.objects.Weapon +# Set which weapons are available from this rack. These are prototype-keys +# defined in tutorial_world.objects.WEAPON_PROTOTYPES. We also set a +# message to use when trying to grab a second weapon. # -@desc axe = - A heavy weapon, but the edge is dull and covered in rust and nicks. +@set barrel/available_weapons = ["knife", "dagger", "sword", "club"] # -@lock blunt = get:not holds(weapon) -# -@set blunt/get_err_msg = "The barkeep crosses his arms: 'Hey, you already have a weapon; no need for another!'" -# -@create/drop patched spear;patched;spear;weapon : tutorial_world.objects.Weapon -# -@desc patched = - The spear tip looks to be in decent condition, but the shaft was broken and is rather poorly mended. -# -@lock patched = get:not holds(weapon) -# -@set patched/get_err_msg = "The barkeep shakes his head: 'You already have a weapon, friend, leave some for the next poor sod.'" +@set barrel/no_more_weapons_msg = + The barkeep shakes his head. He says: 'Sorry pal. We get a lot of needy + adventurers coming through here. One weapon per person only.' # #------------------------------------------------------------ # @@ -458,11 +465,11 @@ north # Back to cliff @teleport tut#02 # -# The bridge uses parent rooms.BridgeRoom, which causes the player to -# take a longer time than expected to cross as they are pummeled by -# wind and a chance to fall off. This room should not have regular -# exits back to the cliff, that is handled by the bridge typeclass -# itself. +# The bridge uses parent tutorial_world.rooms.BridgeRoom, which causes +# the player to take a longer time than expected to cross as they are +# pummeled by wind and a chance to fall off. This room should not have +# regular exits back to the cliff, that is handled by the bridge +# typeclass itself. # @dig The old bridge;bridge;tut#05 : tutorial_world.rooms.BridgeRoom @@ -472,7 +479,8 @@ north # @desc bridge = The hanging bridge's foundation sits at the edge of the cliff to the - east - two heavy stone pillars anchor the bridge on this side. + east - two heavy stone pillars anchor the bridge on this side. The + bridge sways precariously in the storm. # # go to the bridge # @@ -484,18 +492,19 @@ bridge # @set here/west_exit = tut#02 # -# connect other end to gatehouse +# connect other end to gatehouse (we have not created it yet +# but we know it will have alias tut#09 according to our map) # @set here/east_exit = tut#09 # -# Fall location is the cliff ledge +# Fall location is the cliff ledge (created next) # @set here/fall_exit = tut#06 # @set here/tutorial_info = - The bridge is a single room that uses a custom cmdset to overrule the - movement commands. This makes it take a few steps to cross it despite - it being only one room. + All of the bridge is actually a single room that uses a custom cmdset + to overrule the movement commands. This makes it take several steps to + cross it despite it being only one room in the database. The bridge has no normal exits, instead it has a counter that tracks @@ -518,56 +527,42 @@ bridge #------------------------------------------------------------ # # You only end up at the ledge if you fall off the bridge. It -# has no direct connection to the bridge. +# has no direct connection to the bridge but we specified +# it as the target of the "fall_exit", which is a special +# feature of the BridgeRoom. # @dig/teleport Protruding ledge;cliffledge;ledge;tut#06 # @set here/tutorial_info = - This room is stored as an attribute on the 'Bridge' room and used as a - destination should the player fall off the bridge. In our example the - bridge is relatively simple and always drops us to the same ledge; a - more advanced implementation might implement different locations to - end up in depending on what happens on the bridge. + This room is stored as an attribute on the 'Bridge' room and used as + a destination should the player fall off the bridge. It is the only + way to get to this room. In our example the bridge is relatively + simple and always drops us to the same ledge; a more advanced + implementation might implement different locations to end up in + depending on what happens on the bridge. # @desc You are on a narrow ledge protruding from the side of the cliff, about halfway down. The air is saturated with salty sea water, - sprays hitting your face from the crashing waves below. + sprays hitting your face from the crashing waves {wbelow{n. The ledge is covered with a few black-grey brushes. Not far from you the cliff-face is broken down to reveal a narrow natural opening into - the cliff. + the cliff. High above you the {wbridge{n sways and creaks in the wind. # -@create/drop brushes;brush -# -@lock brush = get:false() -# -@desc brush = +@detail brush;brushes = The brushes covering the ledge are gray and dwarfed from constantly being pummeled by salt, rain and wind. # -@create/drop The sea (far below you);sea;ocean +@detail below;sea;ocean;waves = + Below you the gray sea rages, its waves crashing into the cliff so a + thin mist of salt mixes with the rain even this far above it. You can + almost imagine the cliff trembling under its onslaught. # -@set sea/get_err_msg: - Try as you might, the Sea will always get you long before you can - ever get it. -# -@lock sea = get:false() -# -@desc sea = - Below you the gray sea rages. You can almost imagine the - cliff trembling under its onslaught. -# -@create/drop The hang bridge (above you);bridge;hangbridge;above -# -@lock bridge = get:false() -# -@desc bridge = - You can see the shape of the hanging bridge a fair bit above you, partly - obscured by the rain. There is no way to get back up there from this +@detail bridge = + Partly obscured by the rain you can make out the shape of the hanging + bridge high above you. There is no way to get back up there from this ledge. -# -@set bridge/get_err_msg = You can't reach it, it's too far away. #------------------------------------------------------------ @@ -615,29 +610,23 @@ hole simmers down the hole together with rain that ripples the black surface of the pool. # -@create/drop pool;water -# -@lock pool = get:false() -# -@set pool/get_err_msg = - You sift your hands through the black water without feeling any - immediate bottom. It's chilling cold and so dark you don't feel like - taking a sip. -# -@desc pool = +@detail pool;water = The water of the pool is black and opaque. The rain coming down from above does not seem to ripple the surface quite as much as it should. # -@create/drop hole (high above);hole;above +@detail bucket = + The bucket is nearly coming apart, only rusty iron bands holding + the rotten wood together. It's been down here for a long time. # -@lock hole = get:false() -# -@set hole/get_err_msg = You cannot reach it from here. You need to climb the chain. -# -@desc hole = +@detail hole;above = Whereas the lower edges of the hole seem jagged and natural you can - faintly make out that it turns into a man-made circular shaft higher - up. It looks like an old well. + faintly make out it turning into a man-made circular shaft higher up. + It looks like an old well. There must have been much more water + here once. +# +@detail passages;dark = + Those dark passages seem to criss-cross the cliff. No need to + head back into the gloom now that there seems to be a way out. # # From the passages we get back up to the cliff, so we # open up a new exit back there. @@ -684,20 +673,12 @@ hole thick cover of black roots having broken through the cracks from the outside.{n # -@create/drop iron-cast door;iron;door;iron-cast -# -@lock door = get:false() -# -@desc door = +@detail iron-cast door;iron;door;iron-cast = The door is very solid and clad in iron. No matter how much you push at it, it won't budge. It actually doesn't show any signs of having been opened for a very long time. # -@create/drop stone walls;walls;stone -# -@lock stone = get:false() -# -@desc stone = +@detail stone walls;walls;stone;stones;wall = The walls are dripping with moisture and mold. A network of roots have burst through the cracks on one side, bending the stones slightly aside. You feel a faint draft from that direction. @@ -705,9 +686,9 @@ hole # The crumbling wall is in fact an advanced type of Exit, all we need to do is # to supply it with a destination. # -# Puzzle wall is an exit without a given destination at start @create/drop root-covered wall;wall;roots;wines;root : tutorial_world.objects.CrumblingWall # +# # This destination is auto-assigned to the exit when its puzzle is solved # connect to the Underground passages @set root-covered wall/destination = tut#07 @@ -730,11 +711,12 @@ hole # head back up to ground level. We teleport to the bridge # and continue from there. # -# Back to the bridge @teleport tut#05 # # The bridge room should not have any normal exits from it, that is -# handled by the bridge itself. So we teleport away from it. +# handled by the bridge itself. So we teleport away from it. The +# ruined gatehouse is also the east_exit target for the bridge as +# we recall. # @dig/teleport Ruined gatehouse;gatehouse;tut#09 : tutorial_world.rooms.TutorialRoom @@ -762,17 +744,14 @@ hole the remains of the castle. There is also a standing archway offering passage to a path along the old {wsouth{nern inner wall. # -@create/drop fallen portoculis;portoculis;fall;fallen -# -@lock portoculis = get:false() -# -@desc portoculis = +@detail portoculis;fall;fallen;grating = This heavy iron grating used to block off the inner part of the gate house, now it has fallen to the ground together with the stone archway that once help it up. # -# We lock the bridge exit for the mob, so it don't wander out on the bridge +# We lock the bridge exit for the mob, so it don't wander out on the bridge. Only +# traversing objects controlled by a player (i.e. Characters) may cross the bridge. # -@lock bridge = traverse:not attr(is_mob) +@lock bridge = traverse:has_player() #------------------------------------------------------------ # @@ -823,7 +802,7 @@ archway # @desc The ruins opens up to the sky in a small open area, lined by - columns. The open area is dominated by a huge stone obelisk in its + columns. The open area is dominated by a huge stone {wobelisk{n in its center, an ancient ornament miraculously still standing. Previously one could probably continue past the obelisk and eastward @@ -847,20 +826,23 @@ archway # @set obelisk/get_err_msg = It's way too heavy for anyone to move. # -# (the obelisk describes itself, so we need no do it here) +# Set the puzzle clues on the obelisk. The order should correspond +# to the ids later checked by the antechamber puzzle. # -# Create the mobile. This is the start location. -@create/drop Ghostly apparition;ghost;apparition;fog : tutorial_world.mob.Enemy +@set obelisk/puzzle_descs = ("You can briefly make out the image of {ba woman with a blue bird{n.", "You for a moment see the visage of {ba woman on a horse{n.", "For the briefest moment you make out an engraving of {ba regal woman wearing a crown{n.", "You think you can see the outline of {ba flaming shield{n in the stone.", "The surface for a moment seems to portray {ba sharp-faced woman with white hair{n.") + +# Create the mobile. This is its start location. +@create/drop Ghostly apparition;ghost;apparition;fog : tutorial_world.mob.Mob # -@set ghost/full_health = 20 +# Set its home to this location # -@set ghost/defeat_location = dark cell +@home ghost = tut#11 # @lock ghost = get:false() # @set ghost/get_err_msg = Your fingers just pass straight through it! # -@desc ghost = +@set ghost/desc_alive = This ghostly shape could momentarily be mistaken for a thick fog had it not moved with such determination and giving echoing hollow screams as it did. The shape is hard to determine, now and then it @@ -868,33 +850,70 @@ archway later. The thing reeks of almost tangible spite at your presence. This must be the ruin's eternal guardian. # +@set ghost/desc_dead = + The ghostly apparition is nothing but a howling on the wind, an eternal + cold spot that can never be fully eradicated from these walls. While harmless + in this state, there is no doubt that it shall eventually return to this plane + to continue its endless haunting. +# +# We set the ghost to send defeated enemies to the Dark Cell +# +@set ghost/send_defeated_to = tut#08 +# +@set ghost/defeat_msg = + You fall to the ground, defeated. As you do, the ghostly apparition dives + forward and engulf you. + + + The world turns black. +# +@set ghost/defeat_msg_room = + %s falls to the ground, defeated. For a moment their fallen form is + engulfed by the swirling mists of the ghostly apparition. When they + raise lift, the ground is empty! +# +@set ghost/weapon_ineffective_msg = + Your weapon just passes through the swirling mist of the ghostly apparition, causing no effect! +# +@set ghost/hit_msg = + The ghostly apparition howls and writhes, shifts and shivers. +# +@set ghost/death_msg = + After the last strike, the ghostly apparition seems to collapse +inwards. It fades and becomes one with the mist. Its howls rise to a +ear-shattering crescendo before quickly fading away to be nothing more +than the lonely cries of the cold, salty wind. +# # Give the enemy some random echoes (echoed at irregular intervals) -# This 'list structure' [ ... ] is parsed by the batch reader and -# split by commas (so each entry cannot contain commas). # -@set ghost/irregular_echoes = - ["The foggy thing gives off a high-pitched shriek.","For a moment the - fog wraps around a nearby pillar.", "The fog drifts lower to the ground - as if looking for something.", "The fog momentarily takes on a reddish - hue.", "The fog temporarily fills most of the area as it changes - shape.", "You accidentally breathes in some of the fog - you start - coughing from the cold moisture."] +@set ghost/irregular_msgs = + ["The foggy thing gives off a high-pitched shriek.", + "For a moment the fog wraps around a nearby pillar.", + "The fog drifts lower to the ground as if looking for something.", + "The fog momentarily takes on a reddish hue.", + "The fog temporarily fills most of the area as it changes shape.", + "You accidentally breathes in some of the fog - you start coughing from the cold moisture."] # + # give the enemy a tentacle weapon # @create foggy tentacles;tentacles:tutorial_world.objects.Weapon # -# Make the enemy's weapon good - hits at 70% of attacks +# Make the enemy's weapon good - hits at 70% of attacks, but not good at parrying. # @set foggy tentacles/hit = 0.7 # +@set foggy tentacles/parry = 0.1 +# +@set foggy tentacles/damage = 5 +# # Actually give the enemy its weapon # @teleport/quiet tentacles = ghost # -# Clear inactive mode and start the mob +# Start the mob # -@set ghost/inactive = +mobon ghost #------------------------------------------------------------ # @@ -930,11 +949,7 @@ archway withstood the test of time better than many of those around it, it looks like some sort of temple. # -@create/drop old stables;stable;stables;building -# -@lock stable = get:false() -# -@desc stable = +@detail stables;stable;building = The building is empty, if it was indeed once a stable it was abandoned long ago. #------------------------------------------------------------ @@ -960,6 +975,13 @@ archway Stairs lead down to the temple's dungeon on either side of the altar. A gaping door opening shows the a wide courtyard to the west. +# +@detail altar = + The altar is a massive stone slab. It might once have had ornate decorations + but time and the salty air has broken everything down into dust. +# +@detail ceiling = + The dome still looming intact above you is a marvel of engineering. #------------------------------------------------------------ # @@ -975,7 +997,7 @@ archway The stairs are worn by the age-old passage of feet. # # Lock the antechamber so the ghost cannot get in there. -@lock stairs down = traverse:not attr(is_mob) +@lock stairs down = traverse:has_player() # # Go down # @@ -1004,87 +1026,26 @@ stairs down directly relay you on to the Dark Cell. If correct, the tomb teleports to the Ancient Tomb treasure chamber. # -# We create all the tombs +# We create all the tombs. These all teleport to the dark cell +# except one which is the one decided by the scene shown by the +# Obelisk last we looked. # @dig Blue bird tomb : tutorial_world.rooms.TeleportRoom - = Tomb with stone bird;bird;blue;stone + = Blue bird tomb;bird;blue;stone # -@dig Tomb of woman on horse - : tutorial_world.rooms.TeleportRoom - = Tomb with statue of riding woman;horse;riding; +@desc Blue bird tomb = +The entrance to this tomb is decorated with a very lifelike blue bird. # -@dig Tomb of the crowned queen - : tutorial_world.rooms.TeleportRoom - = Tomb with statue of a crowned queen;crown;queen -# -@dig Tomb of the shield - : tutorial_world.rooms.TeleportRoom - = Tomb with shield of arms;shield -# -@dig Tomb of the hero - : tutorial_world.rooms.TeleportRoom - = Tomb depicting a heroine fighting a monster;knight;hero;monster;beast -# -# The puzzle_values are set on Character by looking at the Obelisk in -# the Castle Corner room. If the scenes shown don't match, the -# failure/success_teleport_to attributes will be used to teleport away -# the Character. Since the scene shown by the Obelisk is random, this -# means the right tomb need not be the same. -# -@tel Blue bird tomb +Blue bird tomb # @set here/puzzle_value = 0 # -@set here/failure_teleport_to = falling! +@set here/failure_teleport_to = tut#08 # -@set here/success_teleport_to = Ancient tomb +@set here/success_teleport_to = tut#15 # -@teleport Tomb of woman on horse -# -@set here/puzzle_value = 1 -# -@set here/failure_teleport_to = falling! -# -@set here/success_teleport_to = Ancient tomb -# -@teleport Tomb of the crowned queen -# -@set here/puzzle_value = 2 -# -@set here/failure_teleport_to = falling! -# -@set here/success_teleport_to = Ancient tomb -# -@teleport Tomb of the shield -# -@set here/puzzle_value = 3 -# -@set here/failure_teleport_to = falling! -# -@set here/success_teleport_to = Ancient tomb -# -@teleport Tomb of the hero -# -@set here/puzzle_value = 4 -# -@set here/failure_teleport_to = falling! -# -@set here/success_teleport_to = Ancient tomb - -#------------------------------------------------------------ -# -# Falling room -# -# This is a transition between the trap and the cell room. Character -# is teleported here if they picked the wrong tomb. -# -#------------------------------------------------------------ -# -@dig/teleport Falling!;falling;tut#15 - : tutorial_world.rooms.TeleportRoom -# -@desc +@set here/failure_teleport_msg = The tomb is dark. You fumble your way through it. You think you can make out a coffin in front of you in the gloom. @@ -1102,9 +1063,202 @@ stairs down The air is damp. Where are you? # -@set here/success_teleport_to = dark cell +@set here/success_teleport_msg = + The tomb is dark. You fumble your way through it. You think you can + make out a coffin in front of you in the gloom. + + The coffin comes into view. On and around it are chiseled scenes of a + stern woman in armor. They depict great heroic deeds. This is clearly + the tomb of some sort of ancient heroine - it must be the goal you + have been looking for! # -@set here/failure_teleport_to = dark cell +@tel tut#14 +# +@dig Tomb of woman on horse + : tutorial_world.rooms.TeleportRoom + = Tomb of woman on horse;horse;riding; +# +@desc Tomb of woman on horse = +The entrance to this tomb depicts a scene of a strong +warrior woman on a black horse. She shouts and brandishes +a glowing weapon as she charges down a hill towards +some enemy not depicted. +# +Tomb of woman on horse +# +@set here/puzzle_value = 1 +# +@set here/failure_teleport_to = tut#08 +# +@set here/success_teleport_to = tut#15 +# +@set here/failure_teleport_msg = + The tomb is dark. You fumble your way through it. You think you can + make out a coffin in front of you in the gloom. + + + {rSuddenly you hear a distinct 'click' and the ground abruptly + disappears under your feet! You fall ... things go dark. {n + + + ... + + + ... You come to your senses. You lie down. On stone floor. You + shakily come to your feet. Somehow you suspect that you are not under + the tomb anymore, like you were magically snatched away. + + The air is damp. Where are you? +# +@set here/success_teleport_msg = + The tomb is dark. You fumble your way through it. You think you can + make out a coffin in front of you in the gloom. + + The coffin comes into view. On and around it are chiseled scenes of a + stern woman in armor. They depict great heroic deeds. This is clearly + the tomb of some sort of ancient heroine - it must be the goal you + have been looking for! +# +@tel tut#14 +# +@dig Tomb of the crowned queen + : tutorial_world.rooms.TeleportRoom + = Tomb of the crowned queen;crown;queen +# +@desc Tomb of the crowned queen = +The entrance to this tomb shows a beautiful mural of a queen ruling +from her throne, respectful subjects kneeling before her. On her head +is a crown that seems to shine with magical power. +# +Tomb of the crowned queen +# +@set here/puzzle_value = 2 +# +@set here/failure_teleport_to = tut#08 +# +@set here/success_teleport_to = tut#15 +# +@set here/failure_teleport_msg = + The tomb is dark. You fumble your way through it. You think you can + make out a coffin in front of you in the gloom. + + + {rSuddenly you hear a distinct 'click' and the ground abruptly + disappears under your feet! You fall ... things go dark. {n + + + ... + + + ... You come to your senses. You lie down. On stone floor. You + shakily come to your feet. Somehow you suspect that you are not under + the tomb anymore, like you were magically snatched away. + + The air is damp. Where are you? +# +@set here/success_teleport_msg = + The tomb is dark. You fumble your way through it. You think you can + make out a coffin in front of you in the gloom. + + The coffin comes into view. On and around it are chiseled scenes of a + stern woman in armor. They depict great heroic deeds. This is clearly + the tomb of some sort of ancient heroine - it must be the goal you + have been looking for! +# +@tel tut#14 +# +@dig Tomb of the shield + : tutorial_world.rooms.TeleportRoom + = Tomb of the shield;shield +# +@desc Tomb of the shield = +This tomb shows a warrior woman fighting shadowy creatures from the +top of a hill. Her sword lies broken on the ground before her but she +fights on with her battered shield - the scene depicts her just as she +rams the shield into an enemy in wild desperation. +# +Tomb of the shield +# +@set here/puzzle_value = 3 +# +@set here/failure_teleport_to = tut#08 +# +@set here/success_teleport_to = tut#15 +# +@set here/failure_teleport_msg = + The tomb is dark. You fumble your way through it. You think you can + make out a coffin in front of you in the gloom. + + + {rSuddenly you hear a distinct 'click' and the ground abruptly + disappears under your feet! You fall ... things go dark. {n + + + ... + + + ... You come to your senses. You lie down. On stone floor. You + shakily come to your feet. Somehow you suspect that you are not under + the tomb anymore, like you were magically snatched away. + + The air is damp. Where are you? +# +@set here/success_teleport_msg = + The tomb is dark. You fumble your way through it. You think you can + make out a coffin in front of you in the gloom. + + The coffin comes into view. On and around it are chiseled scenes of a + stern woman in armor. They depict great heroic deeds. This is clearly + the tomb of some sort of ancient heroine - it must be the goal you + have been looking for! +# +@tel tut#14 +# +@dig Tomb of the hero + : tutorial_world.rooms.TeleportRoom + = Tomb of the hero;knight;hero;monster;beast +# +@desc Tomb of the hero = +The entrance to this tomb shows a mural of an aging woman in a +warrior's outfit. She has white hair yet her sword-arm shows no sign +of weakness and her pose is straight. Children are gathered around her +feet and men and women from all the land come to seek the wisdom and +strength of the legendary hero. +# +Tomb of the hero +# +@set here/puzzle_value = 4 +# +@set here/failure_teleport_to = tut#08 +# +@set here/success_teleport_to = tut#15 +# +@set here/failure_teleport_msg = + The tomb is dark. You fumble your way through it. You think you can + make out a coffin in front of you in the gloom. + + + {rSuddenly you hear a distinct 'click' and the ground abruptly + disappears under your feet! You fall ... things go dark. {n + + + ... + + + ... You come to your senses. You lie down. On stone floor. You + shakily come to your feet. Somehow you suspect that you are not under + the tomb anymore, like you were magically snatched away. + + The air is damp. Where are you? +# +@set here/success_teleport_msg = + The tomb is dark. You fumble your way through it. You think you can + make out a coffin in front of you in the gloom. + + The coffin comes into view. On and around it are chiseled scenes of a + stern woman in armor. They depict great heroic deeds. This is clearly + the tomb of some sort of ancient heroine - it must be the goal you + have been looking for! # # back to antechamber @tel tut#14 @@ -1113,23 +1267,20 @@ stairs down # # The ancient tomb # -# This is the real tomb, the goal of the adventure. +# This is the real tomb, the goal of the adventure. It is not +# directly accessible from the Antechamber but you are +# teleported here only if you solve the puzzle of the Obelisk. # #------------------------------------------------------------ -# Create the real tomb # -@dig/teleport Ancient tomb;tut#16 +@dig/teleport Ancient tomb;tut#15 : tutorial_world.rooms.TutorialRoom = ,back to antechamber;antechamber;back # @desc - The tomb is dark. You fumble your way through it. You think you can - make out a coffin in front of you in the gloom. - - The coffin comes into view. On and around it are chiseled scenes of a - stern woman in armor. They depict great heroic deeds. This is clearly - the tomb of some sort of ancient heroine - it must be the goal you - have been looking for! + Apart from the ornate sarcophagus, the tomb is bare from extra decorations. + This is the resting place of a warrior with little patience for + glamour and trinkets. You have reached the end of your quest. # @set here/tutorial_info = Congratulations, you have reached the end of this little tutorial @@ -1140,25 +1291,25 @@ stairs down explore further. You will find this weapon works better against the castle's guardian than any of the others you have found ... # -# The exact nature of the weapon is randomized. The get_text attribute -# on the sarcophagus holds a %s replacement that is filled by the typeclass -# with the name of the weapon when you get the weapon. +# The sarcophagus is a "weapon rack" from which you can extract one +# single weapon. # @create/drop Stone sarcophagus;sarcophagus;stone : tutorial_world.objects.WeaponRack # @desc stone = The lid of the coffin is adorned with a stone statue in full size. The weapon held by - the stone hands looks very realistic ... + the stone hands looks very realistic ... ( + + (try {wget weapon{n) # @set sarcophagus/rack_id = rack_sarcophagus # -@set sarcophagus/min_dmg = 4.0 +@set sarcophagus/available_weapons = ["ornate_longsword","warhammer","rune axe","thruning","slayer waraxe","ghostblade","hawkblade"] # -@set sarcophagus/max_dmg = 11.0 +@set sarcophagus/no_more_weapons_msg = + The tomb has already granted you all the might it will ever do. # -@set sarcophagus/magic = True -# -@set sarcophagus/get_text = +@set sarcophagus/get_weapon_msg = The hands of the statue close on what seems to be a real weapon rather than one in stone. This must be the hero's legendary weapon! The prize you have been looking for! @@ -1183,7 +1334,7 @@ stairs down # #------------------------------------------------------------ # -@dig End of tutorial;end;tut#17 +@dig End of tutorial;end;tut#16 : tutorial_world.rooms.OutroRoom = Exit tutorial;exit;end # @@ -1191,15 +1342,15 @@ stairs down # rack_id. This we can use to check if any such weapon is in inventory # before unlocking the exit. # -@lock Exit tutorial: view:holds(rack_sarcophagus) ; traverse:holds(rack_sarcophagus) +@lock Exit tutorial = view:holds(rack_sarcophagus) ; traverse:holds(rack_sarcophagus) # # to tutorial outro -@tel tut#17 +@tel tut#16 # # we want to clear the weapon-rack ids on the character when exiting. -@set here/wracklist = ["rack_sarcophagus"] +@set here/wracklist = ["rack_barrel", "rack_sarcophagus"] # -# this quits the tutorial and cleans up all variables that was . +# this room quits the tutorial and cleans up all variables that were set. @desc {gThanks for trying out this little Evennia tutorial! @@ -1213,14 +1364,15 @@ stairs down If you went through the tutorial quest once, it can be interesting to do it again to explore the various possibilities and rooms you might - not have come across yet, maybe with the source/build code to one - side. If you play as superuser (user #1) the mobile will ignore you - and teleport rooms etc will not affect you (this will also disable - all locks, so keep that in mind when checking functionality).{n + not have come across yet, maybe with the source/build code next to + you. If you play as superuser (user #1) the mobile will ignore you + and teleport rooms etc will not affect you (this will also disable all + locks, so keep that in mind when checking functionality).{n # @set here/tutorial_info = - This room cleans up all temporary attributes that was put on the - character during the tutorial. Hope you enjoyed the play through! + This room cleans up all temporary attributes and tags that were put + on the character during the tutorial. Hope you enjoyed the play + through! # # Tie this back to Limbo # diff --git a/evennia/contrib/tutorial_world/mob.py b/evennia/contrib/tutorial_world/mob.py new file mode 100644 index 000000000..9656fb570 --- /dev/null +++ b/evennia/contrib/tutorial_world/mob.py @@ -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 + moboff + + 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 ") + 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() diff --git a/contrib/tutorial_world/objects.py b/evennia/contrib/tutorial_world/objects.py similarity index 58% rename from contrib/tutorial_world/objects.py rename to evennia/contrib/tutorial_world/objects.py index 1e7561ff9..513b44d57 100644 --- a/contrib/tutorial_world/objects.py +++ b/evennia/contrib/tutorial_world/objects.py @@ -19,11 +19,12 @@ WeaponRack """ -import time import random -from ev import create_object -from ev import Object, Exit, Command, CmdSet, Script +from evennia import DefaultObject, DefaultExit, Command, CmdSet +from evennia import utils +from evennia.utils import search +from evennia.utils.spawner import spawn #------------------------------------------------------------ # @@ -31,7 +32,7 @@ from ev import Object, Exit, Command, CmdSet, Script # # The TutorialObject is the base class for all items # in the tutorial. They have an attribute "tutorial_info" -# on them that a global tutorial command can use to extract +# on them that the global tutorial command can use to extract # interesting behind-the scenes information about the object. # # TutorialObjects may also be "reset". What the reset means @@ -42,7 +43,7 @@ from ev import Object, Exit, Command, CmdSet, Script #------------------------------------------------------------ -class TutorialObject(Object): +class TutorialObject(DefaultObject): """ This is the baseclass for all objects in the tutorial. """ @@ -51,7 +52,6 @@ class TutorialObject(Object): "Called when the object is first created." super(TutorialObject, self).at_object_creation() self.db.tutorial_info = "No tutorial info is available for this object." - #self.db.last_reset = time.time() def reset(self): "Resets the object, whatever that may mean." @@ -60,16 +60,20 @@ class TutorialObject(Object): #------------------------------------------------------------ # -# Readable - an object one can "read". +# Readable - an object that can be "read" # #------------------------------------------------------------ +# +# Read command +# + class CmdRead(Command): """ Usage: read [obj] - Read some text. + Read some text of a readable object. """ key = "read" @@ -77,7 +81,11 @@ class CmdRead(Command): help_category = "TutorialWorld" def func(self): - "Implement the read command." + """ + Implements the read command. This simply looks for an + Attribute "readable_text" on the object and displays that. + """ + if self.args: obj = self.caller.search(self.args.strip()) else: @@ -94,18 +102,25 @@ class CmdRead(Command): class CmdSetReadable(CmdSet): - "CmdSet for readables" + """ + A CmdSet for readables. + """ def at_cmdset_creation(self): - "called when object is created." + """ + Called when the cmdset is created. + """ self.add(CmdRead()) class Readable(TutorialObject): """ - This object defines some attributes and defines a read method on itself. + This simple object defines some attributes and """ def at_object_creation(self): - "Called when object is created" + """ + Called when object is created. We make sure to set the needed + Attribute and add the readable cmdset. + """ super(Readable, self).at_object_creation() self.db.tutorial_info = "This is an object with a 'read' command defined in a command set on itself." self.db.readable_text = "There is no text written on %s." % self.key @@ -119,14 +134,20 @@ class Readable(TutorialObject): # # The climbable object works so that once climbed, it sets # a flag on the climber to show that it was climbed. A simple -# command 'climb' handles the actual climbing. +# command 'climb' handles the actual climbing. The memory +# of what was last climbed is used in a simple puzzle in the +# tutorial. # #------------------------------------------------------------ class CmdClimb(Command): """ + Climb an object + Usage: climb + + This allows you to climb. """ key = "climb" locks = "cmd:all()" @@ -148,7 +169,8 @@ class CmdClimb(Command): if not ostring: ostring = "You climb %s. Having looked around, you climb down again." % self.obj.name self.caller.msg(ostring) - self.caller.db.last_climbed = self.obj + # set a tag on the caller to remember that we climbed. + self.caller.tags.add("tutorial_climbed_tree", category="tutorial_world") class CmdSetClimbable(CmdSet): @@ -159,7 +181,10 @@ class CmdSetClimbable(CmdSet): class Climbable(TutorialObject): - "A climbable object." + """ + A climbable object. All that is special about it is that it has + the "climb" command available on it. + """ def at_object_creation(self): "Called at initial creation only" @@ -171,44 +196,51 @@ class Climbable(TutorialObject): # # Obelisk - a unique item # -# The Obelisk is an object with a modified return_appearance -# method that causes it to look slightly different every -# time one looks at it. Since what you actually see -# is a part of a game puzzle, the act of looking also -# stores a key attribute on the looking object for later -# reference. +# The Obelisk is an object with a modified return_appearance method +# that causes it to look slightly different every time one looks at it. +# Since what you actually see is a part of a game puzzle, the act of +# looking also stores a key attribute on the looking object (different +# depending on which text you saw) for later reference. # #------------------------------------------------------------ -OBELISK_DESCS = ["You can briefly make out the image of {ba woman with a blue bird{n.", - "You for a moment see the visage of {ba woman on a horse{n.", - "For the briefest moment you make out an engraving of {ba regal woman wearing a crown{n.", - "You think you can see the outline of {ba flaming shield{n in the stone.", - "The surface for a moment seems to portray {ba woman fighting a beast{n."] - - class Obelisk(TutorialObject): """ - This object changes its description randomly. + This object changes its description randomly, and which is shown + determines which order "clue id" is stored on the Character for + future puzzles. + + Important Attribute: + puzzle_descs (list): list of descriptions. One of these is + """ def at_object_creation(self): "Called when object is created." super(Obelisk, self).at_object_creation() self.db.tutorial_info = "This object changes its desc randomly, and makes sure to remember which one you saw." + self.db.puzzle_descs = ["You see a normal stone slab"] # make sure this can never be picked up self.locks.add("get:false()") def return_appearance(self, caller): - "Overload the default version of this hook." - clueindex = random.randint(0, len(OBELISK_DESCS) - 1) - # set this description - string = "The surface of the obelisk seem to waver, shift and writhe under your gaze, with " - string += "different scenes and structures appearing whenever you look at it. " - self.db.desc = string + OBELISK_DESCS[clueindex] - # remember that this was the clue we got. + """ + This hook is called by the look command to get the description + of the object. We overload it with our own version. + """ + # randomly get the index for one of the descriptions + descs = self.db.puzzle_descs + clueindex = random.randint(0, len(descs) - 1) + # set this description, with the random extra + string = "The surface of the obelisk seem to waver, shift and writhe under your gaze, with " \ + "different scenes and structures appearing whenever you look at it. " + self.db.desc = string + descs[clueindex] + # remember that this was the clue we got. The Puzzle room will + # look for this later to determine if you should be teleported + # or not. caller.db.puzzle_clue = clueindex - # call the parent function as normal (this will use db.desc we just set) + # call the parent function as normal (this will use + # the new desc Attribute we just set) return super(Obelisk, self).return_appearance(caller) @@ -216,165 +248,127 @@ class Obelisk(TutorialObject): # # LightSource # -# This object that emits light and can be -# turned on or off. It must be carried to use and has only -# a limited burn-time. -# When burned out, it will remove itself from the carrying -# character's inventory. +# This object emits light. Once it has been turned on it +# cannot be turned off. When it burns out it will delete +# itself. +# +# This could be implemented using a single-repeat Script or by +# registering with the TickerHandler. We do it simpler by +# using the delay() utility function. This is very simple +# to use but does not survive a server @reload. Because of +# where the light matters (in the Dark Room where you can +# find new light sources easily), this is okay here. # #------------------------------------------------------------ -class StateLightSourceOn(Script): +class CmdLight(Command): """ - This script controls how long the light source is burning. When - it runs out of fuel, the lightsource goes out. - """ - def at_script_creation(self): - "Called at creation of script." - self.key = "lightsourceBurn" - self.desc = "Keeps lightsources burning." - self.start_delay = True # only fire after self.interval s. - self.repeats = 1 # only run once. - self.persistent = True # survive a server reboot. - - def at_start(self): - "Called at script start - this can also happen if server is restarted." - self.interval = self.obj.db.burntime - self.db.script_started = time.time() - - def at_repeat(self): - "Called at self.interval seconds" - # this is only called when torch has burnt out - self.obj.db.burntime = -1 - self.obj.reset() - - def at_stop(self): - """ - Since the user may also turn off the light - prematurely, this hook will store the current - burntime. - """ - # calculate remaining burntime, if object is not - # already deleted (because it burned out) - if self.obj: - try: - time_burnt = time.time() - self.db.script_started - except TypeError: - # can happen if script_started is not defined - time_burnt = self.interval - burntime = self.interval - time_burnt - self.obj.db.burntime = burntime - - def is_valid(self): - "This script is only valid as long as the lightsource burns." - return self.obj.db.is_active - - -class CmdLightSourceOn(Command): - """ - Switches on the lightsource. + Creates light where there was none. Something to burn. """ key = "on" - aliases = ["switch on", "turn on", "light"] - locks = "cmd:holds()" # only allow if command.obj is carried by caller. + aliases = ["light", "burn"] + # only allow this command if command.obj is carried by caller. + locks = "cmd:holds()" help_category = "TutorialWorld" def func(self): - "Implements the command" + """ + Implements the light command. Since this command is designed + to sit on a "lightable" object, we operate only on self.obj. + """ - if self.obj.db.is_active: - self.caller.msg("%s is already burning." % self.obj.key) - else: - # set lightsource to active - self.obj.db.is_active = True - # activate the script to track burn-time. - self.obj.scripts.add(StateLightSourceOn) - self.caller.msg("{gYou light {C%s.{n" % self.obj.key) + if self.obj.light(): + self.caller.msg("You light %s." % self.obj.key) self.caller.location.msg_contents("%s lights %s!" % (self.caller, self.obj.key), exclude=[self.caller]) - # run script validation on the room to make light/dark states tick. - self.caller.location.scripts.validate() - # look around - self.caller.execute_cmd("look") - - -class CmdLightSourceOff(Command): - """ - Switch off the lightsource. - """ - key = "off" - aliases = ["switch off", "turn off", "dowse"] - locks = "cmd:holds()" # only allow if command.obj is carried by caller. - help_category = "TutorialWorld" - - def func(self): - "Implements the command " - - if not self.obj.db.is_active: - self.caller.msg("%s is not burning." % self.obj.key) else: - # set lightsource to inactive - self.obj.db.is_active = False - # validating the scripts will kill it now that is_active=False. - self.obj.scripts.validate() - self.caller.msg("{GYou dowse {C%s.{n" % self.obj.key) - self.caller.location.msg_contents("%s dowses %s." % (self.caller, self.obj.key), exclude=[self.caller]) - self.caller.location.scripts.validate() - self.caller.execute_cmd("look") + self.caller.msg("%s is already burning." % self.obj.key) -class CmdSetLightSource(CmdSet): +class CmdSetLight(CmdSet): "CmdSet for the lightsource commands" key = "lightsource_cmdset" + # this is higher than the dark cmdset - important! + priority = 3 def at_cmdset_creation(self): "called at cmdset creation" - self.add(CmdLightSourceOn()) - self.add(CmdLightSourceOff()) + self.add(CmdLight()) class LightSource(TutorialObject): """ This implements a light source object. - When burned out, lightsource will be moved to its home - which by - default is the location it was first created at. + When burned out, the object will be deleted. """ + def at_init(self): + """ + If this is called with the Attribute is_giving_light already + set, we know that the timer got killed by a server + reload/reboot before it had time to finish. So we kill it here + instead. This is the price we pay for the simplicity of the + non-persistent delay() method. + """ + if self.db.is_giving_light: + self.delete() + def at_object_creation(self): "Called when object is first created." super(LightSource, self).at_object_creation() - self.db.tutorial_info = "This object can be turned on off and has a timed script controlling it." - self.db.is_active = False + self.db.tutorial_info = "This object can be lit to create light. It has a timeout for how long it burns." + self.db.is_giving_light = False self.db.burntime = 60 * 3 # 3 minutes + # this is the default desc, it can of course be customized + # when created. self.db.desc = "A splinter of wood with remnants of resin on it, enough for burning." - # add commands - self.cmdset.add_default(CmdSetLightSource, permanent=True) + # add the Light command + self.cmdset.add_default(CmdSetLight, permanent=True) - def reset(self): + def _burnout(self): """ - Can be called by tutorial world runner, or by the script when - the lightsource has burned out. + This is called when this light source burns out. We make no + use of the return value. """ - if self.db.burntime <= 0: - # light burned out. Since the lightsources's "location" should be - # a character, notify them this way. - try: - loc = self.location.location - except AttributeError: - loc = self.location - loc.msg_contents("{c%s{n {Rburns out.{n" % self.key) - self.db.is_active = False + # delete ourselves from the database + self.db.is_giving_light = False try: - # validate in holders current room, if possible - self.location.location.scripts.validate() + self.location.location.msg_contents("%s's %s flickers and dies." % + (self.location, self.key), exclude=self.location) + self.location.msg("Your %s flickers and dies." % self.key) + self.location.location.check_light_state() except AttributeError: - # maybe it was dropped, try validating at current location. try: - self.location.scripts.validate() + self.location.msg_contents("A %s on the floor flickers and dies." % self.key) + self.location.location.check_light_state() except AttributeError: pass self.delete() + def light(self): + """ + Light this object - this is called by Light command. + """ + if self.db.is_giving_light: + return False + # burn for 3 minutes before calling _burnout + self.db.is_giving_light = True + # if we are in a dark room, trigger its light check + try: + self.location.location.check_light_state() + except AttributeError: + try: + # maybe we are directly in the room + self.location.check_light_state() + except AttributeError: + pass + finally: + # start the burn timer. When it runs out, self._burnout + # will be called. + utils.delay(60 * 3, self._burnout) + return True + + #------------------------------------------------------------ # # Crumbling wall - unique exit @@ -382,7 +376,7 @@ class LightSource(TutorialObject): # This implements a simple puzzle exit that needs to be # accessed with commands before one can get to traverse it. # -# The puzzle is currently simply to move roots (that have +# The puzzle-part is simply to move roots (that have # presumably covered the wall) aside until a button for a # secret door is revealed. The original position of the # roots blocks the button, so they have to be moved to a certain @@ -398,25 +392,34 @@ class LightSource(TutorialObject): # along the sides. The goal is to make the center position clear. # (yes, it's really as simple as it sounds, just move the roots # to each side to "win". This is just a tutorial, remember?) +# +# The ShiftRoot command depends on the root object having an +# Attribute root_pos (a dictionary) to describe the current +# position of the roots. class CmdShiftRoot(Command): """ Shifts roots around. - shift blue root left/right - shift red root left/right - shift yellow root up/down - shift green root up/down + Usage: + shift blue root left/right + shift red root left/right + shift yellow root up/down + shift green root up/down """ key = "shift" - aliases = ["move"] - # the locattr() lock looks for the attribute is_dark on the current room. - locks = "cmd:not locattr(is_dark)" + aliases = ["shiftroot", "push", "pull", "move"] + # we only allow to use this command while the + # room is properly lit, so we lock it to the + # setting of Attribute "is_lit" on our location. + locks = "cmd:locattr(is_lit)" help_category = "TutorialWorld" def parse(self): - "custom parser; split input by spaces" + """ + Custom parser; split input by spaces for simplicity. + """ self.arglist = self.args.strip().split() def func(self): @@ -429,14 +432,20 @@ class CmdShiftRoot(Command): if not self.arglist: self.caller.msg("What do you want to move, and in what direction?") return + if "root" in self.arglist: + # we clean out the use of the word "root" self.arglist.remove("root") + # we accept arguments on the form + if not len(self.arglist) > 1: self.caller.msg("You must define which colour of root you want to move, and in which direction.") return + color = self.arglist[0].lower() direction = self.arglist[1].lower() + # get current root positions dict root_pos = self.obj.db.root_pos @@ -475,6 +484,7 @@ class CmdShiftRoot(Command): self.caller.msg("The thick reddish root gets in the way and is pushed back to the left.") else: self.caller.msg("You cannot move the root in that direction.") + # now the horizontal roots (yellow/green). They can be moved up/down elif color == "yellow": if direction == "up": @@ -506,11 +516,14 @@ class CmdShiftRoot(Command): self.caller.msg("The root with yellow flowers gets in the way and is pushed upwards.") else: self.caller.msg("You cannot move the root in that direction.") - # store new position + + # we have moved the root. Store new position self.obj.db.root_pos = root_pos - # check victory condition + + # Check victory condition if root_pos.values().count(0) == 0: # no roots in middle position - self.caller.db.crumbling_wall_found_button = True + # This will affect the cmd: lock of CmdPressButton + self.obj.db.button_exposed = True self.caller.msg("Holding aside the root you think you notice something behind it ...") @@ -520,8 +533,11 @@ class CmdPressButton(Command): """ key = "press" aliases = ["press button", "button", "push", "push button"] - # only accessible if the button was found and there is light. - locks = "cmd:attr(crumbling_wall_found_button) and not locattr(is_dark)" + # only accessible if the button was found and there is light. This checks + # the Attribute button_exposed on the Wall object so that + # you can only push the button when the puzzle is solved. It also + # checks the is_lit Attribute on the location. + locks = "cmd:objattr(button_exposed) and locattr(is_lit)" help_category = "TutorialWorld" def func(self): @@ -533,24 +549,21 @@ class CmdPressButton(Command): return # pushing the button - string = "You move your fingers over the suspicious depression, then gives it a " - string += "decisive push. First nothing happens, then there is a rumble and a hidden " - string += "{wpassage{n opens, dust and pebbles rumbling as part of the wall moves aside." - - # we are done - this will make the exit traversable! - self.caller.db.crumbling_wall_found_exit = True - # this will make it into a proper exit - eloc = self.caller.search(self.obj.db.destination, global_search=True) - if not eloc: - self.caller.msg("The exit leads nowhere, there's just more stone behind it ...") - return - self.obj.destination = eloc + string = "You move your fingers over the suspicious depression, then gives it a " \ + "decisive push. First nothing happens, then there is a rumble and a hidden " \ + "{wpassage{n opens, dust and pebbles rumbling as part of the wall moves aside." self.caller.msg(string) + string = "%s moves their fingers over the suspicious depression, then gives it a " \ + "decisive push. First nothing happens, then there is a rumble and a hidden " \ + "{wpassage{n opens, dust and pebbles rumbling as part of the wall moves aside." + self.caller.location.msg_contents(string % self.caller.key, exclude=self.caller) + self.obj.open_wall() class CmdSetCrumblingWall(CmdSet): "Group the commands for crumblingWall" key = "crumblingwall_cmdset" + priority = 2 def at_cmdset_creation(self): "called when object is first created." @@ -558,38 +571,32 @@ class CmdSetCrumblingWall(CmdSet): self.add(CmdPressButton()) -class CrumblingWall(TutorialObject, Exit): +class CrumblingWall(TutorialObject, DefaultExit): """ - The CrumblingWall can be examined in various - ways, but only if a lit light source is in the room. The traversal - itself is blocked by a traverse: lock on the exit that only - allows passage if a certain attribute is set on the trying - player. + This is a custom Exit. + + The CrumblingWall can be examined in various ways, but only if a + lit light source is in the room. The traversal itself is blocked + by a traverse: lock on the exit that only allows passage if a + certain attribute is set on the trying player. Important attribute destination - this property must be set to make this a valid exit whenever the button is pushed (this hides it as an exit until it actually is) """ + def at_init(self): + """ + Called when object is recalled from cache. + """ + self.reset() + def at_object_creation(self): "called when the object is first created." super(CrumblingWall, self).at_object_creation() self.aliases.add(["secret passage", "passage", "crack", "opening", "secret door"]) - # this is assigned first when pushing button, so assign - # this at creation time! - - self.db.destination = 2 - # locks on the object directly transfer to the exit "command" - self.locks.add("cmd:not locattr(is_dark)") - - self.db.tutorial_info = "This is an Exit with a conditional traverse-lock. Try to shift the roots around." - # the lock is important for this exit; we only allow passage - # if we "found exit". - self.locks.add("traverse:attr(crumbling_wall_found_exit)") - # set cmdset - self.cmdset.add(CmdSetCrumblingWall, permanent=True) # starting root positions. H1/H2 are the horizontally hanging roots, # V1/V2 the vertically hanging ones. Each can have three positions: @@ -598,6 +605,38 @@ class CrumblingWall(TutorialObject, Exit): # ever any other identical value. self.db.root_pos = {"yellow": 0, "green": 0, "red": 0, "blue": 0} + # flags controlling the puzzle victory conditions + self.db.button_exposed = False + self.db.exit_open = False + + # this is not even an Exit until it has a proper destination, and we won't assign + # that until it is actually open. Until then we store the destination here. This + # should be given a reasonable value at creation! + self.db.destination = 2 + + # we lock this Exit so that one can only execute commands on it + # if its location is lit and only traverse it once the Attribute + # exit_open is set to True. + self.locks.add("cmd:locattr(is_lit);traverse:objattr(exit_open)") + # set cmdset + self.cmdset.add(CmdSetCrumblingWall, permanent=True) + + def open_wall(self): + """ + This method is called by the push button command once the puzzle + is solved. It opens the wall and sets a timer for it to reset + itself. + """ + # this will make it into a proper exit (this returns a list) + eloc = search.search_object(self.db.destination) + if not eloc: + self.caller.msg("The exit leads nowhere, there's just more stone behind it ...") + else: + self.destination = eloc[0] + self.exit_open = True + # start a 45 second timer before closing again + utils.delay(45, self.reset) + def _translate_position(self, root, ipos): "Translates the position into words" rootnames = {"red": "The {rreddish{n vertical-hanging root ", @@ -622,19 +661,29 @@ class CrumblingWall(TutorialObject, Exit): This is called when someone looks at the wall. We need to echo the current root positions. """ - if caller.db.crumbling_wall_found_button: - string = "Having moved all the roots aside, you find that the center of the wall, " - string += "previously hidden by the vegetation, hid a curious square depression. It was maybe once " - string += "concealed and made to look a part of the wall, but with the crumbling of stone around it," - string += "it's now easily identifiable as some sort of button." + if self.db.button_exposed: + # we found the button by moving the roots + string = "Having moved all the roots aside, you find that the center of the wall, " \ + "previously hidden by the vegetation, hid a curious square depression. It was maybe once " \ + "concealed and made to look a part of the wall, but with the crumbling of stone around it," \ + "it's now easily identifiable as some sort of button." + elif self.db.exit_open: + # we pressed the button; the exit is open + string = "With the button pressed, a crack has opened in the root-covered wall, just wide enough " \ + "to squeeze through. A cold draft is coming from the hole and you get the feeling the " \ + "opening may close again soon." else: - string = "The wall is old and covered with roots that here and there have permeated the stone. " - string += "The roots (or whatever they are - some of them are covered in small non-descript flowers) " - string += "crisscross the wall, making it hard to clearly see its stony surface.\n" + # puzzle not solved yet. + string = "The wall is old and covered with roots that here and there have permeated the stone. " \ + "The roots (or whatever they are - some of them are covered in small non-descript flowers) " \ + "crisscross the wall, making it hard to clearly see its stony surface. Maybe you could " \ + "try to {wshift{n or {wmove{n them.\n" + # display the root positions to help with the puzzle for key, pos in self.db.root_pos.items(): string += "\n" + self._translate_position(key, pos) self.db.desc = string - # call the parent to continue execution (will use desc we just set) + + # call the parent to continue execution (will use the desc we just set) return super(CrumblingWall, self).return_appearance(caller) def at_after_traverse(self, traverser, source_location): @@ -642,7 +691,7 @@ class CrumblingWall(TutorialObject, Exit): This is called after we traversed this exit. Cleans up and resets the puzzle. """ - del traverser.db.crumbling_wall_found_button + del traverser.db.crumbling_wall_found_buttothe del traverser.db.crumbling_wall_found_exit self.reset() @@ -656,10 +705,11 @@ class CrumblingWall(TutorialObject, Exit): traversed the Exit. """ self.location.msg_contents("The secret door closes abruptly, roots falling back into place.") - for obj in self.location.contents: - # clear eventual puzzle-solved attribues on everyone that didn't - # get out in time. They have to try again. - del obj.db.crumbling_wall_found_exit + + # reset the flags and remove the exit destination + self.db.button_exposed = False + self.db.exit_open = False + self.destination = None # Reset the roots with some random starting positions for the roots: start_pos = [{"yellow":1, "green":0, "red":0, "blue":0}, @@ -667,8 +717,7 @@ class CrumblingWall(TutorialObject, Exit): {"yellow":0, "green":1, "red":-1, "blue":0}, {"yellow":1, "green":0, "red":0, "blue":0}, {"yellow":0, "green":0, "red":0, "blue":1}] - self.db.root_pos = start_pos[random.randint(0, 4)] - self.destination = None + self.db.root_pos = random.choice(start_pos) #------------------------------------------------------------ @@ -755,7 +804,7 @@ class CmdAttack(Command): self.caller.db.combat_parry_mode = False else: self.caller.msg("You fumble with your weapon, unsure of whether to stab, slash or parry ...") - self.caller.location.msg_contents("%s fumbles with their weapon." % self.obj.key) + self.caller.location.msg_contents("%s fumbles with their weapon." % self.caller, exclude=self.caller) self.caller.db.combat_parry_mode = False return @@ -808,7 +857,7 @@ class Weapon(TutorialObject): super(Weapon, self).at_object_creation() self.db.hit = 0.4 # hit chance self.db.parry = 0.8 # parry chance - self.db.damage = 8.0 + self.db.damage = 1.0 self.db.magic = False self.cmdset.add_default(CmdSetWeapon, permanent=True) @@ -828,8 +877,113 @@ class Weapon(TutorialObject): # # Weapon rack - spawns weapons # +# This is a spawner mechanism that creates custom weapons from a +# spawner prototype dictionary. Note that we only create a single typeclass +# (Weapon) yet customize all these different weapons using the spawner. +# The spawner dictionaries could easily sit in separate modules and be +# used to create unique and interesting variations of typeclassed +# objects. +# #------------------------------------------------------------ +WEAPON_PROTOTYPES = { + "weapon": { + "typeclass": "evennia.contrib.tutorial_world.objects.Weapon", + "key": "Weapon", + "hit": 0.2, + "parry": 0.2, + "damage": 1.0, + "magic": False, + "desc": "A generic blade."}, + "knife": { + "prototype": "weapon", + "aliases": "sword", + "key": "Kitchen knife", + "desc":"A rusty kitchen knife. Better than nothing.", + "damage": 3}, + "dagger": { + "prototype": "knife", + "key": "Rusty dagger", + "aliases": ["knife", "dagger"], + "desc": "A double-edged dagger with a nicked edge and a wooden handle.", + "hit": 0.25}, + "sword": { + "prototype": "weapon", + "key": "Rusty sword", + "aliases": ["sword"], + "desc": "A rusty shortsword. It has a leather-wrapped handle covered i food grease.", + "hit": 0.3, + "damage": 5, + "parry": 0.5}, + "club": { + "prototype": "weapon", + "key":"Club", + "desc": "A heavy wooden club, little more than a heavy branch.", + "hit": 0.4, + "damage": 6, + "parry": 0.2}, + "axe": { + "prototype": "weapon", + "key":"Axe", + "desc": "A woodcutter's axe with a keen edge.", + "hit": 0.4, + "damage": 6, + "parry": 0.2}, + "ornate longsword": { + "prototype":"sword", + "key": "Ornate longsword", + "desc": "A fine longsword with some swirling patterns on the handle.", + "hit": 0.5, + "magic": True, + "damage": 5}, + "warhammer": { + "prototype": "club", + "key": "Silver Warhammer", + "aliases": ["hammer", "warhammer", "war"], + "desc": "A heavy war hammer with silver ornaments. This huge weapon causes massive damage - if you can hit.", + "hit": 0.4, + "magic": True, + "damage": 8}, + "rune axe": { + "prototype": "axe", + "key": "Runeaxe", + "aliases": ["axe"], + "hit": 0.4, + "magic": True, + "damage": 6}, + "thruning": { + "prototype": "ornate longsword", + "key": "Broadsword named Thruning", + "desc": "This heavy bladed weapon is marked with the name 'Thruning'. It is very powerful in skilled hands.", + "hit": 0.6, + "parry": 0.6, + "damage": 7}, + "slayer waraxe": { + "prototype": "rune axe", + "key": "Slayer waraxe", + "aliases": ["waraxe", "war", "slayer"], + "desc": "A huge double-bladed axe marked with the runes for 'Slayer'. It has more runic inscriptions on its head, which you cannot decipher.", + "hit": 0.7, + "damage": 8}, + "ghostblade": { + "prototype": "ornate longsword", + "key": "The Ghostblade", + "aliases": ["blade", "ghost"], + "desc": "This massive sword is large as you are tall, yet seems to weigh almost nothing. It's almost like it's not really there.", + "hit": 0.9, + "parry": 0.8, + "damage": 10}, + "hawkblade": { + "prototype": "ghostblade", + "key": "The Hawblade", + "aliases": ["hawk", "blade"], + "desc": "The weapon of a long-dead heroine and a more civilized age, the hawk-shaped hilt of this blade almost has a life of its own.", + "hit": 0.85, + "parry": 0.7, + "damage": 11} + } + + class CmdGetWeapon(Command): """ Usage: @@ -837,40 +991,23 @@ class CmdGetWeapon(Command): This will try to obtain a weapon from the container. """ - key = "get" + key = "get weapon" aliases = "get weapon" locks = "cmd:all()" help_cateogory = "TutorialWorld" def func(self): - "Implement the command" - - rack_id = self.obj.db.rack_id - if self.caller.attributes.get(rack_id): - # we don't allow a player to take more than one weapon from rack. - self.caller.msg("%s has no more to offer you." % self.obj.name) - else: - dmg, name, aliases, desc, magic = self.obj.randomize_type() - new_weapon = create_object(Weapon, key=name, aliases=aliases,location=self.caller, home=self.caller) - new_weapon.db.rack_id = rack_id - new_weapon.db.damage = dmg - new_weapon.db.desc = desc - new_weapon.db.magic = magic - ostring = self.obj.db.get_text - if not ostring: - ostring = "You pick up %s." - if '%s' in ostring: - self.caller.msg(ostring % name) - else: - self.caller.msg(ostring) - # tag the caller so they cannot keep taking objects from the rack. - self.caller.attributes.add(rack_id, True) - + """ + Get a weapon from the container. It will + itself handle all messages. + """ + self.obj.produce_weapon(self.caller) class CmdSetWeaponRack(CmdSet): - "group the rack cmd" + """ + The cmdset for the rack. + """ key = "weaponrack_cmdset" - mergemode = "Replace" def at_cmdset_creation(self): "Called at first creation of cmdset" @@ -879,73 +1016,49 @@ class CmdSetWeaponRack(CmdSet): class WeaponRack(TutorialObject): """ - This will spawn a new weapon for the player unless the player already has - one from this rack. + This object represents a weapon store. When people use the + "get weapon" command on this rack, it will produce one + random weapon from among those registered to exist + on it. This will also set a property on the character + to make sure they can't get more than one at a time. + + Attributes to set on this object: + available_weapons: list of prototype-keys from + WEAPON_PROTOTYPES, the weapons available in this rack. + no_more_weapons_msg - error message to return to players + who already got one weapon from the rack and tries to + grab another one. - attribute to set at creation: - min_dmg - the minimum damage of objects from this rack - max_dmg - the maximum damage of objects from this rack - magic - if weapons should be magical (have the magic flag set) - get_text - the echo text to return when getting the weapon. Give '%s' - to include the name of the weapon. """ def at_object_creation(self): - "called at creation" + """ + called at creation + """ self.cmdset.add_default(CmdSetWeaponRack, permanent=True) self.db.rack_id = "weaponrack_1" - self.db.min_dmg = 1.0 - self.db.max_dmg = 4.0 - self.db.magic = False + # these are prototype names from the prototype + # dictionary above. + self.db.get_weapon_msg = "You find {c%s{n." + self.db.no_more_weapons_msg = "you find nothing else of use." + self.db.available_weapons = ["knife", "dagger", + "sword", "club"] - def randomize_type(self): + def produce_weapon(self, caller): """ - this returns a random weapon + This will produce a new weapon from the rack, + assuming the caller hasn't already gotten one. When + doing so, the caller will get Tagged with the id + of this rack, to make sure they cannot keep + pulling weapons from it indefinitely. """ - min_dmg = float(self.db.min_dmg) - max_dmg = float(self.db.max_dmg) - magic = bool(self.db.magic) - dmg = min_dmg + random.random()*(max_dmg - min_dmg) - aliases = [self.db.rack_id, "weapon"] - if dmg < 1.5: - name = "Knife" - desc = "A rusty kitchen knife. Better than nothing." - elif dmg < 2.0: - name = "Rusty dagger" - desc = "A double-edged dagger with nicked edge. It has a wooden handle." - elif dmg < 3.0: - name = "Sword" - desc = "A rusty shortsword. It has leather wrapped around the handle." - elif dmg < 4.0: - name = "Club" - desc = "A heavy wooden club with some rusty spikes in it." - elif dmg < 5.0: - name = "Ornate Longsword" - aliases.extend(["longsword","ornate"]) - desc = "A fine longsword." - elif dmg < 6.0: - name = "Runeaxe" - aliases.extend(["rune","axe"]) - desc = "A single-bladed axe, heavy but yet easy to use." - elif dmg < 7.0: - name = "Broadsword named Thruning" - aliases.extend(["thruning","broadsword"]) - desc = "This heavy bladed weapon is marked with the name 'Thruning'. It is very powerful in skilled hands." - elif dmg < 8.0: - name = "Silver Warhammer" - aliases.append("warhammer") - desc = "A heavy war hammer with silver ornaments. This huge weapon causes massive damage." - elif dmg < 9.0: - name = "Slayer Waraxe" - aliases.extend(["waraxe","slayer"]) - desc = "A huge double-bladed axe marked with the runes for 'Slayer'. It has more runic inscriptions on its head, which you cannot decipher." - elif dmg < 10.0: - name = "The Ghostblade" - aliases.append("ghostblade") - desc = "This massive sword is large as you are tall. Its metal shine with a bluish glow." + rack_id = self.db.rack_id + if caller.tags.get(rack_id, category="tutorial_world"): + caller.msg(self.db.no_more_weapons_msg) else: - name = "The Hawkblade" - aliases.append("hawkblade") - desc = "White surges of magical power runs up and down this runic blade. The hawks depicted on its hilt almost seems to have a life of their own." - if dmg < 9 and magic: - desc += "\nThe metal seems to glow faintly, as if imbued with more power than what is immediately apparent." - return dmg, name, aliases, desc, magic + prototype = random.choice(self.db.available_weapons) + # use the spawner to create a new Weapon from the + # spawner dictionary, tag the caller + wpn = spawn(WEAPON_PROTOTYPES[prototype], prototype_parents=WEAPON_PROTOTYPES)[0] + caller.tags.add(rack_id, category="tutorial_world") + wpn.location = caller + caller.msg(self.db.get_weapon_msg % wpn.key) diff --git a/evennia/contrib/tutorial_world/rooms.py b/evennia/contrib/tutorial_world/rooms.py new file mode 100644 index 000000000..10d4f3495 --- /dev/null +++ b/evennia/contrib/tutorial_world/rooms.py @@ -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 = + @detail ;;... = 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 + look + look * + + 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") diff --git a/evennia/game_template/.gitignore b/evennia/game_template/.gitignore new file mode 100644 index 000000000..905ce44bc --- /dev/null +++ b/evennia/game_template/.gitignore @@ -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 diff --git a/evennia/game_template/README.md b/evennia/game_template/README.md new file mode 100644 index 000000000..1b20b7996 --- /dev/null +++ b/evennia/game_template/README.md @@ -0,0 +1,36 @@ +# Welcome to Evennia! + +This directory is your game directory, set up to let you start with +your new game right away. + +You can delete this readme file when you've read it and you can +re-arrange things in this game-directory to suit your own sense of +organisation (the only exception is the directory structure of the +server/ directory, which Evennia expects). If you change the structure +you must however also edit/add to your settings file to tell Evennia +where to look for things. + +Your game's main configuration file is found in +`server/conf/settings.py` (but you don't need to change it to get +started). If you just created this directory, `cd` to this directory +then initialize a new database using + + evennia migrate + +To start the server, `cd` to this directory and run + + evennia -i start + +This will start the server so that it logs output to the console. Make +sure to create a superuser when asked. By default you can now connect +to your new game using a MUD client on localhost:4000. You can also +log into the web client by pointing a browser to +http://localhost:8000. + +# Getting started + +It's highly recommended that you look up Evennia's extensive +documentation found here: https://github.com/evennia/evennia/wiki. + +Plenty of beginner's tutorials can be found here: +http://github.com/evennia/evennia/wiki/Tutorials. diff --git a/evennia/game_template/__init__.py b/evennia/game_template/__init__.py new file mode 100644 index 000000000..6e3dbee76 --- /dev/null +++ b/evennia/game_template/__init__.py @@ -0,0 +1,6 @@ +""" +This sub-package holds the template for creating a new game folder. +The new game folder (when running evennia --init) is a copy of this +folder. + +""" diff --git a/evennia/game_template/commands/README.md b/evennia/game_template/commands/README.md new file mode 100644 index 000000000..0425ce6bc --- /dev/null +++ b/evennia/game_template/commands/README.md @@ -0,0 +1,14 @@ +# commands/ + +This folder holds modules for implementing one's own commands and +command sets. All the modules' classes are essentially empty and just +imports the default implementations from Evennia; so adding anything +to them will start overloading the defaults. + +You can change the organisation of this directory as you see fit, just +remember that if you change any of the default command set classes' +locations, you need to add the appropriate paths to +`server/conf/settings.py` so that Evennia knows where to find them. +Also remember that if you create new sub directories you must put +(optionally empty) `__init__.py` files in there so that Python can +find your modules. diff --git a/game/gamesrc/web/__init__.py b/evennia/game_template/commands/__init__.py similarity index 100% rename from game/gamesrc/web/__init__.py rename to evennia/game_template/commands/__init__.py diff --git a/game/gamesrc/commands/examples/command.py b/evennia/game_template/commands/command.py similarity index 92% rename from game/gamesrc/commands/examples/command.py rename to evennia/game_template/commands/command.py index e0c945998..42e778886 100644 --- a/game/gamesrc/commands/examples/command.py +++ b/evennia/game_template/commands/command.py @@ -1,16 +1,12 @@ """ -Example command module template +Commands -Copy this module up one level to gamesrc/commands/ and name it as -befits your use. You can then use it as a template to define your new -commands. To use them you also need to group them in a CommandSet (see -examples/cmdset.py) +Commands describe the input the player can do to the game. """ -from ev import Command as BaseCommand -from ev import default_cmds -from ev import utils +from evennia import Command as BaseCommand +from evennia import default_cmds class Command(BaseCommand): @@ -31,13 +27,14 @@ class Command(BaseCommand): locks = "cmd:all()" help_category = "General" + # optional # auto_help = False # uncomment to deactive auto-help for this command. # arg_regex = r"\s.*?|$" # optional regex detailing how the part after # the cmdname must look to match this command. # (we don't implement hook method access() here, you don't need to # modify that unless you want to change how the lock system works - # (in that case see src.commands.command.Command)) + # (in that case see evennia.commands.command.Command)) def at_pre_cmd(self): """ diff --git a/evennia/game_template/commands/default_cmdsets.py b/evennia/game_template/commands/default_cmdsets.py new file mode 100644 index 000000000..e9c125dec --- /dev/null +++ b/evennia/game_template/commands/default_cmdsets.py @@ -0,0 +1,91 @@ +""" +Command sets + +All commands in the game must be grouped in a cmdset. A given command +can be part of any number of cmdsets and cmdsets can be added/removed +and merged onto entities at runtime. + +To create new commands to populate the cmdset, see +commands/command.py. + +This module wrap the default command sets of Evennia; overload them +to add/remove commands from the default lineup. You can create your +own cmdsets by inheriting from them or directly from evennia.CmdSet. + +""" + +from evennia import default_cmds + +class CharacterCmdSet(default_cmds.CharacterCmdSet): + """ + The CharacterCmdSet contains general in-game commands like look, + get etc available on in-game Character objects. It is merged with + the PlayerCmdSet when a Player puppets a Character. + """ + key = "DefaultCharacter" + + def at_cmdset_creation(self): + """ + Populates the cmdset + """ + super(CharacterCmdSet, self).at_cmdset_creation() + # + # any commands you add below will overload the default ones. + # + + +class PlayerCmdSet(default_cmds.PlayerCmdSet): + """ + This is the cmdset available to the Player at all times. It is + combined with the CharacterCmdSet when the Player puppets a + Character. It holds game-account-specific commands, channel + commands etc. + """ + key = "DefaultPlayer" + + def at_cmdset_creation(self): + """ + Populates the cmdset + """ + super(PlayerCmdSet, self).at_cmdset_creation() + # + # any commands you add below will overload the default ones. + # + + +class UnloggedinCmdSet(default_cmds.UnloggedinCmdSet): + """ + Command set available to the Session before being logged in. This + holds commands like creating a new account, logging in etc. + """ + key = "DefaultUnloggedin" + + def at_cmdset_creation(self): + """ + Populates the cmdset + """ + super(UnloggedinCmdSet, self).at_cmdset_creation() + # + # any commands you add below will overload the default ones. + # + + +class SessionCmdSet(default_cmds.SessionCmdSet): + """ + This cmdset is made available on Session level once logged in. It + is empty by default. + """ + key = "DefaultSession" + + def at_cmdset_creation(self): + """ + This is the only method defined in a cmdset, called during + its creation. It should populate the set with command instances. + + As and example we just add the empty base Command object. + It prints some info. + """ + super(SessionCmdSet, self).at_cmdset_creation() + # + # any commands you add below will overload the default ones. + # diff --git a/evennia/game_template/server/README.md b/evennia/game_template/server/README.md new file mode 100644 index 000000000..bf64052b3 --- /dev/null +++ b/evennia/game_template/server/README.md @@ -0,0 +1,38 @@ +# server/ + +This directory holds files used by and configuring the Evennia server +itself. + +Out of all the subdirectories in the game directory, Evennia does +expect this directory to exist, so you should normally not delete, +rename or change its folder structure. + +When running you will find four new files appear in this directory: + + - `server.pid` and `portal.pid`: These hold the process IDs of the + Portal and Server, so that they can be managed by the launcher. If + Evennia is shut down uncleanly (e.g. by a crash or via a kill + signal), these files might erroneously remain behind. If so Evennia + will tell you they are "stale" and they can be deleted manually. + - `server.restart` and `portal.restart`: These hold flags to tell the + server processes if it should die or start again. You never need to + modify those files. + - `evennia.db3`: This will only appear if you are using the default + SQLite3 database; it a binary file that holds the entire game + database; deleting this file will effectively reset the game for + you and you can start fresh with `evennia migrate` (useful during + development). + +## server/conf/ + +This subdirectory holds the configuration modules for the server. With +them you can change how Evennia operates and also plug in your own +functionality to replace the default. You usually need to restart the +server to apply changes done here. The most importand file is the file +`settings.py´ which is the main configuration file of Evennia. + +## server/logs/ + +This subdirectory holds various log files created by the running +Evennia server. It is also the default location for storing any custom +log files you might want to output using Evennia's logging mechanisms. diff --git a/contrib/procpools/__init__.py b/evennia/game_template/server/__init__.py similarity index 100% rename from contrib/procpools/__init__.py rename to evennia/game_template/server/__init__.py diff --git a/contrib/tutorial_world/__init__.py b/evennia/game_template/server/conf/__init__.py similarity index 100% rename from contrib/tutorial_world/__init__.py rename to evennia/game_template/server/conf/__init__.py diff --git a/game/gamesrc/conf/examples/at_initial_setup.py b/evennia/game_template/server/conf/at_initial_setup.py similarity index 76% rename from game/gamesrc/conf/examples/at_initial_setup.py rename to evennia/game_template/server/conf/at_initial_setup.py index e192331e1..b394a0423 100644 --- a/game/gamesrc/conf/examples/at_initial_setup.py +++ b/evennia/game_template/server/conf/at_initial_setup.py @@ -1,12 +1,6 @@ """ At_initial_setup module template -Copy this module up one level to /gamesrc/conf, name it what you like -and then use it as a template to modify. - -Then edit settings.AT_INITIAL_SETUP_HOOK_MODULE to point to your new -module. - Custom at_initial_setup method. This allows you to hook special modifications to the initial server startup process. Note that this will only be run once - when the server starts up for the very first diff --git a/evennia/game_template/server/conf/at_search.py b/evennia/game_template/server/conf/at_search.py new file mode 100644 index 000000000..fc23e7638 --- /dev/null +++ b/evennia/game_template/server/conf/at_search.py @@ -0,0 +1,90 @@ +""" +Search and multimatch handling + +This module allows for overloading two functions used by Evennia's +search functionality: + + at_search_result: + This is called whenever a result is returned from an object + search (a common operation in commands). It should (together + with at_multimatch_input below) define some way to present and + differentiate between multiple matches (by default these are + presented as 1-ball, 2-ball etc) + at_multimatch_input: + This is called with a search term and should be able to + identify if the user wants to separate a multimatch-result + (such as that from a previous search). By default, this + function understands input on the form 1-ball, 2-ball etc as + indicating that the 1st or 2nd match for "ball" should be + used. + +This module is not called by default. To overload the defaults, add +one or both of the following lines to your settings.py file: + + SEARCH_AT_RESULT = "server.conf.at_search.at_search_result" + SEARCH_AT_MULTIMATCH_INPUT = "server.conf.at_search.at_multimatch_input" + +""" + +def at_search_result(msg_obj, ostring, results, global_search=False, + nofound_string=None, multimatch_string=None, quiet=False): + """ + Called by search methods after a result of any type has been found. + + Takes a search result (a list) and + formats eventual errors. + + msg_obj - object to receive feedback. + ostring - original search string + results - list of found matches (0, 1 or more) + global_search - if this was a global_search or not + (if it is, there might be an idea of supplying + dbrefs instead of only numbers) + nofound_string - optional custom string for not-found error message. + multimatch_string - optional custom string for multimatch error header + quiet - work normally, but don't echo to caller, just return the + results. + + Multiple matches are returned to the searching object + as a list of results ["1-object", "2-object","3-object",...] + A single match is returned on its own. + """ + pass + + +def at_multimatch_input(ostring): + """ + This parser will be called by the engine when a user supplies + a search term. The search term must be analyzed to determine + if the user wants to differentiate between multiple matches + (usually found during a previous search). + + This method should separate out any identifiers from the search + string used to differentiate between same-named objects. The + result should be a tuple (index, search_string) where the index + gives which match among multiple matches should be used (1 being + the lowest number, rather than 0 as in Python). + + The default parser will intercept input on the following form: + + 2-object + + This will be parsed to (2, "object") and, if applicable, will tell + the engine to pick the second from a list of same-named matches of + objects called "object". + + Ex for use in a game session: + + > look + You see: ball, ball, ball and ball. + > get ball + There where multiple matches for ball: + 1-ball + 2-ball + 3-ball + 4-ball + > get 3-ball + You get the ball. + + """ + pass diff --git a/game/gamesrc/conf/examples/at_server_startstop.py b/evennia/game_template/server/conf/at_server_startstop.py similarity index 51% rename from game/gamesrc/conf/examples/at_server_startstop.py rename to evennia/game_template/server/conf/at_server_startstop.py index c3ad6384f..98c29fa28 100644 --- a/game/gamesrc/conf/examples/at_server_startstop.py +++ b/evennia/game_template/server/conf/at_server_startstop.py @@ -1,21 +1,11 @@ """ +Server startstop hooks -At_server_startstop module template +This module contains functions called by Evennia at various +points during its startup, reload and shutdown sequence. It +allows for customizing the server operation as desired. -Copy this module one level up, to gamesrc/conf/, name it what you -will and use it as a template for your modifications. - -Then edit settings.AT_SERVER_STARTSTOP_MODULE to point to your new -module. - -This module contains functions that are imported and called by the -server whenever it changes its running status. At the point these -functions are run, all applicable hooks on individual objects have -already been executed. The main purpose of this is module is to have a -safe place to initialize eventual custom modules that your game needs -to start up or load. - -The module should define at least these global functions: +This module must contain at least these global functions: at_server_start() at_server_stop() @@ -37,8 +27,8 @@ def at_server_start(): def at_server_stop(): """ - This is called just before a server is shut down, regardless - of it is fore a reload, reset or shutdown. + This is called just before the server is shut down, regardless + of it is for a reload, reset or shutdown. """ pass @@ -67,6 +57,7 @@ def at_server_cold_start(): def at_server_cold_stop(): """ - This is called only when the server goes down due to a shutdown or reset. + This is called only when the server goes down due to a shutdown or + reset. """ pass diff --git a/evennia/game_template/server/conf/cmdparser.py b/evennia/game_template/server/conf/cmdparser.py new file mode 100644 index 000000000..b2d024185 --- /dev/null +++ b/evennia/game_template/server/conf/cmdparser.py @@ -0,0 +1,54 @@ +""" +Changing the default command parser + +The cmdparser is responsible for parsing the raw text inserted by the +user, identifying which command/commands match and return one or more +matching command objects. It is called by Evennia's cmdhandler and +must accept input and return results on the same form. The default +handler is very generic so you usually don't need to overload this +unless you have very exotic parsing needs; advanced parsing is best +done at the Command.parse level. + +The default cmdparser understands the following command combinations +(where [] marks optional parts.) + +[cmdname[ cmdname2 cmdname3 ...] [the rest] + +A command may consist of any number of space-separated words of any +length, and contain any character. It may also be empty. + +The parser makes use of the cmdset to find command candidates. The +parser return a list of matches. Each match is a tuple with its first +three elements being the parsed cmdname (lower case), the remaining +arguments, and the matched cmdobject from the cmdset. + + +This module is not accessed by default. To tell Evennia to use it +instead of the default command parser, add the following line to +your settings file: + + COMMAND_PARSER = "server.conf.cmdparser.cmdparser" + +""" + +def cmdparser(raw_string, cmdset, caller, match_index=None): + """ + This function is called by the cmdhandler once it has + gathered and merged all valid cmdsets valid for this particular parsing. + + raw_string - the unparsed text entered by the caller. + cmdset - the merged, currently valid cmdset + caller - the caller triggering this parsing + match_index - an optional integer index to pick a given match in a + list of same-named command matches. + + Returns: + list of tuples: [(cmdname, args, cmdobj, cmdlen, mratio), ...] + where cmdname is the matching command name and args is + everything not included in the cmdname. Cmdobj is the actual + command instance taken from the cmdset, cmdlen is the length + of the command name and the mratio is some quality value to + (possibly) separate multiple matches. + + """ + # Your implementation here diff --git a/evennia/game_template/server/conf/connection_screens.py b/evennia/game_template/server/conf/connection_screens.py new file mode 100644 index 000000000..f7a2c9e9d --- /dev/null +++ b/evennia/game_template/server/conf/connection_screens.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +""" +Connection screen + +Texts in this module will be shown to the user at login-time. + +Evennia will look at global string variables (variables defined +at the "outermost" scope of this module and use it as the +connection screen. If there are more than one, Evennia will +randomize which one it displays. + +The commands available to the user when the connection screen is shown +are defined in commands.default_cmdsets.UnloggedinCmdSet and the +screen is read and displayed by the unlogged-in "look" command. + +""" + +from django.conf import settings +from evennia import utils + +CONNECTION_SCREEN = \ +"""{b=============================================================={n + Welcome to {g%s{n, version %s! + + If you have an existing account, connect to it by typing: + {wconnect {n + If you need to create an account, type (without the <>'s): + {wcreate {n + + If you have spaces in your username, enclose it in quotes. + Enter {whelp{n for more info. {wlook{n will re-show this screen. +{b=============================================================={n""" \ + % (settings.SERVERNAME, utils.get_evennia_version()) diff --git a/evennia/game_template/server/conf/inlinefunc.py b/evennia/game_template/server/conf/inlinefunc.py new file mode 100644 index 000000000..cec7817ea --- /dev/null +++ b/evennia/game_template/server/conf/inlinefunc.py @@ -0,0 +1,42 @@ +""" +Inlinefunc + +Inline functions allow for direct conversion of text users mark in a +special way. Inlinefuncs are deactivated by default. To activate, add + + INLINEFUNC_ENABLED = True + +to your settings file. The default inlinefuncs are found in +evennia.utils.inlinefunc. + +In text, usage is straightforward: + +{funcname([arg1,arg2,...]) text {/funcname + +Example 1 (using the "pad" inlinefunc): + "This is {pad(50,c,-) a center-padded text{/pad of width 50." + -> + "This is -------------- a center-padded text--------------- of width 50." + +Example 2 (using "pad" and "time" inlinefuncs): + "The time is {pad(30){time(){/time{/padright now." + -> + "The time is Oct 25, 11:09 right now." + +To add more inline functions, add them to this module, using +the following call signature: + + def funcname(text, *args) + +where the text is always the part between {funcname(args) and +{/funcname and the *args are taken from the appropriate part of the +call. It is important that the inline function properly clean the +incoming args, checking their type and replacing them with sane +defaults if needed. If impossible to resolve, the unmodified text +should be returned. The inlinefunc should never cause a traceback. + +""" + +#def capitalize(text, *args): +# "Silly capitalize example" +# return text.capitalize() diff --git a/evennia/game_template/server/conf/lockfuncs.py b/evennia/game_template/server/conf/lockfuncs.py new file mode 100644 index 000000000..2ca59c779 --- /dev/null +++ b/evennia/game_template/server/conf/lockfuncs.py @@ -0,0 +1,30 @@ +""" + +Lockfuncs + +Lock functions are functions available when defining lock strings, +which in turn limits access to various game systems. + +All functions defined globally in this module are assumed to be +available for use in lockstrings to determine access. See the +Evennia documentation for more info on locks. + +A lock function is always called with two arguments, accessing_obj and +accessed_obj, followed by any number of arguments. All possible +arguments should be handled with *args, **kwargs. The lock function +should handle all eventual tracebacks by logging the error and +returning False. + +Lock functions in this module extend (and will overload same-named) +lock functions from evennia.locks.lockfuncs. + +""" + +#def myfalse(accessing_obj, accessed_obj, *args, **kwargs): +# """ +# called in lockstring with myfalse(). +# A simple logger that always returns false. Prints to stdout +# for simplicity, should use utils.logger for real operation. +# """ +# print "%s tried to access %s. Access denied." % (accessing_obj, accessed_obj) +# return False diff --git a/game/gamesrc/conf/examples/mssp.py b/evennia/game_template/server/conf/mssp.py similarity index 85% rename from game/gamesrc/conf/examples/mssp.py rename to evennia/game_template/server/conf/mssp.py index df005aaa4..f862ace5a 100644 --- a/game/gamesrc/conf/examples/mssp.py +++ b/evennia/game_template/server/conf/mssp.py @@ -1,22 +1,16 @@ """ -MSSP module template +MSSP (Mud Server Status Protocol) meta information -Copy this module one level up, to gamesrc/conf/, name it -what you want and edit it to your satisfaction. +MUD website listings (that you have registered with) can use this +information to keep up-to-date with your game stats as you change +them. Also number of currently active players and uptime will +automatically be reported. You don't have to fill in everything +(and most are not used by all crawlers); leave the default +if so needed. You need to @reload the game before updated +information is made available to crawlers (reloading does not +affect uptime). -Then change settings.MSSP_META_MODULE to point to your new module. - - MSSP (Mud Server Status Protocol) meta information - - MUD website listings (that you have registered with) can use this - information to keep up-to-date with your game stats as you change - them. Also number of currently active players and uptime will - automatically be reported. You don't have to fill in everything - (and most are not used by all crawlers); leave the default - if so needed. You need to @reload the game before updated - information is made available to crawlers (reloading does not - affect uptime). """ MSSPTable = { diff --git a/evennia/game_template/server/conf/oobfuncs.py b/evennia/game_template/server/conf/oobfuncs.py new file mode 100644 index 000000000..18e1b3565 --- /dev/null +++ b/evennia/game_template/server/conf/oobfuncs.py @@ -0,0 +1,58 @@ +""" +OOB configuration. + +This module should be included in (or replace) the +default module set in settings.OOB_PLUGIN_MODULES + +A function oob_error will be used as optional error management. +The available OOB commands can be extended by changing + + `settings.OOB_PLUGIN_MODULES` + +CMD_MAP: This module must contain a global dictionary CMD_MAP. This is +a dictionary that maps the call-name available to a function in this +module (this allows you to map multiple oob cmdnames to a single +actual Python function, for example). + +oob functions have the following call signature: + + function(session, *args, **kwargs) + +where session is the active session and *args, **kwargs are extra +arguments sent with the oob command. + +A function mapped to the key "oob_error" will retrieve error strings +if it is defined. It will get the error message as its 1st argument. + + oob_error(session, error, *args, **kwargs) + +This allows for customizing error handling. + +Data is usually returned to the user via a return OOB call: + + session.msg(oob=(oobcmdname, (args,), {kwargs})) + +Oobcmdnames are case-sensitive. Note that args, kwargs must be +iterable. Non-iterables will be interpreted as a new command name (you +can send multiple oob commands with one msg() call)) + +""" + +# import the contents of the default msdp module +from evennia.server.oob_cmds import * + + +# def oob_echo(session, *args, **kwargs): +# """ +# Example echo function. Echoes args, kwargs sent to it. +# +# Args: +# session (Session): The Session to receive the echo. +# args (list of str): Echo text. +# kwargs (dict of str, optional): Keyed echo text +# +# """ +# session.msg(oob=("echo", args, kwargs)) +# +## oob command map +# CMD_MAP = {"ECHO": oob_echo} diff --git a/evennia/game_template/server/conf/portal_services_plugins.py b/evennia/game_template/server/conf/portal_services_plugins.py new file mode 100644 index 000000000..b536c5675 --- /dev/null +++ b/evennia/game_template/server/conf/portal_services_plugins.py @@ -0,0 +1,24 @@ +""" +Start plugin services + +This plugin module can define user-created services for the Portal to +start. + +This module must handle all imports and setups required to start +twisted services (see examples in evennia.server.portal.portal). It +must also contain a function start_plugin_services(application). +Evennia will call this function with the main Portal application (so +your services can be added to it). The function should not return +anything. Plugin services are started last in the Portal startup +process. + +""" + + +def start_plugin_services(portal): + """ + This hook is called by Evennia, last in the Portal startup process. + + portal - a reference to the main portal application. + """ + pass diff --git a/evennia/game_template/server/conf/server_services_plugins.py b/evennia/game_template/server/conf/server_services_plugins.py new file mode 100644 index 000000000..e3d41fe3a --- /dev/null +++ b/evennia/game_template/server/conf/server_services_plugins.py @@ -0,0 +1,24 @@ +""" + +Server plugin services + +This plugin module can define user-created services for the Server to +start. + +This module must handle all imports and setups required to start a +twisted service (see examples in evennia.server.server). It must also +contain a function start_plugin_services(application). Evennia will +call this function with the main Server application (so your services +can be added to it). The function should not return anything. Plugin +services are started last in the Server startup process. + +""" + + +def start_plugin_services(server): + """ + This hook is called by Evennia, last in the Server startup process. + + server - a reference to the main server application. + """ + pass diff --git a/evennia/game_template/server/conf/serversession.py b/evennia/game_template/server/conf/serversession.py new file mode 100644 index 000000000..050ff3e96 --- /dev/null +++ b/evennia/game_template/server/conf/serversession.py @@ -0,0 +1,35 @@ +""" +ServerSession + +The serversession is the Server-side in-memory representation of a +user connecting to the game. Evennia manages one Session per +connection to the game. So a user logged into the game with multiple +clients (if Evennia is configured to allow that) will have multiple +sessions tied to one Player object. All communication between Evennia +and the real-world user goes through the Session(s) associated with that user. + +It should be noted that modifying the Session object is not usually +necessary except for the most custom and exotic designs - and even +then it might be enough to just add custom session-level commands to +the SessionCmdSet instead. + +This module is not normally called. To tell Evennia to use the class +in this module instead of the default one, add the following to your +settings file: + + SERVER_SESSION_CLASS = "server.conf.serversession.ServerSession" + +""" + +from evennia.server.serversession import ServerSession as BaseServerSession + +class ServerSession(BaseServerSession): + """ + This class represents a player's session and is a template for + individual protocols to communicate with Evennia. + + Each player gets one or more sessions assigned to them whenever they connect + to the game server. All communication between game and player goes + through their session(s). + """ + pass diff --git a/evennia/game_template/server/conf/settings.py b/evennia/game_template/server/conf/settings.py new file mode 100644 index 000000000..921cc36bc --- /dev/null +++ b/evennia/game_template/server/conf/settings.py @@ -0,0 +1,93 @@ +""" +Evennia settings file. + +The full options are found in the default settings file found here: + +{settings_default} + +Note: Don't copy more from the default file than you actually intend to +change; this will make sure that you don't overload upstream updates +unnecessarily. + +""" + +# Use the defaults from Evennia unless explicitly overridden +import os +from evennia.settings_default import * + +###################################################################### +# Evennia base server config +###################################################################### + +# This is the name of your game. Make it catchy! +SERVERNAME = {servername} + +# Path to the game directory (use EVENNIA_DIR to refer to the +# core evennia library) +GAME_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +# Place to put log files +LOG_DIR = os.path.join(GAME_DIR, "server", "logs") +SERVER_LOG_FILE = os.path.join(LOG_DIR, 'server.log') +PORTAL_LOG_FILE = os.path.join(LOG_DIR, 'portal.log') +HTTP_LOG_FILE = os.path.join(LOG_DIR, 'http_requests.log') + +###################################################################### +# Evennia Database config +###################################################################### + +# Database config syntax: +# ENGINE - path to the the database backend. Possible choices are: +# 'django.db.backends.sqlite3', (default) +# 'django.db.backends.mysql', +# 'django.db.backends.postgresql_psycopg2' (see Issue 241), +# 'django.db.backends.oracle' (untested). +# NAME - database name, or path to the db file for sqlite3 +# USER - db admin (unused in sqlite3) +# PASSWORD - db admin password (unused in sqlite3) +# HOST - empty string is localhost (unused in sqlite3) +# PORT - empty string defaults to localhost (unused in sqlite3) +DATABASES = {{ + 'default': {{ + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(GAME_DIR, "server", "evennia.db3"), + 'USER': '', + 'PASSWORD': '', + 'HOST': '', + 'PORT': '' + }}}} + +###################################################################### +# Django web features +###################################################################### + +# Absolute path to the directory that holds file uploads from web apps. +# Example: "/home/media/media.lawrence.com" +MEDIA_ROOT = os.path.join(GAME_DIR, "gamesrc", "web", "media") + +# The master urlconf file that contains all of the sub-branches to the +# applications. Change this to add your own URLs to the website. +ROOT_URLCONF = 'web.urls' + +# URL prefix for admin media -- CSS, JavaScript and images. Make sure +# to use a trailing slash. Django1.4+ will look for admin files under +# STATIC_URL/admin. +STATIC_URL = '/static/' +STATIC_ROOT = os.path.join(GAME_DIR, "web", "static") + +# Directories from which static files will be gathered from. +STATICFILES_DIRS = ( + os.path.join(GAME_DIR, "web", "static_overrides"), + os.path.join(EVENNIA_DIR, "web", "static"),) + +# We setup the location of the website template as well as the admin site. +TEMPLATE_DIRS = ( + os.path.join(GAME_DIR, "web", "template_overrides"), + os.path.join(EVENNIA_DIR, "web", "templates", ACTIVE_TEMPLATE), + os.path.join(EVENNIA_DIR, "web", "templates"),) + +# The secret key is randomly seeded upon creation. It is used to sign +# Django's cookies. Do not share this with anyone. Changing it will +# log out all active web browsing sessions. Game web client sessions +# may survive. +SECRET_KEY = {secret_key} diff --git a/game/gamesrc/world/examples/__init__.py b/evennia/game_template/server/logs/server.log similarity index 100% rename from game/gamesrc/world/examples/__init__.py rename to evennia/game_template/server/logs/server.log diff --git a/evennia/game_template/typeclasses/README.md b/evennia/game_template/typeclasses/README.md new file mode 100644 index 000000000..e114e5996 --- /dev/null +++ b/evennia/game_template/typeclasses/README.md @@ -0,0 +1,16 @@ +# typeclasses/ + +This directory holds the modules for overloading all the typeclasses +representing the game entities and many systems of the game. Other +server functionality not covered here is usually modified by the +modules in `server/conf/`. + +Each module holds empty classes that just imports Evennia's defaults. +Any modifications done to these classes will overload the defaults. + +You can change the structure of this directory (even rename the +directory itself) as you please, but if you do you must add the +appropriate new paths to your settings.py file so Evennia knows where +to look. Also remember that for Python to find your modules, it +requires you to add an empty `__init__.py` file in any new sub +directories you create. diff --git a/src/__init__.py b/evennia/game_template/typeclasses/__init__.py similarity index 100% rename from src/__init__.py rename to evennia/game_template/typeclasses/__init__.py diff --git a/evennia/game_template/typeclasses/channels.py b/evennia/game_template/typeclasses/channels.py new file mode 100644 index 000000000..3be9a6dde --- /dev/null +++ b/evennia/game_template/typeclasses/channels.py @@ -0,0 +1,60 @@ +""" +Channel + +The channel class represents the out-of-character chat-room usable by +Players in-game. It is mostly overloaded to change its appearance, but +channels can be used to implement many different forms of message +distribution systems. + +Note that sending data to channels are handled via the CMD_CHANNEL +syscommand (see evennia.syscmds). The sending should normally not need +to be modified. + +""" + +from evennia import DefaultChannel + +class Channel(DefaultChannel): + """ + Working methods: + at_channel_creation() - called once, when the channel is created + has_connection(player) - check if the given player listens to this channel + connect(player) - connect player to this channel + disconnect(player) - disconnect player from channel + access(access_obj, access_type='listen', default=False) - check the + access on this channel (default access_type is listen) + delete() - delete this channel + message_transform(msg, emit=False, prefix=True, + sender_strings=None, external=False) - called by + the comm system and triggers the hooks below + msg(msgobj, header=None, senders=None, sender_strings=None, + persistent=None, online=False, emit=False, external=False) - main + send method, builds and sends a new message to channel. + tempmsg(msg, header=None, senders=None) - wrapper for sending non-persistent + messages. + distribute_message(msg, online=False) - send a message to all + connected players on channel, optionally sending only + to players that are currently online (optimized for very large sends) + + Useful hooks: + channel_prefix(msg, emit=False) - how the channel should be + prefixed when returning to user. Returns a string + format_senders(senders) - should return how to display multiple + senders to a channel + pose_transform(msg, sender_string) - should detect if the + sender is posing, and if so, modify the string + format_external(msg, senders, emit=False) - format messages sent + from outside the game, like from IRC + format_message(msg, emit=False) - format the message body before + displaying it to the user. 'emit' generally means that the + message should not be displayed with the sender's name. + + pre_join_channel(joiner) - if returning False, abort join + post_join_channel(joiner) - called right after successful join + pre_leave_channel(leaver) - if returning False, abort leave + post_leave_channel(leaver) - called right after successful leave + pre_send_message(msg) - runs just before a message is sent to channel + post_send_message(msg) - called just after message was sent to channel + + """ + pass diff --git a/game/gamesrc/objects/examples/character.py b/evennia/game_template/typeclasses/characters.py similarity index 50% rename from game/gamesrc/objects/examples/character.py rename to evennia/game_template/typeclasses/characters.py index 4d0d42140..9dd5b9b93 100644 --- a/game/gamesrc/objects/examples/character.py +++ b/evennia/game_template/typeclasses/characters.py @@ -1,31 +1,20 @@ """ +Characters -Template for Characters - -Copy this module up one level and name it as you like, then -use it as a template to create your own Character class. - -To make new logins default to creating characters -of your new type, change settings.BASE_CHARACTER_TYPECLASS to point to -your new class, e.g. - -settings.BASE_CHARACTER_TYPECLASS = "game.gamesrc.objects.mychar.MyChar" - -Note that objects already created in the database will not notice -this change, you have to convert them manually e.g. with the -@typeclass command. +Characters are (by default) Objects setup to be puppeted by Players. +They are what you "see" in game. The Character class in this module +is setup to be the "default" character type created by the default +creation commands. """ -from ev import Character as DefaultCharacter - +from evennia import DefaultCharacter class Character(DefaultCharacter): """ - The Character is like any normal Object (see example/object.py for - a list of properties and methods), except it actually implements - some of its hook methods to do some work: + The Character defaults to implementing some of its hook methods with the + following standard functionality: - at_basetype_setup - always assigns the default_cmdset to this object type + at_basetype_setup - always assigns the DefaultCmdSet to this object type (important!)sets locks so character cannot be picked up and its commands only be called by itself, not anyone else. (to change things, use at_object_creation() instead) diff --git a/game/gamesrc/objects/examples/exit.py b/evennia/game_template/typeclasses/exits.py similarity index 72% rename from game/gamesrc/objects/examples/exit.py rename to evennia/game_template/typeclasses/exits.py index 888fe426c..212ca970a 100644 --- a/game/gamesrc/objects/examples/exit.py +++ b/evennia/game_template/typeclasses/exits.py @@ -1,23 +1,12 @@ """ +Exits -Template module for Exits - -Copy this module up one level and name it as you like, then -use it as a template to create your own Exits. - -To make the default commands (such as @dig/@open) default to creating exits -of your new type, change settings.BASE_EXIT_TYPECLASS to point to -your new class, e.g. - -settings.BASE_EXIT_TYPECLASS = "game.gamesrc.objects.myexit.MyExit" - -Note that objects already created in the database will not notice -this change, you have to convert them manually e.g. with the -@typeclass command. +Exits are connectors between Rooms. An exit always has a destination property +set and has a single command defined on itself with the same name as its key, +for allowing Characters to traverse the exit to its destination. """ -from ev import Exit as DefaultExit - +from evennia import DefaultExit class Exit(DefaultExit): """ diff --git a/game/gamesrc/objects/examples/object.py b/evennia/game_template/typeclasses/objects.py similarity index 90% rename from game/gamesrc/objects/examples/object.py rename to evennia/game_template/typeclasses/objects.py index 57dbc7024..82c83453e 100644 --- a/game/gamesrc/objects/examples/object.py +++ b/evennia/game_template/typeclasses/objects.py @@ -1,24 +1,16 @@ """ +Object -Template for Objects +The Object is the "naked" base class for things in the game world. -Copy this module up one level and name it as you like, then -use it as a template to create your own Objects. - -To make the default commands default to creating objects of your new -type (and also change the "fallback" object used when typeclass -creation fails), change settings.BASE_OBJECT_TYPECLASS to point to -your new class, e.g. - -settings.BASE_OBJECT_TYPECLASS = "game.gamesrc.objects.myobj.MyObj" - -Note that objects already created in the database will not notice -this change, you have to convert them manually e.g. with the -@typeclass command. +Note that the default Character, Room and Exit does not inherit from +this Object, but from their respective default implementations in the +evennia library. If you want to use this class as a parent to change +the other types, you can do so by adding this as a multiple +inheritance. """ -from ev import Object as DefaultObject - +from evennia import DefaultObject class Object(DefaultObject): """ @@ -44,10 +36,7 @@ class Object(DefaultObject): aliases (list of strings) - aliases to the object. Will be saved to database as AliasDB entries but returned as strings. dbref (int, read-only) - unique #id-number. Also "id" can be used. - dbobj (Object, read-only) - link to database model. dbobj.typeclass points back to this class - typeclass (Object, read-only) - this links back to this class as an - identified only. Use self.swap_typeclass() to switch. date_created (string) - time stamp of object creation permissions (list of strings) - list of permission strings diff --git a/game/gamesrc/objects/examples/player.py b/evennia/game_template/typeclasses/players.py similarity index 73% rename from game/gamesrc/objects/examples/player.py rename to evennia/game_template/typeclasses/players.py index 339e0994e..5c1d3beb1 100644 --- a/game/gamesrc/objects/examples/player.py +++ b/evennia/game_template/typeclasses/players.py @@ -1,23 +1,28 @@ +""" +Player + +The Player represents the game "account" and each login has only one +Player object. A Player is what chats on default channels but has no +other in-game-world existance. Rather the Player puppets Objects (such +as Characters) in order to actually participate in the game world. + + +Guest + +Guest players are simple low-level accounts that are created/deleted +on the fly and allows users to test the game without the committment +of a full registration. Guest accounts are deactivated by default; to +activate them, add the following line to your settings file: + + GUEST_ENABLED = True + +You will also need to modify the connection screen to reflect the +possibility to connect with a guest account. The setting file accepts +several more options for customizing the Guest account system. + """ -Template module for Players - -Copy this module up one level and name it as you like, then -use it as a template to create your own Player class. - -To make the default account login default to using a Player -of your new type, change settings.BASE_PLAYER_TYPECLASS to point to -your new class, e.g. - -settings.BASE_PLAYER_TYPECLASS = "game.gamesrc.objects.myplayer.MyPlayer" - -Note that objects already created in the database will not notice -this change, you have to convert them manually e.g. with the -@typeclass command. - -""" -from ev import Player as DefaultPlayer - +from evennia import DefaultPlayer, DefaultGuest class Player(DefaultPlayer): """ @@ -39,8 +44,6 @@ class Player(DefaultPlayer): name (string)- wrapper for user.username aliases (list of strings) - aliases to the object. Will be saved to database as AliasDB entries but returned as strings. dbref (int, read-only) - unique #id-number. Also "id" can be used. - dbobj (Player, read-only) - link to database model. dbobj.typeclass points back to this class - typeclass (Player, read-only) - this links back to this class as an identified only. Use self.swap_typeclass() to switch. date_created (string) - time stamp of object creation permissions (list of strings) - list of permission strings @@ -89,3 +92,11 @@ class Player(DefaultPlayer): """ pass + + +class Guest(DefaultGuest): + """ + This class is used for guest logins. Unlike Players, Guests and their + characters are deleted after disconnection. + """ + pass diff --git a/evennia/game_template/typeclasses/rooms.py b/evennia/game_template/typeclasses/rooms.py new file mode 100644 index 000000000..e71de91f1 --- /dev/null +++ b/evennia/game_template/typeclasses/rooms.py @@ -0,0 +1,21 @@ +""" +Room + +Rooms are simple containers that has no location of their own. + +""" + +from evennia import DefaultRoom + + +class Room(DefaultRoom): + """ + Rooms are like any Object, except their location is None + (which is default). They also use basetype_setup() to + add locks so they cannot be puppeted or picked up. + (to change that, use at_object_creation instead) + + See examples/object.py for a list of + properties and methods available on all Objects. + """ + pass diff --git a/game/gamesrc/scripts/examples/script.py b/evennia/game_template/typeclasses/scripts.py similarity index 77% rename from game/gamesrc/scripts/examples/script.py rename to evennia/game_template/typeclasses/scripts.py index 05b670fb9..5814f4795 100644 --- a/game/gamesrc/scripts/examples/script.py +++ b/evennia/game_template/typeclasses/scripts.py @@ -1,29 +1,21 @@ """ +Scripts -Template module for Scripts +Scripts are powerful jacks-of-all-trades. They have no in-game +existence and can be used to represent persistent game systems in some +circumstances. Scripts can also have a time component that allows them +to "fire" regularly or a limited number of times. -Copy this module up one level to gamesrc/scripts and name it -appropriately, then use that as a template to create your own script. - -Test scripts in-game e.g. with the @script command. In code you can -create new scripts of a given class with - script = ev.create.script("path.to.module.and.class") - -Scripts are objects that handle everything in the game having -a time-component (i.e. that may change with time, with or without -a player being involved in the change). Scripts can work like "events", -in that they are triggered at regular intervals to do a certain script, -but an Script set on an object can also be responsible for silently -checking if its state changes, so as to update it. Evennia use several -in-built scripts to keep track of things like time, to clean out -dropped connections etc. +There is generally no "tree" of Scripts inheriting from each other. +Rather, each script tends to inherit from the base Script class and +just overloads its hooks to have it perform its function. """ -from ev import Script as BaseScript +from evennia import DefaultScript -class ExampleScript(BaseScript): +class Script(DefaultScript): """ A script type is customized by redefining some or all of its hook methods and variables. @@ -35,10 +27,6 @@ class ExampleScript(BaseScript): aliases (list of strings) - aliases to the object. Will be saved to database as AliasDB entries but returned as strings. dbref (int, read-only) - unique #id-number. Also "id" can be used. - dbobj (Object, read-only) - link to database model. dbobj.typeclass - points back to this class - typeclass (Object, read-only) - this links back to this class as an - identified only. Use self.swap_typeclass() to switch. date_created (string) - time stamp of object creation permissions (list of strings) - list of permission strings diff --git a/src/objects/migrations/__init__.py b/evennia/game_template/web/__init__.py similarity index 100% rename from src/objects/migrations/__init__.py rename to evennia/game_template/web/__init__.py diff --git a/game/gamesrc/web/media/README.md b/evennia/game_template/web/media/README.md similarity index 100% rename from game/gamesrc/web/media/README.md rename to evennia/game_template/web/media/README.md diff --git a/game/gamesrc/web/static/README.md b/evennia/game_template/web/static/README.md similarity index 100% rename from game/gamesrc/web/static/README.md rename to evennia/game_template/web/static/README.md diff --git a/evennia/game_template/web/static_overrides/README.md b/evennia/game_template/web/static_overrides/README.md new file mode 100644 index 000000000..ab9a09eb9 --- /dev/null +++ b/evennia/game_template/web/static_overrides/README.md @@ -0,0 +1,13 @@ +If you want to override one of the static files (such as a CSS or JS file) used by Evennia or a Django app installed in your Evennia project, +copy it into this directory's corresponding subdirectory, and it will be placed in the static folder when you run: + + python manage.py collectstatic + +...or when you reload the server via the command line. + +Do note you may have to reproduce any preceeding directory structures for the file to end up in the right place. + +Also note that you may need to clear out existing static files for your new ones to be gathered in some cases. Deleting files in static/ +will force them to be recollected. + +To see what files can be overridden, find where your evennia package is installed, and look in `evennia/web/static/` diff --git a/evennia/game_template/web/static_overrides/evennia_general/css/README.md b/evennia/game_template/web/static_overrides/evennia_general/css/README.md new file mode 100644 index 000000000..04db92fb7 --- /dev/null +++ b/evennia/game_template/web/static_overrides/evennia_general/css/README.md @@ -0,0 +1,3 @@ +You can replace the CSS files for Evennia's homepage here. + +You can find the original files in `evennia/web/static/evennia_general/css/` diff --git a/evennia/game_template/web/static_overrides/evennia_general/images/README.md b/evennia/game_template/web/static_overrides/evennia_general/images/README.md new file mode 100644 index 000000000..770067730 --- /dev/null +++ b/evennia/game_template/web/static_overrides/evennia_general/images/README.md @@ -0,0 +1,3 @@ +You can replace the image files for Evennia's home page here. + +You can find the original files in `evennia/web/static/evennia_general/images/` diff --git a/evennia/game_template/web/template_overrides/README.md b/evennia/game_template/web/template_overrides/README.md new file mode 100644 index 000000000..4a8a86497 --- /dev/null +++ b/evennia/game_template/web/template_overrides/README.md @@ -0,0 +1,4 @@ +Place your own version of templates into this file to override the default ones. +For instance, if there's a template at: `evennia/web/templates/evennia_general/index.html` +and you want to replace it, create the file `template_overrides/evennia_general/index.html` +and it will be loaded instead. diff --git a/evennia/game_template/web/template_overrides/evennia_general/README.md b/evennia/game_template/web/template_overrides/evennia_general/README.md new file mode 100644 index 000000000..0ed259de3 --- /dev/null +++ b/evennia/game_template/web/template_overrides/evennia_general/README.md @@ -0,0 +1,3 @@ +Replace Evennia's Django templates with your own here. + +You can find the original files in evennia/web/templates/evennia_general diff --git a/evennia/game_template/web/template_overrides/prosimii/README.md b/evennia/game_template/web/template_overrides/prosimii/README.md new file mode 100644 index 000000000..1a47fabaf --- /dev/null +++ b/evennia/game_template/web/template_overrides/prosimii/README.md @@ -0,0 +1,4 @@ +You can replace the theme templates for the default theme 'prosimii' here. You may wish to create a different +theme instead, however, and update your settings accordingly. + +You can find the original files in `evennia/web/templates/prosimii/` diff --git a/evennia/game_template/web/template_overrides/prosimii/flatpages/README.md b/evennia/game_template/web/template_overrides/prosimii/flatpages/README.md new file mode 100644 index 000000000..0fd3c1ff6 --- /dev/null +++ b/evennia/game_template/web/template_overrides/prosimii/flatpages/README.md @@ -0,0 +1,3 @@ +Flatpages require a default.html template, which can be overwritten by placing it in this folder. + +You can find the original files in `evennia/web/templates/evennia_general/prosimii/flatpages/` diff --git a/evennia/game_template/web/template_overrides/prosimii/registration/README.md b/evennia/game_template/web/template_overrides/prosimii/registration/README.md new file mode 100644 index 000000000..7b668922f --- /dev/null +++ b/evennia/game_template/web/template_overrides/prosimii/registration/README.md @@ -0,0 +1,3 @@ +The templates involving login/logout can be overwritten here. + +You can find the original files in `evennia/web/templates/prosimii/registration/` diff --git a/game/gamesrc/web/examples/urls.py b/evennia/game_template/web/urls.py similarity index 91% rename from game/gamesrc/web/examples/urls.py rename to evennia/game_template/web/urls.py index 58754f51c..18d03c6cf 100644 --- a/game/gamesrc/web/examples/urls.py +++ b/evennia/game_template/web/urls.py @@ -1,6 +1,6 @@ from django.conf.urls import url, include -from src.web.urls import urlpatterns +from evennia.web.urls import urlpatterns # # File that determines what each URL points to. This uses _Python_ regular @@ -25,4 +25,4 @@ patterns = [ # url(r'/desired/url/', view, name='example'), ] -urlpatterns = patterns + urlpatterns \ No newline at end of file +urlpatterns = patterns + urlpatterns diff --git a/evennia/game_template/world/README.md b/evennia/game_template/world/README.md new file mode 100644 index 000000000..0f3862dad --- /dev/null +++ b/evennia/game_template/world/README.md @@ -0,0 +1,10 @@ +# world/ + +This folder is meant as a miscellanous folder for all that other stuff +related to the game. Code which are not commands or typeclasses go +here, like custom economy systems, combat code, batch-files etc. + +You can restructure and even rename this folder as best fits your +sense of organisation. Just remember that if you add new sub +directories, you must add (optionally empty) `__init__.py` files in +them for Python to be able to find the modules within. diff --git a/src/tests/__init__.py b/evennia/game_template/world/__init__.py similarity index 100% rename from src/tests/__init__.py rename to evennia/game_template/world/__init__.py diff --git a/evennia/game_template/world/batch_cmds.ev b/evennia/game_template/world/batch_cmds.ev new file mode 100644 index 000000000..1513e6696 --- /dev/null +++ b/evennia/game_template/world/batch_cmds.ev @@ -0,0 +1,26 @@ +# +# A batch-command file is a way to build a game world +# in a programmatic way, by placing a sequence of +# build commands after one another. This allows for +# using a real text editor to edit e.g. descriptions +# rather than entering text on the command line. +# +# A batch-command file is loaded with @batchprocess in-game: +# +# @batchprocess[/interactive] tutorial_examples.batch_cmds +# +# A # as the first symbol on a line begins a comment and +# marks the end of a previous command definition. This is important, +# - every command must be separated by at least one line of comment. +# +# All supplied commands are given as normal, on their own line +# and accepts arguments in any format up until the first next +# comment line begins. Extra whitespace is removed; an empty +# line in a command definition translates into a newline. +# +# See evennia/contrib/tutorial_examples/batch_cmds.ev for +# an example of a batch-command code. See also the batch-code +# system for loading python-code in this way. +# + + diff --git a/evennia/game_template/world/prototypes.py b/evennia/game_template/world/prototypes.py new file mode 100644 index 000000000..85f750fd5 --- /dev/null +++ b/evennia/game_template/world/prototypes.py @@ -0,0 +1,70 @@ +""" +Prototypes + +A prototype is a simple way to create individualized instances of a +given Typeclass. For example, you might have a Sword typeclass that +implements everything a Sword would need to do. The only difference +between different individual Swords would be their key, description +and some Attributes. The Prototype system allows to create a range of +such Swords with only minor variations. Prototypes can also inherit +and combine together to form entire hierarchies (such as giving all +Sabres and all Broadswords some common properties). Note that bigger +variations, such as custom commands or functionality belong in a +hierarchy of typeclasses instead. + +Example prototypes are read by the @spawn command but is also easily +available to use from code via evennia.spawn or evennia.utils.spawner. +Each prototype should be a dictionary. Use the same name as the +variable to refer to other prototypes. + +Possible keywords are: + prototype - string pointing to parent prototype of this structure + key - string, the main object identifier + typeclass - string, if not set, will use settings.BASE_OBJECT_TYPECLASS + location - this should be a valid object or #dbref + home - valid object or #dbref + destination - only valid for exits (object or dbref) + + permissions - string or list of permission strings + locks - a lock-string + aliases - string or list of strings + + ndb_ - value of a nattribute (the "ndb_" part is ignored) + any other keywords are interpreted as Attributes and their values. + +See the @spawn command and evennia.utils.spawner for more info. + +""" + +#from random import randint +# +#NOBODY = {} +# +#GOBLIN = { +# "key": "goblin grunt", +# "health": lambda: randint(20,30), +# "resists": ["cold", "poison"], +# "attacks": ["fists"], +# "weaknesses": ["fire", "light"] +# } +# +#GOBLIN_WIZARD = { +# "prototype": "GOBLIN", +# "key": "goblin wizard", +# "spells": ["fire ball", "lighting bolt"] +# } +# +#GOBLIN_ARCHER = { +# "prototype": "GOBLIN", +# "key": "goblin archer", +# "attacks": ["short bow"] +#} +# +#ARCHWIZARD = { +# "attacks": ["archwizard staff"], +#} +# +#GOBLIN_ARCHWIZARD = { +# "key": "goblin archwizard", +# "prototype" : ("GOBLIN_WIZARD", "ARCHWIZARD") +#} diff --git a/evennia/help/__init__.py b/evennia/help/__init__.py new file mode 100644 index 000000000..0aa1b685f --- /dev/null +++ b/evennia/help/__init__.py @@ -0,0 +1,7 @@ +""" +This sub-package defines the help system of Evennia. It is pretty +simple, mainly consisting of a database model to hold help entries. +The auto-cmd-help is rather handled by the default 'help' command +itself. + +""" diff --git a/src/help/admin.py b/evennia/help/admin.py similarity index 96% rename from src/help/admin.py rename to evennia/help/admin.py index 6c41b23b4..cffc14784 100644 --- a/src/help/admin.py +++ b/evennia/help/admin.py @@ -3,7 +3,7 @@ This defines how to edit help entries in Admin. """ from django import forms from django.contrib import admin -from src.help.models import HelpEntry +from evennia.help.models import HelpEntry diff --git a/src/help/manager.py b/evennia/help/manager.py similarity index 96% rename from src/help/manager.py rename to evennia/help/manager.py index 5233285fa..45664e720 100644 --- a/src/help/manager.py +++ b/evennia/help/manager.py @@ -2,7 +2,7 @@ Custom manager for HelpEntry objects. """ from django.db import models -from src.utils import logger, utils +from evennia.utils import logger, utils __all__ = ("HelpEntryManager",) @@ -20,7 +20,7 @@ class HelpEntryManager(models.Manager): find_topicsuggestions find_topics_with_category all_to_category - search_help (equivalent to ev.search_helpentry) + search_help (equivalent to evennia.search_helpentry) """ def find_topicmatch(self, topicstr, exact=False): diff --git a/src/help/migrations/0001_initial.py b/evennia/help/migrations/0001_initial.py similarity index 100% rename from src/help/migrations/0001_initial.py rename to evennia/help/migrations/0001_initial.py diff --git a/game/__init__.py b/evennia/help/migrations/__init__.py similarity index 100% rename from game/__init__.py rename to evennia/help/migrations/__init__.py diff --git a/evennia/help/models.py b/evennia/help/models.py new file mode 100644 index 000000000..4d17bfc0f --- /dev/null +++ b/evennia/help/models.py @@ -0,0 +1,104 @@ +""" +Models for the help system. + +The database-tied help system is only half of Evennia's help +functionality, the other one being the auto-generated command help +that is created on the fly from each command's __doc__ string. The +persistent database system defined here is intended for all other +forms of help that do not concern commands, like information about the +game world, policy info, rules and similar. + +""" +from django.db import models +from evennia.utils.idmapper.models import SharedMemoryModel +from evennia.help.manager import HelpEntryManager +from evennia.typeclasses.models import Tag, TagHandler +from evennia.locks.lockhandler import LockHandler +from evennia.utils.utils import lazy_property +__all__ = ("HelpEntry",) + + +#------------------------------------------------------------ +# +# HelpEntry +# +#------------------------------------------------------------ + +class HelpEntry(SharedMemoryModel): + """ + A generic help entry. + + An HelpEntry object has the following properties defined: + key - main name of entry + help_category - which category entry belongs to (defaults to General) + entrytext - the actual help text + permissions - perm strings + + Method: + access + + """ + + # + # HelpEntry Database Model setup + # + # + # These database fields are all set using their corresponding properties, + # named same as the field, but withtout the db_* prefix. + + # title of the help entry + db_key = models.CharField('help key', max_length=255, unique=True, help_text='key to search for') + # help category + db_help_category = models.CharField("help category", max_length=255, default="General", + help_text='organizes help entries in lists') + # the actual help entry text, in any formatting. + db_entrytext = models.TextField('help entry', blank=True, help_text='the main body of help text') + # lock string storage + db_lock_storage = models.TextField('locks', blank=True, help_text='normally view:all().') + # tags are primarily used for permissions + db_tags = models.ManyToManyField(Tag, null=True, + help_text='tags on this object. Tags are simple string markers to identify, group and alias objects.') + # (deprecated, only here to allow MUX helpfile load (don't use otherwise)). + # TODO: remove this when not needed anymore. + db_staff_only = models.BooleanField(default=False) + + # Database manager + objects = HelpEntryManager() + _is_deleted = False + + # lazy-loaded handlers + + @lazy_property + def locks(self): + return LockHandler(self) + + @lazy_property + def tags(self): + return TagHandler(self) + + + class Meta: + "Define Django meta options" + verbose_name = "Help Entry" + verbose_name_plural = "Help Entries" + + # + # + # HelpEntry main class methods + # + # + + def __str__(self): + return self.key + + def __unicode__(self): + return u'%s' % self.key + + def access(self, accessing_obj, access_type='read', 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) diff --git a/locale/README b/evennia/locale/README similarity index 100% rename from locale/README rename to evennia/locale/README diff --git a/locale/pt/LC_MESSAGES/django.po b/evennia/locale/pt/LC_MESSAGES/django.po similarity index 76% rename from locale/pt/LC_MESSAGES/django.po rename to evennia/locale/pt/LC_MESSAGES/django.po index 9ee8a7bd8..d3dfc67b7 100644 --- a/locale/pt/LC_MESSAGES/django.po +++ b/evennia/locale/pt/LC_MESSAGES/django.po @@ -20,148 +20,148 @@ msgstr "" "X-Poedit-Country: BRAZIL\n" "X-Poedit-SourceCharset: utf-8\n" -#: src/commands/cmdhandler.py:321 +#: evennia/commands/cmdhandler.py:321 msgid "There were multiple matches." msgstr "Havia várias correspondências." -#: src/commands/cmdhandler.py:349 +#: evennia/commands/cmdhandler.py:349 #, python-format msgid "Command '%s' is not available." msgstr "Comando '%s' não está disponível." -#: src/commands/cmdhandler.py:354 +#: evennia/commands/cmdhandler.py:354 #, python-format msgid " Maybe you meant %s?" msgstr " Você talvez quis dizer %s?" -#: src/commands/cmdhandler.py:354 +#: evennia/commands/cmdhandler.py:354 msgid "or" msgstr "ou" -#: src/commands/cmdhandler.py:356 +#: evennia/commands/cmdhandler.py:356 msgid " Type \"help\" for help." msgstr "Digite \"help\" para obter ajuda." -#: src/commands/cmdparser.py:153 +#: evennia/commands/cmdparser.py:153 #, python-format msgid "Could not find '%s'." msgstr "Não foi possível encontrar '%s'." -#: src/commands/cmdparser.py:174 +#: evennia/commands/cmdparser.py:174 msgid "location" msgstr "localização" -#: src/commands/cmdparser.py:175 +#: evennia/commands/cmdparser.py:175 msgid " (carried)" msgstr "(carregado)" -#: src/commands/cmdparser.py:255 +#: evennia/commands/cmdparser.py:255 msgid " (channel)" msgstr "(canal)" -#: src/commands/cmdsethandler.py:127 +#: evennia/commands/cmdsethandler.py:127 #, python-format msgid "Error loading cmdset: Couldn't import module '%s': %s." msgstr "Erro ao carregar cmdset: Não foi possível importar o módulo '%s': %s." -#: src/commands/cmdsethandler.py:130 +#: evennia/commands/cmdsethandler.py:130 #, python-format msgid "Error in loading cmdset: No cmdset class '%(classname)s' in %(modulepath)s." msgstr "Erro ao carregar cmdset: Nenhuma classe cmdset '%(classname)s' em %(modulepath)s." -#: src/commands/cmdsethandler.py:134 +#: evennia/commands/cmdsethandler.py:134 #, python-format msgid "SyntaxError encountered when loading cmdset '%s': %s." msgstr "Encontrado SyntaxError ao carregar cmdset '%s': %s." -#: src/commands/cmdsethandler.py:137 +#: evennia/commands/cmdsethandler.py:137 #, python-format msgid "Compile/Run error when loading cmdset '%s': %s." msgstr "Erro de Compilação / Execução ao carregar cmdset '%s': %s." -#: src/commands/cmdsethandler.py:214 +#: evennia/commands/cmdsethandler.py:214 #, python-format msgid "custom %(mergetype)s on cmdset '%(merged_on)s'" msgstr "%(mergetype)s personalizado em cmdset '%(merged_on)s'" -#: src/commands/cmdsethandler.py:217 +#: evennia/commands/cmdsethandler.py:217 #, python-format msgid " : %(current)s" msgstr " : %(current)s" -#: src/commands/cmdsethandler.py:225 +#: evennia/commands/cmdsethandler.py:225 #, python-format msgid " <%(key)s (%(mergetype)s, prio %(prio)i, %(permstring)s)>: %(keylist)s" msgstr " <%(key)s (%(mergetype)s, prio %(prio)i, %(permstring)s)>: %(keylist)s" -#: src/commands/cmdsethandler.py:299 -#: src/commands/cmdsethandler.py:332 +#: evennia/commands/cmdsethandler.py:299 +#: evennia/commands/cmdsethandler.py:332 msgid "Only CmdSets can be added to the cmdsethandler!" msgstr "Somente CmdSets podems er adicionados ao cmdsethandler!" -#: src/locks/lockhandler.py:219 +#: evennia/locks/lockhandler.py:219 #, python-format msgid "Lock: function '%s' is not available." msgstr "Travado: a função '%s' não está disponível." -#: src/locks/lockhandler.py:232 +#: evennia/locks/lockhandler.py:232 #, python-format msgid "Lock: definition '%s' has syntax errors." msgstr "Travado: a definição '%s' possui erros de sintaxe." -#: src/locks/lockhandler.py:236 +#: evennia/locks/lockhandler.py:236 #, python-format msgid "LockHandler on %(obj)s: access type '%(access_type)s' changed from '%(source)s' to '%(goal)s' " msgstr "LockHandler em %(obj)s: tipo de acesso '%(access_type)s' alterado de '%(source)s' para '%(goal)s' " -#: src/locks/lockhandler.py:276 +#: evennia/locks/lockhandler.py:276 #, python-format msgid "Lock: '%s' contains no colon (:)." msgstr "Travado: '%s' não contém dois pontos (:)." -#: src/locks/lockhandler.py:280 +#: evennia/locks/lockhandler.py:280 #, python-format msgid "Lock: '%s' has no access_type (left-side of colon is empty)." msgstr "Travado: '%s' não possui tipo de acesso (o lado esquerdo dos dois pontos está vazio)." -#: src/locks/lockhandler.py:283 +#: evennia/locks/lockhandler.py:283 #, python-format msgid "Lock: '%s' has mismatched parentheses." msgstr "Travado: '%s' possui parêntesis descasados." -#: src/locks/lockhandler.py:286 +#: evennia/locks/lockhandler.py:286 #, python-format msgid "Lock: '%s' has no valid lock functions." msgstr "Travado: '%s' não possui funções travamento válidas." -#: src/objects/models.py:609 +#: evennia/objects/models.py:609 #, python-format msgid "Couldn't perform move ('%s'). Contact an admin." msgstr "Não foi possível realizar o movimento ('%s'). Contacte um admin." -#: src/objects/models.py:619 +#: evennia/objects/models.py:619 msgid "The destination doesn't exist." msgstr "O destino não existe." -#: src/objects/models.py:733 +#: evennia/objects/models.py:733 #, python-format msgid "Could not find default home '(#%d)'." msgstr "Não foi possível encontrar o home '(#%d)' padrão." -#: src/objects/models.py:749 +#: evennia/objects/models.py:749 msgid "Something went wrong! You are dumped into nowhere. Contact an admin." msgstr "Algo deu errado! Você foi parar em lugar nenhum. Contacte um administrador." -#: src/objects/models.py:821 +#: evennia/objects/models.py:821 #, python-format msgid "Your character %s has been destroyed." msgstr "Seu personagemr %s foi destruído." -#: src/players/models.py:422 +#: evennia/players/models.py:422 msgid "Player being deleted." msgstr "Jogador sendo deletado." -#: src/scripts/scripthandler.py:45 +#: evennia/scripts/scripthandler.py:45 #, python-format msgid "" "\n" @@ -170,89 +170,89 @@ msgstr "" "\n" " '%(key)s' (%(next_repeat)s/%(interval)s, %(repeats)s repeats): %(desc)s" -#: src/scripts/scripts.py:156 +#: evennia/scripts/scripts.py:156 #, python-format msgid "Script %(key)s(#%(dbid)s) of type '%(cname)s': at_repeat() error '%(err)s'." msgstr "Script %(key)s(#%(dbid)s) do tipo '%(cname)s': at_repeat() error '%(err)s'." -#: src/scripts/scripts.py:510 +#: evennia/scripts/scripts.py:510 msgid "This is an empty placeholder script." msgstr "Este é um script placeholder vazio." -#: src/scripts/scripts.py:518 +#: evennia/scripts/scripts.py:518 msgid "This is a generic storage container." msgstr "Este é um recipiente de armazenamento genérico." -#: src/scripts/scripts.py:526 +#: evennia/scripts/scripts.py:526 msgid "Checks sessions so they are live." msgstr "Sessões de checagem para ver se estão vivos." -#: src/scripts/scripts.py:547 +#: evennia/scripts/scripts.py:547 msgid "Restrains size of idmapper cache." msgstr "Restringe o tamanho do cache do idmapper." -#: src/scripts/scripts.py:563 +#: evennia/scripts/scripts.py:563 msgid "Validates all scripts regularly." msgstr "Valida todos os scripts regularmente." -#: src/scripts/scripts.py:578 +#: evennia/scripts/scripts.py:578 msgid "Updates the channel handler" msgstr "Atualiza o manipulador de canal" -#: src/server/initial_setup.py:75 +#: evennia/server/initial_setup.py:75 msgid "This is User #1." msgstr "Este é o Usuário #1." -#: src/server/initial_setup.py:85 +#: evennia/server/initial_setup.py:85 msgid "Limbo" msgstr "Limbo" -#: src/server/sessionhandler.py:236 +#: evennia/server/sessionhandler.py:236 #, python-format msgid "Connection dropped: %s %s (%s)" msgstr "Conexão descartada: %s %s (%s)" -#: src/server/sessionhandler.py:273 +#: evennia/server/sessionhandler.py:273 msgid " ... Server restarted." msgstr "... Servidor reiniciado." -#: src/server/sessionhandler.py:337 +#: evennia/server/sessionhandler.py:337 #, python-format msgid "Logged in: %s %s (%s)" msgstr "Conectado em: %s %s (%s)" -#: src/server/sessionhandler.py:361 +#: evennia/server/sessionhandler.py:361 #, python-format msgid "Logged out: %s %s (%s)" msgstr "Desconectado em: %s %s (%s)" -#: src/server/sessionhandler.py:381 +#: evennia/server/sessionhandler.py:381 msgid "You have been disconnected." msgstr "Você foi desconectado." -#: src/server/sessionhandler.py:394 +#: evennia/server/sessionhandler.py:394 msgid "Logged in from elsewhere. Disconnecting." msgstr "Conectado de outro lugar. Desconectando." -#: src/server/sessionhandler.py:412 +#: evennia/server/sessionhandler.py:412 msgid "Idle timeout exceeded, disconnecting." msgstr "Tempo de inatividade excedido, desconectando." -#: src/server/portal/imc2.py:167 +#: evennia/server/portal/imc2.py:167 #, python-format msgid "Whois reply from %(origin)s: %(msg)s" msgstr "Resposta Whois de %(origin)s: %(msg)s" -#: src/server/portal/imc2.py:175 +#: evennia/server/portal/imc2.py:175 #, python-format msgid "{c%(sender)s@%(origin)s{n {wpages (over IMC):{n %(msg)s" msgstr "{c%(sender)s@%(origin)s{n {wpages (over IMC):{n %(msg)s" -#: src/server/portal/imc2.py:193 +#: evennia/server/portal/imc2.py:193 msgid "IMC2 server rejected connection." msgstr "Servidor IMC2 rejeitou a conexão." -#: src/server/portal/imc2.py:201 +#: evennia/server/portal/imc2.py:201 msgid "IMC2: Autosetup response found." msgstr "ICM2: encontrada resposta do autosetup." diff --git a/locale/sv/LC_MESSAGES/django.mo b/evennia/locale/sv/LC_MESSAGES/django.mo similarity index 100% rename from locale/sv/LC_MESSAGES/django.mo rename to evennia/locale/sv/LC_MESSAGES/django.mo diff --git a/locale/sv/LC_MESSAGES/django.po b/evennia/locale/sv/LC_MESSAGES/django.po similarity index 71% rename from locale/sv/LC_MESSAGES/django.po rename to evennia/locale/sv/LC_MESSAGES/django.po index 0ee6ffaf6..6c1ca152a 100644 --- a/locale/sv/LC_MESSAGES/django.po +++ b/evennia/locale/sv/LC_MESSAGES/django.po @@ -17,151 +17,151 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: src/commands/cmdhandler.py:320 +#: evennia/commands/cmdhandler.py:320 msgid "There were multiple matches." msgstr "Det fanns många träffar." -#: src/commands/cmdhandler.py:348 +#: evennia/commands/cmdhandler.py:348 #, python-format msgid "Command '%s' is not available." msgstr "Kommandot '%s' är inte tillgängligt." -#: src/commands/cmdhandler.py:353 +#: evennia/commands/cmdhandler.py:353 #, python-format msgid " Maybe you meant %s?" msgstr "Menade du kanske %s?" -#: src/commands/cmdhandler.py:353 +#: evennia/commands/cmdhandler.py:353 msgid "or" msgstr "eller" -#: src/commands/cmdhandler.py:355 +#: evennia/commands/cmdhandler.py:355 msgid " Type \"help\" for help." msgstr "Skriv \"help\" för hjälp." -#: src/commands/cmdparser.py:153 +#: evennia/commands/cmdparser.py:153 #, python-format msgid "Could not find '%s'." msgstr "Kunde inte hitta '%s'." -#: src/commands/cmdparser.py:174 +#: evennia/commands/cmdparser.py:174 msgid "location" msgstr "plats" -#: src/commands/cmdparser.py:175 +#: evennia/commands/cmdparser.py:175 msgid " (carried)" msgstr "(buren)" -#: src/commands/cmdparser.py:255 +#: evennia/commands/cmdparser.py:255 msgid " (channel)" msgstr "(kanal)" -#: src/commands/cmdsethandler.py:123 +#: evennia/commands/cmdsethandler.py:123 #, python-format msgid "Error loading cmdset: Couldn't import module '%s'." msgstr "Fel medan cmdset laddades: Kunde inte importera modulen '%s'." -#: src/commands/cmdsethandler.py:127 +#: evennia/commands/cmdsethandler.py:127 #, python-format msgid "Error in loading cmdset: No cmdset class '%(classname)s' in %(modulepath)s." msgstr "Fel medan cmdset laddades: Ingen cmdset-klass med namn '%(classname)s' i %(modulepath)s." -#: src/commands/cmdsethandler.py:132 +#: evennia/commands/cmdsethandler.py:132 #, python-format msgid "Compile/Run error when loading cmdset '%s'. Error was logged." msgstr "Kompilerings/Körningsfel när cmdset '%s' laddades. Felet skrevs i loggen." -#: src/commands/cmdsethandler.py:208 +#: evennia/commands/cmdsethandler.py:208 #, python-format msgid "custom %(mergetype)s on cmdset '%(merged_on)s'" msgstr "särskild %(mergetype)s på cmdset '%(merged_on)s'" -#: src/commands/cmdsethandler.py:211 +#: evennia/commands/cmdsethandler.py:211 #, python-format msgid " : %(current)s" msgstr ": %(current)s" -#: src/commands/cmdsethandler.py:219 +#: evennia/commands/cmdsethandler.py:219 #, python-format msgid " <%(key)s (%(mergetype)s, prio %(prio)i, %(permstring)s)>: %(keylist)s" msgstr " <%(key)s (%(mergetype)s, prio %(prio)i, %(permstring)s)>: %(keylist)s" -#: src/commands/cmdsethandler.py:294 -#: src/commands/cmdsethandler.py:326 +#: evennia/commands/cmdsethandler.py:294 +#: evennia/commands/cmdsethandler.py:326 msgid "Only CmdSets can be added to the cmdsethandler!" msgstr "Bara CmdSets can läggas till cmdsethandler!" -#: src/locks/lockhandler.py:223 +#: evennia/locks/lockhandler.py:223 #, python-format msgid "Lock: function '%s' is not available." msgstr "Lås: funktionen '%s' kunde inte hittas." -#: src/locks/lockhandler.py:236 +#: evennia/locks/lockhandler.py:236 #, python-format msgid "Lock: definition '%s' has syntax errors." msgstr "Lås: definitionen '%s' har syntaktiska fel." -#: src/locks/lockhandler.py:240 +#: evennia/locks/lockhandler.py:240 #, python-format msgid "Lock: access type '%(access_type)s' changed from '%(source)s' to '%(goal)s' " msgstr "Lås: låstypen '%(access_type)s' ändrade sig från '%(source)s' till '%(goal)s'" -#: src/locks/lockhandler.py:284 +#: evennia/locks/lockhandler.py:284 #, python-format msgid "Lock: '%s' contains no colon (:)." msgstr "Lås: '%s' innehåller inget kolon (:)." -#: src/locks/lockhandler.py:288 +#: evennia/locks/lockhandler.py:288 #, python-format msgid "Lock: '%s' has no access_type (left-side of colon is empty)." msgstr "Lås: '%s' saknar låstyp (ingenting till vänster om kolonet)." -#: src/locks/lockhandler.py:291 +#: evennia/locks/lockhandler.py:291 #, python-format msgid "Lock: '%s' has mismatched parentheses." msgstr "Lås: '%s' has ickematchande parenteser." -#: src/locks/lockhandler.py:294 +#: evennia/locks/lockhandler.py:294 #, python-format msgid "Lock: '%s' has no valid lock functions." msgstr "Lås: '%s' innehåller inga acceptabla låsfunktioner." -#: src/objects/models.py:551 +#: evennia/objects/models.py:551 #, python-format msgid "Couldn't perform move ('%s'). Contact an admin." msgstr "Kunde inte utföra föflyttningen ('%s'). Kontakta en admininstrator." -#: src/objects/models.py:561 +#: evennia/objects/models.py:561 msgid "The destination doesn't exist." msgstr "Destinationen existerar inte." -#: src/objects/models.py:675 +#: evennia/objects/models.py:675 #, python-format msgid "Could not find default home '(#%d)'." msgstr "Kunde inte hitta standard-hem '(#'%d)'." -#: src/objects/models.py:690 +#: evennia/objects/models.py:690 msgid "Something went wrong! You are dumped into nowhere. Contact an admin." msgstr "Nåtgot gick fel! Du har blivit flyttat till en icke-existerande plats. Kontakta en admininstrator." -#: src/objects/models.py:762 +#: evennia/objects/models.py:762 #, python-format msgid "Your character %s has been destroyed." msgstr "Din karaktär %s har blivit raderad." -#: src/players/models.py:38 +#: evennia/players/models.py:38 msgid "me" msgstr "jag" -#: src/players/models.py:39 +#: evennia/players/models.py:39 msgid "self" msgstr "self" -#: src/players/models.py:417 +#: evennia/players/models.py:417 msgid "Player being deleted." msgstr "Spelaren raderas." -#: src/scripts/scripthandler.py:45 +#: evennia/scripts/scripthandler.py:45 #, python-format msgid "" "\n" @@ -170,187 +170,187 @@ msgstr "" "\n" " '%(key)s' (%(next_repeat)s/%(interval)s, %(repeats)s repetitioner): %(desc)s" -#: src/scripts/scripts.py:158 +#: evennia/scripts/scripts.py:158 #, python-format msgid "Script %(key)s(#%(dbid)i) of type '%(cname)s': at_repeat() error '%(err)s'." msgstr "Script %(key)s(#%(dbid)i) av typ '%(cname)s': at_repeat() fel '%(err)s'." -#: src/scripts/scripts.py:512 +#: evennia/scripts/scripts.py:512 msgid "This is an empty placeholder script." msgstr "Detta är ett tomt platshållar-script." -#: src/scripts/scripts.py:520 +#: evennia/scripts/scripts.py:520 msgid "This is a generic storage container." msgstr "Detta är en generisk lagringskontainer." -#: src/scripts/scripts.py:528 +#: evennia/scripts/scripts.py:528 msgid "Checks sessions so they are live." msgstr "Kollar sessioner så att de är aktiva." -#: src/scripts/scripts.py:547 +#: evennia/scripts/scripts.py:547 msgid "Validates all scripts regularly." msgstr "Validerar alla script regelbundet." -#: src/scripts/scripts.py:562 +#: evennia/scripts/scripts.py:562 msgid "Updates the channel handler" msgstr "Uppdaterar kanalhanteraren" -#: src/server/initial_setup.py:75 +#: evennia/server/initial_setup.py:75 msgid "This is User #1." msgstr "Detta är användare #1." -#: src/server/initial_setup.py:85 +#: evennia/server/initial_setup.py:85 msgid "Limbo" msgstr "Limbo" -#: src/server/sessionhandler.py:221 +#: evennia/server/sessionhandler.py:221 #, python-format msgid "Connection dropped: %s %s (%s)" msgstr "Uppkopplingen förlorades: %s %s (%s)" -#: src/server/sessionhandler.py:258 +#: evennia/server/sessionhandler.py:258 msgid " ... Server restarted." msgstr "... Servern startades om." -#: src/server/sessionhandler.py:322 +#: evennia/server/sessionhandler.py:322 #, python-format msgid "Logged in: %s %s (%s)" msgstr "Loggade in: %s %s (%s)" -#: src/server/sessionhandler.py:346 +#: evennia/server/sessionhandler.py:346 #, python-format msgid "Logged out: %s %s (%s)" msgstr "Loggade ut: %s %s (%s)" -#: src/server/sessionhandler.py:366 +#: evennia/server/sessionhandler.py:366 msgid "You have been disconnected." msgstr "Du har blivit frånkopplad." -#: src/server/sessionhandler.py:379 +#: evennia/server/sessionhandler.py:379 msgid "Logged in from elsewhere. Disconnecting." msgstr "Inloggad från någon annanstans ifrån. Kopplas ifrån." -#: src/server/sessionhandler.py:397 +#: evennia/server/sessionhandler.py:397 msgid "Idle timeout exceeded, disconnecting." msgstr "Timeout. Kopplar ur." -#: src/server/portal/imc2.py:167 +#: evennia/server/portal/imc2.py:167 #, python-format msgid "Whois reply from %(origin)s: %(msg)s" msgstr "Whois-svar från %(origin)s: %(msg)s" -#: src/server/portal/imc2.py:175 +#: evennia/server/portal/imc2.py:175 #, python-format msgid "{c%(sender)s@%(origin)s{n {wpages (over IMC):{n %(msg)s" msgstr "{c%(sender)s@%(origin)s{n {wskickar (over IMC):{n %(msg)s" -#: src/server/portal/imc2.py:193 +#: evennia/server/portal/imc2.py:193 msgid "IMC2 server rejected connection." msgstr "IMC2 server avvisade uppkopplingen." -#: src/server/portal/imc2.py:201 +#: evennia/server/portal/imc2.py:201 msgid "IMC2: Autosetup response found." msgstr "IMC2: Autosetup-svar hittat." -#: src/web/templates/admin/base_site.html:4 +#: evennia/web/templates/admin/base_site.html:4 msgid "Evennia site admin" msgstr "Evennia site admin" -#: src/web/templates/admin/base_site.html:7 +#: evennia/web/templates/admin/base_site.html:7 msgid "Evennia database administration" msgstr "Evennia databasadministration" -#: src/web/templates/admin/index.html:29 -#: src/web/templates/admin/index.html:73 -#: src/web/templates/admin/index.html:117 +#: evennia/web/templates/admin/index.html:29 +#: evennia/web/templates/admin/index.html:73 +#: evennia/web/templates/admin/index.html:117 #, python-format msgid "Models available in the %(name)s application." msgstr "Modeller tillgängliga i %(name)s applikation." -#: src/web/templates/admin/index.html:30 -#: src/web/templates/admin/index.html:74 -#: src/web/templates/admin/index.html:118 +#: evennia/web/templates/admin/index.html:30 +#: evennia/web/templates/admin/index.html:74 +#: evennia/web/templates/admin/index.html:118 #, python-format msgid "%(name)s" msgstr "%(name)s" -#: src/web/templates/admin/index.html:41 -#: src/web/templates/admin/index.html:84 -#: src/web/templates/admin/index.html:128 -#: src/web/templates/admin/players/change_form.html:22 +#: evennia/web/templates/admin/index.html:41 +#: evennia/web/templates/admin/index.html:84 +#: evennia/web/templates/admin/index.html:128 +#: evennia/web/templates/admin/players/change_form.html:22 msgid "Add" msgstr "Lägg till" -#: src/web/templates/admin/index.html:47 -#: src/web/templates/admin/index.html:90 -#: src/web/templates/admin/index.html:134 +#: evennia/web/templates/admin/index.html:47 +#: evennia/web/templates/admin/index.html:90 +#: evennia/web/templates/admin/index.html:134 msgid "Change" msgstr "Ändra" -#: src/web/templates/admin/index.html:149 +#: evennia/web/templates/admin/index.html:149 msgid "You don't have permission to edit anything." msgstr "Du har inte rättigheter att ändra någonting." -#: src/web/templates/admin/index.html:157 +#: evennia/web/templates/admin/index.html:157 msgid "Recent Actions" msgstr "Senaste Aktiviteter" -#: src/web/templates/admin/index.html:158 +#: evennia/web/templates/admin/index.html:158 msgid "My Actions" msgstr "Mina Aktiviteter" -#: src/web/templates/admin/index.html:162 +#: evennia/web/templates/admin/index.html:162 msgid "None yet." msgstr "Inga än." -#: src/web/templates/admin/index.html:176 +#: evennia/web/templates/admin/index.html:176 msgid "Unknown content" msgstr "Okänt innehåll" -#: src/web/templates/admin/players/add_form.html:6 +#: evennia/web/templates/admin/players/add_form.html:6 msgid "First, enter a username and password. Then you'll be able to edit more Player options." msgstr "Först, ange ett användarnamn och lösenord. Då kommer du att kunna modifiera fler Player-inställningar." -#: src/web/templates/admin/players/add_form.html:8 +#: evennia/web/templates/admin/players/add_form.html:8 msgid "Enter a username and password." msgstr "Ange ett användarnamn och lösenord." -#: src/web/templates/admin/players/change_form.html:19 -#: src/web/templates/admin/players/change_list.html:41 +#: evennia/web/templates/admin/players/change_form.html:19 +#: evennia/web/templates/admin/players/change_list.html:41 msgid "Home" msgstr "Hem" -#: src/web/templates/admin/players/change_form.html:30 +#: evennia/web/templates/admin/players/change_form.html:30 msgid "History" msgstr "Historia" -#: src/web/templates/admin/players/change_form.html:31 -#: src/web/templates/admin/players/stacked.html:9 +#: evennia/web/templates/admin/players/change_form.html:31 +#: evennia/web/templates/admin/players/stacked.html:9 msgid "View on site" msgstr "Inspektera på sida" -#: src/web/templates/admin/players/change_form.html:42 -#: src/web/templates/admin/players/change_list.html:72 +#: evennia/web/templates/admin/players/change_form.html:42 +#: evennia/web/templates/admin/players/change_list.html:72 msgid "Please correct the error below." msgid_plural "Please correct the errors below." msgstr[0] "Vänligen korrigera felet nedan." msgstr[1] "Vänligen korrigera felet nedan.\t\t\t\t" -#: src/web/templates/admin/players/change_list.html:63 +#: evennia/web/templates/admin/players/change_list.html:63 #, python-format msgid "Add %(name)s" msgstr "Lägg till %(name)s" -#: src/web/templates/admin/players/change_list.html:83 +#: evennia/web/templates/admin/players/change_list.html:83 msgid "Filter" msgstr "Filtrera" -#: src/web/templates/admin/players/stacked.html:67 +#: evennia/web/templates/admin/players/stacked.html:67 #, python-format msgid "Add another %(verbose_name)s" msgstr "Lägg till ännu ett %(verbose_name)s" -#: src/web/templates/admin/players/stacked.html:70 +#: evennia/web/templates/admin/players/stacked.html:70 msgid "Remove" msgstr "Ta bort" diff --git a/evennia/locks/__init__.py b/evennia/locks/__init__.py new file mode 100644 index 000000000..3b5361021 --- /dev/null +++ b/evennia/locks/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +""" +This sub-package defines the lock (access) mechanism of Evennia. All +lock strings are processed through the lockhandler in this package. It +also contains the default lock functions used in lock definitions. + +""" diff --git a/src/locks/lockfuncs.py b/evennia/locks/lockfuncs.py similarity index 87% rename from src/locks/lockfuncs.py rename to evennia/locks/lockfuncs.py index 461dc6daf..b46117b4b 100644 --- a/src/locks/lockfuncs.py +++ b/evennia/locks/lockfuncs.py @@ -82,14 +82,14 @@ DefaultLock: Exits: controls who may traverse the exit to """ from django.conf import settings -from src.utils import utils +from evennia.utils import utils _PERMISSION_HIERARCHY = [p.lower() for p in settings.PERMISSION_HIERARCHY] def _to_player(accessing_obj): "Helper function. Makes sure an accessing object is a player object" - if utils.inherits_from(accessing_obj, "src.objects.objects.Object"): + if utils.inherits_from(accessing_obj, "evennia.objects.objects.DefaultObject"): # an object. Convert to player. accessing_obj = accessing_obj.player return accessing_obj @@ -125,7 +125,7 @@ def self(accessing_obj, accessed_obj, *args, **kwargs): This can be used to lock specifically only to the same object that the lock is defined on. """ - return accessing_obj.typeclass == accessed_obj.typeclass + return accessing_obj == accessed_obj def perm(accessing_obj, accessed_obj, *args, **kwargs): @@ -158,7 +158,7 @@ def perm(accessing_obj, accessed_obj, *args, **kwargs): except (AttributeError, IndexError): return False - if utils.inherits_from(accessing_obj, "src.objects.objects.Object") and accessing_obj.player: + if utils.inherits_from(accessing_obj, "evennia.objects.objects.DefaultObject") and accessing_obj.player: player = accessing_obj.player perms_player = [p.lower() for p in player.permissions.all()] is_quell = player.attributes.get("_quell") @@ -176,7 +176,7 @@ def perm(accessing_obj, accessed_obj, *args, **kwargs): else: return hpos_target <= min(hpos_player, hpos_object) elif gtmode: - return gtmode and hpos_target < hpos_player + return hpos_target < hpos_player else: return hpos_target <= hpos_player elif not is_quell and perm in perms_player: @@ -291,12 +291,15 @@ def attr(accessing_obj, accessed_obj, *args, **kwargs): compare=gt means the accessing_obj must have a value greater than the one given). - Searches attributes *and* properties stored on the checking - object. The first form works like a flag - if the - attribute/property exists on the object, the value is checked for - True/False. The second form also requires that the value of the - attribute/property matches. Note that all retrieved values will be - converted to strings before doing the comparison. + Searches attributes *and* properties stored on the accessing_obj. + if accessing_obj has a property "obj", then this is used as + accessing_obj (this makes this usable for Commands too) + + The first form works like a flag - if the attribute/property + exists on the object, the value is checked for True/False. The + second form also requires that the value of the attribute/property + matches. Note that all retrieved values will be converted to + strings before doing the comparison. """ # deal with arguments if not args: @@ -318,6 +321,13 @@ def attr(accessing_obj, accessed_obj, *args, **kwargs): # that cannot be compared return False + if hasattr(accessing_obj, "obj"): + # NOTE: this is relevant for Commands. It may clash with scripts + # (they have Attributes and .obj) , but are scripts really + # used so that one ever wants to check the property on the + # Script rather than on its owner? + accessing_obj = accessing_obj.obj + # first, look for normal properties on the object trying to gain access if hasattr(accessing_obj, attrname): if value: @@ -342,13 +352,10 @@ def objattr(accessing_obj, accessed_obj, *args, **kwargs): objattr(attrname, value, compare=type) Works like attr, except it looks for an attribute on - accessing_obj.obj, if such an entity exists. Suitable - for commands. + accessed_obj instead. """ - if hasattr(accessing_obj, "obj"): - return attr(accessing_obj.obj, accessed_obj, *args, **kwargs) - + return attr(accessed_obj, accessed_obj, *args, **kwargs) def locattr(accessing_obj, accessed_obj, *args, **kwargs): """ @@ -360,7 +367,12 @@ def locattr(accessing_obj, accessed_obj, *args, **kwargs): Works like attr, except it looks for an attribute on accessing_obj.location, if such an entity exists. + if accessing_obj has a property ".obj" (such as is the case for a + Command), then accessing_obj.obj.location is used instead. + """ + if hasattr(accessing_obj, "obj"): + accessing_obj = accessing_obj.obj if hasattr(accessing_obj, "location"): return attr(accessing_obj.location, accessed_obj, *args, **kwargs) @@ -422,6 +434,31 @@ def attr_ne(accessing_obj, accessed_obj, *args, **kwargs): """ return attr(accessing_obj, accessed_obj, *args, **{'compare': 'ne'}) +def tag(accessing_obj, accessed_obj, *args, **kwargs): + """ + Usage: + tag(tagkey) + tag(tagkey, category) + + Only true if accessing_obj has the specified tag and optional + category. + If accessing_obj has the ".obj" property (such as is the case for + a command), then accessing_obj.obj is used instead. + """ + if hasattr(accessing_obj, "obj"): + accessing_obj = accessing_obj = accessing_obj.obj + return accessing_obj.tags.get(*args) + +def objtag(accessing_obj, accessed_obj, *args, **kwargs): + """ + Usage: + objtag(tagkey) + objtag(tagkey, category) + + Only true if accessed_obj has the specified tag and optional + category. + """ + return accessed_obj.tags.get(*args) def inside(accessing_obj, accessed_obj, *args, **kwargs): """ @@ -495,6 +532,15 @@ def superuser(*args, **kwargs): """ return False +def has_player(accessing_obj, accessed_obj, *args, **kwargs): + """ + Only returns true if accessing_obj has_player is true, that is, + this is a player-controlled object. It fails on actual players! + + This is a useful lock for traverse-locking Exits to restrain NPC + mobiles from moving outside their areas. + """ + return hasattr(accessing_obj, "has_player") and accessing_obj.has_player def serversetting(accessing_obj, accessed_obj, *args, **kwargs): """ @@ -503,7 +549,7 @@ def serversetting(accessing_obj, accessed_obj, *args, **kwargs): Usage: serversetting(IRC_ENABLED) - serversetting(BASE_SCRIPT_PATH, [game.gamesrc.scripts]) + serversetting(BASE_SCRIPT_PATH, ['types']) A given True/False or integers will be converted properly. """ diff --git a/src/locks/lockhandler.py b/evennia/locks/lockhandler.py similarity index 95% rename from src/locks/lockhandler.py rename to evennia/locks/lockhandler.py index b7bf5699b..1a7c8d024 100644 --- a/src/locks/lockhandler.py +++ b/evennia/locks/lockhandler.py @@ -108,7 +108,7 @@ to any other identifier you can use. import re import inspect from django.conf import settings -from src.utils import logger, utils +from evennia.utils import logger, utils from django.utils.translation import ugettext as _ __all__ = ("LockHandler", "LockException") @@ -135,7 +135,6 @@ def _cache_lockfuncs(): global _LOCKFUNCS _LOCKFUNCS = {} for modulepath in settings.LOCK_FUNC_MODULES: - modulepath = utils.pypath_to_realpath(modulepath) mod = utils.mod_import(modulepath) if mod: for tup in (tup for tup in inspect.getmembers(mod) if callable(tup[1])): @@ -385,7 +384,14 @@ class LockHandler(object): else: return default - def check_lockstring(self, accessing_obj, lockstring, no_superuser_bypass=False): + def _eval_access_type(self, accessing_obj, locks, access_type): + evalstring, func_tup, raw_string = locks[access_type] + true_false = tuple(tup[0](accessing_obj, self.obj, *tup[1], **tup[2]) + for tup in func_tup) + return eval(evalstring % true_false) + + def check_lockstring(self, accessing_obj, lockstring, no_superuser_bypass=False, + default=False, access_type=None): """ Do a direct check against a lockstring ('atype:func()..'), without any intermediary storage on the accessed object (this can be left @@ -402,11 +408,16 @@ class LockHandler(object): return True locks = self._parse_lockstring(lockstring) + + if access_type: + if not access_type in locks: + return default + else: + return self._eval_access_type( + accessing_obj, locks, access_type) + for access_type in locks: - evalstring, func_tup, raw_string = locks[access_type] - true_false = tuple(tup[0](accessing_obj, self.obj, *tup[1],**tup[2]) - for tup in func_tup) - return eval(evalstring % true_false) + return self._eval_access_type(accessing_obj, locks, access_type) def _test(): diff --git a/src/locks/tests.py b/evennia/locks/tests.py similarity index 78% rename from src/locks/tests.py rename to evennia/locks/tests.py index 1247598b3..50f99f9ab 100644 --- a/src/locks/tests.py +++ b/evennia/locks/tests.py @@ -2,11 +2,12 @@ """ This is part of Evennia's unittest framework, for testing -the stability and integrrity of the codebase during updates. +the stability and integrity of the codebase during updates. This module tests the lock functionality of Evennia. """ +from evennia.utils.test_resources import EvenniaTest try: # this is a special optimized Django version, only available in current Django devel @@ -14,25 +15,14 @@ try: except ImportError: from django.test import TestCase -from django.conf import settings -from src.locks import lockfuncs -from src.utils import create +from evennia.locks import lockfuncs -#------------------------------------------------------------ -# +# ------------------------------------------------------------ # Lock testing -# -#------------------------------------------------------------ +# ------------------------------------------------------------ -class LockTest(TestCase): - "Defines the lock test base" - def setUp(self): - "sets up the testing environment" - self.obj1 = create.create_object(settings.BASE_OBJECT_TYPECLASS, key="obj1") - self.obj2 = create.create_object(settings.BASE_OBJECT_TYPECLASS, key="obj2") - -class TestLockCheck(LockTest): +class TestLockCheck(EvenniaTest): def testrun(self): dbref = self.obj2.dbref self.obj1.locks.add("owner:dbref(%s);edit:dbref(%s) or perm(Wizards);examine:perm(Builders) and id(%s);delete:perm(Wizards);get:all()" % (dbref, dbref, dbref)) @@ -45,7 +35,9 @@ class TestLockCheck(LockTest): self.obj1.locks.add("get:false()") self.assertEquals(False, self.obj1.locks.check(self.obj2, 'get')) self.assertEquals(True, self.obj1.locks.check(self.obj2, 'not_exist', default=True)) -class TestLockfuncs(LockTest): + + +class TestLockfuncs(EvenniaTest): def testrun(self): self.obj2.permissions.add('Wizards') self.assertEquals(True, lockfuncs.true(self.obj2, self.obj1)) diff --git a/evennia/objects/__init__.py b/evennia/objects/__init__.py new file mode 100644 index 000000000..28056809a --- /dev/null +++ b/evennia/objects/__init__.py @@ -0,0 +1,6 @@ +""" +This sub-package defines the basic in-game "Object". All in-game +objects inherit from classes in this package. + +""" +from objects import DefaultObject, DefaultRoom, DefaultExit, DefaultCharacter diff --git a/src/objects/admin.py b/evennia/objects/admin.py similarity index 97% rename from src/objects/admin.py rename to evennia/objects/admin.py index 4f8db3e77..bc2cf4015 100644 --- a/src/objects/admin.py +++ b/evennia/objects/admin.py @@ -6,9 +6,9 @@ from django import forms from django.conf import settings from django.contrib import admin +from evennia.typeclasses.admin import AttributeInline, TagInline +from evennia.objects.models import ObjectDB from django.contrib.admin.utils import flatten_fieldsets -from src.typeclasses.admin import AttributeInline, TagInline -from src.objects.models import ObjectDB class ObjectAttributeInline(AttributeInline): @@ -104,7 +104,6 @@ class ObjectDBAdmin(admin.ModelAdmin): obj.save() if not change: # adding a new object - obj = obj.typeclass obj.basetype_setup() obj.basetype_posthook_setup() obj.at_object_creation() diff --git a/src/objects/manager.py b/evennia/objects/manager.py similarity index 95% rename from src/objects/manager.py rename to evennia/objects/manager.py index ac5c80d62..753f485fd 100644 --- a/src/objects/manager.py +++ b/evennia/objects/manager.py @@ -5,10 +5,10 @@ from itertools import chain from django.db.models import Q from django.conf import settings from django.db.models.fields import exceptions -from src.typeclasses.managers import TypedObjectManager -from src.typeclasses.managers import returns_typeclass, returns_typeclass_list -from src.utils import utils -from src.utils.utils import to_unicode, is_iter, make_iter, string_partial_matching +from evennia.typeclasses.managers import TypedObjectManager, TypeclassManager +from evennia.typeclasses.managers import returns_typeclass, returns_typeclass_list +from evennia.utils import utils +from evennia.utils.utils import to_unicode, is_iter, make_iter, string_partial_matching __all__ = ("ObjectManager",) _GA = object.__getattribute__ @@ -22,7 +22,7 @@ _ATTR = None _AT_MULTIMATCH_INPUT = utils.variable_from_module(*settings.SEARCH_AT_MULTIMATCH_INPUT.rsplit('.', 1)) -class ObjectManager(TypedObjectManager): +class ObjectDBManager(TypedObjectManager): """ This ObjectManager implementes methods for searching and manipulating Objects directly from the database. @@ -45,7 +45,7 @@ class ObjectManager(TypedObjectManager): get_objs_with_key_or_alias get_contents object_search (interface to many of the above methods, - equivalent to ev.search_object) + equivalent to evennia.search_object) copy_object """ @@ -124,7 +124,7 @@ class ObjectManager(TypedObjectManager): # We have to loop for safety since the referenced lookup gives deepcopy error if attribute value is an object. global _ATTR if not _ATTR: - from src.typeclasses.models import Attribute as _ATTR + from evennia.typeclasses.models import Attribute as _ATTR cands = list(self.filter(cand_restriction & type_restriction & Q(db_attributes__db_key=attribute_name))) results = [attr.objectdb_set.all() for attr in _ATTR.objects.filter(objectdb__in=cands, db_value=attribute_value)] return chain(*results) @@ -156,8 +156,6 @@ class ObjectManager(TypedObjectManager): if isinstance(property_name, basestring): if not property_name.startswith('db_'): property_name = "db_%s" % property_name - if hasattr(property_value, 'dbobj'): - property_value = property_value.dbobj querykwargs = {property_name:property_value} cand_restriction = candidates != None and Q(pk__in=[_GA(obj, "id") for obj in make_iter(candidates) if obj]) or Q() type_restriction = typeclasses and Q(db_typeclass_path__in=make_iter(typeclasses)) or Q() @@ -166,15 +164,14 @@ class ObjectManager(TypedObjectManager): except exceptions.FieldError: return [] except ValueError: - from src.utils import logger + from evennia.utils import logger logger.log_errmsg("The property '%s' does not support search criteria of the type %s." % (property_name, type(property_value))) return [] @returns_typeclass_list def get_contents(self, location, excludeobj=None): """ - Get all objects that has a location - set to this one. + Get all objects that has a location set to this one. excludeobj - one or more object keys to exclude from the match """ @@ -299,7 +296,7 @@ class ObjectManager(TypedObjectManager): if candidates: # Convenience check to make sure candidates are really dbobjs - candidates = [cand.dbobj for cand in make_iter(candidates) if cand] + candidates = [cand for cand in make_iter(candidates) if cand] if typeclass: candidates = [cand for cand in candidates if _GA(cand, "db_typeclass_path") in typeclass] @@ -309,7 +306,7 @@ class ObjectManager(TypedObjectManager): # Easiest case - dbref matching (always exact) dbref_match = self.dbref_search(dbref) if dbref_match: - if not candidates or dbref_match.dbobj in candidates: + if not candidates or dbref_match in candidates: return [dbref_match] else: return [] @@ -376,8 +373,8 @@ class ObjectManager(TypedObjectManager): new_destination = original_object.destination # create new object - from src.utils import create - from src.scripts.models import ScriptDB + from evennia.utils import create + from evennia.scripts.models import ScriptDB new_object = create.create_object(typeclass_path, key=new_key, location=new_location, @@ -402,7 +399,7 @@ class ObjectManager(TypedObjectManager): # copy over all scripts, if any for script in original_object.scripts.all(): - ScriptDB.objects.copy_script(script, new_obj=new_object.dbobj) + ScriptDB.objects.copy_script(script, new_obj=new_object) return new_object @@ -413,3 +410,6 @@ class ObjectManager(TypedObjectManager): """ self.filter(db_sessid__isnull=False).update(db_sessid=None) + +class ObjectManager(ObjectDBManager, TypeclassManager): + pass diff --git a/src/objects/migrations/0001_initial.py b/evennia/objects/migrations/0001_initial.py similarity index 100% rename from src/objects/migrations/0001_initial.py rename to evennia/objects/migrations/0001_initial.py diff --git a/src/objects/migrations/0002_auto_20140917_0756.py b/evennia/objects/migrations/0002_auto_20140917_0756.py similarity index 100% rename from src/objects/migrations/0002_auto_20140917_0756.py rename to evennia/objects/migrations/0002_auto_20140917_0756.py diff --git a/evennia/objects/migrations/0003_defaultcharacter_defaultexit_defaultobject_defaultroom.py b/evennia/objects/migrations/0003_defaultcharacter_defaultexit_defaultobject_defaultroom.py new file mode 100644 index 000000000..fd72933e4 --- /dev/null +++ b/evennia/objects/migrations/0003_defaultcharacter_defaultexit_defaultobject_defaultroom.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('objects', '0002_auto_20140917_0756'), + ] + + operations = [ + migrations.CreateModel( + name='DefaultObject', + fields=[ + ], + options={ + 'proxy': True, + }, + bases=('objects.objectdb',), + ), + migrations.CreateModel( + name='DefaultExit', + fields=[ + ], + options={ + 'proxy': True, + }, + bases=('objects.defaultobject',), + ), + migrations.CreateModel( + name='DefaultCharacter', + fields=[ + ], + options={ + 'proxy': True, + }, + bases=('objects.defaultobject',), + ), + migrations.CreateModel( + name='DefaultRoom', + fields=[ + ], + options={ + 'proxy': True, + }, + bases=('objects.defaultobject',), + ), + ] diff --git a/evennia/objects/migrations/0004_auto_20150118_1622.py b/evennia/objects/migrations/0004_auto_20150118_1622.py new file mode 100644 index 000000000..3cc4d8bee --- /dev/null +++ b/evennia/objects/migrations/0004_auto_20150118_1622.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + +def convert_defaults(apps, schema_editor): + ObjectDB = apps.get_model("objects", "ObjectDB") + for obj in ObjectDB.objects.filter(db_typeclass_path="src.objects.objects.Object"): + obj.db_typeclass_path = "typeclasses.objects.Object" + obj.save() + +class Migration(migrations.Migration): + + dependencies = [ + ('objects', '0003_defaultcharacter_defaultexit_defaultobject_defaultroom'), + ] + + operations = [ + migrations.RunPython(convert_defaults), + ] diff --git a/src/utils/dummyrunner/__init__.py b/evennia/objects/migrations/__init__.py similarity index 100% rename from src/utils/dummyrunner/__init__.py rename to evennia/objects/migrations/__init__.py diff --git a/evennia/objects/models.py b/evennia/objects/models.py new file mode 100644 index 000000000..826ed2520 --- /dev/null +++ b/evennia/objects/models.py @@ -0,0 +1,293 @@ +""" +This module defines the database models for all in-game objects, that +is, all objects that has an actual existence in-game. + +Each database object is 'decorated' with a 'typeclass', a normal +python class that implements all the various logics needed by the game +in question. Objects created of this class transparently communicate +with its related database object for storing all attributes. The +admin should usually not have to deal directly with this database +object layer. + +Attributes are separate objects that store values persistently onto +the database object. Like everything else, they can be accessed +transparently through the decorating TypeClass. +""" + +from django.db import models +from django.core.exceptions import ObjectDoesNotExist + +from evennia.typeclasses.models import TypedObject +from evennia.objects.manager import ObjectDBManager +from evennia.utils import logger +from evennia.utils.utils import (make_iter, dbref, lazy_property) + + +class ContentsHandler(object): + """ + Handles and caches the contents of an object + to avoid excessive lookups (this is done very + often due to cmdhandler needing to look for + object-cmdsets). It is stored on the 'contents_cache' + property of the ObjectDB. + """ + def __init__(self, obj): + """ + Sets up the contents handler. + + Args: + obj (Object): The object on which the + handler is defined + + """ + self.obj = obj + self._cache = {} + self.init() + + def init(self): + """ + Re-initialize the content cache + + """ + self._cache.update(dict((obj.pk, obj) for obj in + ObjectDB.objects.filter(db_location=self.obj))) + + def get(self, exclude=None): + """ + Return the contents of the cache. + + Args: + exclude (Object or list of Object): object(s) to ignore + + Returns: + objects (list): the Objects inside this location + + """ + if exclude: + exclude = [excl.pk for excl in make_iter(exclude)] + return [obj for key, obj in self._cache.items() if key not in exclude] + return self._cache.values() + + def add(self, obj): + """ + Add a new object to this location + + Args: + obj (Object): object to add + + """ + self._cache[obj.pk] = obj + + def remove(self, obj): + """ + Remove object from this location + + Args: + obj (Object): object to remove + + """ + self._cache.pop(obj.pk, None) + + def clear(self): + """ + Clear the contents cache and re-initialize + + """ + self._cache = {} + self._init() + +#------------------------------------------------------------ +# +# ObjectDB +# +#------------------------------------------------------------ + +class ObjectDB(TypedObject): + """ + All objects in the game use the ObjectDB model to store + data in the database. This is handled transparently through + the typeclass system. + + Note that the base objectdb is very simple, with + few defined fields. Use attributes to extend your + type class with new database-stored variables. + + The TypedObject supplies the following (inherited) properties: + key - main name + name - alias for key + typeclass_path - the path to the decorating typeclass + typeclass - auto-linked typeclass + date_created - time stamp of object creation + permissions - perm strings + locks - lock definitions (handler) + dbref - #id of object + db - persistent attribute storage + ndb - non-persistent attribute storage + + The ObjectDB adds the following properties: + player - optional connected player (always together with sessid) + sessid - optional connection session id (always together with player) + location - in-game location of object + home - safety location for object (handler) + + scripts - scripts assigned to object (handler from typeclass) + cmdset - active cmdset on object (handler from typeclass) + aliases - aliases for this object (property) + nicks - nicknames for *other* things in Evennia (handler) + sessions - sessions connected to this object (see also player) + has_player - bool if an active player is currently connected + contents - other objects having this object as location + exits - exits from this object + """ + # + # ObjectDB Database model setup + # + # + # inherited fields (from TypedObject): + # db_key (also 'name' works), db_typeclass_path, db_date_created, + # db_permissions + # + # These databse fields (including the inherited ones) should normally be + # managed by their corresponding wrapper properties, named same as the + # field, but without the db_* prefix (e.g. the db_key field is set with + # self.key instead). The wrappers are created at the metaclass level and + # will automatically save and cache the data more efficiently. + + # If this is a character object, the player is connected here. + db_player = models.ForeignKey("players.PlayerDB", null=True, verbose_name='player', on_delete=models.SET_NULL, + help_text='a Player connected to this object, if any.') + # the session id associated with this player, if any + db_sessid = models.CommaSeparatedIntegerField(null=True, max_length=32, verbose_name="session id", + help_text="csv list of session ids of connected Player, if any.") + # The location in the game world. Since this one is likely + # to change often, we set this with the 'location' property + # to transparently handle Typeclassing. + db_location = models.ForeignKey('self', related_name="locations_set", db_index=True, on_delete=models.SET_NULL, + blank=True, null=True, verbose_name='game location') + # a safety location, this usually don't change much. + db_home = models.ForeignKey('self', related_name="homes_set", on_delete=models.SET_NULL, + blank=True, null=True, verbose_name='home location') + # destination of this object - primarily used by exits. + db_destination = models.ForeignKey('self', related_name="destinations_set", db_index=True, on_delete=models.SET_NULL, + blank=True, null=True, verbose_name='destination', + help_text='a destination, used only by exit objects.') + # database storage of persistant cmdsets. + db_cmdset_storage = models.CharField('cmdset', max_length=255, null=True, blank=True, + help_text="optional python path to a cmdset class.") + + # Database manager + objects = ObjectDBManager() + + @lazy_property + def contents_cache(self): + return ContentsHandler(self) + + # cmdset_storage property handling + def __cmdset_storage_get(self): + "getter" + storage = self.db_cmdset_storage + return [path.strip() for path in storage.split(',')] if storage else [] + + def __cmdset_storage_set(self, value): + "setter" + self.db_cmdset_storage = ",".join(str(val).strip() for val in make_iter(value)) + self.save(update_fields=["db_cmdset_storage"]) + + def __cmdset_storage_del(self): + "deleter" + self.db_cmdset_storage = None + self.save(update_fields=["db_cmdset_storage"]) + cmdset_storage = property(__cmdset_storage_get, __cmdset_storage_set, __cmdset_storage_del) + + # location getsetter + def __location_get(self): + "Get location" + return self.db_location + + def __location_set(self, location): + "Set location, checking for loops and allowing dbref" + if isinstance(location, (basestring, int)): + # allow setting of #dbref + dbid = dbref(location, reqhash=False) + if dbid: + try: + location = ObjectDB.objects.get(id=dbid) + except ObjectDoesNotExist: + # maybe it is just a name that happens to look like a dbid + pass + try: + def is_loc_loop(loc, depth=0): + "Recursively traverse target location, trying to catch a loop." + if depth > 10: + return + elif loc == self: + raise RuntimeError + elif loc == None: + raise RuntimeWarning + return is_loc_loop(loc.db_location, depth + 1) + try: + is_loc_loop(location) + except RuntimeWarning: + pass + + # if we get to this point we are ready to change location + + old_location = self.db_location + + # this is checked in _db_db_location_post_save below + self._safe_contents_update = True + + # actually set the field (this will error if location is invalid) + self.db_location = location + self.save(update_fields=["db_location"]) + + # remove the safe flag + del self._safe_contents_update + + # update the contents cache + if old_location: + old_location.contents_cache.remove(self) + if self.db_location: + self.db_location.contents_cache.add(self) + + except RuntimeError: + errmsg = "Error: %s.location = %s creates a location loop." % (self.key, location) + logger.log_errmsg(errmsg) + raise #RuntimeError(errmsg) + except Exception, e: + errmsg = "Error (%s): %s is not a valid location." % (str(e), location) + logger.log_errmsg(errmsg) + raise #Exception(errmsg) + + def __location_del(self): + "Cleanly delete the location reference" + self.db_location = None + self.save(update_fields=["db_location"]) + location = property(__location_get, __location_set, __location_del) + + def at_db_location_postsave(self, new): + """ + This is called automatically after the location field was + saved, no matter how. It checks for a variable + _safe_contents_update to know if the save was triggered via + the location handler (which updates the contents cache) or + not. + + """ + if not hasattr(self, "_safe_contents_update"): + # changed/set outside of the location handler + if new: + # if new, there is no previous location to worry about + if self.db_location: + self.db_location.contents_cache.add(self) + else: + # Since we cannot know at this point was old_location was, we + # trigger a full-on contents_cache update here. + logger.log_warn("db_location direct save triggered contents_cache.init() for all objects!") + [o.contents_cache.init() for o in self.__dbclass__.get_all_cached_instances()] + + class Meta: + "Define Django meta options" + verbose_name = "Object" + verbose_name_plural = "Objects" + diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py new file mode 100644 index 000000000..6fb7e2a52 --- /dev/null +++ b/evennia/objects/objects.py @@ -0,0 +1,1506 @@ +""" +This is the basis of the typeclass system. + +The idea is have the object as a normal class with the +database-connection tied to itself through a property. + +The instances of all the different object types are all tied to their +own database object stored in the 'dbobj' property. All attribute +get/set operations are channeled transparently to the database object +as desired. You should normally never have to worry about the database +abstraction, just do everything on the TypeClass object. + +That an object is controlled by a player/user is just defined by its +'user' property being set. This means a user may switch which object +they control by simply linking to a new object's user property. +""" + +import traceback +from django.conf import settings + +from evennia.typeclasses.models import TypeclassBase +from evennia.typeclasses.attributes import NickHandler +from evennia.objects.manager import ObjectManager +from evennia.objects.models import ObjectDB +from evennia.scripts.scripthandler import ScriptHandler +from evennia.commands import cmdset, command +from evennia.commands.cmdsethandler import CmdSetHandler +from evennia.commands import cmdhandler +from evennia.utils.logger import log_depmsg, log_trace, log_errmsg +from evennia.utils.utils import (variable_from_module, lazy_property, + make_iter, to_str, to_unicode) + +MULTISESSION_MODE = settings.MULTISESSION_MODE + +_ScriptDB = None +_SESSIONS = None + +_AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1)) +# the sessid_max is based on the length of the db_sessid csv field (excluding commas) +_SESSID_MAX = 16 if MULTISESSION_MODE in (1, 3) else 1 + +from django.utils.translation import ugettext as _ + +class SessidHandler(object): + """ + Handles the get/setting of the sessid + comma-separated integer field + """ + def __init__(self, obj): + self.obj = obj + self._cache = set() + self._recache() + + def _recache(self): + self._cache = list(set(int(val) for val in (self.obj.db_sessid or "").split(",") if val)) + + def get(self): + "Returns a list of one or more session ids" + return self._cache + all = get # alias + + def add(self, sessid): + "Add sessid to handler" + _cache = self._cache + if sessid not in _cache: + if len(_cache) >= _SESSID_MAX: + return + _cache.append(sessid) + self.obj.db_sessid = ",".join(str(val) for val in _cache) + self.obj.save(update_fields=["db_sessid"]) + + def remove(self, sessid): + "Remove sessid from handler" + _cache = self._cache + if sessid in _cache: + _cache.remove(sessid) + self.obj.db_sessid = ",".join(str(val) for val in _cache) + self.obj.save(update_fields=["db_sessid"]) + + def clear(self): + "Clear sessids" + self._cache = [] + self.obj.db_sessid = None + self.obj.save(update_fields=["db_sessid"]) + + def count(self): + "Return amount of sessions connected" + return len(self._cache) + + + +# +# Base class to inherit from. +# + +class DefaultObject(ObjectDB): + """ + This is the root typeclass object, representing all entities + that have an actual presence in-game. Objects generally have a + location. They can also be manipulated and looked at. Most + game entities you define should inherit from Object at some distance. + Evennia defines some important subclasses of Object by default, namely + Characters, Exits and Rooms (see the bottom of this module). + + Note that all new Objects and their subclasses *must* always be + created using the evennia.create_object() function. This is so the + typeclass system can be correctly initiated behind the scenes. + + + Object Typeclass API: + + * Available properties (only available on *initiated* typeclass objects) + + key (string) - name of object + name (string) - same as key + aliases (list of strings) - aliases to the object. Will be saved to + database as AliasDB entries but returned as strings. + dbref (int, read-only) - unique #id-number. Also "id" can be used. + date_created (string) - time stamp of object creation + permissions (list of strings) - list of permission strings + + player (Player) - controlling player (if any, only set together with + sessid below) + sessid (int, read-only) - session id (if any, only set together with + player above) + location (Object) - current location. Is None if this is a room + home (Object) - safety start-location + sessions (list of Sessions, read-only) - returns all sessions + connected to this object + has_player (bool, read-only)- will only return *connected* players + contents (list of Objects, read-only) - returns all objects inside + this object (including exits) + exits (list of Objects, read-only) - returns all exits from this + object, if any + destination (Object) - only set if this object is an exit. + is_superuser (bool, read-only) - True/False if this user is a superuser + + * Handlers available + + locks - lock-handler: use locks.add() to add new lock strings + db - attribute-handler: store/retrieve database attributes on this + self.db.myattr=val, val=self.db.myattr + ndb - non-persistent attribute handler: same as db but does not + create a database entry when storing data + scripts - script-handler. Add new scripts to object with scripts.add() + cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object + nicks - nick-handler. New nicks with nicks.add(). + + * Helper methods (see evennia.objects.objects.py for full headers) + + search(ostring, global_search=False, use_nicks=True, + typeclass=None, + attribute_name=None, use_nicks=True, location=None, + quiet=False, exact=False) + execute_cmd(raw_string) + msg(text=None, from_obj=None, sessid=0, **kwargs) + msg_contents(message, exclude=None, from_obj=None, **kwargs) + move_to(destination, quiet=False, emit_to_obj=None, + use_destination=True, to_none=False) + copy(new_key=None) + delete() + is_typeclass(typeclass, exact=False) + swap_typeclass(new_typeclass, clean_attributes=False, no_default=True) + access(accessing_obj, access_type='read', default=False) + check_permstring(permstring) + + * Hook methods + + basetype_setup() - only called once, used for behind-the-scenes + setup. Normally not modified. + basetype_posthook_setup() - customization in basetype, after the + object has been created; Normally not modified. + + at_object_creation() - only called once, when object is first created. + Object customizations go here. + at_object_delete() - called just before deleting an object. If + returning False, deletion is aborted. Note that + all objects inside a deleted object are + automatically moved to their , they don't + need to be removed here. + + at_init() called whenever typeclass is cached from + memory, at least once every server restart/reload + at_cmdset_get(**kwargs) - this is called just before the command + handler requests a cmdset from this object, usually + without any kwargs + at_pre_puppet(player)- (player-controlled objects only) called just + before puppeting + at_post_puppet() - (player-controlled objects only) called just + after completing connection player<->object + at_pre_unpuppet() - (player-controlled objects only) called just + before un-puppeting + at_post_unpuppet(player) (player-controlled objects only) called + just after disconnecting player<->object link + at_server_reload() - called before server is reloaded + at_server_shutdown() - called just before server is fully shut down + + at_before_move(destination) called just before moving + object to the destination. If returns + False, move is cancelled. + announce_move_from(destination) - called in old location, just before + move, if obj.move_to() has + quiet=False + announce_move_to(source_location) - called in new location, + just after move, if obj.move_to() + has quiet=False + at_after_move(source_location) - always called after a move + has been successfully performed. + at_object_leave(obj, target_location) - called when an object leaves + this object in any fashion + at_object_receive(obj, source_location) - called when this object + receives another object + at_access(result, **kwargs) - this is called with the result of an + access call, along with any kwargs used + for that call. The return of this + method does not affect the result of the + lock check. + at_before_traverse(traversing_object) - (exit-objects only) called + just before an object + traverses this object + at_after_traverse(traversing_object, source_location) - (exit-objects + only) called just after a traversal has happened. + at_failed_traverse(traversing_object) - (exit-objects only) called + if traversal fails and property err_traverse is not defined. + + at_msg_receive(self, msg, from_obj=None, data=None) - called when a + message (via self.msg()) is sent to this obj. + If returns false, aborts send. + at_msg_send(self, msg, to_obj=None, data=None) - called when this + objects sends a message to someone via self.msg(). + + return_appearance(looker) - describes this object. Used by "look" + command by default + at_desc(looker=None) - called by 'look' whenever the appearance + is requested. + at_get(getter) - called after object has been picked up. + Does not stop pickup. + at_drop(dropper) - called when this object has been dropped. + at_say(speaker, message) - by default, called if an object inside + this object speaks + + """ + # typeclass setup + __metaclass__ = TypeclassBase + objects = ObjectManager() + + # on-object properties + + @lazy_property + def cmdset(self): + return CmdSetHandler(self, True) + + @lazy_property + def scripts(self): + return ScriptHandler(self) + + @lazy_property + def nicks(self): + return NickHandler(self) + + @lazy_property + def sessid(self): + return SessidHandler(self) + + @property + def sessions(self): + """ + Retrieve sessions connected to this object. + """ + # if the player is not connected, this will simply be an empty list. + if self.db_player: + return self.db_player.get_all_sessions() + return [] + + @property + def has_player(self): + """ + Convenience function for checking if an active player is + currently connected to this object + """ + return any(self.sessions) + + @property + def is_superuser(self): + "Check if user has a player, and if so, if it is a superuser." + return self.db_player and self.db_player.is_superuser \ + and not self.db_player.attributes.get("_quell") + + def contents_get(self, exclude=None): + """ + Returns the contents of this object, i.e. all + objects that has this object set as its location. + This should be publically available. + + exclude is one or more objects to not return + """ + return self.contents_cache.get(exclude=exclude) + contents = property(contents_get) + + + @property + def exits(self): + """ + Returns all exits from this object, i.e. all objects + at this location having the property destination != None. + """ + return [exi for exi in self.contents if exi.destination] + + # main methods + + ## methods inherited from the database object (overload them here) + + def search(self, searchdata, + global_search=False, + use_nicks=True, # should this default to off? + typeclass=None, + location=None, + attribute_name=None, + quiet=False, + exact=False, + candidates=None, + nofound_string=None, + multimatch_string=None): + """ + Returns the typeclass of an Object matching a search string/condition + + Perform a standard object search in the database, handling + multiple results and lack thereof gracefully. By default, only + objects in self's current location or inventory is searched. + Note: to find Players, use eg. evennia.player_search. + + Args: + searchdata (str or obj): Primary search criterion. Will be matched + against object.key (with object.aliases second) unless + the keyword attribute_name specifies otherwise. + Special strings: + # - search by unique dbref. This is always + a global search. + me,self - self-reference to this object + - - can be used to differentiate + between multiple same-named matches + global_search (bool): Search all objects globally. This is overruled + by "location" keyword. + use_nicks (bool): Use nickname-replace (nicktype "object") on the + search string + typeclass (str or Typeclass, or list of either): Limit search only + to Objects with this typeclass. May be a list of typeclasses + for a broader search. + location (Object): Specify a location to search, if different from the + self's given location plus its contents. This can also + be a list of locations. + attribute_name (str): Define which property to search. If set, no + key+alias search will be performed. This can be used to + search database fields (db_ will be automatically + appended), and if that fails, it will try to return + objects having Attributes with this name and value + equal to searchdata. A special use is to search for + "key" here if you want to do a key-search without + including aliases. + quiet (bool): don't display default error messages - this tells the + search method that the user wants to handle all errors + themselves. It also changes the return value type, see + below. + exact (bool): if unset (default) - prefers to match to beginning of + string rather than not matching at all. If set, requires + exact mathing of entire string. + candidates (list of objects): this is an optional custom list of objects + to search (filter) between. It is ignored if global_search + is given. If not set, this list will automatically be defined + to include the location, the contents of location and the + caller's contents (inventory). + nofound_string (str): optional custom string for not-found error message + multimatch_string (str): optional custom string for multimatch error header + + Returns: + match (Object, None or list): will return an Object/None if quiet=False, + otherwise it will return a list of 0, 1 or more matches. + + Notes: + If quiet=False, error messages will be handled by settings.SEARCH_AT_RESULT + and echoed automatically (on error, return will be None). If quiet=True, the + error messaging is assumed to be handled by the caller. + + """ + is_string = isinstance(searchdata, basestring) + + if is_string: + # searchdata is a string; wrap some common self-references + if searchdata.lower() in ("here", ): + return [self.location] if quiet else self.location + if searchdata.lower() in ("me", "self",): + return [self] if quiet else self + + if use_nicks: + # do nick-replacement on search + searchdata = self.nicks.nickreplace(searchdata, categories=("object", "player"), include_player=True) + + if(global_search or (is_string and searchdata.startswith("#") and + len(searchdata) > 1 and searchdata[1:].isdigit())): + # only allow exact matching if searching the entire database + # or unique #dbrefs + exact = True + elif not candidates: + # no custom candidates given - get them automatically + if location: + # location(s) were given + candidates = [] + for obj in make_iter(location): + candidates.extend(obj.contents) + else: + # local search. Candidates are taken from + # self.contents, self.location and + # self.location.contents + location = self.location + candidates = self.contents + if location: + candidates = candidates + [location] + location.contents + else: + # normally we don't need this since we are + # included in location.contents + candidates.append(self) + + results = ObjectDB.objects.object_search(searchdata, + attribute_name=attribute_name, + typeclass=typeclass, + candidates=candidates, + exact=exact) + if quiet: + return results + return _AT_SEARCH_RESULT(self, searchdata, results, global_search, nofound_string, multimatch_string) + + def search_player(self, searchdata, quiet=False): + """ + Simple shortcut wrapper to search for players, not characters. + + searchdata - search criterion - the key or dbref of the player + to search for. If this is "here" or "me", search + for the player connected to this object. + quiet - return the results as a list rather than echo eventual + standard error messages. + + Returns: + quiet=False (default): + no match or multimatch: + auto-echoes errors to self.msg, then returns None + (results are handled by settings.SEARCH_AT_RESULT + and settings.SEARCH_AT_MULTIMATCH_INPUT) + match: + a unique player match + quiet=True: + no match or multimatch: + returns None or list of multi-matches + match: + a unique object match + """ + if isinstance(searchdata, basestring): + # searchdata is a string; wrap some common self-references + if searchdata.lower() in ("me", "self",): + return self.player + + results = self.player.__class__.objects.player_search(searchdata) + + if quiet: + return results + return _AT_SEARCH_RESULT(self, searchdata, results, global_search=True) + + def execute_cmd(self, raw_string, sessid=None, **kwargs): + """ + Do something as this object. This method is a copy of the execute_ + cmd method on the session. This is never called normally, it's only + used when wanting specifically to let an object be the caller of a + command. It makes use of nicks of eventual connected players as well. + + Argument: + raw_string (string) - raw command input + sessid (int) - optional session id to return results to + **kwargs - other keyword arguments will be added to the found command + object instace as variables before it executes. This is + unused by default Evennia but may be used to set flags and + change operating paramaters for commands at run-time. + + Returns Deferred - this is an asynchronous Twisted object that will + not fire until the command has actually finished executing. To + overload this one needs to attach callback functions to it, with + addCallback(function). This function will be called with an + eventual return value from the command execution. + + This return is not used at all by Evennia by default, but might + be useful for coders intending to implement some sort of nested + command structure. + """ + # nick replacement - we require full-word matching. + # do text encoding conversion + raw_string = to_unicode(raw_string) + raw_string = self.nicks.nickreplace(raw_string, + categories=("inputline", "channel"), include_player=True) + return cmdhandler.cmdhandler(self, raw_string, callertype="object", sessid=sessid, **kwargs) + + + def msg(self, text=None, from_obj=None, sessid=0, **kwargs): + """ + Emits something to a session attached to the object. + + Args: + text (str, optional): The message to send + from_obj (obj, optional): object that is sending. If + given, at_msg_send will be called + sessid (int or list, optional): sessid or list of + sessids to relay to, if any. If set, will + force send regardless of MULTISESSION_MODE. + Notes: + `at_msg_receive` will be called on this Object. + All extra kwargs will be passed on to the protocol. + + """ + text = to_str(text, force_string=True) if text else "" + if from_obj: + # call hook + try: + from_obj.at_msg_send(text=text, to_obj=self, **kwargs) + except Exception: + log_trace() + try: + if not self.at_msg_receive(text=text, **kwargs): + # if at_msg_receive returns false, we abort message to this object + return + except Exception: + log_trace() + + # session relay + kwargs['_nomulti'] = kwargs.get('_nomulti', True) + + if self.player: + # for there to be a session there must be a Player. + if sessid: + sessions = make_iter(self.player.get_session(sessid)) + else: + # Send to all sessions connected to this object + sessions = [self.player.get_session(sessid) for sessid in self.sessid.get()] + if sessions: + sessions[0].msg(text=text, session=sessions, **kwargs) + + def msg_contents(self, message, exclude=None, from_obj=None, **kwargs): + """ + Emits something to all objects inside an object. + + exclude is a list of objects not to send to. See self.msg() for + more info. + """ + contents = self.contents + if exclude: + exclude = make_iter(exclude) + contents = [obj for obj in contents if obj not in exclude] + for obj in contents: + obj.msg(message, from_obj=from_obj, **kwargs) + + def move_to(self, destination, quiet=False, + emit_to_obj=None, use_destination=True, to_none=False, move_hooks=True): + """ + Moves this object to a new location. + + Moves this object to a new location. Note that if is an + exit object (i.e. it has "destination"!=None), the move_to will + happen to this destination and -not- into the exit object itself, unless + use_destination=False. Note that no lock checks are done by this + function, such things are assumed to have been handled before calling + move_to. + + Args: + destination (Object): Reference to the object to move to. This + can also be an exit object, in which case the + destination property is used as destination. + quiet (bool): If true, turn off the calling of the emit hooks + (announce_move_to/from etc) + emit_to_obj (Object): object to receive error messages + use_destination (bool): Default is for objects to use the "destination" + property of destinations as the target to move to. Turning off this + keyword allows objects to move "inside" exit objects. + to_none (bool): Allow destination to be None. Note that no hooks are run when + moving to a None location. If you want to run hooks, run them manually + (and make sure they can manage None locations). + move_hooks (bool): If False, turn off the calling of move-related hooks (at_before/after_move etc) + with quiet=True, this is as quiet a move as can be done. + + Returns: + result (bool): True/False depending on if there were problems with the move. + This method may also return various error messages to the + emit_to_obj. + """ + def logerr(string=""): + trc = traceback.format_exc() + errstring = "%s%s" % (trc, string) + log_trace() + self.msg(errstring) + + errtxt = _("Couldn't perform move ('%s'). Contact an admin.") + if not emit_to_obj: + emit_to_obj = self + + if not destination: + if to_none: + # immediately move to None. There can be no hooks called since + # there is no destination to call them with. + self.location = None + return True + emit_to_obj.msg(_("The destination doesn't exist.")) + return + if destination.destination and use_destination: + # traverse exits + destination = destination.destination + + # Before the move, call eventual pre-commands. + if move_hooks: + try: + if not self.at_before_move(destination): + return + except Exception: + logerr(errtxt % "at_before_move()") + #emit_to_obj.msg(errtxt % "at_before_move()") + #logger.log_trace() + return False + + # Save the old location + source_location = self.location + if not source_location: + # there was some error in placing this room. + # we have to set one or we won't be able to continue + if self.home: + source_location = self.home + else: + default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME) + source_location = default_home + + # Call hook on source location + if move_hooks: + try: + source_location.at_object_leave(self, destination) + except Exception: + logerr(errtxt % "at_object_leave()") + #emit_to_obj.msg(errtxt % "at_object_leave()") + #logger.log_trace() + return False + + if not quiet: + #tell the old room we are leaving + try: + self.announce_move_from(destination) + except Exception: + logerr(errtxt % "at_announce_move()") + #emit_to_obj.msg(errtxt % "at_announce_move()" ) + #logger.log_trace() + return False + + # Perform move + try: + #print "move_to location:", destination + self.location = destination + except Exception: + emit_to_obj.msg(errtxt % "location change") + log_trace() + return False + + if not quiet: + # Tell the new room we are there. + try: + self.announce_move_to(source_location) + except Exception: + logerr(errtxt % "announce_move_to()") + #emit_to_obj.msg(errtxt % "announce_move_to()") + #logger.log_trace() + return False + + if move_hooks: + # Perform eventual extra commands on the receiving location + # (the object has already arrived at this point) + try: + destination.at_object_receive(self, source_location) + except Exception: + logerr(errtxt % "at_object_receive()") + #emit_to_obj.msg(errtxt % "at_object_receive()") + #logger.log_trace() + return False + + # Execute eventual extra commands on this object after moving it + # (usually calling 'look') + if move_hooks: + try: + self.at_after_move(source_location) + except Exception: + logerr(errtxt % "at_after_move") + #emit_to_obj.msg(errtxt % "at_after_move()") + #logger.log_trace() + return False + return True + + def clear_exits(self): + """ + Destroys all of the exits and any exits pointing to this + object as a destination. + """ + for out_exit in [exi for exi in ObjectDB.objects.get_contents(self) if exi.db_destination]: + out_exit.delete() + for in_exit in ObjectDB.objects.filter(db_destination=self): + in_exit.delete() + + def clear_contents(self): + """ + Moves all objects (players/things) to their home + location or to default home. + """ + # Gather up everything that thinks this is its location. + default_home_id = int(settings.DEFAULT_HOME.lstrip("#")) + try: + default_home = ObjectDB.objects.get(id=default_home_id) + if default_home.dbid == self.dbid: + # we are deleting default home! + default_home = None + except Exception: + string = _("Could not find default home '(#%d)'.") + log_errmsg(string % default_home_id) + default_home = None + + for obj in self.contents: + home = obj.home + # Obviously, we can't send it back to here. + if not home or (home and home.dbid == self.dbid): + obj.home = default_home + home = default_home + + # If for some reason it's still None... + if not home: + string = "Missing default home, '%s(#%d)' " + string += "now has a null location." + obj.location = None + obj.msg(_("Something went wrong! You are dumped into nowhere. Contact an admin.")) + log_errmsg(string % (obj.name, obj.dbid)) + return + + if obj.has_player: + if home: + string = "Your current location has ceased to exist," + string += " moving you to %s(#%d)." + obj.msg(_(string) % (home.name, home.dbid)) + else: + # Famous last words: The player should never see this. + string = "This place should not exist ... contact an admin." + obj.msg(_(string)) + obj.move_to(home) + + + def copy(self, new_key=None): + """ + Makes an identical copy of this object. If you want to customize the + copy by changing some settings, use ObjectDB.object.copy_object() + directly. + + new_key (string) - new key/name of copied object. If new_key is not + specified, the copy will be named _copy + by default. + Returns: Object (copy of this one) + """ + def find_clone_key(): + """ + Append 01, 02 etc to obj.key. Checks next higher number in the + same location, then adds the next number available + + returns the new clone name on the form keyXX + """ + key = self.key + num = 1 + for obj in (obj for obj in self.location.contents + if obj.key.startswith(key) and + obj.key.lstrip(key).isdigit()): + num += 1 + return "%s%03i" % (key, num) + new_key = new_key or find_clone_key() + return ObjectDB.objects.copy_object(self, new_key=new_key) + + delete_iter = 0 + def delete(self): + """ + Deletes this object. + Before deletion, this method makes sure to move all contained + objects to their respective home locations, as well as clean + up all exits to/from the object. + """ + global _ScriptDB + if not _ScriptDB: + from evennia.scripts.models import ScriptDB as _ScriptDB + + if self.delete_iter > 0: + # make sure to only call delete once on this object + # (avoid recursive loops) + return False + + if not self.at_object_delete(): + # this is an extra pre-check + # run before deletio field-related properties + # is kicked into gear. + self.delete_iter = 0 + return False + + self.delete_iter += 1 + + # See if we need to kick the player off. + + for session in self.sessions: + session.msg(_("Your character %s has been destroyed.") % self.key) + # no need to disconnect, Player just jumps to OOC mode. + # sever the connection (important!) + if self.player: + for sessid in self.sessid.all(): + self.player.unpuppet_object(sessid) + self.player = None + + for script in _ScriptDB.objects.get_all_scripts_on_obj(self): + script.stop() + + # Destroy any exits to and from this room, if any + self.clear_exits() + # Clear out any non-exit objects located within the object + self.clear_contents() + self.attributes.clear() + self.nicks.clear() + self.aliases.clear() + self.location = None # this updates contents_cache for our location + + # Perform the deletion of the object + super(ObjectDB, self).delete() + return True + + + def __eq__(self, other): + """ + Checks for equality against an id string or another object or user. + + This has be located at this level, having it in the + parent doesn't work. + """ + try: + return self.dbid == other.dbid + except AttributeError: + # compare players instead + try: + return self.player.uid == other.player.uid + except AttributeError: + return False + + def at_first_save(self): + """ + This is called by the typeclass system whenever an instance of + this class is saved for the first time. It is a generic hook + for calling the startup hooks for the various game entities. + When overloading you generally don't overload this but + overload the hooks called by this method. + """ + self.basetype_setup() + self.at_object_creation() + + if hasattr(self, "_createdict"): + # this will only be set if the utils.create function + # was used to create the object. We want the create + # call's kwargs to override the values set by hooks. + cdict = self._createdict + updates = [] + if not cdict.get("key"): + if not self.db_key: + self.db_key = "#%i" % self.dbid + updates.append("db_key") + elif self.key != cdict.get("key"): + updates.append("db_key") + self.db_key = cdict["key"] + if cdict.get("location") and self.location != cdict["location"]: + self.db_location = cdict["location"] + updates.append("db_location") + if cdict.get("home") and self.home != cdict["home"]: + self.home = cdict["home"] + updates.append("db_home") + if cdict.get("destination") and self.destination != cdict["destination"]: + self.destination = cdict["destination"] + updates.append("db_destination") + if updates: + self.save(update_fields=updates) + + if cdict.get("permissions"): + self.permissions.add(cdict["permissions"]) + if cdict.get("locks"): + self.locks.add(cdict["locks"]) + if cdict.get("aliases"): + self.aliases.add(cdict["aliases"]) + if cdict.get("location"): + cdict["location"].at_object_receive(self, None) + self.at_after_move(None) + if cdict.get("attributes"): + # this should be a dict of attrname:value + keys, values = cdict["attributes"].keys(), cdict["attributes"].values() + self.attributes.batch_add(keys, values) + if cdict.get("nattributes"): + # this should be a dict of nattrname:value + for key, value in cdict["nattributes"].items(): + self.nattributes.add(key, value) + + del self._createdict + + self.basetype_posthook_setup() + + + ## hooks called by the game engine + + def basetype_setup(self): + """ + This sets up the default properties of an Object, + just before the more general at_object_creation. + + You normally don't need to change this unless you change some + fundamental things like names of permission groups. + """ + # the default security setup fallback for a generic + # object. Overload in child for a custom setup. Also creation + # commands may set this (create an item and you should be its + # controller, for example) + + self.locks.add(";".join([ + "control:perm(Immortals)", # edit locks/permissions, delete + "examine:perm(Builders)", # examine properties + "view:all()", # look at object (visibility) + "edit:perm(Wizards)", # edit properties/attributes + "delete:perm(Wizards)", # delete object + "get:all()", # pick up object + "call:true()", # allow to call commands on this object + "tell:perm(Wizards)", # allow emits to this object + "puppet:pperm(Immortals)"])) # lock down puppeting only to staff by default + + def basetype_posthook_setup(self): + """ + Called once, after basetype_setup and at_object_creation. This + should generally not be overloaded unless you are redefining + how a room/exit/object works. It allows for basetype-like + setup after the object is created. An example of this is + EXITs, who need to know keys, aliases, locks etc to set up + their exit-cmdsets. + """ + pass + + def at_object_creation(self): + """ + Called once, when this object is first created. This is + the normal hook to overload for most object types. + """ + pass + + def at_object_delete(self): + """ + Called just before the database object is + permanently delete()d from the database. If + this method returns False, deletion is aborted. + """ + return True + + def at_init(self): + """ + This is always called whenever this object is initiated -- + that is, whenever it its typeclass is cached from memory. This + happens on-demand first time the object is used or activated + in some way after being created but also after each server + restart or reload. + """ + pass + + def at_cmdset_get(self, **kwargs): + """ + Called just before cmdsets on this object are requested by the + command handler. If changes need to be done on the fly to the + cmdset before passing them on to the cmdhandler, this is the + place to do it. This is called also if the object currently + have no cmdsets. **kwargs are usually not set but could be + used e.g. to force rebuilding of a dynamically created cmdset + or similar. + """ + pass + + def at_pre_puppet(self, player, sessid=None): + """ + Called just before a Player connects to this object + to puppet it. + + player - connecting player object + sessid - session id controlling the connection + """ + pass + + def at_post_puppet(self): + """ + Called just after puppeting has been completed and + all Player<->Object links have been established. + """ + self.player.db._last_puppet = self + + def at_pre_unpuppet(self): + """ + Called just before beginning to un-connect a puppeting + from this Player. + """ + pass + + def at_post_unpuppet(self, player, sessid=None): + """ + Called just after the Player successfully disconnected + from this object, severing all connections. + + player - the player object that just disconnected from + this object. + sessid - session id controlling the connection + """ + pass + + def at_server_reload(self): + """ + This hook is called whenever the server is shutting down for + restart/reboot. If you want to, for example, save non-persistent + properties across a restart, this is the place to do it. + """ + pass + + def at_server_shutdown(self): + """ + This hook is called whenever the server is shutting down fully + (i.e. not for a restart). + """ + pass + + def at_access(self, result, accessing_obj, access_type, **kwargs): + """ + This is called with the result of an access call, along with + any kwargs used for that call. The return of this method does + not affect the result of the lock check. It can be used e.g. to + customize error messages in a central location or other effects + based on the access result. + """ + pass + + + # hooks called when moving the object + + def at_before_move(self, destination): + """ + Called just before starting to move + this object to destination. + + destination - the object we are moving to + + If this method returns False/None, the move + is cancelled before it is even started. + """ + #return has_perm(self, destination, "can_move") + return True + + def announce_move_from(self, destination): + """ + Called if the move is to be announced. This is + called while we are still standing in the old + location. + + destination - the place we are going to. + """ + if not self.location: + return + name = self.name + loc_name = "" + loc_name = self.location.name + dest_name = destination.name + string = "%s is leaving %s, heading for %s." + self.location.msg_contents(string % (name, loc_name, dest_name), exclude=self) + + def announce_move_to(self, source_location): + """ + Called after the move if the move was not quiet. At this + point we are standing in the new location. + + source_location - the place we came from + """ + + name = self.name + if not source_location and self.location.has_player: + # This was created from nowhere and added to a player's + # inventory; it's probably the result of a create command. + string = "You now have %s in your possession." % name + self.location.msg(string) + return + + src_name = "nowhere" + loc_name = self.location.name + if source_location: + src_name = source_location.name + string = "%s arrives to %s from %s." + self.location.msg_contents(string % (name, loc_name, src_name), exclude=self) + + def at_after_move(self, source_location): + """ + Called after move has completed, regardless of quiet mode or not. + Allows changes to the object due to the location it is now in. + + source_location - where we came from. This may be None. + """ + pass + + def at_object_leave(self, moved_obj, target_location): + """ + Called just before an object leaves from inside this object + + moved_obj - the object leaving + target_location - where the object is going. + """ + pass + + def at_object_receive(self, moved_obj, source_location): + """ + Called after an object has been moved into this object. + + moved_obj - the object moved into this one + source_location - where moved_object came from. + """ + pass + + def at_before_traverse(self, traversing_object): + """ + Called just before an object uses this object to + traverse to another object (i.e. this object is a type of Exit) + + The target location should normally be available as self.destination. + """ + pass + + def at_traverse(self, traversing_object, target_location): + """ + This hook is responsible for handling the actual traversal, normally + by calling traversing_object.move_to(target_location). It is normally + only implemented by Exit objects. If it returns False (usually because + move_to returned False), at_after_traverse below should not be called + and instead at_failed_traverse should be called. + """ + pass + + def at_after_traverse(self, traversing_object, source_location): + """ + Called just after an object successfully used this object to + traverse to another object (i.e. this object is a type of Exit) + + The target location should normally be available as self.destination. + """ + pass + + def at_failed_traverse(self, traversing_object): + """ + This is called if an object fails to traverse this object for some + reason. It will not be called if the attribute err_traverse is defined, + that attribute will then be echoed back instead. + """ + pass + + def at_msg_receive(self, text=None, **kwargs): + """ + This hook is called whenever someone + sends a message to this object. + + Note that from_obj may be None if the sender did + not include itself as an argument to the obj.msg() + call - so you have to check for this. . + + Consider this a pre-processing method before + msg is passed on to the user sesssion. If this + method returns False, the msg will not be + passed on. + Input: + msg = the message received + from_obj = the one sending the message + Output: + boolean True/False + """ + return True + + def at_msg_send(self, text=None, to_obj=None, **kwargs): + """ + This is a hook that is called when /this/ object + sends a message to another object with obj.msg() + while also specifying that it is the one sending. + + Note that this method is executed on the object + passed along with the msg() function (i.e. using + obj.msg(msg, from_obj=caller) will then launch caller.at_msg()) + and if no object was passed, it will never be called. + """ + pass + + # hooks called by the default cmdset. + + def return_appearance(self, pobject): + """ + This is a convenient hook for a 'look' + command to call. + """ + if not pobject: + return + # get and identify all objects + visible = (con for con in self.contents if con != pobject and + con.access(pobject, "view")) + exits, users, things = [], [], [] + for con in visible: + key = con.key + if con.destination: + exits.append(key) + elif con.has_player: + users.append("{c%s{n" % key) + else: + things.append(key) + # get description, build string + string = "{c%s{n\n" % self.key + desc = self.db.desc + if desc: + string += "%s" % desc + if exits: + string += "\n{wExits:{n " + ", ".join(exits) + if users or things: + string += "\n{wYou see:{n " + ", ".join(users + things) + return string + + def at_desc(self, looker=None): + """ + This is called whenever someone looks + at this object. Looker is the looking + object. + """ + pass + + def at_get(self, getter): + """ + Called when this object has been picked up. Obs- + this method cannot stop the pickup - use permissions + for that! + + getter - the object getting this object. + """ + pass + + def at_drop(self, dropper): + """ + Called when this object has been dropped. + + dropper - the object which just dropped this object. + """ + pass + + def at_say(self, speaker, message): + """ + Called on this object if an object inside this object speaks. + The string returned from this method is the final form + of the speech. Obs - you don't have to add things like + 'you say: ' or similar, that is handled by the say command. + + speaker - the object speaking + message - the words spoken. + """ + return message + + +# +# Base Character object +# + +class DefaultCharacter(DefaultObject): + """ + This is just like the Object except it implements its own + version of the at_object_creation to set up the script + that adds the default cmdset to the object. + """ + + def basetype_setup(self): + """ + Setup character-specific security + + You should normally not need to overload this, but if you do, make + sure to reproduce at least the two last commands in this method (unless + you want to fundamentally change how a Character object works). + + """ + super(DefaultCharacter, self).basetype_setup() + self.locks.add(";".join(["get:false()", # noone can pick up the character + "call:false()"])) # no commands can be called on character from outside + # add the default cmdset + self.cmdset.add_default(settings.CMDSET_CHARACTER, permanent=True) + + def at_object_creation(self): + """ + All this does (for now) is to add the default cmdset. Since + the script is permanently stored to this object (the permanent + keyword creates a script to do this), we should never need to + do this again for as long as this object exists. + """ + pass + + def at_after_move(self, source_location): + "Default is to look around after a move." + self.execute_cmd('look') + + def at_pre_puppet(self, player, sessid=None): + """ + This recovers the character again after having been "stoved away" + at the unpuppet + """ + if self.db.prelogout_location: + # try to recover + self.location = self.db.prelogout_location + if self.location is None: + # make sure location is never None (home should always exist) + self.location = self.home + if self.location: + # save location again to be sure + self.db.prelogout_location = self.location + self.location.at_object_receive(self, self.location) + else: + player.msg("{r%s has no location and no home is set.{n" % self, sessid=sessid) + + def at_post_puppet(self): + """ + Called just after puppeting has completed. + """ + self.msg("\nYou become {c%s{n.\n" % self.name) + self.execute_cmd("look") + if self.location: + self.location.msg_contents("%s has entered the game." % self.name, exclude=[self]) + + def at_post_unpuppet(self, player, sessid=None): + """ + We stove away the character when the player goes ooc/logs off, + otherwise the character object will remain in the room also after the + player logged off ("headless", so to say). + """ + if self.location: # have to check, in case of multiple connections closing + self.location.msg_contents("%s has left the game." % self.name, exclude=[self]) + self.db.prelogout_location = self.location + self.location = None + +# +# Base Room object +# + +class DefaultRoom(DefaultObject): + """ + This is the base room object. It's just like any Object except its + location is None. + """ + def basetype_setup(self): + """ + Simple setup, shown as an example + (since default is None anyway) + """ + + super(DefaultRoom, self).basetype_setup() + self.locks.add(";".join(["get:false()", + "puppet:false()"])) # would be weird to puppet a room ... + self.location = None + + +# +# Base Exit object +# + +class DefaultExit(DefaultObject): + """ + This is the base exit object - it connects a location to another. + This is done by the exit assigning a "command" on itself with the + same name as the exit object (to do this we need to remember to + re-create the command when the object is cached since it must be + created dynamically depending on what the exit is called). This + command (which has a high priority) will thus allow us to traverse + exits simply by giving the exit-object's name on its own. + """ + + # Helper classes and methods to implement the Exit. These need not + # be overloaded unless one want to change the foundation for how + # Exits work. See the end of the class for hook methods to overload. + + def create_exit_cmdset(self, exidbobj): + """ + Helper function for creating an exit command set + command. + + The command of this cmdset has the same name as the Exit object + and allows the exit to react when the player enter the exit's name, + triggering the movement between rooms. + + Note that exitdbobj is an ObjectDB instance. This is necessary + for handling reloads and avoid tracebacks if this is called while + the typeclass system is rebooting. + """ + class ExitCommand(command.Command): + """ + This is a command that simply cause the caller + to traverse the object it is attached to. + """ + obj = None + + def func(self): + "Default exit traverse if no syscommand is defined." + + if self.obj.access(self.caller, 'traverse'): + # we may traverse the exit. + self.obj.at_traverse(self.caller, self.obj.destination) + else: + # exit is locked + if self.obj.db.err_traverse: + # if exit has a better error message, let's use it. + self.caller.msg(self.obj.db.err_traverse) + else: + # No shorthand error message. Call hook. + self.obj.at_failed_traverse(self.caller) + + # create an exit command. We give the properties here, + # to always trigger metaclass preparations + cmd = ExitCommand(key=exidbobj.db_key.strip().lower(), + aliases=exidbobj.aliases.all(), + locks=str(exidbobj.locks), + auto_help=False, + destination=exidbobj.db_destination, + arg_regex=r"^$", + is_exit=True, + obj=exidbobj) + # create a cmdset + exit_cmdset = cmdset.CmdSet(None) + exit_cmdset.key = '_exitset' + exit_cmdset.priority = 101 + exit_cmdset.duplicates = True + # add command to cmdset + exit_cmdset.add(cmd) + return exit_cmdset + + # Command hooks + def basetype_setup(self): + """ + Setup exit-security + + You should normally not need to overload this - if you do make sure you + include all the functionality in this method. + """ + super(DefaultExit, self).basetype_setup() + + # setting default locks (overload these in at_object_creation() + self.locks.add(";".join(["puppet:false()", # would be weird to puppet an exit ... + "traverse:all()", # who can pass through exit by default + "get:false()"])) # noone can pick up the exit + + # an exit should have a destination (this is replaced at creation time) + if self.location: + self.destination = self.location + + def at_cmdset_get(self, **kwargs): + """ + Called when the cmdset is requested from this object, just before the + cmdset is actually extracted. If no Exit-cmdset is cached, create + it now. + + kwargs: + force_init=True - force a re-build of the cmdset (for example to update aliases) + """ + + if "force_init" in kwargs or not self.cmdset.has_cmdset("_exitset", must_be_default=True): + # we are resetting, or no exit-cmdset was set. Create one dynamically. + self.cmdset.add_default(self.create_exit_cmdset(self), permanent=False) + + # this and other hooks are what usually can be modified safely. + + def at_object_creation(self): + "Called once, when object is first created (after basetype_setup)." + pass + + def at_traverse(self, traversing_object, target_location): + """ + This implements the actual traversal. The traverse lock has already been + checked (in the Exit command) at this point. + """ + source_location = traversing_object.location + if traversing_object.move_to(target_location): + self.at_after_traverse(traversing_object, source_location) + else: + if self.db.err_traverse: + # if exit has a better error message, let's use it. + self.caller.msg(self.db.err_traverse) + else: + # No shorthand error message. Call hook. + self.at_failed_traverse(traversing_object) + + def at_after_traverse(self, traversing_object, source_location): + """ + Called after a successful traverse. + """ + pass + + def at_failed_traverse(self, traversing_object): + """ + This is called if an object fails to traverse this object for some + reason. It will not be called if the attribute "err_traverse" is + defined, that attribute will then be echoed back instead as a + convenient shortcut. + + (See also hooks at_before_traverse and at_after_traverse). + """ + traversing_object.msg("You cannot go there.") diff --git a/evennia/players/__init__.py b/evennia/players/__init__.py new file mode 100644 index 000000000..321d69a8e --- /dev/null +++ b/evennia/players/__init__.py @@ -0,0 +1,7 @@ +""" +This sub-package defines the out-of-character entities known as +Players. These are equivalent to 'accounts' and can puppet one or +more Objects depending on settings. A Player has no in-game existence. + +""" +from players import DefaultGuest, DefaultPlayer diff --git a/src/players/admin.py b/evennia/players/admin.py similarity index 98% rename from src/players/admin.py rename to evennia/players/admin.py index d01646cd6..a4a0003cd 100644 --- a/src/players/admin.py +++ b/evennia/players/admin.py @@ -8,9 +8,9 @@ from django.conf import settings from django.contrib import admin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.forms import UserChangeForm, UserCreationForm -from src.players.models import PlayerDB -from src.typeclasses.admin import AttributeInline, TagInline -from src.utils import create +from evennia.players.models import PlayerDB +from evennia.typeclasses.admin import AttributeInline, TagInline +from evennia.utils import create # handle the custom User editor @@ -204,7 +204,7 @@ class PlayerDBAdmin(BaseUserAdmin): obj.save() if not change: #calling hooks for new player - ply = obj.typeclass + ply = obj ply.basetype_setup() ply.at_player_creation() diff --git a/src/players/bots.py b/evennia/players/bots.py similarity index 89% rename from src/players/bots.py rename to evennia/players/bots.py index 6a32b1926..7df6e315a 100644 --- a/src/players/bots.py +++ b/evennia/players/bots.py @@ -5,11 +5,11 @@ Player that are controlled by the server. """ from django.conf import settings -from src.players.player import Player -from src.scripts.scripts import Script -from src.commands.command import Command -from src.commands.cmdset import CmdSet -from src.utils import search +from evennia.players.players import DefaultPlayer +from evennia.scripts.scripts import DefaultScript +from evennia.commands.command import Command +from evennia.commands.cmdset import CmdSet +from evennia.utils import search _IDLE_TIMEOUT = settings.IDLE_TIMEOUT @@ -18,7 +18,7 @@ _SESSIONS = None # Bot helper utilities -class BotStarter(Script): +class BotStarter(DefaultScript): """ This non-repeating script has the sole purpose of kicking its bot @@ -50,7 +50,7 @@ class BotStarter(Script): """ global _SESSIONS if not _SESSIONS: - from src.server.sessionhandler import SESSIONS as _SESSIONS + from evennia.server.sessionhandler import SESSIONS as _SESSIONS for session in _SESSIONS.sessions_from_player(self.player): session.update_session_counters(idle=True) @@ -76,7 +76,7 @@ class CmdBotListen(Command): key = "bot_data_in" def func(self): "Relay to typeclass" - self.obj.typeclass.execute_cmd(self.args.strip(), sessid=self.sessid) + self.obj.execute_cmd(self.args.strip(), sessid=self.sessid) class BotCmdSet(CmdSet): "Holds the BotListen command" @@ -87,7 +87,7 @@ class BotCmdSet(CmdSet): # Bot base class -class Bot(Player): +class Bot(DefaultPlayer): """ A Bot will start itself when the server starts (it will generally not do so @@ -155,7 +155,7 @@ class IRCBot(Bot): """ global _SESSIONS if not _SESSIONS: - from src.server.sessionhandler import SESSIONS as _SESSIONS + from evennia.server.sessionhandler import SESSIONS as _SESSIONS # if keywords are given, store (the BotStarter script # will not give any keywords, so this should normally only @@ -186,7 +186,7 @@ class IRCBot(Bot): "channel": self.db.irc_channel , "network": self.db.irc_network, "port": self.db.irc_port} - _SESSIONS.start_bot_session("src.server.portal.irc.IRCBotFactory", configdict) + _SESSIONS.start_bot_session("evennia.server.portal.irc.IRCBotFactory", configdict) def msg(self, text=None, **kwargs): """ @@ -196,9 +196,9 @@ class IRCBot(Bot): # cache channel lookup self.ndb.ev_channel = self.db.ev_channel if "from_channel" in kwargs and text and self.ndb.ev_channel.dbid == kwargs["from_channel"]: - if "from_obj" not in kwargs or kwargs["from_obj"] != [self.dbobj.id]: + if "from_obj" not in kwargs or kwargs["from_obj"] != [self.id]: text = "bot_data_out %s" % text - self.dbobj.msg(text=text) + self.msg(text=text) def execute_cmd(self, text=None, sessid=None): """ @@ -209,7 +209,7 @@ class IRCBot(Bot): # cache channel lookup self.ndb.ev_channel = self.db.ev_channel if self.ndb.ev_channel: - self.ndb.ev_channel.msg(text, senders=self.dbobj.id) + self.ndb.ev_channel.msg(text, senders=self.id) # RSS @@ -229,7 +229,7 @@ class RSSBot(Bot): """ global _SESSIONS if not _SESSIONS: - from src.server.sessionhandler import SESSIONS as _SESSIONS + from evennia.server.sessionhandler import SESSIONS as _SESSIONS if ev_channel: # connect to Evennia channel @@ -247,7 +247,7 @@ class RSSBot(Bot): configdict = {"uid": self.dbid, "url": self.db.rss_url, "rate": self.db.rss_rate} - _SESSIONS.start_bot_session("src.server.portal.rss.RSSBotFactory", configdict) + _SESSIONS.start_bot_session("evennia.server.portal.rss.RSSBotFactory", configdict) def execute_cmd(self, text=None, sessid=None): """ @@ -258,7 +258,7 @@ class RSSBot(Bot): # cache channel lookup self.ndb.ev_channel = self.db.ev_channel if self.ndb.ev_channel: - self.ndb.ev_channel.msg(text, senders=self.dbobj.id) + self.ndb.ev_channel.msg(text, senders=self.id) class IMC2Bot(Bot): """ @@ -277,7 +277,7 @@ class IMC2Bot(Bot): """ global _SESSIONS if not _SESSIONS: - from src.server.sessionhandler import SESSIONS as _SESSIONS + from evennia.server.sessionhandler import SESSIONS as _SESSIONS if ev_channel: # connect to Evennia channel channel = search.channel_search(ev_channel) @@ -308,7 +308,7 @@ class IMC2Bot(Bot): "client_pwd": self.db.client_pwd, "server_pwd": self.db.server_pwd} - _SESSIONS.start_bot_session("src.server.portal.imc2.IMC2BotFactory", configdict) + _SESSIONS.start_bot_session("evennia.server.portal.imc2.IMC2BotFactory", configdict) def msg(self, text=None, **kwargs): """ @@ -318,9 +318,9 @@ class IMC2Bot(Bot): # cache channel lookup self.ndb.ev_channel = self.db.ev_channel if "from_channel" in kwargs and text and self.ndb.ev_channel.dbid == kwargs["from_channel"]: - if "from_obj" not in kwargs or kwargs["from_obj"] != [self.dbobj.id]: + if "from_obj" not in kwargs or kwargs["from_obj"] != [self.id]: text = "bot_data_out %s" % text - self.dbobj.msg(text=text) + self.msg(text=text) def execute_cmd(self, text=None, sessid=None): """ @@ -330,5 +330,5 @@ class IMC2Bot(Bot): # cache channel lookup self.ndb.ev_channel = self.db.ev_channel if self.ndb.ev_channel: - self.ndb.ev_channel.msg(text, senders=self.dbobj.id) + self.ndb.ev_channel.msg(text, senders=self.id) diff --git a/src/players/manager.py b/evennia/players/manager.py similarity index 72% rename from src/players/manager.py rename to evennia/players/manager.py index d7526c294..5cc72a1d0 100644 --- a/src/players/manager.py +++ b/evennia/players/manager.py @@ -5,8 +5,9 @@ The managers for the custom Player object and permissions. import datetime from django.contrib.auth.models import UserManager #from functools import update_wrapper -from src.typeclasses.managers import returns_typeclass_list, returns_typeclass, TypedObjectManager -#from src.utils import logger +from evennia.typeclasses.managers import (returns_typeclass_list, returns_typeclass, + TypedObjectManager, TypeclassManager) +#from evennia.utils import logger __all__ = ("PlayerManager",) @@ -14,7 +15,7 @@ __all__ = ("PlayerManager",) # Player Manager # -class PlayerManager(TypedObjectManager, UserManager): +class PlayerDBManager(TypedObjectManager, UserManager): """ This PlayerManager implements methods for searching and manipulating Players directly from the database. @@ -35,8 +36,8 @@ class PlayerManager(TypedObjectManager, UserManager): get_player_from_email get_player_from_uid get_player_from_name - player_search (equivalent to ev.search_player) - swap_character + player_search (equivalent to evennia.search_player) + #swap_character """ def num_total_players(self): @@ -122,30 +123,33 @@ class PlayerManager(TypedObjectManager, UserManager): else: return self.filter(username__icontains=ostring) - def swap_character(self, player, new_character, delete_old_character=False): - """ - This disconnects a player from the current character (if any) and - connects to a new character object. +# def swap_character(self, player, new_character, delete_old_character=False): +# """ +# This disconnects a player from the current character (if any) and +# connects to a new character object. +# +# """ +# +# if new_character.player: +# # the new character is already linked to a player! +# return False +# +# # do the swap +# old_character = player.character +# if old_character: +# old_character.player = None +# try: +# player.character = new_character +# new_character.player = player +# except Exception: +# # recover old setup +# if old_character: +# old_character.player = player +# player.character = old_character +# return False +# if old_character and delete_old_character: +# old_character.delete() +# return True - """ - - if new_character.player: - # the new character is already linked to a player! - return False - - # do the swap - old_character = player.character - if old_character: - old_character.player = None - try: - player.character = new_character - new_character.player = player - except Exception: - # recover old setup - if old_character: - old_character.player = player - player.character = old_character - return False - if old_character and delete_old_character: - old_character.delete() - return True +class PlayerManager(PlayerDBManager, TypeclassManager): + pass diff --git a/src/players/migrations/0001_initial.py b/evennia/players/migrations/0001_initial.py similarity index 100% rename from src/players/migrations/0001_initial.py rename to evennia/players/migrations/0001_initial.py diff --git a/evennia/players/migrations/0002_move_defaults.py b/evennia/players/migrations/0002_move_defaults.py new file mode 100644 index 000000000..cb16bfd5e --- /dev/null +++ b/evennia/players/migrations/0002_move_defaults.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + +def convert_defaults(apps, schema_editor): + PlayerDB = apps.get_model("players", "PlayerDB") + for player in PlayerDB.objects.filter(db_typeclass_path="src.players.player.Player"): + player.db_typeclass_path = "typeclasses.players.Player" + player.save() + +class Migration(migrations.Migration): + + dependencies = [ + ('players', '0001_initial'), + ] + + operations = [ + migrations.RunPython(convert_defaults), + ] diff --git a/evennia/players/migrations/0003_auto_20150209_2234.py b/evennia/players/migrations/0003_auto_20150209_2234.py new file mode 100644 index 000000000..71d588bbc --- /dev/null +++ b/evennia/players/migrations/0003_auto_20150209_2234.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('players', '0002_move_defaults'), + ] + + operations = [ + migrations.CreateModel( + name='DefaultPlayer', + fields=[ + ], + options={ + 'proxy': True, + }, + bases=('players.playerdb',), + ), + migrations.CreateModel( + name='DefaultGuest', + fields=[ + ], + options={ + 'proxy': True, + }, + bases=('players.defaultplayer',), + ), + migrations.AlterModelOptions( + name='playerdb', + options={'verbose_name': 'Player'}, + ), + ] diff --git a/game/gamesrc/__init__.py b/evennia/players/migrations/__init__.py similarity index 100% rename from game/gamesrc/__init__.py rename to evennia/players/migrations/__init__.py diff --git a/evennia/players/models.py b/evennia/players/models.py new file mode 100644 index 000000000..141f30e99 --- /dev/null +++ b/evennia/players/models.py @@ -0,0 +1,173 @@ +""" +Player + +The player class is an extension of the default Django user class, +and is customized for the needs of Evennia. + +We use the Player to store a more mud-friendly style of permission +system as well as to allow the admin more flexibility by storing +attributes on the Player. Within the game we should normally use the +Player manager's methods to create users so that permissions are set +correctly. + +To make the Player model more flexible for your own game, it can also +persistently store attributes of its own. This is ideal for extra +account info and OOC account configuration variables etc. + +""" + +from django.conf import settings +from django.db import models +from django.contrib.auth.models import AbstractUser +from django.utils.encoding import smart_str + +from evennia.players.manager import PlayerDBManager +from evennia.typeclasses.models import TypedObject +from evennia.utils.utils import make_iter + +__all__ = ("PlayerDB",) + +#_ME = _("me") +#_SELF = _("self") + +_MULTISESSION_MODE = settings.MULTISESSION_MODE + +_GA = object.__getattribute__ +_SA = object.__setattr__ +_DA = object.__delattr__ + +_TYPECLASS = None + + +#------------------------------------------------------------ +# +# PlayerDB +# +#------------------------------------------------------------ + +class PlayerDB(TypedObject, AbstractUser): + """ + This is a special model using Django's 'profile' functionality + and extends the default Django User model. It is defined as such + by use of the variable AUTH_PROFILE_MODULE in the settings. + One accesses the fields/methods. We try use this model as much + as possible rather than User, since we can customize this to + our liking. + + The TypedObject supplies the following (inherited) properties: + key - main name + typeclass_path - the path to the decorating typeclass + typeclass - auto-linked typeclass + date_created - time stamp of object creation + permissions - perm strings + dbref - #id of object + db - persistent attribute storage + ndb - non-persistent attribute storage + + The PlayerDB adds the following properties: + user - Connected User object. django field, needs to be save():d. + name - alias for user.username + sessions - sessions connected to this player + is_superuser - bool if this player is a superuser + is_bot - bool if this player is a bot and not a real player + + """ + + # + # PlayerDB Database model setup + # + # inherited fields (from TypedObject): + # db_key, db_typeclass_path, db_date_created, db_permissions + + # store a connected flag here too, not just in sessionhandler. + # This makes it easier to track from various out-of-process locations + db_is_connected = models.BooleanField(default=False, + verbose_name="is_connected", + help_text="If player is connected to game or not") + # database storage of persistant cmdsets. + db_cmdset_storage = models.CharField('cmdset', max_length=255, null=True, + help_text="optional python path to a cmdset class. If creating a Character, this will default to settings.CMDSET_CHARACTER.") + # marks if this is a "virtual" bot player object + db_is_bot = models.BooleanField(default=False, verbose_name="is_bot", help_text="Used to identify irc/imc2/rss bots") + + # Database manager + objects = PlayerDBManager() + + class Meta: + app_label = 'players' + verbose_name = 'Player' + + # alias to the objs property + def __characters_get(self): + return self.objs + + def __characters_set(self, value): + self.objs = value + + def __characters_del(self): + raise Exception("Cannot delete name") + characters = property(__characters_get, __characters_set, __characters_del) + + # cmdset_storage property + # This seems very sensitive to caching, so leaving it be for now /Griatch + #@property + def cmdset_storage_get(self): + """ + Getter. Allows for value = self.name. Returns a list of cmdset_storage. + """ + storage = self.db_cmdset_storage + # we need to check so storage is not None + return [path.strip() for path in storage.split(',')] if storage else [] + + #@cmdset_storage.setter + def cmdset_storage_set(self, value): + """ + Setter. Allows for self.name = value. Stores as a comma-separated + string. + """ + _SA(self, "db_cmdset_storage", ",".join(str(val).strip() for val in make_iter(value))) + _GA(self, "save")() + + #@cmdset_storage.deleter + def cmdset_storage_del(self): + "Deleter. Allows for del self.name" + _SA(self, "db_cmdset_storage", None) + _GA(self, "save")() + cmdset_storage = property(cmdset_storage_get, cmdset_storage_set, cmdset_storage_del) + + # + # property/field access + # + + def __str__(self): + return smart_str("%s(player %s)" % (self.name, self.dbid)) + + def __unicode__(self): + return u"%s(player#%s)" % (self.name, self.dbid) + + #@property + def __username_get(self): + return self.username + + def __username_set(self, value): + self.username = value + self.save(update_fields=["username"]) + + def __username_del(self): + del self.username + + # aliases + name = property(__username_get, __username_set, __username_del) + key = property(__username_get, __username_set, __username_del) + + #@property + def __uid_get(self): + "Getter. Retrieves the user id" + return self.id + + def __uid_set(self, value): + raise Exception("User id cannot be set!") + + def __uid_del(self): + raise Exception("User id cannot be deleted!") + uid = property(__uid_get, __uid_set, __uid_del) diff --git a/evennia/players/players.py b/evennia/players/players.py new file mode 100644 index 000000000..30e525c68 --- /dev/null +++ b/evennia/players/players.py @@ -0,0 +1,734 @@ +""" +Typeclass for Player objects + +Note that this object is primarily intended to +store OOC information, not game info! This +object represents the actual user (not their +character) and has NO actual precence in the +game world (this is handled by the associated +character object, so you should customize that +instead for most things). + +""" + +import datetime +from django.conf import settings +from evennia.typeclasses.models import TypeclassBase +from evennia.players.manager import PlayerManager +from evennia.players.models import PlayerDB +from evennia.comms.models import ChannelDB +from evennia.commands import cmdhandler +from evennia.utils import logger +from evennia.utils.utils import (lazy_property, to_str, + make_iter, to_unicode, + variable_from_module) +from evennia.typeclasses.attributes import NickHandler +from evennia.scripts.scripthandler import ScriptHandler +from evennia.commands.cmdsethandler import CmdSetHandler + +from django.utils.translation import ugettext as _ + +__all__ = ("DefaultPlayer",) + +_SESSIONS = None + +_AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1)) +_MULTISESSION_MODE = settings.MULTISESSION_MODE +_CMDSET_PLAYER = settings.CMDSET_PLAYER +_CONNECT_CHANNEL = None + +class DefaultPlayer(PlayerDB): + """ + This is the base Typeclass for all Players. Players represent + the person playing the game and tracks account info, password + etc. They are OOC entities without presence in-game. A Player + can connect to a Character Object in order to "enter" the + game. + + Player Typeclass API: + + * Available properties (only available on initiated typeclass objects) + + key (string) - name of player + name (string)- wrapper for user.username + aliases (list of strings) - aliases to the object. Will be saved to + database as AliasDB entries but returned as strings. + dbref (int, read-only) - unique #id-number. Also "id" can be used. + date_created (string) - time stamp of object creation + permissions (list of strings) - list of permission strings + + user (User, read-only) - django User authorization object + obj (Object) - game object controlled by player. 'character' can also + be used. + sessions (list of Sessions) - sessions connected to this player + is_superuser (bool, read-only) - if the connected user is a superuser + + * Handlers + + locks - lock-handler: use locks.add() to add new lock strings + db - attribute-handler: store/retrieve database attributes on this + self.db.myattr=val, val=self.db.myattr + ndb - non-persistent attribute handler: same as db but does not + create a database entry when storing data + scripts - script-handler. Add new scripts to object with scripts.add() + cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object + nicks - nick-handler. New nicks with nicks.add(). + + * Helper methods + + msg(outgoing_string, from_obj=None, **kwargs) + #swap_character(new_character, delete_old_character=False) + execute_cmd(raw_string) + search(ostring, global_search=False, attribute_name=None, + use_nicks=False, location=None, + ignore_errors=False, player=False) + is_typeclass(typeclass, exact=False) + swap_typeclass(new_typeclass, clean_attributes=False, no_default=True) + access(accessing_obj, access_type='read', default=False) + check_permstring(permstring) + + * Hook methods + + basetype_setup() + at_player_creation() + + - note that the following hooks are also found on Objects and are + usually handled on the character level: + + at_init() + at_access() + at_cmdset_get(**kwargs) + at_first_login() + at_post_login(sessid=None) + at_disconnect() + at_message_receive() + at_message_send() + at_server_reload() + at_server_shutdown() + + """ + + __metaclass__ = TypeclassBase + objects = PlayerManager() + + # properties + @lazy_property + def cmdset(self): + return CmdSetHandler(self, True) + + @lazy_property + def scripts(self): + return ScriptHandler(self) + + @lazy_property + def nicks(self): + return NickHandler(self) + + + # session-related methods + + def get_session(self, sessid): + """ + Return session with given sessid connected to this player. + note that the sessionhandler also accepts sessid as an iterable. + """ + global _SESSIONS + if not _SESSIONS: + from evennia.server.sessionhandler import SESSIONS as _SESSIONS + return _SESSIONS.session_from_player(self, sessid) + + def get_all_sessions(self): + "Return all sessions connected to this player" + global _SESSIONS + if not _SESSIONS: + from evennia.server.sessionhandler import SESSIONS as _SESSIONS + return _SESSIONS.sessions_from_player(self) + sessions = property(get_all_sessions) # alias shortcut + + def disconnect_session_from_player(self, sessid): + """ + Access method for disconnecting a given session from the player + (connection happens automatically in the sessionhandler) + """ + # this should only be one value, loop just to make sure to + # clean everything + sessions = (session for session in self.get_all_sessions() + if session.sessid == sessid) + for session in sessions: + # this will also trigger unpuppeting + session.sessionhandler.disconnect(session) + + # puppeting operations + + def puppet_object(self, sessid, obj): + """ + Use the given session to control (puppet) the given object (usually + a Character type). + + Args: + sessid (int): session id of session to connect + obj (Object): the object to start puppeting + + Raises: + RuntimeError with message if puppeting is not possible + + returns True if successful, False otherwise + """ + # safety checks + if not obj: + raise RuntimeError("Object not found") + session = self.get_session(sessid) + if not session: + raise RuntimeError("Session not found") + if self.get_puppet(sessid) == obj: + # already puppeting this object + raise RuntimeError("You are already puppeting this object.") + if not obj.access(self, 'puppet'): + # no access + raise RuntimeError("You don't have permission to puppet '%s'." % obj.key) + if obj.player: + # object already puppeted + if obj.player == self: + if obj.sessid.count(): + # we may take over another of our sessions + # output messages to the affected sessions + if _MULTISESSION_MODE in (1, 3): + txt1 = "{c%s{n{G is now shared from another of your sessions.{n" + txt2 = "Sharing {c%s{n with another of your sessions." + else: + txt1 = "{c%s{n{R is now acted from another of your sessions.{n" + txt2 = "Taking over {c%s{n from another of your sessions." + self.unpuppet_object(obj.sessid.get()) + self.msg(txt1 % obj.name, sessid=obj.sessid.get(), _forced_nomulti=True) + self.msg(txt2 % obj.name, sessid=sessid, _forced_nomulti=True) + elif obj.player.is_connected: + # controlled by another player + raise RuntimeError("{R{c%s{R is already puppeted by another Player.") + + # do the puppeting + if session.puppet: + # cleanly unpuppet eventual previous object puppeted by this session + self.unpuppet_object(sessid) + # if we get to this point the character is ready to puppet or it + # was left with a lingering player/sessid reference from an unclean + # server kill or similar + + obj.at_pre_puppet(self, sessid=sessid) + + # do the connection + obj.sessid.add(sessid) + obj.player = self + session.puid = obj.id + session.puppet = obj + # validate/start persistent scripts on object + obj.scripts.validate() + + obj.at_post_puppet() + + # re-cache locks to make sure superuser bypass is updated + obj.locks.cache_lock_bypass(obj) + + def unpuppet_object(self, sessid): + """ + Disengage control over an object + + Args: + sessid(int): the session id to disengage + + Raises: + RuntimeError with message about error. + """ + if _MULTISESSION_MODE == 1: + sessions = self.get_all_sessions() + else: + sessions = self.get_session(sessid) + if not sessions: + raise RuntimeError("No session was found.") + for session in make_iter(sessions): + obj = session.puppet or None + if not obj: + raise RuntimeError("No puppet was found to disconnect from.") + elif obj: + # do the disconnect, but only if we are the last session to puppet + obj.at_pre_unpuppet() + obj.sessid.remove(session.sessid) + if not obj.sessid.count(): + del obj.player + obj.at_post_unpuppet(self, sessid=sessid) + # Just to be sure we're always clear. + session.puppet = None + session.puid = None + + def unpuppet_all(self): + """ + Disconnect all puppets. This is called by server + before a reset/shutdown. + """ + for session in (sess for sess in self.get_all_sessions() if sess.puppet): + self.unpuppet_object(session.sessid) + + def get_puppet(self, sessid, return_dbobj=False): + """ + Get an object puppeted by this session through this player. This is + the main method for retrieving the puppeted object from the + player's end. + + sessid - return character connected to this sessid, + + """ + session = self.get_session(sessid) + if not session: + return None + if return_dbobj: + return session.puppet + return session.puppet and session.puppet or None + + def get_all_puppets(self, return_dbobj=False): + """ + Get all currently puppeted objects as a list + """ + puppets = [session.puppet for session in self.get_all_sessions() + if session.puppet] + if return_dbobj: + return puppets + return [puppet for puppet in puppets] + + def __get_single_puppet(self): + """ + This is a legacy convenience link for users of + MULTISESSION_MODE 0 or 1. It will return + only the first puppet. For mode 2, this returns + a list of all characters. + """ + puppets = self.get_all_puppets() + if _MULTISESSION_MODE in (0, 1): + return puppets and puppets[0] or None + return puppets + character = property(__get_single_puppet) + puppet = property(__get_single_puppet) + + # utility methods + + def delete(self, *args, **kwargs): + """ + Deletes the player permanently. + """ + for session in self.get_all_sessions(): + # unpuppeting all objects and disconnecting the user, if any + # sessions remain (should usually be handled from the + # deleting command) + self.unpuppet_object(session.sessid) + session.sessionhandler.disconnect(session, reason=_("Player being deleted.")) + self.scripts.stop() + self.attributes.clear() + self.nicks.clear() + self.aliases.clear() + super(PlayerDB, self).delete(*args, **kwargs) + ## methods inherited from database model + + def msg(self, text=None, from_obj=None, sessid=None, **kwargs): + """ + Evennia -> User + This is the main route for sending data back to the user from the + server. + + Args: + text (str, optional): text data to send + from_obj (Object or Player, optional): object sending. If given, + its at_msg_send() hook will be called. + sessid (int or list, optional): session id or ids to receive this + send. If given, overrules MULTISESSION_MODE. + Notes: + All other keywords are passed on to the protocol. + """ + text = to_str(text, force_string=True) if text else "" + if from_obj: + # call hook + try: + from_obj.at_msg_send(text=text, to_obj=self, **kwargs) + except Exception: + pass + + # session relay + + if sessid: + # this could still be an iterable if sessid is an iterable + sessions = self.get_session(sessid) + if sessions: + # this is a special instruction to ignore MULTISESSION_MODE + # and only relay to this given session. + kwargs["_nomulti"] = True + for session in make_iter(sessions): + session.msg(text=text, **kwargs) + return + # we only send to the first of any connected sessions - the sessionhandler + # will disperse this to the other sessions based on MULTISESSION_MODE. + sessions = self.get_all_sessions() + if sessions: + sessions[0].msg(text=text, **kwargs) + + def execute_cmd(self, raw_string, sessid=None, **kwargs): + """ + Do something as this player. This method is never called normally, + but only when the player object itself is supposed to execute the + command. It takes player nicks into account, but not nicks of + eventual puppets. + + raw_string - raw command input coming from the command line. + sessid - the optional session id to be responsible for the command-send + **kwargs - other keyword arguments will be added to the found command + object instace as variables before it executes. This is + unused by default Evennia but may be used to set flags and + change operating paramaters for commands at run-time. + """ + raw_string = to_unicode(raw_string) + raw_string = self.nicks.nickreplace(raw_string, + categories=("inputline", "channel"), include_player=False) + if not sessid and _MULTISESSION_MODE in (0, 1): + # in this case, we should either have only one sessid, or the sessid + # should not matter (since the return goes to all of them we can + # just use the first one as the source) + try: + sessid = self.get_all_sessions()[0].sessid + except IndexError: + # this can happen for bots + sessid = None + return cmdhandler.cmdhandler(self, raw_string, + callertype="player", sessid=sessid, **kwargs) + + def search(self, searchdata, return_puppet=False, + nofound_string=None, multimatch_string=None, **kwargs): + """ + This is similar to the ObjectDB search method but will search for + Players only. Errors will be echoed, and None returned if no Player + is found. + searchdata - search criterion, the Player's key or dbref to search for + return_puppet - will try to return the object the player controls + instead of the Player object itself. If no + puppeted object exists (since Player is OOC), None will + be returned. + nofound_string - optional custom string for not-found error message. + multimatch_string - optional custom string for multimatch error header. + Extra keywords are ignored, but are allowed in call in order to make + API more consistent with objects.models.TypedObject.search. + """ + # handle me, self and *me, *self + if isinstance(searchdata, basestring): + # handle wrapping of common terms + if searchdata.lower() in ("me", "*me", "self", "*self",): + return self + matches = self.__class__.objects.player_search(searchdata) + matches = _AT_SEARCH_RESULT(self, searchdata, matches, global_search=True, + nofound_string=nofound_string, + multimatch_string=multimatch_string) + if matches and return_puppet: + try: + return matches.puppet + except AttributeError: + return None + return matches + + def is_typeclass(self, typeclass, exact=False): + """ + Returns true if this object has this type + OR has a typeclass which is an subclass of + the given typeclass. + + typeclass - can be a class object or the + python path to such an object to match against. + + exact - returns true only if the object's + type is exactly this typeclass, ignoring + parents. + + Returns: Boolean + """ + return super(DefaultPlayer, self).is_typeclass(typeclass, exact=exact) + + def swap_typeclass(self, new_typeclass, clean_attributes=False, no_default=True): + """ + This performs an in-situ swap of the typeclass. This means + that in-game, this object will suddenly be something else. + Player will not be affected. To 'move' a player to a different + object entirely (while retaining this object's type), use + self.player.swap_object(). + + Note that this might be an error prone operation if the + old/new typeclass was heavily customized - your code + might expect one and not the other, so be careful to + bug test your code if using this feature! Often its easiest + to create a new object and just swap the player over to + that one instead. + + Arguments: + new_typeclass (path/classobj) - type to switch to + clean_attributes (bool/list) - will delete all attributes + stored on this object (but not any + of the database fields such as name or + location). You can't get attributes back, + but this is often the safest bet to make + sure nothing in the new typeclass clashes + with the old one. If you supply a list, + only those named attributes will be cleared. + no_default - if this is active, the swapper will not allow for + swapping to a default typeclass in case the given + one fails for some reason. Instead the old one + will be preserved. + Returns: + boolean True/False depending on if the swap worked or not. + + """ + super(DefaultPlayer, self).swap_typeclass(new_typeclass, + clean_attributes=clean_attributes, no_default=no_default) + + def access(self, accessing_obj, access_type='read', default=False, **kwargs): + """ + Determines if another object has permission to access this object + in whatever way. + + accessing_obj (Object)- object trying to access this one + access_type (string) - type of access sought + default (bool) - what to return if no lock of access_type was found + **kwargs - passed to the at_access hook along with the result. + """ + result = super(DefaultPlayer, self).access(accessing_obj, access_type=access_type, default=default) + self.at_access(result, accessing_obj, access_type, **kwargs) + return result + + def check_permstring(self, permstring): + """ + This explicitly checks the given string against this object's + 'permissions' property without involving any locks. + + permstring (string) - permission string that need to match a permission + on the object. (example: 'Builders') + Note that this method does -not- call the at_access hook. + """ + return super(DefaultPlayer, self).check_permstring(permstring) + + ## player hooks + + def basetype_setup(self): + """ + This sets up the basic properties for a player. + Overload this with at_player_creation rather than + changing this method. + + """ + # A basic security setup + lockstring = "examine:perm(Wizards);edit:perm(Wizards);delete:perm(Wizards);boot:perm(Wizards);msg:all()" + self.locks.add(lockstring) + + # The ooc player cmdset + self.cmdset.add_default(_CMDSET_PLAYER, permanent=True) + + def at_player_creation(self): + """ + This is called once, the very first time + the player is created (i.e. first time they + register with the game). It's a good place + to store attributes all players should have, + like configuration values etc. + """ + # set an (empty) attribute holding the characters this player has + lockstring = "attrread:perm(Admins);attredit:perm(Admins);attrcreate:perm(Admins)" + self.attributes.add("_playable_characters", [], lockstring=lockstring) + + def at_init(self): + """ + This is always called whenever this object is initiated -- + that is, whenever it its typeclass is cached from memory. This + happens on-demand first time the object is used or activated + in some way after being created but also after each server + restart or reload. In the case of player objects, this usually + happens the moment the player logs in or reconnects after a + reload. + """ + pass + + + # Note that the hooks below also exist in the character object's + # typeclass. You can often ignore these and rely on the character + # ones instead, unless you are implementing a multi-character game + # and have some things that should be done regardless of which + # character is currently connected to this player. + + def at_first_save(self): + """ + This is a generic hook called by Evennia when this object is + saved to the database the very first time. You generally + don't override this method but the hooks called by it. + """ + self.basetype_setup() + self.at_player_creation() + + permissions = settings.PERMISSION_PLAYER_DEFAULT + if hasattr(self, "_createdict"): + # this will only be set if the utils.create_player + # function was used to create the object. + cdict = self._createdict + if cdict.get("locks"): + self.locks.add(cdict["locks"]) + if cdict.get("permissions"): + permissions = cdict["permissions"] + del self._createdict + + self.permissions.add(permissions) + + def at_access(self, result, accessing_obj, access_type, **kwargs): + """ + This is called with the result of an access call, along with + any kwargs used for that call. The return of this method does + not affect the result of the lock check. It can be used e.g. to + customize error messages in a central location or other effects + based on the access result. + """ + pass + + def at_cmdset_get(self, **kwargs): + """ + Called just before cmdsets on this player are requested by the + command handler. If changes need to be done on the fly to the + cmdset before passing them on to the cmdhandler, this is the + place to do it. This is called also if the player currently + have no cmdsets. kwargs are usually not used unless the + cmdset is generated dynamically. + """ + pass + + def at_first_login(self): + """ + Called the very first time this player logs into the game. + """ + pass + + def at_pre_login(self): + """ + Called every time the user logs in, just before the actual + login-state is set. + """ + pass + + def _send_to_connect_channel(self, message): + "Helper method for loading the default comm channel" + global _CONNECT_CHANNEL + if not _CONNECT_CHANNEL: + try: + _CONNECT_CHANNEL = ChannelDB.objects.filter(db_key=settings.DEFAULT_CHANNELS[1]["key"])[0] + except Exception: + logger.log_trace() + now = datetime.datetime.now() + now = "%02i-%02i-%02i(%02i:%02i)" % (now.year, now.month, + now.day, now.hour, now.minute) + if _CONNECT_CHANNEL: + _CONNECT_CHANNEL.tempmsg("[%s, %s]: %s" % (_CONNECT_CHANNEL.key, now, message)) + else: + logger.log_infomsg("[%s]: %s" % (now, message)) + + def at_post_login(self, sessid=None): + """ + Called at the end of the login process, just before letting + the player loose. This is called before an eventual Character's + at_post_login hook. + """ + self._send_to_connect_channel("{G%s connected{n" % self.key) + if _MULTISESSION_MODE == 0: + # in this mode we should have only one character available. We + # try to auto-connect to our last conneted object, if any + self.puppet_object(sessid, self.db._last_puppet) + elif _MULTISESSION_MODE == 1: + # in this mode all sessions connect to the same puppet. + self.puppet_object(sessid, self.db._last_puppet) + elif _MULTISESSION_MODE in (2, 3): + # In this mode we by default end up at a character selection + # screen. We execute look on the player. + self.execute_cmd("look", sessid=sessid) + + def at_disconnect(self, reason=None): + """ + Called just before user is disconnected. + """ + reason = reason and "(%s)" % reason or "" + self._send_to_connect_channel("{R%s disconnected %s{n" % (self.key, reason)) + + def at_post_disconnect(self): + """ + This is called after disconnection is complete. No messages + can be relayed to the player from here. After this call, the + player should not be accessed any more, making this a good + spot for deleting it (in the case of a guest player account, + for example). + """ + pass + + def at_message_receive(self, message, from_obj=None): + """ + Called when any text is emitted to this + object. If it returns False, no text + will be sent automatically. + """ + return True + + def at_message_send(self, message, to_object): + """ + Called whenever this object tries to send text + to another object. Only called if the object supplied + itself as a sender in the msg() call. + """ + pass + + def at_server_reload(self): + """ + This hook is called whenever the server is shutting down for + restart/reboot. If you want to, for example, save non-persistent + properties across a restart, this is the place to do it. + """ + pass + + def at_server_shutdown(self): + """ + This hook is called whenever the server is shutting down fully + (i.e. not for a restart). + """ + pass + + +class DefaultGuest(DefaultPlayer): + """ + This class is used for guest logins. Unlike Players, Guests and their + characters are deleted after disconnection. + """ + def at_post_login(self, sessid=None): + """ + In theory, guests only have one character regardless of which + MULTISESSION_MODE we're in. They don't get a choice. + """ + self._send_to_connect_channel("{G%s connected{n" % self.key) + self._go_ic_at_login(sessid=sessid) + + def at_disconnect(self): + """ + A Guest's characters aren't meant to linger on the server. When a + Guest disconnects, we remove its character. + """ + super(DefaultGuest, self).at_disconnect() + characters = self.db._playable_characters + for character in filter(None, characters): + character.delete() + + def at_server_shutdown(self): + """ + We repeat at_disconnect() here just to be on the safe side. + """ + super(DefaultGuest, self).at_server_shutdown() + characters = self.db._playable_characters + for character in filter(None, characters): + character.delete() + + def at_post_disconnect(self): + """ + Guests aren't meant to linger on the server, either. We need to wait + until after the Guest disconnects to delete it, though. + """ + super(DefaultGuest, self).at_post_disconnect() + self.delete() diff --git a/evennia/scripts/__init__.py b/evennia/scripts/__init__.py new file mode 100644 index 000000000..46f4e0c3b --- /dev/null +++ b/evennia/scripts/__init__.py @@ -0,0 +1,8 @@ +""" +This sub-package holds the Scripts system. Scripts are database +entities that can store data both in connection to Objects and Players +or globally. They may also have a timer-component to execute various +timed effects. + +""" +from scripts import DefaultScript diff --git a/src/scripts/admin.py b/evennia/scripts/admin.py similarity index 90% rename from src/scripts/admin.py rename to evennia/scripts/admin.py index 76e2562d8..6ea87a07e 100644 --- a/src/scripts/admin.py +++ b/evennia/scripts/admin.py @@ -2,9 +2,9 @@ # This sets up how models are displayed # in the web admin interface. # -from src.typeclasses.admin import AttributeInline, TagInline +from evennia.typeclasses.admin import AttributeInline, TagInline -from src.scripts.models import ScriptDB +from evennia.scripts.models import ScriptDB from django.contrib import admin diff --git a/src/scripts/manager.py b/evennia/scripts/manager.py similarity index 94% rename from src/scripts/manager.py rename to evennia/scripts/manager.py index 1dc4b5f63..5d2ee432f 100644 --- a/src/scripts/manager.py +++ b/evennia/scripts/manager.py @@ -3,16 +3,16 @@ The custom manager for Scripts. """ from django.db.models import Q -from src.typeclasses.managers import TypedObjectManager -from src.typeclasses.managers import returns_typeclass_list -from src.utils.utils import make_iter +from evennia.typeclasses.managers import TypedObjectManager, TypeclassManager +from evennia.typeclasses.managers import returns_typeclass_list +from evennia.utils.utils import make_iter __all__ = ("ScriptManager",) _GA = object.__getattribute__ VALIDATE_ITERATION = 0 -class ScriptManager(TypedObjectManager): +class ScriptDBManager(TypedObjectManager): """ This Scriptmanager implements methods for searching and manipulating Scripts directly from the database. @@ -31,7 +31,7 @@ class ScriptManager(TypedObjectManager): delete_script remove_non_persistent validate - script_search (equivalent to ev.search_script) + script_search (equivalent to evennia.search_script) copy_script """ @@ -44,7 +44,6 @@ class ScriptManager(TypedObjectManager): """ if not obj: return [] - obj = obj.dbobj player = _GA(_GA(obj, "__class__"), "__name__") == "PlayerDB" if key: dbref = self.dbref(key) @@ -161,7 +160,7 @@ class ScriptManager(TypedObjectManager): # turn off the activity flag for all remaining scripts scripts = self.get_all_scripts() for script in scripts: - script.dbobj.is_active = False + script.is_active = False elif not scripts: # normal operation @@ -180,7 +179,7 @@ class ScriptManager(TypedObjectManager): #print "scripts to validate: [%s]" % (", ".join(script.key for script in scripts)) for script in scripts: - #print "validating %s (%i) (init_mode=%s)" % (script.key, id(script.dbobj), init_mode) + #print "validating %s (%i) (init_mode=%s)" % (script.key, id(script), init_mode) if script.is_valid(): nr_started += script.start(force_restart=init_mode) #print "back from start. nr_started=", nr_started @@ -212,7 +211,7 @@ class ScriptManager(TypedObjectManager): return [dbref_match] # not a dbref; normal search - obj_restriction = obj and Q(db_obj=obj.dbobj) or Q() + obj_restriction = obj and Q(db_obj=obj) or Q() timed_restriction = only_timed and Q(interval__gt=0) or Q() scripts = self.filter(timed_restriction & obj_restriction & Q(db_key__iexact=ostring)) return scripts @@ -226,7 +225,10 @@ class ScriptManager(TypedObjectManager): new_obj = new_obj if new_obj is not None else original_script.obj new_locks = new_locks if new_locks is not None else original_script.db_lock_storage - from src.utils import create + from evennia.utils import create new_script = create.create_script(typeclass, key=new_key, obj=new_obj, locks=new_locks, autostart=True) return new_script + +class ScriptManager(ScriptDBManager, TypeclassManager): + pass diff --git a/src/scripts/migrations/0001_initial.py b/evennia/scripts/migrations/0001_initial.py similarity index 100% rename from src/scripts/migrations/0001_initial.py rename to evennia/scripts/migrations/0001_initial.py diff --git a/evennia/scripts/migrations/0002_auto_20150118_1625.py b/evennia/scripts/migrations/0002_auto_20150118_1625.py new file mode 100644 index 000000000..3d9c550c9 --- /dev/null +++ b/evennia/scripts/migrations/0002_auto_20150118_1625.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + +def convert_defaults(apps, schema_editor): + ScriptDB = apps.get_model("scripts", "ScriptDB") + for script in ScriptDB.objects.filter(db_typeclass_path="src.scripts.scripts.Script"): + script.db_typeclass_path = "typeclasses.scripts.Script" + script.save() + for script in ScriptDB.objects.filter(db_typeclass_path="src.utils.gametime.GameTime"): + script.db_typeclass_path = "evennia.utils.gametime.GameTime" + script.save() + +class Migration(migrations.Migration): + + dependencies = [ + ('scripts', '0001_initial'), + ] + + operations = [ + migrations.RunPython(convert_defaults), + ] diff --git a/evennia/scripts/migrations/0003_checksessions_defaultscript_donothing_scriptbase_store_validatechannelhandler_validateidmappercache_.py b/evennia/scripts/migrations/0003_checksessions_defaultscript_donothing_scriptbase_store_validatechannelhandler_validateidmappercache_.py new file mode 100644 index 000000000..baa3b0d2e --- /dev/null +++ b/evennia/scripts/migrations/0003_checksessions_defaultscript_donothing_scriptbase_store_validatechannelhandler_validateidmappercache_.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('scripts', '0002_auto_20150118_1625'), + ] + + operations = [ + migrations.CreateModel( + name='ScriptBase', + fields=[ + ], + options={ + 'proxy': True, + }, + bases=('scripts.scriptdb',), + ), + migrations.CreateModel( + name='DefaultScript', + fields=[ + ], + options={ + 'proxy': True, + }, + bases=('scripts.scriptbase',), + ), + migrations.CreateModel( + name='DoNothing', + fields=[ + ], + options={ + 'proxy': True, + }, + bases=('scripts.defaultscript',), + ), + migrations.CreateModel( + name='CheckSessions', + fields=[ + ], + options={ + 'proxy': True, + }, + bases=('scripts.defaultscript',), + ), + migrations.CreateModel( + name='Store', + fields=[ + ], + options={ + 'proxy': True, + }, + bases=('scripts.defaultscript',), + ), + migrations.CreateModel( + name='ValidateChannelHandler', + fields=[ + ], + options={ + 'proxy': True, + }, + bases=('scripts.defaultscript',), + ), + migrations.CreateModel( + name='ValidateIdmapperCache', + fields=[ + ], + options={ + 'proxy': True, + }, + bases=('scripts.defaultscript',), + ), + migrations.CreateModel( + name='ValidateScripts', + fields=[ + ], + options={ + 'proxy': True, + }, + bases=('scripts.defaultscript',), + ), + ] diff --git a/game/gamesrc/commands/__init__.py b/evennia/scripts/migrations/__init__.py similarity index 100% rename from game/gamesrc/commands/__init__.py rename to evennia/scripts/migrations/__init__.py diff --git a/src/scripts/models.py b/evennia/scripts/models.py similarity index 81% rename from src/scripts/models.py rename to evennia/scripts/models.py index 45347cff9..3dcfc9589 100644 --- a/src/scripts/models.py +++ b/evennia/scripts/models.py @@ -27,9 +27,9 @@ Common examples of uses of Scripts: from django.conf import settings from django.db import models from django.core.exceptions import ObjectDoesNotExist -from src.typeclasses.models import TypedObject -from src.scripts.manager import ScriptManager -from src.utils.utils import dbref, to_str +from evennia.typeclasses.models import TypedObject +from evennia.scripts.manager import ScriptDBManager +from evennia.utils.utils import dbref, to_str __all__ = ("ScriptDB",) _GA = object.__getattribute__ @@ -73,7 +73,7 @@ class ScriptDB(TypedObject): # # ScriptDB Database Model setup # - # These databse fields are all set using their corresponding properties, + # These database fields are all set using their corresponding properties, # named same as the field, but withtou the db_* prefix. # inherited fields (from TypedObject): @@ -98,11 +98,7 @@ class ScriptDB(TypedObject): db_is_active = models.BooleanField('script active', default=False) # Database manager - objects = ScriptManager() - - # caches for quick lookups - _typeclass_paths = settings.SCRIPT_TYPECLASS_PATHS - _default_typeclass_path = settings.BASE_SCRIPT_TYPECLASS or "src.scripts.scripts.DoNothing" + objects = ScriptDBManager() class Meta: "Define Django meta options" @@ -124,8 +120,7 @@ class ScriptDB(TypedObject): obj = _GA(self, "db_player") if not obj: obj = _GA(self, "db_obj") - if obj: - return obj.typeclass + return obj def __set_obj(self, value): """ @@ -137,7 +132,7 @@ class ScriptDB(TypedObject): except AttributeError: pass if isinstance(value, (basestring, int)): - from src.objects.models import ObjectDB + from evennia.objects.models import ObjectDB value = to_str(value, force_string=True) if (value.isdigit() or value.startswith("#")): dbid = dbref(value, reqhash=False) @@ -159,23 +154,23 @@ class ScriptDB(TypedObject): object = property(__get_obj, __set_obj) - def at_typeclass_error(self): - """ - If this is called, it means the typeclass has a critical - error and cannot even be loaded. We don't allow a script - to be created under those circumstances. Already created, - permanent scripts are set to already be active so they - won't get activated now (next reboot the bug might be fixed) - """ - # By setting is_active=True, we trick the script not to run "again". - self.is_active = True - return super(ScriptDB, self).at_typeclass_error() - - delete_iter = 0 - def delete(self): - "Delete script" - if self.delete_iter > 0: - return - self.delete_iter += 1 - _GA(self, "attributes").clear() - super(ScriptDB, self).delete() +# def at_typeclass_error(self): +# """ +# If this is called, it means the typeclass has a critical +# error and cannot even be loaded. We don't allow a script +# to be created under those circumstances. Already created, +# permanent scripts are set to already be active so they +# won't get activated now (next reboot the bug might be fixed) +# """ +# # By setting is_active=True, we trick the script not to run "again". +# self.is_active = True +# return super(ScriptDB, self).at_typeclass_error() +# +# delete_iter = 0 +# def delete(self): +# "Delete script" +# if self.delete_iter > 0: +# return +# self.delete_iter += 1 +# _GA(self, "attributes").clear() +# super(ScriptDB, self).delete() diff --git a/src/scripts/scripthandler.py b/evennia/scripts/scripthandler.py similarity index 96% rename from src/scripts/scripthandler.py rename to evennia/scripts/scripthandler.py index 796d9564e..3eda89d96 100644 --- a/src/scripts/scripthandler.py +++ b/evennia/scripts/scripthandler.py @@ -5,9 +5,9 @@ An scripthandler is automatically added to all game objects. You access it through the property 'scripts' on the game object. """ -from src.scripts.models import ScriptDB -from src.utils import create -from src.utils import logger +from evennia.scripts.models import ScriptDB +from evennia.utils import create +from evennia.utils import logger from django.utils.translation import ugettext as _ @@ -58,7 +58,7 @@ class ScriptHandler(object): definition) autostart - start the script upon adding it """ - if self.obj.dbobj.__class__.__name__ == "PlayerDB": + if self.obj.__class__.__name__ == "PlayerDB": # we add to a Player, not an Object script = create.create_script(scriptclass, key=key, player=self.obj, autostart=autostart) diff --git a/src/scripts/scripts.py b/evennia/scripts/scripts.py similarity index 64% rename from src/scripts/scripts.py rename to evennia/scripts/scripts.py index f7749ffa1..50633fca0 100644 --- a/src/scripts/scripts.py +++ b/evennia/scripts/scripts.py @@ -9,12 +9,13 @@ from twisted.internet.defer import Deferred, maybeDeferred from twisted.internet.task import LoopingCall from django.conf import settings from django.utils.translation import ugettext as _ -from src.typeclasses.typeclass import TypeClass -from src.scripts.models import ScriptDB -from src.comms import channelhandler -from src.utils import logger +from evennia.typeclasses.models import TypeclassBase +from evennia.scripts.models import ScriptDB +from evennia.scripts.manager import ScriptManager +from evennia.comms import channelhandler +from evennia.utils import logger -__all__ = ["Script", "DoNothing", "CheckSessions", +__all__ = ["DefaultScript", "DoNothing", "CheckSessions", "ValidateScripts", "ValidateChannelHandler"] _GA = object.__getattribute__ @@ -106,14 +107,15 @@ class ExtendedLoopingCall(LoopingCall): return None # -# Base script, inherit from Script below instead. +# Base script, inherit from DefaultScript below instead. # -class ScriptBase(TypeClass): +class ScriptBase(ScriptDB): """ Base class for scripts. Don't inherit from this, inherit from the class 'Script' instead. """ - # private methods + __metaclass__ = TypeclassBase + objects = ScriptManager() def __eq__(self, other): """ @@ -133,7 +135,7 @@ class ScriptBase(TypeClass): if self.db._paused_time: # the script was paused; restarting callcount = self.db._paused_callcount or 0 - self.ndb._task.start(self.dbobj.db_interval, + self.ndb._task.start(self.db_interval, now=False, start_delay=self.db._paused_time, count_start=callcount) @@ -141,8 +143,8 @@ class ScriptBase(TypeClass): del self.db._paused_repeats else: # starting script anew - self.ndb._task.start(self.dbobj.db_interval, - now=not self.dbobj.db_start_delay) + self.ndb._task.start(self.db_interval, + now=not self.db_start_delay) def _stop_task(self): "stop task runner" @@ -157,7 +159,7 @@ class ScriptBase(TypeClass): {"key": self.key, "dbid": self.dbid, "cname": cname, "err": e.getErrorMessage()} try: - self.dbobj.db_obj.msg(estring) + self.db_obj.msg(estring) except Exception: pass logger.log_errmsg(estring) @@ -174,7 +176,7 @@ class ScriptBase(TypeClass): # check repeats callcount = self.ndb._task.callcount - maxcount = self.dbobj.db_repeats + maxcount = self.db_repeats if maxcount > 0 and maxcount <= callcount: #print "stopping script!" self.stop() @@ -208,7 +210,7 @@ class ScriptBase(TypeClass): "Get the number of returning repeats. Returns None if unlimited repeats." task = self.ndb._task if task: - return max(0, self.dbobj.db_repeats - task.callcount) + return max(0, self.db_repeats - task.callcount) def start(self, force_restart=False): """ @@ -222,10 +224,7 @@ class ScriptBase(TypeClass): Used in counting. """ - #print "Script %s (%s) start (active:%s, force:%s) ..." % (self.key, id(self.dbobj), - # self.is_active, force_restart) - - if self.dbobj.is_active and not force_restart: + if self.is_active and not force_restart: # script already runs and should not be restarted. return 0 @@ -233,11 +232,11 @@ class ScriptBase(TypeClass): if obj: # check so the scripted object is valid and initalized try: - _GA(obj.dbobj, 'cmdset') + obj.cmdset except AttributeError: # this means the object is not initialized. logger.log_trace() - self.dbobj.is_active = False + self.is_active = False return 0 # try to restart a paused script @@ -245,13 +244,13 @@ class ScriptBase(TypeClass): return 1 # start the script from scratch - self.dbobj.is_active = True + self.is_active = True try: self.at_start() except Exception: logger.log_trace() - if self.dbobj.db_interval > 0: + if self.db_interval > 0: self._start_task() return 1 @@ -272,7 +271,7 @@ class ScriptBase(TypeClass): logger.log_trace() self._stop_task() try: - self.dbobj.delete() + self.delete() except AssertionError: logger.log_trace() return 0 @@ -290,7 +289,7 @@ class ScriptBase(TypeClass): self.db._paused_time = task.next_call_time() self.db._paused_callcount = task.callcount self._stop_task() - self.dbobj.is_active = False + self.is_active = False def unpause(self): """ @@ -298,7 +297,7 @@ class ScriptBase(TypeClass): """ if self.db._paused_time: # only unpause if previously paused - self.dbobj.is_active = True + self.is_active = True try: self.at_start() @@ -343,116 +342,141 @@ class ScriptBase(TypeClass): "called when typeclass re-caches. Usually not used for scripts." pass - # # Base Script - inherit from this # -class Script(ScriptBase): - """ - This is the class you should inherit from, it implements - the hooks called by the script machinery. +class DefaultScript(ScriptBase): """ + This is the base TypeClass for all Scripts. Scripts describe events, + timers and states in game, they can have a time component or describe + a state that changes under certain conditions. - def __init__(self, dbobj): + Script API: + + * Available properties (only available on initiated Typeclass objects) + + key (string) - name of object + name (string)- same as key + aliases (list of strings) - aliases to the object. Will be saved to + database as AliasDB entries but returned as strings. + dbref (int, read-only) - unique #id-number. Also "id" can be used. + date_created (string) - time stamp of object creation + permissions (list of strings) - list of permission strings + + desc (string) - optional description of script, shown in listings + obj (Object) - optional object that this script is connected to + and acts on (set automatically + by obj.scripts.add()) + interval (int) - how often script should run, in seconds. + <=0 turns off ticker + start_delay (bool) - if the script should start repeating right + away or wait self.interval seconds + repeats (int) - how many times the script should repeat before + stopping. <=0 means infinite repeats + persistent (bool) - if script should survive a server shutdown or not + is_active (bool) - if script is currently running + + * Handlers + + locks - lock-handler: use locks.add() to add new lock strings + db - attribute-handler: store/retrieve database attributes on this + self.db.myattr=val, val=self.db.myattr + ndb - non-persistent attribute handler: same as db but does not + create a database entry when storing data + + * Helper methods + + start() - start script (this usually happens automatically at creation + and obj.script.add() etc) + stop() - stop script, and delete it + pause() - put the script on hold, until unpause() is called. If script + is persistent, the pause state will survive a shutdown. + unpause() - restart a previously paused script. The script will + continue as if it was never paused. + force_repeat() - force-step the script, regardless of how much remains + until next step. This counts like a normal firing in all ways. + time_until_next_repeat() - if a timed script (interval>0), returns + time until next tick + remaining_repeats() - number of repeats remaining, if limited + + * Hook methods + + at_script_creation() - called only once, when an object of this + class is first created. + is_valid() - is called to check if the script is valid to be running + at the current time. If is_valid() returns False, the + running script is stopped and removed from the game. You + can use this to check state changes (i.e. an script + tracking some combat stats at regular intervals is only + valid to run while there is actual combat going on). + at_start() - Called every time the script is started, which for + persistent scripts is at least once every server start. + Note that this is unaffected by self.delay_start, which + only delays the first call to at_repeat(). It will also + be called after a pause, to allow for setting up the script. + at_repeat() - Called every self.interval seconds. It will be called + immediately upon launch unless self.delay_start is True, + which will delay the first call of this method by + self.interval seconds. If self.interval<=0, this method + will never be called. + at_stop() - Called as the script object is stopped and is about to + be removed from the game, e.g. because is_valid() + returned False or self.stop() was called manually. + at_server_reload() - Called when server reloads. Can be used to save + temporary variables you want should survive a reload. + at_server_shutdown() - called at a full server shutdown. + + """ + def at_first_save(self): """ - This is the base TypeClass for all Scripts. Scripts describe events, - timers and states in game, they can have a time component or describe - a state that changes under certain conditions. + This is called after very first time this object is saved. + Generally, you don't need to overload this, but only the hooks + called by this method. + """ + self.at_script_creation() - Script API: + if hasattr(self, "_createdict"): + # this will only be set if the utils.create_script + # function was used to create the object. We want + # the create call's kwargs to override the values + # set by hooks. + cdict = self._createdict + updates = [] + if not cdict.get("key"): + if not self.db_key: + self.db_key = "#%i" % self.dbid + updates.append("db_key") + elif self.db_key != cdict["key"]: + self.db_key = cdict["key"] + updates.append("db_key") + if cdict.get("interval") and self.interval != cdict["interval"]: + self.db_interval = cdict["interval"] + updates.append("db_interval") + if cdict.get("start_delay") and self.start_delay != cdict["start_delay"]: + self.db_start_delay = cdict["start_delay"] + updates.append("db_start_delay") + if cdict.get("repeats") and self.repeats != cdict["repeats"]: + self.db_repeats = cdict["repeats"] + updates.append("db_repeats") + if cdict.get("persistent") and self.persistent != cdict["persistent"]: + self.db_persistent = cdict["persistent"] + updates.append("db_persistent") + if updates: + self.save(update_fields=updates) + if not cdict.get("autostart"): + # don't auto-start the script + return - * Available properties (only available on initiated Typeclass objects) + # auto-start script (default) + self.start() - key (string) - name of object - name (string)- same as key - aliases (list of strings) - aliases to the object. Will be saved to - database as AliasDB entries but returned as strings. - dbref (int, read-only) - unique #id-number. Also "id" can be used. - dbobj (Object, read-only) - link to database model. dbobj.typeclass - points back to this class - typeclass (Object, read-only) - this links back to this class as an - identified only. Use self.swap_typeclass() to switch. - date_created (string) - time stamp of object creation - permissions (list of strings) - list of permission strings - - desc (string) - optional description of script, shown in listings - obj (Object) - optional object that this script is connected to - and acts on (set automatically - by obj.scripts.add()) - interval (int) - how often script should run, in seconds. - <=0 turns off ticker - start_delay (bool) - if the script should start repeating right - away or wait self.interval seconds - repeats (int) - how many times the script should repeat before - stopping. <=0 means infinite repeats - persistent (bool) - if script should survive a server shutdown or not - is_active (bool) - if script is currently running - - * Handlers - - locks - lock-handler: use locks.add() to add new lock strings - db - attribute-handler: store/retrieve database attributes on this - self.db.myattr=val, val=self.db.myattr - ndb - non-persistent attribute handler: same as db but does not - create a database entry when storing data - - * Helper methods - - start() - start script (this usually happens automatically at creation - and obj.script.add() etc) - stop() - stop script, and delete it - pause() - put the script on hold, until unpause() is called. If script - is persistent, the pause state will survive a shutdown. - unpause() - restart a previously paused script. The script will - continue as if it was never paused. - force_repeat() - force-step the script, regardless of how much remains - until next step. This counts like a normal firing in all ways. - time_until_next_repeat() - if a timed script (interval>0), returns - time until next tick - remaining_repeats() - number of repeats remaining, if limited - - * Hook methods - - at_script_creation() - called only once, when an object of this - class is first created. - is_valid() - is called to check if the script is valid to be running - at the current time. If is_valid() returns False, the - running script is stopped and removed from the game. You - can use this to check state changes (i.e. an script - tracking some combat stats at regular intervals is only - valid to run while there is actual combat going on). - at_start() - Called every time the script is started, which for - persistent scripts is at least once every server start. - Note that this is unaffected by self.delay_start, which - only delays the first call to at_repeat(). It will also - be called after a pause, to allow for setting up the script. - at_repeat() - Called every self.interval seconds. It will be called - immediately upon launch unless self.delay_start is True, - which will delay the first call of this method by - self.interval seconds. If self.interval<=0, this method - will never be called. - at_stop() - Called as the script object is stopped and is about to - be removed from the game, e.g. because is_valid() - returned False or self.stop() was called manually. - at_server_reload() - Called when server reloads. Can be used to save - temporary variables you want should survive a reload. - at_server_shutdown() - called at a full server shutdown. - - - """ - super(Script, self).__init__(dbobj) def at_script_creation(self): """ Only called once, by the create function. """ - self.key = "" - self.desc = "" - self.interval = 0 # infinite - self.start_delay = False - self.repeats = 0 # infinite - self.persistent = False + pass def is_valid(self): """ @@ -502,7 +526,7 @@ class Script(ScriptBase): # Some useful default Script types used by Evennia. -class DoNothing(Script): +class DoNothing(DefaultScript): "An script that does nothing. Used as default fallback." def at_script_creation(self): "Setup the script" @@ -510,7 +534,7 @@ class DoNothing(Script): self.desc = _("This is an empty placeholder script.") -class Store(Script): +class Store(DefaultScript): "Simple storage script" def at_script_creation(self): "Setup the script" @@ -518,7 +542,7 @@ class Store(Script): self.desc = _("This is a generic storage container.") -class CheckSessions(Script): +class CheckSessions(DefaultScript): "Check sessions regularly." def at_script_creation(self): "Setup the script" @@ -531,14 +555,14 @@ class CheckSessions(Script): "called every 60 seconds" global _SESSIONS if not _SESSIONS: - from src.server.sessionhandler import SESSIONS as _SESSIONS + from evennia.server.sessionhandler import SESSIONS as _SESSIONS #print "session check!" #print "ValidateSessions run" _SESSIONS.validate_sessions() _FLUSH_CACHE = None _IDMAPPER_CACHE_MAX_MEMORY = settings.IDMAPPER_CACHE_MAXSIZE -class ValidateIdmapperCache(Script): +class ValidateIdmapperCache(DefaultScript): """ Check memory use of idmapper cache """ @@ -552,10 +576,10 @@ class ValidateIdmapperCache(Script): "Called every ~5 mins" global _FLUSH_CACHE if not _FLUSH_CACHE: - from src.utils.idmapper.base import conditional_flush as _FLUSH_CACHE + from evennia.utils.idmapper.models import conditional_flush as _FLUSH_CACHE _FLUSH_CACHE(_IDMAPPER_CACHE_MAX_MEMORY) -class ValidateScripts(Script): +class ValidateScripts(DefaultScript): "Check script validation regularly" def at_script_creation(self): "Setup the script" @@ -570,7 +594,7 @@ class ValidateScripts(Script): ScriptDB.objects.validate() -class ValidateChannelHandler(Script): +class ValidateChannelHandler(DefaultScript): "Update the channelhandler to make sure it's in sync." def at_script_creation(self): "Setup the script" diff --git a/src/tests/test_scripts_models.py b/evennia/scripts/tests.py similarity index 62% rename from src/tests/test_scripts_models.py rename to evennia/scripts/tests.py index 43f7ca27a..aae438de2 100644 --- a/src/tests/test_scripts_models.py +++ b/evennia/scripts/tests.py @@ -1,15 +1,9 @@ -try: - # this is an optimized version only available in later Django versions - from django.utils.unittest import TestCase -except ImportError: - # if the first fails, we use the old version - from django.test import TestCase +# this is an optimized version only available in later Django versions +from django.utils.unittest import TestCase -from src.scripts.models import ScriptDB, ObjectDoesNotExist -from src.utils.create import create_script -from src.scripts import DoNothing -import unittest -from django.conf import settings +from evennia.scripts.models import ScriptDB, ObjectDoesNotExist +from evennia.utils.create import create_script +from evennia.scripts.scripts import DoNothing class TestScriptDB(TestCase): @@ -35,12 +29,6 @@ class TestScriptDB(TestCase): self.scr.delete() self.scr.delete() - #@unittest.skip("not implemented") - #def test___init__fails(self): # Users should be told not to do this - # - No they should not; ScriptDB() is required internally. /Griatch - # with self.assertRaises(Exception): - # ScriptDB() - def test_deleted_script_fails_start(self): "Would it ever be necessary to start a deleted script?" self.scr.delete() @@ -53,7 +41,3 @@ class TestScriptDB(TestCase): "Can deleted scripts be said to be valid?" self.scr.delete() self.assertFalse(self.scr.is_valid()) # assertRaises? See issue #509 - - -if __name__ == '__main__': - unittest.main() diff --git a/src/scripts/tickerhandler.py b/evennia/scripts/tickerhandler.py similarity index 78% rename from src/scripts/tickerhandler.py rename to evennia/scripts/tickerhandler.py index cf3421a9b..79fbd096d 100644 --- a/src/scripts/tickerhandler.py +++ b/evennia/scripts/tickerhandler.py @@ -11,7 +11,7 @@ server reloads and be started automaticall on boot. Example: - from src.scripts.tickerhandler import TICKER_HANDLER + from evennia.scripts.tickerhandler import TICKER_HANDLER # tick myobj every 15 seconds TICKER_HANDLER.add(myobj, 15) @@ -49,15 +49,22 @@ call the handler's save() and restore() methods when the server reboots. """ from twisted.internet.defer import inlineCallbacks -from src.scripts.scripts import ExtendedLoopingCall -from src.server.models import ServerConfig -from src.utils.logger import log_trace -from src.utils.dbserialize import dbserialize, dbunserialize, pack_dbobj, unpack_dbobj +from django.core.exceptions import ObjectDoesNotExist +from evennia.scripts.scripts import ExtendedLoopingCall +from evennia.server.models import ServerConfig +from evennia.utils.logger import log_trace, log_err +from evennia.utils.dbserialize import dbserialize, dbunserialize, pack_dbobj, unpack_dbobj _GA = object.__getattribute__ _SA = object.__setattr__ +_ERROR_ADD_INTERVAL = \ +"""TickerHandler: Tried to add a ticker with invalid interval: +obj={obj}, interval={interval}, args={args}, kwargs={kwargs} +store_key={store_key} +Ticker was not added.""" + class Ticker(object): """ Represents a repeatedly running task that calls @@ -78,14 +85,17 @@ class Ticker(object): The callback should ideally work under @inlineCallbacks so it can yield appropriately. """ - for key, (obj, args, kwargs) in self.subscriptions.items(): - hook_key = yield kwargs.get("hook_key", "at_tick") - if not obj: + for store_key, (obj, args, kwargs) in self.subscriptions.items(): + hook_key = yield kwargs.get("_hook_key", "at_tick") + if not obj or not obj.pk: # object was deleted between calls - self.validate() + self.remove(store_key) continue try: yield _GA(obj, hook_key)(*args, **kwargs) + except ObjectDoesNotExist: + log_trace() + self.remove(store_key) except Exception: log_trace() @@ -112,7 +122,6 @@ class Ticker(object): if not subs: self.task.stop() elif subs: - #print "starting with start_delay=", start_delay self.task.start(self.interval, now=False, start_delay=start_delay) def add(self, store_key, obj, *args, **kwargs): @@ -155,6 +164,11 @@ class TickerPool(object): """ Add new ticker subscriber """ + if not interval: + log_err(_ERROR_ADD_INTERVAL.format(store_key=store_key, obj=obj, + interval=interval, args=args, kwargs=kwargs)) + return + if interval not in self.tickers: self.tickers[interval] = self.ticker_class(interval) self.tickers[interval].add(store_key, obj, *args, **kwargs) @@ -202,19 +216,10 @@ class TickerHandler(object): is a boolean True if obj was a database object, False otherwise. """ - try: - obj = obj.typeclass - except AttributeError: - pass - dbobj = None - try: - dbobj = obj.dbobj - except AttributeError: - pass - isdb = True - if dbobj: + if hasattr(obj, "db_key"): # create a store_key using the database representation - objkey = pack_dbobj(dbobj) + objkey = pack_dbobj(obj) + isdb = True else: # non-db object, look for a property "key" on it, otherwise # use its memory location. @@ -245,6 +250,7 @@ class TickerHandler(object): ServerConfig.objects.conf(key=self.save_name, value=dbserialize(self.ticker_storage)) else: + # make sure we have nothing lingering in the database ServerConfig.objects.conf(key=self.save_name, delete=True) def restore(self): @@ -257,30 +263,56 @@ class TickerHandler(object): self.ticker_storage = dbunserialize(ticker_storage) #print "restore:", self.ticker_storage for store_key, (args, kwargs) in self.ticker_storage.items(): - if len(store_key) == 2: - # old form of store_key - update it - store_key = (store_key[0], store_key[1], "") obj, interval, idstring = store_key obj = unpack_dbobj(obj) _, store_key = self._store_key(obj, interval, idstring) self.ticker_pool.add(store_key, obj, interval, *args, **kwargs) - def add(self, obj, interval, idstring="", *args, **kwargs): + def add(self, obj, interval, idstring="", hook_key="at_tick", *args, **kwargs): """ - Add object to tickerhandler. The object must have an at_tick - method. This will be called every interval seconds until the - object is unsubscribed from the ticker. + Add object to tickerhandler + + Args: + obj (Object): The object to subscribe to the ticker. + interval (int): Interval in seconds between calling + `hook_key` below. + idstring (str, optional): Identifier for separating + this ticker-subscription from others with the same + interval. Allows for managing multiple calls with + the same time interval + hook_key (str, optional): The name of the hook method + on `obj` to call every `interval` seconds. Defaults to + `at_tick(*args, **kwargs`. All hook methods must + always accept *args, **kwargs. + args, kwargs (optional): These will be passed into the + method given by `hook_key` every time it is called. + + Notes: + The combination of `obj`, `interval` and `idstring` + together uniquely defines the ticker subscription. They + must all be supplied in order to unsubscribe from it + later. + """ isdb, store_key = self._store_key(obj, interval, idstring) if isdb: self.ticker_storage[store_key] = (args, kwargs) self.save() + kwargs["_hook_key"] = hook_key self.ticker_pool.add(store_key, obj, interval, *args, **kwargs) def remove(self, obj, interval=None, idstring=""): """ - Remove object from ticker, or only this object ticking - at a given interval. + Remove object from ticker or only remove it from tickers with + a given interval. + + Args: + obj (Object): The object subscribing to the ticker. + interval (int, optional): Interval of ticker to remove. If + `None`, all tickers on this object matching `idstring` + will be removed, regardless of their `interval` setting. + idstring (str, optional): Identifier id of ticker to remove. + """ if interval: isdb, store_key = self._store_key(obj, interval, idstring) diff --git a/evennia/server/__init__.py b/evennia/server/__init__.py new file mode 100644 index 000000000..5e96b1099 --- /dev/null +++ b/evennia/server/__init__.py @@ -0,0 +1,7 @@ +""" +This sub-package holds the Server and Portal programs - the "core" of +Evennia. It also contains the SessionHandler that manages all +connected users as well as defines all the connection protocols used +to connect to the game. + +""" diff --git a/src/server/admin.py b/evennia/server/admin.py similarity index 90% rename from src/server/admin.py rename to evennia/server/admin.py index 3a043dc69..710ffffcf 100644 --- a/src/server/admin.py +++ b/evennia/server/admin.py @@ -4,7 +4,7 @@ # from django.contrib import admin -from src.server.models import ServerConfig +from evennia.server.models import ServerConfig class ServerConfigAdmin(admin.ModelAdmin): diff --git a/src/server/amp.py b/evennia/server/amp.py similarity index 98% rename from src/server/amp.py rename to evennia/server/amp.py index b2dbe731c..f03bd0364 100644 --- a/src/server/amp.py +++ b/evennia/server/amp.py @@ -26,7 +26,7 @@ except ImportError: from twisted.protocols import amp from twisted.internet import protocol from twisted.internet.defer import Deferred -from src.utils.utils import to_str, variable_from_module +from evennia.utils.utils import to_str, variable_from_module # communication bits @@ -215,7 +215,7 @@ class FunctionCall(amp.Command): # Helper functions -dumps = lambda data: to_str(pickle.dumps(data, pickle.HIGHEST_PROTOCOL)) +dumps = lambda data: to_str(pickle.dumps(to_str(data), pickle.HIGHEST_PROTOCOL)) loads = lambda data: pickle.loads(to_str(data)) # multipart message store @@ -348,10 +348,10 @@ class AMPProtocol(amp.AMP): data comes in multiple chunks; if so (nparts>1) we buffer the data and wait for the remaining parts to arrive before continuing. """ - #print "msg portal -> server (server side):", sessid, msg, data ret = self.safe_recv(MsgPortal2Server, sessid, ipart, nparts, text=msg, data=data) if ret is not None: + #print "msg portal -> server (server side):", sessid, msg, loads(ret["data"]) self.factory.server.sessions.data_in(sessid, text=ret["text"], **loads(ret["data"])) @@ -373,10 +373,10 @@ class AMPProtocol(amp.AMP): """ Relays message to Portal. This method is executed on the Portal. """ - #print "msg server->portal (portal side):", sessid, msg ret = self.safe_recv(MsgServer2Portal, sessid, ipart, nparts, text=msg, data=data) if ret is not None: + #print "msg server->portal (portal side):", sessid, ret["text"], loads(ret["data"]) self.factory.portal.sessions.data_out(sessid, text=ret["text"], **loads(ret["data"])) diff --git a/evennia/server/evennia_launcher.py b/evennia/server/evennia_launcher.py new file mode 100644 index 000000000..dbd1f25bc --- /dev/null +++ b/evennia/server/evennia_launcher.py @@ -0,0 +1,978 @@ +#!/usr/bin/env python +""" +EVENNIA SERVER LAUNCHER SCRIPT + +This is the start point for running Evennia. + +Sets the appropriate environmental variables and launches the server +and portal through the evennia_runner. Run without arguments to get a +menu. Run the script with the -h flag to see usage information. + +""" +import os +import sys +import signal +import shutil +import importlib +from argparse import ArgumentParser +from subprocess import Popen, check_output, call, CalledProcessError, STDOUT +import django + +# Signal processing +SIG = signal.SIGINT + +# Set up the main python paths to Evennia +EVENNIA_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import evennia +EVENNIA_LIB = os.path.join(os.path.dirname(os.path.abspath(evennia.__file__))) +EVENNIA_SERVER = os.path.join(EVENNIA_LIB, "server") +EVENNIA_RUNNER = os.path.join(EVENNIA_SERVER, "evennia_runner.py") +EVENNIA_TEMPLATE = os.path.join(EVENNIA_LIB, "game_template") +EVENNIA_PROFILING = os.path.join(EVENNIA_SERVER, "profiling") +EVENNIA_DUMMYRUNNER = os.path.join(EVENNIA_PROFILING, "dummyrunner.py") + +TWISTED_BINARY = "twistd" + +# Game directory structure +SETTINGFILE = "settings.py" +SERVERDIR = "server" +CONFDIR = os.path.join(SERVERDIR, "conf") +SETTINGS_PATH = os.path.join(CONFDIR, SETTINGFILE) +SETTINGS_DOTPATH = "server.conf.settings" +CURRENT_DIR = os.getcwd() +GAMEDIR = CURRENT_DIR + +# Operational setup +SERVER_LOGFILE = None +PORTAL_LOGFILE = None +HTTP_LOGFILE = None +SERVER_PIDFILE = None +PORTAL_PIDFILE = None +SERVER_RESTART = None +PORTAL_RESTART = None +SERVER_PY_FILE = None +PORTAL_PY_FILE = None + +PYTHON_MIN = '2.7' +TWISTED_MIN = '12.0' +DJANGO_MIN = '1.7' +DJANGO_REC = '1.7' + +sys.path[1] = EVENNIA_ROOT + +#------------------------------------------------------------ +# +# Messages +# +#------------------------------------------------------------ + +CREATED_NEW_GAMEDIR = \ + """ + Welcome to Evennia! + Created a new Evennia game directory '{gamedir}'. + + You can now optionally edit your new settings file + at {settings_path}. If you don't, the defaults + will work out of the box. When ready to continue, 'cd' to your + game directory and run: + + evennia migrate + + This initializes the database. To start the server for the first + time, run: + + evennia -i start + + Make sure to create a superuser when asked for it. You should now + be able to (by default) connect to your server on server + 'localhost', port 4000 using a telnet/mud client or + http://localhost:8000 using your web browser. If things don't + work, check so those ports are open. + + """ + +ERROR_INPUT = \ + """ + The argument(s) + {args} {kwargs} + is/are not recognized by Evennia nor Django. Use -h for help. + """ + +ERROR_NO_GAMEDIR = \ + """ + No Evennia settings file was found. You must run this command from + inside a valid game directory first created with --init. + """ + +WARNING_MOVING_SUPERUSER = \ + """ + Evennia expects a Player superuser with id=1. No such Player was + found. However, another superuser ('{other_key}', id={other_id}) + was found in the database. If you just created this superuser and + still see this text it is probably due to the database being + flushed recently - in this case the database's internal + auto-counter might just start from some value higher than one. + + We will fix this by assigning the id 1 to Player '{other_key}'. + Please confirm this is acceptable before continuing. + """ + +WARNING_RUNSERVER = \ + """ + WARNING: There is no need to run the Django development + webserver to test out Evennia web features (the web client + will in fact not work since the Django test server knows + nothing about MUDs). Instead, just start Evennia with the + webserver component active (this is the default). + """ + +ERROR_SETTINGS = \ + """ + There was an error importing Evennia's config file {settingspath}. There is usually + one of three reasons for this: + 1) You are not running this command from your game directory. + Change directory to your game directory and try again (or + create a new game directory using evennia --init ) + 2) The settings file contains a syntax error. If you see a + traceback above, review it, resolve the problem and try again. + 3) Django is not correctly installed. This usually shows as + errors mentioning 'DJANGO_SETTINGS_MODULE'. If you run a + virtual machine, it might be worth to restart it to see if + this resolves the issue. + """.format(settingsfile=SETTINGFILE, settingspath=SETTINGS_PATH) + +ERROR_DATABASE = \ + """ + Your database does not seem to be set up correctly. + (error was '{traceback}') + + Standing in your game directory, run + + evennia migrate + + to initialize/update the database according to your settings. + """ + +ERROR_WINDOWS_WIN32API = \ + """ + ERROR: Unable to import win32api, which Twisted requires to run. + You may download it from: + + http://sourceforge.net/projects/pywin32/files/pywin32/ + + If you are running in a virtual environment, browse to the + location of the latest win32api exe file for your computer and + Python version and copy the url to it; then paste it into a call + to easy_install: + + easy_install http:// + """ + +INFO_WINDOWS_BATFILE = \ + """ + INFO: Since you are running Windows, a file 'twistd.bat' was + created for you. This is a simple batch file that tries to call + the twisted executable. Evennia determined this to be: + + {twistd_path} + + If you run into errors at startup you might need to edit + twistd.bat to point to the actual location of the Twisted + executable (usually called twistd.py) on your machine. + + This procedure is only done once. Run evennia.py again when you + are ready to start the server. + """ + +CMDLINE_HELP = \ + """ + Starts or operates the Evennia MU* server. Also allows for + initializing a new game directory and manages the game's database. + You can also pass most standard django-admin arguments and + options. + """ + + +VERSION_INFO = \ + """ + {about} + Evennia {version} + OS: {os} + Python: {python} + Twisted: {twisted} + Django: {django} + """ + +ABOUT_INFO= \ + """ + Evennia MUD/MUX/MU* development system + + Licence: BSD 3-Clause Licence + Web: http://www.evennia.com + Irc: #evennia on FreeNode + Forum: http://www.evennia.com/discussions + Maintainer (2010-): Griatch (griatch AT gmail DOT com) + Maintainer (2006-10): Greg Taylor + + Use -h for command line options. + """ + +HELP_ENTRY = \ + """ + Enter 'evennia -h' for command-line options. + + Use option (1) in a production environment. During development (2) is + usually enough, portal debugging is usually only useful if you are + adding new protocols or are debugging Evennia itself. + + Reload with (5) to update the server with your changes without + disconnecting any players. + + Note: Reload and stop are sometimes poorly supported in Windows. If you have + issues, log into the game to stop or restart the server instead. + """ + +MENU = \ + """ + +----Evennia Launcher-------------------------------------------+ + | | + +--- Starting --------------------------------------------------+ + | | + | 1) (normal): All output to logfiles | + | 2) (server devel): Server logs to terminal (-i option) | + | 3) (portal devel): Portal logs to terminal | + | 4) (full devel): Both Server and Portal logs to terminal | + | | + +--- Restarting ------------------------------------------------+ + | | + | 5) Reload the Server | + | 6) Reload the Portal (only works with portal/full debug) | + | | + +--- Stopping --------------------------------------------------+ + | | + | 7) Stopping both Portal and Server | + | 8) Stopping only Server | + | 9) Stopping only Portal | + | | + +---------------------------------------------------------------+ + | h) Help i) About info q) Abort | + +---------------------------------------------------------------+ + """ + +ERROR_PYTHON_VERSION = \ + """ + ERROR: Python {pversion} used. Evennia requires version + {python_min} or higher (but not 3.x). + """ + +WARNING_TWISTED_VERSION = \ + """ + WARNING: Twisted {tversion} found. Evennia recommends + v{twisted_min} or higher." + """ + +ERROR_NOTWISTED = \ + """ + ERROR: Twisted does not seem to be installed. + """ + +ERROR_DJANGO_MIN = \ + """ + ERROR: Django {dversion} found. Evennia requires version + {django_min} or higher. + """ + +NOTE_DJANGO_MIN = \ + """ + NOTE: Django {dversion} found. This will work, but v{django_rec} + is recommended for production. + """ + +NOTE_DJANGO_NEW = \ + """ + NOTE: Django {dversion} found. This is newer than Evennia's + recommended version (v{django_rec}). It will probably work, but + may be new enough not to be fully tested yet. Report any issues." + """ + +ERROR_NODJANGO = \ + """ + ERROR: Django does not seem to be installed. + """ + +#------------------------------------------------------------ +# +# Functions +# +#------------------------------------------------------------ + +def evennia_version(): + """ + Get the Evennia version info from the main package. + """ + version = "Unknown" + try: + import evennia + version = evennia.__version__ + except ImportError: + pass + try: + version = "%s (rev %s)" % (version, check_output("git rev-parse --short HEAD", shell=True, cwd=EVENNIA_ROOT, stderr=STDOUT).strip()) + except (IOError, CalledProcessError): + pass + return version + +EVENNIA_VERSION = evennia_version() + + +def check_main_evennia_dependencies(): + """ + Checks and imports the Evennia dependencies. This must be done + already before the paths are set up. + """ + error = False + + # Python + pversion = ".".join(str(num) for num in sys.version_info if type(num) == int) + if pversion < PYTHON_MIN: + print ERROR_PYTHON_VERSION.format(pversion=pversion, python_min=PYTHON_MIN) + error = True + # Twisted + try: + import twisted + tversion = twisted.version.short() + if tversion < TWISTED_MIN: + print WARNING_TWISTED_VERSION.format(tversion=tversion, twisted_min=TWISTED_MIN) + except ImportError: + print ERROR_NOTWISTED + error = True + # Django + try: + dversion = ".".join(str(num) for num in django.VERSION if type(num) == int) + # only the main version (1.5, not 1.5.4.0) + dversion_main = ".".join(dversion.split(".")[:2]) + if dversion < DJANGO_MIN: + print ERROR_DJANGO_MIN.format(dversion=dversion_main, django_min=DJANGO_MIN) + error = True + elif DJANGO_MIN <= dversion < DJANGO_REC: + print NOTE_DJANGO_MIN.format(dversion=dversion_main, django_rec=DJANGO_REC) + elif DJANGO_REC < dversion_main: + print NOTE_DJANGO_NEW.format(dversion=dversion_main, django_rec=DJANGO_REC) + except ImportError: + print ERROR_NODJANGO + error = True + if error: + sys.exit() + + +def set_gamedir(path): + """ + Set GAMEDIR based on path, by figuring out where the setting file + is inside the directory tree. + """ + + global GAMEDIR + if os.path.exists(os.path.join(path, SETTINGS_PATH)): + # path at root of game dir + GAMEDIR = os.path.abspath(path) + elif os.path.exists(os.path.join(path, os.path.pardir, SETTINGS_PATH)): + # path given to somewhere one level down + GAMEDIR = os.path.dirname(path) + elif os.path.exists(os.path.join(path, os.path.pardir, os.path.pardir, SETTINGS_PATH)): + # path given to somwhere two levels down + GAMEDIR = os.path.dirname(os.path.dirname(path)) + elif os.path.exists(os.path.join(path, os.path.pardir, os.path. pardir, os.path.pardir, SETTINGS_PATH)): + # path given to somewhere three levels down (custom directories) + GAMEDIR = os.path.dirname(os.path.dirname(os.path.dirname(path))) + else: + # we don't look further down than this ... + print ERROR_NO_GAMEDIR + sys.exit() + + +def create_secret_key(): + """ + Randomly create the secret key for the settings file + """ + import random + import string + secret_key = list((string.letters + + string.digits + string.punctuation).replace("\\", "").replace("'", '"')) + random.shuffle(secret_key) + secret_key = "".join(secret_key[:40]) + return secret_key + + +def create_settings_file(): + """ + Uses the template settings file to build a working + settings file. + """ + settings_path = os.path.join(GAMEDIR, "server", "conf", "settings.py") + with open(settings_path, 'r') as f: + settings_string = f.read() + + # tweak the settings + setting_dict = {"settings_default": os.path.join(EVENNIA_LIB, "settings_default.py"), + "servername":"\"%s\"" % GAMEDIR.rsplit(os.path.sep, 1)[1].capitalize(), + "secret_key":"\'%s\'" % create_secret_key()} + + # modify the settings + settings_string = settings_string.format(**setting_dict) + + with open(settings_path, 'w') as f: + f.write(settings_string) + + +def create_game_directory(dirname): + """ + Initialize a new game directory named dirname + at the current path. This means copying the + template directory from evennia's root. + """ + global GAMEDIR + GAMEDIR = os.path.abspath(os.path.join(CURRENT_DIR, dirname)) + if os.path.exists(GAMEDIR): + print "Cannot create new Evennia game dir: '%s' already exists." % dirname + sys.exit() + # copy template directory + shutil.copytree(EVENNIA_TEMPLATE, GAMEDIR) + # pre-build settings file in the new GAMEDIR + create_settings_file() + + +def create_superuser(): + "Create the superuser player" + print "\nCreate a superuser below. The superuser is Player #1, the 'owner' account of the server.\n" + django.core.management.call_command("createsuperuser", interactive=True) + + +def check_database(): + """ + Check database exists + """ + # Check so a database exists and is accessible + from django.db import connection + tables = connection.introspection.get_table_list(connection.cursor()) + if tables and u'players_playerdb' in tables: + # database exists and seems set up. Initialize evennia. + import evennia + evennia._init() + # Try to get Player#1 + from evennia.players.models import PlayerDB + try: + PlayerDB.objects.get(id=1) + except django.db.utils.OperationalError, e: + print ERROR_DATABASE.format(traceback=e) + sys.exit() + except PlayerDB.DoesNotExist: + # no superuser yet. We need to create it. + + other_superuser = PlayerDB.objects.filter(is_superuser=True) + if other_superuser: + # Another superuser was found, but not with id=1. This may + # happen if using flush (the auto-id starts at a higher + # value). Wwe copy this superuser into id=1. To do + # this we must deepcopy it, delete it then save the copy + # with the new id. This allows us to avoid the UNIQUE + # constraint on usernames. + other = other_superuser[0] + other_id = other.id + other_key = other.username + print WARNING_MOVING_SUPERUSER.format(other_key=other_key, + other_id=other_id) + res = "" + while res.upper() != "Y": + # ask for permission + res = raw_input("Continue [Y]/N: ") + if res.upper() == "N": + sys.exit() + elif not res: + break + # continue with the + from copy import deepcopy + new = deepcopy(other) + other.delete() + new.id = 1 + new.save() + else: + create_superuser() + check_database() + return True + + +def getenv(): + """ + Get current environment and add PYTHONPATH + """ + sep = ";" if os.name == 'nt' else ":" + env = os.environ.copy() + env['PYTHONPATH'] = sep.join(sys.path) + return env + + +def get_pid(pidfile): + """ + Get the PID (Process ID) by trying to access + an PID file. + """ + pid = None + if os.path.exists(pidfile): + f = open(pidfile, 'r') + pid = f.read() + return pid + + +def del_pid(pidfile): + """ + The pidfile should normally be removed after a process has + finished, but when sending certain signals they remain, so we need + to clean them manually. + """ + if os.path.exists(pidfile): + os.remove(pidfile) + + +def kill(pidfile, signal=SIG, succmsg="", errmsg="", restart_file=SERVER_RESTART, restart=False): + """ + Send a kill signal to a process based on PID. A customized + success/error message will be returned. If clean=True, the system + will attempt to manually remove the pid file. + """ + pid = get_pid(pidfile) + if pid: + if os.name == 'nt': + os.remove(pidfile) + # set restart/norestart flag + if restart: + django.core.management.call_command('collectstatic', interactive=False, verbosity=0) + with open(restart_file, 'w') as f: + f.write("reload") + else: + with open(restart_file, 'w') as f: + f.write("shutdown") + try: + os.kill(int(pid), signal) + except OSError: + print "Process %(pid)s cannot be stopped. "\ + "The PID file 'server/%(pidfile)s' seems stale. "\ + "Try removing it." % {'pid': pid, 'pidfile': pidfile} + return + print "Evennia:", succmsg + return + print "Evennia:", errmsg + + +def show_version_info(about=False): + """ + Display version info + """ + import os, sys + import twisted + import django + + return VERSION_INFO.format(version=EVENNIA_VERSION, + about=ABOUT_INFO if about else "", + os=os.name, python=sys.version.split()[0], + twisted=twisted.version.short(), + django=django.get_version()) + + +def error_check_python_modules(): + """ + Import settings modules in settings. This will raise exceptions on + pure python-syntax issues which are hard to catch gracefully + with exceptions in the engine (since they are formatting errors in + the python source files themselves). Best they fail already here + before we get any further. + """ + from django.conf import settings + def imp(path, split=True): + mod, fromlist = path, "None" + if split: + mod, fromlist = path.rsplit('.', 1) + __import__(mod, fromlist=[fromlist]) + + # core modules + imp(settings.COMMAND_PARSER) + imp(settings.SEARCH_AT_RESULT) + imp(settings.SEARCH_AT_MULTIMATCH_INPUT) + imp(settings.CONNECTION_SCREEN_MODULE) + #imp(settings.AT_INITIAL_SETUP_HOOK_MODULE, split=False) + for path in settings.LOCK_FUNC_MODULES: + imp(path, split=False) + # cmdsets + + deprstring = "settings.%s should be renamed to %s. If defaults are used, " \ + "their path/classname must be updated (see evennia/settings_default.py)." + if hasattr(settings, "CMDSET_DEFAULT"): + raise DeprecationWarning(deprstring % ("CMDSET_DEFAULT", "CMDSET_CHARACTER")) + if hasattr(settings, "CMDSET_OOC"): + raise DeprecationWarning(deprstring % ("CMDSET_OOC", "CMDSET_PLAYER")) + if settings.WEBSERVER_ENABLED and not isinstance(settings.WEBSERVER_PORTS[0], tuple): + raise DeprecationWarning("settings.WEBSERVER_PORTS must be on the form [(proxyport, serverport), ...]") + if hasattr(settings, "BASE_COMM_TYPECLASS"): + raise DeprecationWarning(deprstring % ("BASE_COMM_TYPECLASS", "BASE_CHANNEL_TYPECLASS")) + if hasattr(settings, "COMM_TYPECLASS_PATHS"): + raise DeprecationWarning(deprstring % ("COMM_TYPECLASS_PATHS", "CHANNEL_TYPECLASS_PATHS")) + if hasattr(settings, "CHARACTER_DEFAULT_HOME"): + raise DeprecationWarning("settings.CHARACTER_DEFAULT_HOME should be renamed to DEFAULT_HOME. " \ + "See also settings.START_LOCATION (see evennia/settings_default.py).") + + from evennia.commands import cmdsethandler + if not cmdsethandler.import_cmdset(settings.CMDSET_UNLOGGEDIN, None): print "Warning: CMDSET_UNLOGGED failed to load!" + if not cmdsethandler.import_cmdset(settings.CMDSET_CHARACTER, None): print "Warning: CMDSET_CHARACTER failed to load" + if not cmdsethandler.import_cmdset(settings.CMDSET_PLAYER, None): print "Warning: CMDSET_PLAYER failed to load" + # typeclasses + imp(settings.BASE_PLAYER_TYPECLASS) + imp(settings.BASE_OBJECT_TYPECLASS) + imp(settings.BASE_CHARACTER_TYPECLASS) + imp(settings.BASE_ROOM_TYPECLASS) + imp(settings.BASE_EXIT_TYPECLASS) + imp(settings.BASE_SCRIPT_TYPECLASS) + + +def init_game_directory(path, check_db=True): + """ + Try to analyze the given path to find settings.py - this defines + the game directory and also sets PYTHONPATH as well as the + django path. + """ + # set the GAMEDIR path + set_gamedir(path) + + # Add gamedir to python path + sys.path.insert(0, GAMEDIR) + + if sys.argv[1] == 'test': + os.environ['DJANGO_SETTINGS_MODULE'] = 'evennia.settings_default' + else: + os.environ['DJANGO_SETTINGS_MODULE'] = SETTINGS_DOTPATH + + # required since django1.7 + django.setup() + + # test existence of the settings module + try: + from django.conf import settings + except Exception, ex: + if not str(ex).startswith("No module named"): + import traceback + print traceback.format_exc().strip() + print ERROR_SETTINGS + sys.exit() + + # this will both check the database and initialize the evennia dir. + if check_db: + check_database() + + # set up the Evennia executables and log file locations + global SERVER_PY_FILE, PORTAL_PY_FILE + global SERVER_LOGFILE, PORTAL_LOGFILE, HTTP_LOGFILE + global SERVER_PIDFILE, PORTAL_PIDFILE + global SERVER_RESTART, PORTAL_RESTART + global EVENNIA_VERSION + + SERVER_PY_FILE = os.path.join(EVENNIA_LIB, "server", "server.py") + PORTAL_PY_FILE = os.path.join(EVENNIA_LIB, "portal", "portal", "portal.py") + + SERVER_PIDFILE = os.path.join(GAMEDIR, SERVERDIR, "server.pid") + PORTAL_PIDFILE = os.path.join(GAMEDIR, SERVERDIR, "portal.pid") + + SERVER_RESTART = os.path.join(GAMEDIR, SERVERDIR, "server.restart") + PORTAL_RESTART = os.path.join(GAMEDIR, SERVERDIR, "portal.restart") + + SERVER_LOGFILE = settings.SERVER_LOG_FILE + PORTAL_LOGFILE = settings.PORTAL_LOG_FILE + HTTP_LOGFILE = settings.HTTP_LOG_FILE + + if os.name == 'nt': + # We need to handle Windows twisted separately. We create a + # batchfile in game/server, linking to the actual binary + + global TWISTED_BINARY + TWISTED_BINARY = "twistd.bat" + + # add path so system can find the batfile + sys.path.insert(1, os.path.join(GAMEDIR, SERVERDIR)) + + try: + importlib.import_module("win32api") + except ImportError: + print ERROR_WINDOWS_WIN32API + sys.exit() + + batpath = os.path.join(EVENNIA_SERVER, TWISTED_BINARY) + if not os.path.exists(batpath): + # Test for executable twisted batch file. This calls the + # twistd.py executable that is usually not found on the + # path in Windows. It's not enough to locate + # scripts.twistd, what we want is the executable script + # C:\PythonXX/Scripts/twistd.py. Alas we cannot hardcode + # this location since we don't know if user has Python in + # a non-standard location. So we try to figure it out. + twistd = importlib.import_module("twisted.scripts.twistd") + twistd_dir = os.path.dirname(twistd.__file__) + + # note that we hope the twistd package won't change here, since we + # try to get to the executable by relative path. + twistd_path = os.path.abspath(os.path.join(twistd_dir, + os.pardir, os.pardir, os.pardir, os.pardir, + 'scripts', 'twistd.py')) + + with open(batpath, 'w') as bat_file: + # build a custom bat file for windows + bat_file.write("@\"%s\" \"%s\" %%*" % (sys.executable, twistd_path)) + + print INFO_WINDOWS_BATFILE.format(twistd_path=twistd_path) + +def run_dummyrunner(number_of_dummies): + """ + Start an instance of the dummyrunner + + The dummy players' behavior can be customized by adding a + dummyrunner_settings.py config file in the game's conf directory. + """ + number_of_dummies = str(int(number_of_dummies)) if number_of_dummies else 1 + cmdstr = [sys.executable, EVENNIA_DUMMYRUNNER, "-N", number_of_dummies] + config_file = os.path.join(SETTINGS_PATH, "dummyrunner_settings.py") + if os.path.exists(config_file): + cmdstr.extend(["--config", config_file]) + try: + call(cmdstr, env=getenv()) + except KeyboardInterrupt: + pass + +def run_menu(): + """ + This launches an interactive menu. + """ + while True: + # menu loop + + print MENU + inp = raw_input(" option > ") + + # quitting and help + if inp.lower() == 'q': + return + elif inp.lower() == 'h': + print HELP_ENTRY + raw_input("press to continue ...") + continue + elif inp.lower() in ('v', 'i', 'a'): + print show_version_info(about=True) + raw_input("press to continue ...") + continue + + # options + try: + inp = int(inp) + except ValueError: + print "Not a valid option." + continue + if inp == 1: + # start everything, log to log files + server_operation("start", "all", False, False) + elif inp == 2: + # start everything, server interactive start + server_operation("start", "all", True, False) + elif inp == 3: + # start everything, portal interactive start + server_operation("start", "server", False, False) + server_operation("start", "portal", True, False) + elif inp == 4: + # start both server and portal interactively + server_operation("start", "server", True, False) + server_operation("start", "portal", True, False) + elif inp == 5: + # reload the server + server_operation("reload", "server", None, None) + elif inp == 6: + # reload the portal + server_operation("reload", "portal", None, None) + elif inp == 7: + # stop server and portal + server_operation("stop", "all", None, None) + elif inp == 8: + # stop server + server_operation("stop", "server", None, None) + elif inp == 9: + # stop portal + server_operation("stop", "portal", None, None) + else: + print "Not a valid option." + continue + return + + +def server_operation(mode, service, interactive, profiler): + """ + Handle argument options given on the command line. + + mode - str; start/stop etc + service - str; server, portal or all + interactive - bool; use interactive mode or daemon + profiler - run the service under the profiler + """ + + cmdstr = [sys.executable, EVENNIA_RUNNER] + errmsg = "The %s does not seem to be running." + + if mode == 'start': + + # launch the error checker. Best to catch the errors already here. + error_check_python_modules() + + # starting one or many services + if service == 'server': + if profiler: + cmdstr.append('--pserver') + if interactive: + cmdstr.append('--iserver') + cmdstr.append('--noportal') + elif service == 'portal': + if profiler: + cmdstr.append('--pportal') + if interactive: + cmdstr.append('--iportal') + cmdstr.append('--noserver') + django.core.management.call_command('collectstatic', verbosity=1, interactive=False) + else: # all + # for convenience we don't start logging of + # portal, only of server with this command. + if profiler: + cmdstr.append('--pserver') # this is the common case + if interactive: + cmdstr.append('--iserver') + django.core.management.call_command('collectstatic', verbosity=1, interactive=False) + cmdstr.extend([GAMEDIR, TWISTED_BINARY, SERVER_LOGFILE, PORTAL_LOGFILE, HTTP_LOGFILE]) + # start the server + Popen(cmdstr, env=getenv()) + + elif mode == 'reload': + # restarting services + if os.name == 'nt': + print "Restarting from command line is not supported under Windows. Log into the game to restart." + return + if service == 'server': + kill(SERVER_PIDFILE, SIG, "Server reloaded.", errmsg % 'Server', SERVER_RESTART, restart=True) + elif service == 'portal': + print """ + Note: Portal usually doesnt't need to be reloaded unless you are debugging in interactive mode. + If Portal was running in default Daemon mode, it cannot be restarted. In that case you have + to restart it manually with 'evennia.py start portal' + """ + kill(PORTAL_PIDFILE, SIG, "Portal reloaded (or stopped, if it was in daemon mode).", errmsg % 'Portal', PORTAL_RESTART, restart=True) + else: # all + # default mode, only restart server + kill(SERVER_PIDFILE, SIG, "Server reload.", errmsg % 'Server', SERVER_RESTART, restart=True) + + elif mode == 'stop': + # stop processes, avoiding reload + if service == 'server': + kill(SERVER_PIDFILE, SIG, "Server stopped.", errmsg % 'Server', SERVER_RESTART) + elif service == 'portal': + kill(PORTAL_PIDFILE, SIG, "Portal stopped.", errmsg % 'Portal', PORTAL_RESTART) + else: + kill(PORTAL_PIDFILE, SIG, "Portal stopped.", errmsg % 'Portal', PORTAL_RESTART) + kill(SERVER_PIDFILE, SIG, "Server stopped.", errmsg % 'Server', SERVER_RESTART) + + + +def main(): + """ + Run the evennia main program. + """ + + # set up argument parser + + parser = ArgumentParser(description=CMDLINE_HELP) + parser.add_argument('-v', '--version', action='store_true', + dest='show_version', default=False, + help="Show version info.") + parser.add_argument('-i', '--interactive', action='store_true', + dest='interactive', default=False, + help="Start given processes in interactive mode.") + parser.add_argument('--init', action='store', dest="init", metavar="name", + help="Creates a new game directory 'name' at the current location.") + parser.add_argument('--profiler', action='store_true', dest='profiler', default=False, + help="Start given server component under the Python profiler.") + parser.add_argument('--dummyrunner', nargs=1, action='store', dest='dummyrunner', metavar="N", + help="Tests a running server by connecting N dummy players to it.") + parser.add_argument("option", nargs='?', default="noop", + help="Operational mode: 'start', 'stop', 'restart' or 'menu'.") + parser.add_argument("service", metavar="component", nargs='?', default="all", + help="Server component to operate on: 'server', 'portal' or 'all' (default).") + parser.epilog = "Example django-admin commands: 'migrate', 'flush', 'shell' and 'dbshell'. " \ + "See the django documentation for more django-admin commands." + + args, unknown_args = parser.parse_known_args() + + # handle arguments + + option, service = args.option, args.service + + check_main_evennia_dependencies() + + if not args: + print CMDLINE_HELP + sys.exit() + elif args.init: + create_game_directory(args.init) + print CREATED_NEW_GAMEDIR.format(gamedir=args.init, + settings_path=os.path.join(args.init, SETTINGS_PATH)) + sys.exit() + + if args.show_version: + print show_version_info(option=="help") + sys.exit() + + if args.dummyrunner: + # launch the dummy runner + init_game_directory(CURRENT_DIR, check_db=True) + run_dummyrunner(args.dummyrunner[0]) + elif option == 'menu': + # launch menu for operation + init_game_directory(CURRENT_DIR, check_db=True) + run_menu() + elif option in ('start', 'reload', 'stop'): + # operate the server directly + init_game_directory(CURRENT_DIR, check_db=True) + server_operation(option, service, args.interactive, args.profiler) + elif option != "noop": + # pass-through to django manager + check_db = False + if option in ('runserver', 'testserver'): + print WARNING_RUNSERVER + if option == "shell": + # to use the shell we need to initialize it first, + # and this only works if the database is set up + check_db = True + init_game_directory(CURRENT_DIR, check_db=check_db) + + args = [option] + kwargs = {} + if service not in ("all", "server", "portal"): + args.append(service) + if unknown_args: + for arg in unknown_args: + if arg.startswith("--"): + kwargs[arg.lstrip("--")] = True + else: + args.append(arg) + try: + django.core.management.call_command(*args, **kwargs) + except django.core.management.base.CommandError: + args = ", ".join(args) + kwargs = ", ".join(["--%s" % kw for kw in kwargs]) + print ERROR_INPUT.format(args=args, kwargs=kwargs) + else: + # no input; print evennia info + print ABOUT_INFO + + +if __name__ == '__main__': + # start Evennia from the command line + main() diff --git a/game/runner.py b/evennia/server/evennia_runner.py similarity index 51% rename from game/runner.py rename to evennia/server/evennia_runner.py index d9a4b02f3..00a5d77a5 100644 --- a/game/runner.py +++ b/evennia/server/evennia_runner.py @@ -1,10 +1,10 @@ #!/usr/bin/env python """ -This runner is controlled by evennia.py and should normally not be - launched directly. It manages the two main Evennia processes (Server - and Portal) and most importanly runs a passive, threaded loop that - makes sure to restart Server whenever it shuts down. +This runner is controlled by the evennia launcher and should normally +not be launched directly. It manages the two main Evennia processes +(Server and Portal) and most importanly runs a passive, threaded loop +that makes sure to restart Server whenever it shuts down. Since twistd does not allow for returning an optional exit code we need to handle the current reload state for server and portal with @@ -16,10 +16,10 @@ matter the value of this file. """ import os import sys -from optparse import OptionParser +from argparse import ArgumentParser from subprocess import Popen import Queue, thread -import django +import evennia try: # check if launched with pypy @@ -27,65 +27,56 @@ try: except ImportError: is_pypy = False -# -# System Configuration -# +SERVER = None +PORTAL = None -SERVER_PIDFILE = "server.pid" -PORTAL_PIDFILE = "portal.pid" +EVENNIA_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +EVENNIA_BIN = os.path.join(EVENNIA_ROOT, "bin") +EVENNIA_LIB = os.path.dirname(evennia.__file__) -SERVER_RESTART = "server.restart" -PORTAL_RESTART = "portal.restart" +SERVER_PY_FILE = os.path.join(EVENNIA_LIB,'server', 'server.py') +PORTAL_PY_FILE = os.path.join(EVENNIA_LIB, 'server', 'portal', 'portal.py') -# Set the Python path up so we can get to settings.py from here. -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -os.environ['DJANGO_SETTINGS_MODULE'] = 'game.settings' +GAMEDIR = None +SERVERDIR = "server" +SERVER_PIDFILE = None +PORTAL_PIDFILE = None +SERVER_RESTART = None +PORTAL_RESTART = None +SERVER_LOGFILE = None +PORTAL_LOGFILE = None +HTTP_LOGFILE = None +PPROFILER_LOGFILE = None +SPROFILER_LOGFILE = None -if not os.path.exists('settings.py'): +# messages - print "No settings.py file found. Run evennia.py to create it." - sys.exit() +CMDLINE_HELP = \ + """ + This program manages the running Evennia processes. It is called + by evennia and should not be started manually. Its main task is to + sit and watch the Server and restart it whenever the user reloads. + The runner depends on four files for its operation, two PID files + and two RESTART files for Server and Portal respectively; these + are stored in the game's server/ directory. + """ -django.setup() +PROCESS_ERROR = \ + """ + {component} process error: {traceback}. + """ -# Get the settings -from django.conf import settings +PROCESS_IOERROR = \ + """ + {component} IOError: {traceback} + One possible explanation is that 'twistd' was not found. + """ -# Setup access of the evennia server itself -SERVER_PY_FILE = os.path.join(settings.SRC_DIR, 'server/server.py') -PORTAL_PY_FILE = os.path.join(settings.SRC_DIR, 'server/portal/portal.py') +PROCESS_RESTART = "{component} restarting ..." -# Get logfile names -SERVER_LOGFILE = settings.SERVER_LOG_FILE -PORTAL_LOGFILE = settings.PORTAL_LOG_FILE -HTTP_LOGFILE = settings.HTTP_LOG_FILE.strip() - -CYCLE_LOGFILES = settings.CYCLE_LOGFILES - -# Add this to the environmental variable for the 'twistd' command. -currpath = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -if 'PYTHONPATH' in os.environ: - os.environ['PYTHONPATH'] += (":%s" % currpath) -else: - os.environ['PYTHONPATH'] = currpath - -TWISTED_BINARY = 'twistd' -if os.name == 'nt': - TWISTED_BINARY = 'twistd.bat' - err = False - try: - import win32api # Test for for win32api - except ImportError: - err = True - if not os.path.exists(TWISTED_BINARY): - err = True - if err: - print "Twisted binary for Windows is not ready to use. Please run evennia.py." - sys.exit() # Functions - def set_restart_mode(restart_file, flag="reload"): """ This sets a flag file for the restart mode. @@ -94,6 +85,17 @@ def set_restart_mode(restart_file, flag="reload"): f.write(str(flag)) +def getenv(): + """ + Get current environment and add PYTHONPATH + """ + sep = ";" if os.name == "nt" else ":" + env = os.environ.copy() + sys.path.insert(0, GAMEDIR) + env['PYTHONPATH'] = sep.join(sys.path) + return env + + def get_restart_mode(restart_file): """ Parse the server/portal restart status @@ -130,9 +132,6 @@ def cycle_logfile(logfile): # Start program management -SERVER = None -PORTAL = None - def start_services(server_argv, portal_argv): """ @@ -144,18 +143,18 @@ def start_services(server_argv, portal_argv): def server_waiter(queue): try: - rc = Popen(server_argv).wait() + rc = Popen(server_argv, env=getenv()).wait() except Exception, e: - print "Server process error: %(e)s" % {'e': e} + print PROCESS_ERROR.format(component="Server", traceback=e) return # this signals the controller that the program finished queue.put(("server_stopped", rc)) def portal_waiter(queue): try: - rc = Popen(portal_argv).wait() + rc = Popen(portal_argv, env=getenv()).wait() except Exception, e: - print "Portal process error: %(e)s" % {'e': e} + print PROCESS_ERROR.format(component="Portal", traceback=e) return # this signals the controller that the program finished queue.put(("portal_stopped", rc)) @@ -168,9 +167,9 @@ def start_services(server_argv, portal_argv): else: # normal operation: start portal as a daemon; # we don't care to monitor it for restart - PORTAL = Popen(portal_argv) + PORTAL = Popen(portal_argv, env=getenv()) except IOError, e: - print "Portal IOError: %s\nA possible explanation for this is that 'twistd' is not found." % e + print PROCESS_IOERROR.format(component="Portal", traceback=e) return try: @@ -178,7 +177,7 @@ def start_services(server_argv, portal_argv): # start server as a reloadable thread SERVER = thread.start_new_thread(server_waiter, (processes, )) except IOError, e: - print "Server IOError: %s\nA possible explanation for this is that 'twistd' is not found." % e + print PROCESS_IOERROR.format(component="Server", traceback=e) return # Reload loop @@ -190,53 +189,67 @@ def start_services(server_argv, portal_argv): # restart only if process stopped cleanly if (message == "server_stopped" and int(rc) == 0 and get_restart_mode(SERVER_RESTART) in ("True", "reload", "reset")): - print "Evennia Server stopped. Restarting ..." + print PROCESS_RESTART.format(component="Server") SERVER = thread.start_new_thread(server_waiter, (processes, )) continue # normally the portal is not reloaded since it's run as a daemon. if (message == "portal_stopped" and int(rc) == 0 and get_restart_mode(PORTAL_RESTART) == "True"): - print "Evennia Portal stopped in interactive mode. Restarting ..." + print PROCESS_RESTART.format(component="Portal") PORTAL = thread.start_new_thread(portal_waiter, (processes, )) continue break -# Setup signal handling def main(): """ - This handles the command line input of the runner - (it's most often called by evennia.py) + This handles the command line input of the runner, usually created by + the evennia launcher """ - parser = OptionParser(usage="%prog [options] start", - description="This runner should normally *not* be called directly - it is called automatically from the evennia.py main program. It manages the Evennia game server and portal processes an hosts a threaded loop to restart the Server whenever it is stopped (this constitues Evennia's reload mechanism).") - parser.add_option('-s', '--noserver', action='store_true', - dest='noserver', default=False, - help='Do not start Server process') - parser.add_option('-p', '--noportal', action='store_true', - dest='noportal', default=False, - help='Do not start Portal process') - parser.add_option('-i', '--iserver', action='store_true', - dest='iserver', default=False, - help='output server log to stdout instead of logfile') - parser.add_option('-d', '--iportal', action='store_true', - dest='iportal', default=False, - help='output portal log to stdout. Does not make portal a daemon.') - parser.add_option('-S', '--profile-server', action='store_true', - dest='sprof', default=False, - help='run server under cProfile') - parser.add_option('-P', '--profile-portal', action='store_true', - dest='pprof', default=False, - help='run portal under cProfile') + parser = ArgumentParser(description=CMDLINE_HELP) + parser.add_argument('--noserver', action='store_true', dest='noserver', + default=False, help='Do not start Server process') + parser.add_argument('--noportal', action='store_true', dest='noportal', + default=False, help='Do not start Portal process') + parser.add_argument('--iserver', action='store_true', dest='iserver', + default=False, help='Server in interactive mode') + parser.add_argument('--iportal', action='store_true', dest='iportal', + default=False, help='Portal in interactive mode') + parser.add_argument('--pserver', action='store_true', dest='pserver', + default=False, help='Profile Server') + parser.add_argument('--pportal', action='store_true', dest='pportal', + default=False, help='Profile Portal') + parser.add_argument('--nologcycle', action='store_false', dest='nologcycle', + default=True, help='Do not cycle log files') + parser.add_argument('gamedir', help="path to game dir") + parser.add_argument('twistdbinary', help="path to twistd binary") + parser.add_argument('slogfile', help="path to server log file") + parser.add_argument('plogfile', help="path to portal log file") + parser.add_argument('hlogfile', help="path to http log file") - options, args = parser.parse_args() + args = parser.parse_args() - if not args or args[0] != 'start': - # this is so as to avoid runner.py be accidentally launched manually. - parser.print_help() - sys.exit() + global GAMEDIR + global SERVER_LOGFILE, PORTAL_LOGFILE, HTTP_LOGFILE + global SERVER_PIDFILE, PORTAL_PIDFILE + global SERVER_RESTART, PORTAL_RESTART + global SPROFILER_LOGFILE, PPROFILER_LOGFILE + + GAMEDIR = args.gamedir + sys.path.insert(1, os.path.join(GAMEDIR, SERVERDIR)) + + SERVER_PIDFILE = os.path.join(GAMEDIR, SERVERDIR, "server.pid") + PORTAL_PIDFILE = os.path.join(GAMEDIR, SERVERDIR, "portal.pid") + SERVER_RESTART = os.path.join(GAMEDIR, SERVERDIR, "server.restart") + PORTAL_RESTART = os.path.join(GAMEDIR, SERVERDIR, "portal.restart") + SERVER_LOGFILE = args.slogfile + PORTAL_LOGFILE = args.plogfile + HTTP_LOGFILE = args.hlogfile + TWISTED_BINARY = args.twistdbinary + SPROFILER_LOGFILE = os.path.join(GAMEDIR, SERVERDIR, "logs", "server.prof") + PPROFILER_LOGFILE = os.path.join(GAMEDIR, SERVERDIR, "logs", "portal.prof") # set up default project calls server_argv = [TWISTED_BINARY, @@ -251,57 +264,57 @@ def main(): # Profiling settings (read file from python shell e.g with # p = pstats.Stats('server.prof') - sprof_argv = ['--savestats', - '--profiler=cprofile', - '--profile=server.prof'] - pprof_argv = ['--savestats', - '--profiler=cprofile', - '--profile=portal.prof'] + pserver_argv = ['--savestats', + '--profiler=cprofile', + '--profile=%s' % SPROFILER_LOGFILE] + pportal_argv = ['--savestats', + '--profiler=cprofile', + '--profile=%s' % PPROFILER_LOGFILE] # Server pid = get_pid(SERVER_PIDFILE) - if pid and not options.noserver: + if pid and not args.noserver: print "\nEvennia Server is already running as process %(pid)s. Not restarted." % {'pid': pid} - options.noserver = True - if options.noserver: + args.noserver = True + if args.noserver: server_argv = None else: set_restart_mode(SERVER_RESTART, "shutdown") - if options.iserver: + if args.iserver: # don't log to server logfile del server_argv[2] print "\nStarting Evennia Server (output to stdout)." else: - if CYCLE_LOGFILES: + if not args.nologcycle: cycle_logfile(SERVER_LOGFILE) print "\nStarting Evennia Server (output to server logfile)." - if options.sprof: - server_argv.extend(sprof_argv) + if args.pserver: + server_argv.extend(pserver_argv) print "\nRunning Evennia Server under cProfile." # Portal pid = get_pid(PORTAL_PIDFILE) - if pid and not options.noportal: + if pid and not args.noportal: print "\nEvennia Portal is already running as process %(pid)s. Not restarted." % {'pid': pid} - options.noportal = True - if options.noportal: + args.noportal = True + if args.noportal: portal_argv = None else: - if options.iportal: + if args.iportal: # make portal interactive portal_argv[1] = '--nodaemon' set_restart_mode(PORTAL_RESTART, True) print "\nStarting Evennia Portal in non-Daemon mode (output to stdout)." else: - if CYCLE_LOGFILES: + if not args.nologcycle: cycle_logfile(PORTAL_LOGFILE) cycle_logfile(HTTP_LOGFILE) set_restart_mode(PORTAL_RESTART, False) print "\nStarting Evennia Portal in Daemon mode (output to portal logfile)." - if options.pprof: - portal_argv.extend(pprof_argv) + if args.pportal: + portal_argv.extend(pportal_argv) print "\nRunning Evennia Portal under cProfile." # Windows fixes (Windows don't support pidfiles natively) @@ -315,6 +328,4 @@ def main(): start_services(server_argv, portal_argv) if __name__ == '__main__': - from src.utils.utils import check_evennia_dependencies - if check_evennia_dependencies(): - main() + main() diff --git a/src/server/initial_setup.py b/evennia/server/initial_setup.py similarity index 67% rename from src/server/initial_setup.py rename to evennia/server/initial_setup.py index a7af784ed..fd5480db7 100644 --- a/src/server/initial_setup.py +++ b/evennia/server/initial_setup.py @@ -8,10 +8,39 @@ Everything starts at handle_setup() import django from django.conf import settings -from django.contrib.auth import get_user_model -from src.server.models import ServerConfig -from src.utils import create from django.utils.translation import ugettext as _ +from evennia.players.models import PlayerDB +from evennia.server.models import ServerConfig +from evennia.utils import create + + +ERROR_NO_SUPERUSER = \ + """ + No superuser exists yet. The superuser is the 'owner' account on + the Evennia server. Create a new superuser using the command + + evennia createsuperuser + + Follow the prompts, then restart the server. + """ + + +LIMBO_DESC = \ + _(""" +Welcome to your new {wEvennia{n-based game! Visit http://www.evennia.com if you need +help, want to contribute, report issues or just join the community. +As Player #1 you can create a demo/tutorial area with {w@batchcommand tutorial_world.build{n. + """) + + +WARNING_POSTGRESQL_FIX = \ + """ + PostgreSQL-psycopg2 compatability fix: + The in-game channels {chan1}, {chan2} and {chan3} were created, + but the superuser was not yet connected to them. Please use in + game commands to connect Player #1 to those channels when first + logging in. + """ def create_config_values(): @@ -21,21 +50,14 @@ def create_config_values(): ServerConfig.objects.conf("site_name", settings.SERVERNAME) ServerConfig.objects.conf("idle_timeout", settings.IDLE_TIMEOUT) - def get_god_player(): """ - Creates the god user. + Creates the god user and don't take no for an answer. """ - PlayerDB = get_user_model() try: god_player = PlayerDB.objects.get(id=1) except PlayerDB.DoesNotExist: - txt = "\n\nNo superuser exists yet. The superuser is the 'owner'" - txt += "\account on the Evennia server. Create a new superuser using" - txt += "\nthe command" - txt += "\n\n python manage.py createsuperuser" - txt += "\n\nFollow the prompts, then restart the server." - raise Exception(txt) + raise PlayerDB.DoesNotExist(ERROR_NO_SUPERUSER) return god_player @@ -56,7 +78,7 @@ def create_objects(): # run all creation hooks on god_player (we must do so manually # since the manage.py command does not) - god_player.typeclass_path = player_typeclass + god_player.swap_typeclass(player_typeclass, clean_attributes=True) god_player.basetype_setup() god_player.at_player_creation() god_player.locks.add("examine:perm(Immortals);edit:false();delete:false();boot:false();msg:all()") @@ -69,14 +91,15 @@ def create_objects(): # it to exist in Limbo. character_typeclass = settings.BASE_CHARACTER_TYPECLASS god_character = create.create_object(character_typeclass, - key=god_player.username, nohome=True) + key=god_player.username, + nohome=True) god_character.id = 1 + god_character.save() god_character.db.desc = _('This is User #1.') god_character.locks.add("examine:perm(Immortals);edit:false();delete:false();boot:false();msg:all();puppet:false()") god_character.permissions.add("Immortals") - god_character.save() god_player.attributes.add("_first_login", True) god_player.attributes.add("_last_puppet", god_character) god_player.db._playable_characters.append(god_character) @@ -84,16 +107,8 @@ def create_objects(): room_typeclass = settings.BASE_ROOM_TYPECLASS limbo_obj = create.create_object(room_typeclass, _('Limbo'), nohome=True) limbo_obj.id = 2 - string = " ".join([ - "Welcome to your new {wEvennia{n-based game. From here you are ready", - "to begin development. Visit http://evennia.com if you should need", - "help or would like to participate in community discussions. If you", - "are logged in as User #1 you can create a demo/tutorial area with", - "'@batchcommand contrib.tutorial_world.build'. Log out and create", - "a new non-admin account at the login screen to play the tutorial", - "properly."]) - string = _(string) - limbo_obj.db.desc = string + limbo_obj.save() + limbo_obj.db.desc = LIMBO_DESC.strip() limbo_obj.save() # Now that Limbo exists, try to set the user up in Limbo (unless @@ -110,48 +125,17 @@ def create_channels(): """ print " Creating default channels ..." - # public channel - key1, aliases, desc, locks = settings.CHANNEL_PUBLIC - pchan = create.create_channel(key1, aliases, desc, locks=locks) - # mudinfo channel - key2, aliases, desc, locks = settings.CHANNEL_MUDINFO - ichan = create.create_channel(key2, aliases, desc, locks=locks) - # connectinfo channel - key3, aliases, desc, locks = settings.CHANNEL_CONNECTINFO - cchan = create.create_channel(key3, aliases, desc, locks=locks) - - # TODO: postgresql-psycopg2 has a strange error when trying to - # connect the user to the default channels. It works fine from inside - # the game, but not from the initial startup. We are temporarily bypassing - # the problem with the following fix. See Evennia Issue 151. - if ((".".join(str(i) for i in django.VERSION) < "1.2" - and settings.DATABASE_ENGINE == "postgresql_psycopg2") - or (hasattr(settings, 'DATABASES') - and settings.DATABASES.get("default", {}).get('ENGINE', None) - == 'django.db.backends.postgresql_psycopg2')): - warning = """ - PostgreSQL-psycopg2 compatability fix: - The in-game channels %s, %s and %s were created, - but the superuser was not yet connected to them. Please use in - game commands to onnect Player #1 to those channels when first - logging in. - """ % (key1, key2, key3) - print warning - return - - # connect the god user to all these channels by default. goduser = get_god_player() - pchan.connect(goduser) - ichan.connect(goduser) - cchan.connect(goduser) - + for channeldict in settings.DEFAULT_CHANNELS: + channel = create.create_channel(**channeldict) + channel.connect(goduser) def create_system_scripts(): """ Setup the system repeat scripts. They are automatically started by the create_script function. """ - from src.scripts import scripts + from evennia.scripts import scripts print " Creating and starting global scripts ..." @@ -176,7 +160,7 @@ def start_game_time(): (the uptime can also be found directly from the server though). """ print " Starting in-game time ..." - from src.utils import gametime + from evennia.utils import gametime gametime.init_gametime() @@ -206,7 +190,7 @@ def reset_server(): ones, particularly it cleans all caches for the special objects. It also checks so the warm-reset mechanism works as it should. """ - from src.server.sessionhandler import SESSIONS + from evennia.server.sessionhandler import SESSIONS print " Initial setup complete. Restarting Server once." SESSIONS.server.shutdown(mode='reset') @@ -250,17 +234,12 @@ def handle_setup(last_step): setup_func() except Exception: if last_step + num == 2: - from src.players.models import PlayerDB - from src.objects.models import ObjectDB - + from evennia.objects.models import ObjectDB for obj in ObjectDB.objects.all(): obj.delete() - for profile in PlayerDB.objects.all(): - profile.delete() elif last_step + num == 3: - from src.comms.models import ChannelDB, PlayerChannelConnection + from evennia.comms.models import ChannelDB ChannelDB.objects.all().delete() - PlayerChannelConnection.objects.all().delete() raise ServerConfig.objects.conf("last_initial_setup_step", last_step + num + 1) # We got through the entire list. Set last_step to -1 so we don't diff --git a/src/server/manager.py b/evennia/server/manager.py similarity index 100% rename from src/server/manager.py rename to evennia/server/manager.py diff --git a/src/server/migrations/0001_initial.py b/evennia/server/migrations/0001_initial.py similarity index 100% rename from src/server/migrations/0001_initial.py rename to evennia/server/migrations/0001_initial.py diff --git a/game/gamesrc/commands/examples/__init__.py b/evennia/server/migrations/__init__.py similarity index 100% rename from game/gamesrc/commands/examples/__init__.py rename to evennia/server/migrations/__init__.py diff --git a/src/server/models.py b/evennia/server/models.py similarity index 95% rename from src/server/models.py rename to evennia/server/models.py index 0b423f6e1..e12ef647c 100644 --- a/src/server/models.py +++ b/evennia/server/models.py @@ -14,9 +14,9 @@ except ImportError: import pickle from django.db import models -from src.utils.idmapper.models import WeakSharedMemoryModel -from src.utils import logger, utils -from src.server.manager import ServerConfigManager +from evennia.utils.idmapper.models import WeakSharedMemoryModel +from evennia.utils import logger, utils +from evennia.server.manager import ServerConfigManager #------------------------------------------------------------ diff --git a/evennia/server/oob_cmds.py b/evennia/server/oob_cmds.py new file mode 100644 index 000000000..4af506186 --- /dev/null +++ b/evennia/server/oob_cmds.py @@ -0,0 +1,382 @@ +""" +Out-of-band default plugin commands available for OOB handler. + +This module implements commands as defined by the MSDP standard +(http://tintin.sourceforge.net/msdp/), but is independent of the +actual transfer protocol (webclient, MSDP, GMCP etc). It also +implements several OOB commands unique to Evennia (both some +external and some for testing) + +The available OOB commands can be extended by changing + + `settings.OOB_PLUGIN_MODULES` + +This module must contain a global dictionary CMD_MAP. This is a +dictionary that maps the call available in the OOB call to a function +in this module (this allows you to map multiple oob cmdnames to a +single actual Python function, for example). + +For example, if the OOB strings received looks like this: + + MDSP.LISTEN [desc, key] # GMCP (wrapping to MSDP) + LISTEN ARRAY VAL desc VAL key # MSDP + +and CMD_MAP = {"LISTEN", listen} then this would result in a call to a +function "listen" in this module, with the arguments *("desc", "key"). + +oob functions have the following call signature: + + function(session, *args, **kwargs) + +where session is the active session and *args, **kwargs are extra +arguments sent with the oob command. + +A function mapped to the key "oob_error" will retrieve error strings +if it is defined. It will get the error message as its 1st argument. + + oob_error(session, error, *args, **kwargs) + +This allows for customizing error handling. + +Data is usually returned to the user via a return OOB call: + + session.msg(oob=(oobcmdname, (args,), {kwargs})) + +Oobcmdnames (like "MSDP.LISTEN" / "LISTEN" above) are case-sensitive. +Note that args, kwargs must be iterable. Non-iterables will be +interpreted as a new command name (you can send multiple oob commands +with one msg() call)) + +Evennia introduces two internal extensions to MSDP, and that is the +MSDP_ARRAY and MSDP_TABLE commands. These are never sent across the +wire to the client (so this is fully compliant with the MSDP +protocol), but tells the Evennia OOB Protocol that you want to send a +"bare" array or table to the client, without prepending any command +name. + +""" + +from django.conf import settings +from evennia.utils.utils import to_str +from evennia import OOB_HANDLER + +_GA = object.__getattribute__ +_SA = object.__setattr__ +_NA = lambda o: "N/A" + + +#------------------------------------------------------------ +# All OOB commands must be on the form +# cmdname(oobhandler, session, *args, **kwargs) +#------------------------------------------------------------ + +# +# General OOB commands +# + +def oob_error(session, errmsg, *args, **kwargs): + """ + Error handling method. Error messages are relayed here. + + Args: + session (Session): The session to receive the error + errmsg (str): The failure message + + A function with this name is special and is also called by the + oobhandler when an error occurs already at the execution stage + (such as the oob function not being recognized or having the wrong + args etc). Call this from other oob functions to centralize error + management. + + """ + session.msg(oob=("error", ("OOB ERROR: %s" % errmsg,))) + +def oob_echo(session, *args, **kwargs): + """ + Test echo function. Echoes args, kwargs sent to it. + + Args: + session (Session): The Session to receive the echo. + args (list of str): Echo text. + kwargs (dict of str, optional): Keyed echo text + + """ + session.msg(oob=("echo", args, kwargs)) + +##OOB{"repeat":10} +def oob_repeat(session, oobfuncname, interval, *args, **kwargs): + """ + Called as REPEAT + Repeats a given OOB command with a certain frequency. + + Args: + session (Session): Session creating the repeat + oobfuncname (str): OOB function called every interval seconds + interval (int): Interval of repeat, in seconds. + + Notes: + The command checks so that it cannot repeat itself. + + """ + if not oobfuncname: + oob_error(session, "Usage: REPEAT , ") + return + # limit repeat actions to minimum 5 seconds interval + interval = 20 if not interval else (max(5, interval)) + obj = session.get_puppet_or_player() + if obj and oobfuncname != "REPEAT": + OOB_HANDLER.add_repeater(obj, session.sessid, oobfuncname, interval, *args, **kwargs) + + +##OOB{"UNREPEAT":10} +def oob_unrepeat(session, oobfuncname, interval): + """ + Called with UNREPEAT + Disable repeating callback. + + Args: + session (Session): Session controlling the repeater + oobfuncname (str): OOB function called every interval seconds + interval (int): Interval of repeater, in seconds. + + Notes: + The command checks so that it cannot repeat itself. + + + """ + obj = session.get_puppet_or_player() + if obj: + OOB_HANDLER.remove_repeater(obj, session.sessid, oobfuncname, interval) + + +# +# MSDP protocol standard commands +# +# MSDP suggests the following standard name conventions for making +# different properties available to the player + +# "CHARACTER_NAME", "SERVER_ID", "SERVER_TIME", "AFFECTS", "ALIGNMENT", "EXPERIENCE", "EXPERIENCE_MAX", "EXPERIENCE_TNL", +# "HEALTH", "HEALTH_MAX", "LEVEL", "RACE", "CLASS", "MANA", "MANA_MAX", "WIMPY", "PRACTICE", "MONEY", "MOVEMENT", +# "MOVEMENT_MAX", "HITROLL", "DAMROLL", "AC", "STR", "INT", "WIS", "DEX", "CON", "OPPONENT_HEALTH", "OPPONENT_HEALTH_MAX", +# "OPPONENT_LEVEL", "OPPONENT_NAME", "AREA_NAME", "ROOM_EXITS", "ROOM_VNUM", "ROOM_NAME", "WORLD_TIME", "CLIENT_ID", +# "CLIENT_VERSION", "PLUGIN_ID", "ANSI_COLORS", "XTERM_256_COLORS", "UTF_8", "SOUND", "MXP", "BUTTON_1", "BUTTON_2", +# "BUTTON_3", "BUTTON_4", "BUTTON_5", "GAUGE_1", "GAUGE_2","GAUGE_3", "GAUGE_4", "GAUGE_5" + + +# mapping from MSDP standard names to Evennia variables +OOB_SENDABLE = { + "CHARACTER_NAME": lambda o: o.key, + "SERVER_ID": lambda o: settings.SERVERNAME, + "ROOM_NAME": lambda o: o.db_location.key, + "ANSI_COLORS": lambda o: True, + "XTERM_256_COLORS": lambda o: True, + "UTF_8": lambda o: True + } + + +##OOB{"SEND":"CHARACTER_NAME"} - from webclient +def oob_send(session, *args, **kwargs): + """ + Called with the SEND MSDP command. + This function directly returns the value of the given variable to + the session. It assumes the object on which the variable sits + belongs to the session. + + Args: + session (Session): Session object + args (str): any number of properties to return. These + must belong to the OOB_SENDABLE dictionary. + Examples: + oob input: ("SEND", "CHARACTER_NAME", "SERVERNAME") + oob output: ("MSDP_TABLE", "CHARACTER_NAME", "Amanda", + "SERVERNAME", "Evennia") + + """ + # mapping of MSDP name to a property + obj = session.get_puppet_or_player() + ret = {} + if obj: + for name in (a.upper() for a in args if a): + try: + #print "MSDP SEND inp:", name + value = OOB_SENDABLE.get(name, _NA)(obj) + ret[name] = value + except Exception, e: + ret[name] = str(e) + # return, make sure to use the right case + session.msg(oob=("MSDP_TABLE", (), ret)) + else: + oob_error(session, "You must log in first.") + + +# mapping standard MSDP keys to Evennia field names +OOB_REPORTABLE = { + "CHARACTER_NAME": "db_key", + "ROOM_NAME": "db_location", + "TEST" : "test" + } + +##OOB{"REPORT":"TEST"} +def oob_report(session, *args, **kwargs): + """ + Called with the `REPORT PROPNAME` MSDP command. + Monitors the changes of given property name. Assumes reporting + happens on an objcet controlled by the session. + + Args: + session (Session): The Session doing the monitoring. The + property is assumed to sit on the entity currently + controlled by the Session. If puppeting, this is an + Object, otherwise the object will be the Player the + Session belongs to. + args (str or list): One or more property names to monitor changes in. + If a name starts with `db_`, the property is assumed to + be a field, otherwise an Attribute of the given name will + be monitored (if it exists). + + Notes: + When the property updates, the monitor will send a MSDP_ARRAY + to the session of the form `(SEND, fieldname, new_value)` + Examples: + ("REPORT", "CHARACTER_NAME") + ("MSDP_TABLE", "CHARACTER_NAME", "Amanda") + + """ + obj = session.get_puppet_or_player() + if obj: + ret = [] + for name in args: + propname = OOB_REPORTABLE.get(name, None) + if not propname: + oob_error(session, "No Reportable property '%s'. Use LIST REPORTABLE_VARIABLES." % propname) + # the field_monitors require an oob function as a callback when they report a change. + elif propname.startswith("db_"): + OOB_HANDLER.add_field_monitor(obj, session.sessid, propname, "return_field_report") + ret.append(to_str(_GA(obj, propname), force_string=True)) + else: + OOB_HANDLER.add_attribute_monitor(obj, session.sessid, propname, "return_attribute_report") + ret.append(_GA(obj, "db_value")) + #print "ret:", ret + session.msg(oob=("MSDP_ARRAY", ret)) + else: + oob_error(session, "You must log in first.") + + +def oob_return_field_report(session, fieldname, obj, *args, **kwargs): + """ + This is a helper command called by the monitor when fieldname + changes. It is not part of the official MSDP specification but is + a callback used by the monitor to format the result before sending + it on. + """ + session.msg(oob=("MSDP_TABLE", (), + {fieldname: to_str(getattr(obj, fieldname), force_string=True)})) + + +def oob_return_attribute_report(session, fieldname, obj, *args, **kwargs): + """ + This is a helper command called by the monitor when an Attribute + changes. We need to handle this a little differently from fields + since we are generally not interested in the field name (it's + always db_value for Attributes) but the Attribute's name. + + This command is not part of the official MSDP specification but is + a callback used by the monitor to format the result before sending + it on. + """ + session.msg(oob=("MSDP_TABLE", (), + {obj.db_key: to_str(getattr(obj, fieldname), force_string=True)})) + + +##OOB{"UNREPORT": "TEST"} +def oob_unreport(session, *args, **kwargs): + """ + This removes tracking for the given data. + """ + obj = session.get_puppet_or_player() + if obj: + for name in (a.upper() for a in args if a): + propname = OOB_REPORTABLE.get(name, None) + if not propname: + oob_error(session, "No Un-Reportable property '%s'. Use LIST REPORTABLE_VARIABLES." % propname) + elif propname.startswith("db_"): + OOB_HANDLER.remove_field_monitor(obj, session.sessid, propname, "oob_return_field_report") + else: # assume attribute + OOB_HANDLER.remove_attribute_monitor(obj, session.sessid, propname, "oob_return_attribute_report") + else: + oob_error(session, "You must log in first.") + + +##OOB{"LIST":"COMMANDS"} +def oob_list(session, mode, *args, **kwargs): + """ + Called with the `LIST ` MSDP command. + + Args: + session (Session): The Session asking for the information + mode (str): The available properties. One of + "COMMANDS" Request an array of commands supported + by the server. + "LISTS" Request an array of lists supported + by the server. + "CONFIGURABLE_VARIABLES" Request an array of variables the client + can configure. + "REPORTABLE_VARIABLES" Request an array of variables the server + will report. + "REPORTED_VARIABLES" Request an array of variables currently + being reported. + "SENDABLE_VARIABLES" Request an array of variables the server + will send. + Examples: + oob in: LIST COMMANDS + oob out: (COMMANDS, (SEND, REPORT, LIST, ...) + """ + mode = mode.upper() + if mode == "COMMANDS": + session.msg(oob=("COMMANDS", ("LIST", + "REPORT", + "UNREPORT", + # "RESET", + "SEND"))) + elif mode == "REPORTABLE_VARIABLES": + session.msg(oob=("REPORTABLE_VARIABLES", tuple(key for key in OOB_REPORTABLE.keys()))) + elif mode == "REPORTED_VARIABLES": + # we need to check so as to use the right return value depending on if it is + # an Attribute (identified by tracking the db_value field) or a normal database field + # reported is a list of tuples (obj, propname, args, kwargs) + reported = OOB_HANDLER.get_all_monitors(session.sessid) + reported = [rep[0].key if rep[1] == "db_value" else rep[1] for rep in reported] + session.msg(oob=("REPORTED_VARIABLES", reported)) + elif mode == "SENDABLE_VARIABLES": + session.msg(oob=("SENDABLE_VARIABLES", tuple(key for key in OOB_REPORTABLE.keys()))) + elif mode == "CONFIGURABLE_VARIABLES": + # Not implemented (game specific) + oob_error(session, "Not implemented (game specific)") + else: + # mode == "LISTS" or not given + session.msg(oob=("LISTS",("REPORTABLE_VARIABLES", + "REPORTED_VARIABLES", + # "CONFIGURABLE_VARIABLES", + "SENDABLE_VARIABLES"))) + +# +# Cmd mapping +# + +# this maps the commands to the names available to use from +# the oob call. The standard MSDP commands are capitalized +# as per the protocol, Evennia's own commands are not. +CMD_MAP = {"oob_error": oob_error, # will get error messages + "return_field_report": oob_return_field_report, + "return_attribute_report": oob_return_attribute_report, + # MSDP + "REPEAT": oob_repeat, + "UNREPEAT": oob_unrepeat, + "SEND": oob_send, + "ECHO": oob_echo, + "REPORT": oob_report, + "UNREPORT": oob_unreport, + "LIST": oob_list, + # GMCP + } + diff --git a/evennia/server/oobhandler.py b/evennia/server/oobhandler.py new file mode 100644 index 000000000..1a13f228e --- /dev/null +++ b/evennia/server/oobhandler.py @@ -0,0 +1,437 @@ +""" +OOBHandler - Out Of Band Handler + +The OOBHandler.execute_cmd is called by the sessionhandler when it +detects an `oob` keyword in the outgoing data (usually called via +`msg(oob=...)` + +How this works is that the handler executes an oobfunction, which is +defined in a user-supplied module. This function can then make use of +the oobhandler's functionality to return data, register a monitor on +an object's properties or start a repeating action. + +""" + +from collections import defaultdict +from django.conf import settings +from evennia.server.models import ServerConfig +from evennia.server.sessionhandler import SESSIONS +from evennia.scripts.tickerhandler import TickerHandler +from evennia.utils.dbserialize import dbserialize, dbunserialize, pack_dbobj, unpack_dbobj +from evennia.utils import logger +from evennia.utils.utils import make_iter, mod_import + +_SA = object.__setattr__ +_GA = object.__getattribute__ +_DA = object.__delattr__ + +# load resources from plugin module +_OOB_FUNCS = {} +for modname in make_iter(settings.OOB_PLUGIN_MODULES): + _OOB_FUNCS.update(mod_import(modname).CMD_MAP) + +# get the command to receive eventual error strings +_OOB_ERROR = _OOB_FUNCS.get("oob_error", None) +if not _OOB_ERROR: + # no custom error set; create default oob error message function + def oob_error(session, errmsg, *args, **kwargs): + """ + Fallback error handler. This will be used if no custom + oob_error is defined and just echoes the error back to the + session. + """ + session.msg(oob=("err", ("ERROR ", errmsg))) + _OOB_ERROR = oob_error + + +# +# TrackerHandler is assigned to objects that should notify themselves to +# the OOB system when some property changes. This is never assigned manually +# but automatically through the OOBHandler. +# + +class OOBFieldMonitor(object): + """ + This object should be stored on the + tracked object as "_oob_at__postsave". + the update() method w ill be called by the + save mechanism, which in turn will call the + user-customizable func() + """ + def __init__(self, obj): + """ + This initializes the monitor with the object it sits on. + + Args: + obj (Object): object handler is defined on. + """ + self.obj = obj + self.subscribers = defaultdict(list) + + def __call__(self, fieldname): + """ + Called by the save() mechanism when the given + field has updated. + """ + for sessid, oobtuples in self.subscribers.items(): + # oobtuples is a list [(oobfuncname, args, kwargs), ...], + # a potential list of oob commands to call when this + # field changes. + for (oobfuncname, args, kwargs) in oobtuples: + OOB_HANDLER.execute_cmd(sessid, oobfuncname, fieldname, self.obj, *args, **kwargs) + + def add(self, sessid, oobfuncname, *args, **kwargs): + """ + Add a specific tracking callback to monitor + + Args: + sessid (int): Session id + oobfuncname (str): oob command to call when field updates + args,kwargs: arguments to pass to oob commjand + + Notes: + Each sessid may have a list of (oobfuncname, args, kwargs) + tuples, all of which will be executed when the + field updates. + """ + self.subscribers[sessid].append((oobfuncname, args, kwargs)) + + def remove(self, sessid, oobfuncname=None): + """ + Remove a subscribing session from the monitor + + Args: + sessid(int): Session id + Keyword Args: + oobfuncname (str, optional): Only delete this cmdname. + If not given, delete all. + + """ + if oobfuncname: + self.subscribers[sessid] = [item for item in self.subscribers[sessid] + if item[0] != oobfuncname] + else: + self.subscribers.pop(sessid, None) + + +class OOBAtRepeater(object): + """ + This object is created and used by the `OOBHandler.repeat` method. + It will be assigned to a target object as a custom variable, e.g.: + + `obj._oob_ECHO_every_20s_for_sessid_1 = AtRepater()` + + It will be called every interval seconds by the OOBHandler, + triggering whatever OOB function it is set to use. + + """ + + def __call__(self, *args, **kwargs): + "Called at regular intervals. Calls the oob function" + OOB_HANDLER.execute_cmd(kwargs["_sessid"], kwargs["_oobfuncname"], *args, **kwargs) + + +# Main OOB Handler + +class OOBHandler(TickerHandler): + """ + The OOBHandler manages all server-side OOB functionality + """ + + def __init__(self, *args, **kwargs): + super(OOBHandler, self).__init__(*args, **kwargs) + self.save_name = "oob_ticker_storage" + self.oob_save_name = "oob_monitor_storage" + self.oob_monitor_storage = {} + + def _get_repeater_hook_name(self, oobfuncname, interval, sessid): + "Return the unique repeater call hook name for this object" + return "_oob_%s_every_%ss_for_sessid_%s" % (oobfuncname, interval, sessid) + + def _get_fieldmonitor_name(self, fieldname): + "Return the fieldmonitor name" + return "_oob_at_%s_postsave" % fieldname + + def _add_monitor(self, obj, sessid, fieldname, oobfuncname, *args, **kwargs): + """ + Create a fieldmonitor and store it on the object. This tracker + will be updated whenever the given field changes. + """ + fieldmonitorname = self._get_fieldmonitor_name(fieldname) + if not hasattr(obj, fieldmonitorname): + # assign a new fieldmonitor to the object + _SA(obj, fieldmonitorname, OOBFieldMonitor(obj)) + # register the session with the monitor + _GA(obj, fieldmonitorname).add(sessid, oobfuncname, *args, **kwargs) + + # store calling arguments as a pickle for retrieval at reload + storekey = (pack_dbobj(obj), sessid, fieldname, oobfuncname) + stored = (args, kwargs) + self.oob_monitor_storage[storekey] = stored + + def _remove_monitor(self, obj, sessid, fieldname, oobfuncname=None): + """ + Remove the OOB from obj. If oob implements an + at_delete hook, this will be called with args, kwargs + """ + fieldmonitorname = self._get_fieldmonitor_name(fieldname) + try: + _GA(obj, fieldmonitorname).remove(sessid, oobfuncname=oobfuncname) + if not _GA(obj, fieldmonitorname).subscribers: + _DA(obj, fieldmonitorname) + except AttributeError: + pass + # remove the pickle from storage + store_key = (pack_dbobj(obj), sessid, fieldname, oobfuncname) + self.oob_monitor_storage.pop(store_key, None) + + def save(self): + """ + Handles saving of the OOBHandler data when the server reloads. + Called from the Server process. + """ + # save ourselves as a tickerhandler + super(OOBHandler, self).save() + # handle the extra oob monitor store + if self.ticker_storage: + ServerConfig.objects.conf(key=self.oob_save_name, + value=dbserialize(self.oob_monitor_storage)) + else: + # make sure we have nothing lingering in the database + ServerConfig.objects.conf(key=self.oob_save_name, delete=True) + + def restore(self): + """ + Called when the handler recovers after a Server reload. Called + by the Server process as part of the reload upstart. Here we + overload the tickerhandler's restore method completely to make + sure we correctly re-apply and re-initialize the correct + monitor and repeater objecth on all saved objects. + """ + # load the oob monitors and initialize them + oob_storage = ServerConfig.objects.conf(key=self.oob_save_name) + if oob_storage: + self.oob_storage = dbunserialize(oob_storage) + for store_key, (args, kwargs) in self.oob_storage.items(): + # re-create the monitors + obj, sessid, fieldname, oobfuncname = store_key + obj = unpack_dbobj(obj) + self._add_monitor(obj, sessid, fieldname, oobfuncname, *args, **kwargs) + # handle the tickers (same as in TickerHandler except we call + # the add_repeater method which makes sure to add the hooks before + # starting the tickerpool) + ticker_storage = ServerConfig.objects.conf(key=self.save_name) + if ticker_storage: + self.ticker_storage = dbunserialize(ticker_storage) + for store_key, (args, kwargs) in self.ticker_storage.items(): + obj, interval, idstring = store_key + obj = unpack_dbobj(obj) + # we saved these in add_repeater before, can now retrieve them + sessid = kwargs["_sessid"] + oobfuncname = kwargs["_oobfuncname"] + self.add_repeater(obj, sessid, oobfuncname, interval, *args, **kwargs) + + def add_repeater(self, obj, sessid, oobfuncname, interval=20, *args, **kwargs): + """ + Set an oob function to be repeatedly called. + + Args: + obj (Object) - the object on which to register the repeat + sessid (int) - session id of the session registering + oobfuncname (str) - oob function name to call every interval seconds + interval (int, optional) - interval to call oobfunc, in seconds + Notes: + *args, **kwargs are used as extra arguments to the oobfunc. + """ + # check so we didn't get a session instead of a sessid + if not isinstance(sessid, int): + sessid = sessid.sessid + + hook = OOBAtRepeater() + hookname = self._get_repeater_hook_name(oobfuncname, interval, sessid) + _SA(obj, hookname, hook) + # we store these in kwargs so that tickerhandler saves them with the rest + kwargs.update({"_sessid":sessid, "_oobfuncname":oobfuncname}) + super(OOBHandler, self).add(obj, int(interval), oobfuncname, hookname, *args, **kwargs) + + def remove_repeater(self, obj, sessid, oobfuncname, interval=20): + """ + Remove the repeatedly calling oob function + + Args: + obj (Object): The object on which the repeater sits + sessid (int): Session id of the Session that registered the repeater + oobfuncname (str): Name of oob function to call at repeat + interval (int, optional): Number of seconds between repeats + + """ + # check so we didn't get a session instead of a sessid + if not isinstance(sessid, int): + sessid = sessid.sessid + super(OOBHandler, self).remove(obj, interval, idstring=oobfuncname) + hookname = self._get_repeater_hook_name(oobfuncname, interval, sessid) + try: + _DA(obj, hookname) + except AttributeError: + pass + + def add_field_monitor(self, obj, sessid, field_name, oobfuncname, *args, **kwargs): + """ + Add a monitor tracking a database field + + Args: + obj (Object): The object who'se field is to be monitored + sessid (int): Session if of the session monitoring + field_name (str): Name of database field to monitor. The db_* can optionally + be skipped (it will be automatically appended if missing) + oobfuncname (str): OOB function to call when field changes + + Notes: + When the field updates the given oobfunction will be called as + + `oobfuncname(session, fieldname, obj, *args, **kwargs)` + + where `fieldname` is the name of the monitored field and + `obj` is the object on which the field sits. From this you + can also easily get the new field value if you want. + + """ + # check so we didn't get a session instead of a sessid + if not isinstance(sessid, int): + sessid = sessid.sessid + # all database field names starts with db_* + field_name = field_name if field_name.startswith("db_") else "db_%s" % field_name + self._add_monitor(obj, sessid, field_name, oobfuncname, *args, **kwargs) + + def remove_field_monitor(self, obj, sessid, field_name, oobfuncname=None): + """ + Un-tracks a database field + + Args: + obj (Object): Entity with the monitored field + sessid (int): Session id of session that monitors + field_name (str): database field monitored (the db_* can optionally be + skipped (it will be auto-appended if missing) + oobfuncname (str, optional): OOB command to call on that field + + Notes: + When the Attributes db_value updates the given oobfunction + will be called as + + `oobfuncname(session, fieldname, obj, *args, **kwargs)` + + where `fieldname` is the name of the monitored field and + `obj` is the object on which the field sits. From this you + can also easily get the new field value if you want. + """ + # check so we didn't get a session instead of a sessid + if not isinstance(sessid, int): + sessid = sessid.sessid + field_name = field_name if field_name.startswith("db_") else "db_%s" % field_name + self._remove_monitor(obj, sessid, field_name, oobfuncname=oobfuncname) + + def add_attribute_monitor(self, obj, sessid, attr_name, oobfuncname, *args, **kwargs): + """ + Monitor the changes of an Attribute on an object. Will trigger when + the Attribute's `db_value` field updates. + + Args: + obj (Object): Object with the Attribute to monitor. + sessid (int): Session id of monitoring Session. + attr_name (str): Name (key) of Attribute to monitor. + oobfuncname (str): OOB function to call when Attribute updates. + + """ + # check so we didn't get a session instead of a sessid + if not isinstance(sessid, int): + sessid = sessid.sessid + # get the attribute object if we can + attrobj = obj.attributes.get(attr_name, return_obj=True) + if attrobj: + self._add_monitor(attrobj, sessid, "db_value", oobfuncname) + + def remove_attribute_monitor(self, obj, sessid, attr_name, oobfuncname): + """ + Deactivate tracking for a given object's Attribute + + Args: + obj (Object): Object monitored. + sessid (int): Session id of monitoring Session. + attr_name (str): Name of Attribute monitored. + oobfuncname (str): OOB function name called when Attribute updates. + + """ + # check so we didn't get a session instead of a sessid + if not isinstance(sessid, int): + sessid = sessid.sessid + attrobj = obj.attributes.get(attr_name, return_obj=True) + if attrobj: + self._remove_monitor(attrobj, sessid, "db_value", attr_name, oobfuncname) + + def get_all_monitors(self, sessid): + """ + Get the names of all variables this session is tracking. + + Args: + sessid (id): Session id of monitoring Session + Returns: + stored monitors (tuple): A list of tuples + `(obj, fieldname, args, kwargs)` representing all + the monitoring the Session with the given sessid is doing. + """ + # check so we didn't get a session instead of a sessid + if not isinstance(sessid, int): + sessid = sessid.sessid + # [(obj, fieldname, args, kwargs), ...] + return [(unpack_dbobj(key[0]), key[2], stored[0], stored[1]) + for key, stored in self.oob_monitor_storage.items() if key[1] == sessid] + + + # access method - called from session.msg() + + def execute_cmd(self, session, oobfuncname, *args, **kwargs): + """ + Execute an oob command + + Args: + session (Session or int): Session or Session.sessid calling + the oob command + oobfuncname (str): The name of the oob command (case sensitive) + + Notes: + If the oobfuncname is a valid oob function, `args` and + `kwargs` are passed into the oob command. + + """ + if isinstance(session, int): + # a sessid. Convert to a session + session = SESSIONS.session_from_sessid(session) + if not session: + errmsg = "OOB Error: execute_cmd(%s,%s,%s,%s) - no valid session" % \ + (session, oobfuncname, args, kwargs) + raise RuntimeError(errmsg) + + #print "execute_oob:", session, oobfuncname, args, kwargs + try: + oobfunc = _OOB_FUNCS[oobfuncname] + except Exception: + errmsg = "'%s' is not a valid OOB command. Commands available:\n %s" % (oobfuncname, ", ".join(_OOB_FUNCS)) + if _OOB_ERROR: + _OOB_ERROR(session, errmsg, *args, **kwargs) + errmsg = "OOB ERROR: %s" % errmsg + logger.log_trace(errmsg) + return + + # we found an oob command. Execute it. + try: + oobfunc(session, *args, **kwargs) + except Exception, err: + errmsg = "Exception in %s(*%s, **%s):\n%s" % (oobfuncname, args, kwargs, err) + if _OOB_ERROR: + _OOB_ERROR(session, errmsg, *args, **kwargs) + errmsg = "OOB ERROR: %s" % errmsg + logger.log_trace(errmsg) + + +# access object +OOB_HANDLER = OOBHandler() diff --git a/game/gamesrc/conf/__init__.py b/evennia/server/portal/__init__.py similarity index 100% rename from game/gamesrc/conf/__init__.py rename to evennia/server/portal/__init__.py diff --git a/src/server/portal/imc2.py b/evennia/server/portal/imc2.py similarity index 98% rename from src/server/portal/imc2.py rename to evennia/server/portal/imc2.py index 8629cdd20..667a2b80d 100644 --- a/src/server/portal/imc2.py +++ b/evennia/server/portal/imc2.py @@ -8,10 +8,10 @@ from twisted.application import internet from twisted.internet import protocol from twisted.conch import telnet -from src.server.session import Session -from src.utils import logger, utils -from src.server.portal.imc2lib import imc2_ansi -from src.server.portal.imc2lib import imc2_packets as pck +from evennia.server.session import Session +from evennia.utils import logger, utils +from evennia.server.portal.imc2lib import imc2_ansi +from evennia.server.portal.imc2lib import imc2_packets as pck from django.utils.translation import ugettext as _ @@ -337,7 +337,7 @@ class IMC2Bot(telnet.StatefulTelnetProtocol, Session): class IMC2BotFactory(protocol.ReconnectingClientFactory): """ Creates instances of the IMC2Protocol. Should really only ever - need to create one connection. Tied in via src/server.py. + need to create one connection. Tied in via evennia/server.py. """ initialDelay = 1 factor = 1.5 diff --git a/game/gamesrc/conf/examples/__init__.py b/evennia/server/portal/imc2lib/__init__.py similarity index 100% rename from game/gamesrc/conf/examples/__init__.py rename to evennia/server/portal/imc2lib/__init__.py diff --git a/src/server/portal/imc2lib/imc2_ansi.py b/evennia/server/portal/imc2lib/imc2_ansi.py similarity index 98% rename from src/server/portal/imc2lib/imc2_ansi.py rename to evennia/server/portal/imc2lib/imc2_ansi.py index 26815b7bb..23deb704e 100644 --- a/src/server/portal/imc2lib/imc2_ansi.py +++ b/evennia/server/portal/imc2lib/imc2_ansi.py @@ -6,7 +6,7 @@ This is a IMC2 complacent version. """ import re -from src.utils import ansi +from evennia.utils import ansi class IMCANSIParser(ansi.ANSIParser): diff --git a/src/server/portal/imc2lib/imc2_packets.py b/evennia/server/portal/imc2lib/imc2_packets.py similarity index 100% rename from src/server/portal/imc2lib/imc2_packets.py rename to evennia/server/portal/imc2lib/imc2_packets.py diff --git a/src/server/portal/irc.py b/evennia/server/portal/irc.py similarity index 98% rename from src/server/portal/irc.py rename to evennia/server/portal/irc.py index 7eb1e0987..fd6c34e79 100644 --- a/src/server/portal/irc.py +++ b/evennia/server/portal/irc.py @@ -7,8 +7,8 @@ more Evennia channels. from twisted.application import internet from twisted.words.protocols import irc from twisted.internet import protocol -from src.server.session import Session -from src.utils import logger +from evennia.server.session import Session +from evennia.utils import logger # IRC bot diff --git a/src/server/portal/mccp.py b/evennia/server/portal/mccp.py similarity index 100% rename from src/server/portal/mccp.py rename to evennia/server/portal/mccp.py diff --git a/src/server/portal/mssp.py b/evennia/server/portal/mssp.py similarity index 99% rename from src/server/portal/mssp.py rename to evennia/server/portal/mssp.py index 96b1a9e05..958812a49 100644 --- a/src/server/portal/mssp.py +++ b/evennia/server/portal/mssp.py @@ -11,7 +11,7 @@ active players and so on. """ from django.conf import settings -from src.utils import utils +from evennia.utils import utils MSSP = chr(70) MSSP_VAR = chr(1) diff --git a/src/server/portal/mxp.py b/evennia/server/portal/mxp.py similarity index 100% rename from src/server/portal/mxp.py rename to evennia/server/portal/mxp.py diff --git a/src/server/portal/naws.py b/evennia/server/portal/naws.py similarity index 98% rename from src/server/portal/naws.py rename to evennia/server/portal/naws.py index e148ccaa2..01338b9c0 100644 --- a/src/server/portal/naws.py +++ b/evennia/server/portal/naws.py @@ -11,7 +11,6 @@ it when the size changes """ from django.conf import settings -from src.utils import utils NAWS = chr(31) IS = chr(0) diff --git a/src/server/portal/portal.py b/evennia/server/portal/portal.py similarity index 92% rename from src/server/portal/portal.py rename to evennia/server/portal/portal.py index 6af386bf8..1c1c522de 100644 --- a/src/server/portal/portal.py +++ b/evennia/server/portal/portal.py @@ -10,27 +10,26 @@ by game/evennia.py). import sys import os -if os.name == 'nt': - # For Windows batchfile we need an extra path insertion here. - sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname( - os.path.dirname(os.path.abspath(__file__)))))) -from src.server.webserver import EvenniaReverseProxyResource + from twisted.application import internet, service from twisted.internet import protocol, reactor from twisted.web import server import django - django.setup() - from django.conf import settings -from src.utils.utils import get_evennia_version, mod_import, make_iter -from src.server.portal.portalsessionhandler import PORTAL_SESSIONS + +import evennia +evennia._init() + +from evennia.utils.utils import get_evennia_version, mod_import, make_iter +from evennia.server.portal.portalsessionhandler import PORTAL_SESSIONS +from evennia.server.webserver import EvenniaReverseProxyResource PORTAL_SERVICES_PLUGIN_MODULES = [mod_import(module) for module in make_iter(settings.PORTAL_SERVICES_PLUGIN_MODULES)] if os.name == 'nt': # For Windows we need to handle pid files manually. - PORTAL_PIDFILE = os.path.join(settings.GAME_DIR, 'portal.pid') + PORTAL_PIDFILE = os.path.join(settings.GAME_DIR, "server", 'portal.pid') #------------------------------------------------------------ # Evennia Portal settings @@ -40,7 +39,7 @@ VERSION = get_evennia_version() SERVERNAME = settings.SERVERNAME -PORTAL_RESTART = os.path.join(settings.GAME_DIR, 'portal.restart') +PORTAL_RESTART = os.path.join(settings.GAME_DIR, "server", 'portal.restart') TELNET_PORTS = settings.TELNET_PORTS SSL_PORTS = settings.SSL_PORTS @@ -164,7 +163,7 @@ if AMP_ENABLED: # the portal and the mud server. Only reason to ever deactivate # it would be during testing and debugging. - from src.server import amp + from evennia.server import amp print ' amp (to Server): %s' % AMP_PORT @@ -181,7 +180,7 @@ if TELNET_ENABLED: # Start telnet game connections - from src.server.portal import telnet + from evennia.server.portal import telnet for interface in TELNET_INTERFACES: ifacestr = "" @@ -203,7 +202,7 @@ if SSL_ENABLED: # Start SSL game connection (requires PyOpenSSL). - from src.server.portal import ssl + from evennia.server.portal import ssl for interface in SSL_INTERFACES: ifacestr = "" @@ -229,7 +228,7 @@ if SSH_ENABLED: # Start SSH game connections. Will create a keypair in # evennia/game if necessary. - from src.server.portal import ssh + from evennia.server.portal import ssh for interface in SSH_INTERFACES: ifacestr = "" @@ -262,7 +261,7 @@ if WEBSERVER_ENABLED: webclientstr = "" if WEBCLIENT_ENABLED: # create ajax client processes at /webclientdata - from src.server.portal.webclient import WebClient + from evennia.server.portal.webclient import WebClient webclient = WebClient() webclient.sessionhandler = PORTAL_SESSIONS @@ -272,8 +271,8 @@ if WEBSERVER_ENABLED: if WEBSOCKET_CLIENT_ENABLED and not websocket_started: # start websocket client port for the webclient # we only support one websocket client - from src.server.portal import websocket_client - from src.utils.txws import WebSocketFactory + from evennia.server.portal import websocket_client + from evennia.utils.txws import WebSocketFactory interface = WEBSOCKET_CLIENT_INTERFACE port = WEBSOCKET_CLIENT_PORT diff --git a/src/server/portal/portalsessionhandler.py b/evennia/server/portal/portalsessionhandler.py similarity index 59% rename from src/server/portal/portalsessionhandler.py rename to evennia/server/portal/portalsessionhandler.py index 290dd04b4..28bcc7b46 100644 --- a/src/server/portal/portalsessionhandler.py +++ b/evennia/server/portal/portalsessionhandler.py @@ -1,11 +1,18 @@ """ Sessionhandler for portal sessions """ -import time -from src.server.sessionhandler import SessionHandler, PCONN, PDISCONN, PSYNC, PCONNSYNC +from collections import deque +from time import time +from twisted.internet import reactor, task +from evennia.server.sessionhandler import SessionHandler, PCONN, PDISCONN, PCONNSYNC +_CONNECTION_RATE = 5.0 +_MIN_TIME_BETWEEN_CONNECTS = 1.0 / _CONNECTION_RATE _MOD_IMPORT = None +#_MAX_CMD_RATE = 80.0 +#_ERROR_COMMAND_OVERFLOW = "You entered commands too fast. Wait a moment and try again." + #------------------------------------------------------------ # Portal-SessionHandler class #------------------------------------------------------------ @@ -28,8 +35,9 @@ class PortalSessionHandler(SessionHandler): self.portal = None self.sessions = {} self.latest_sessid = 0 - self.uptime = time.time() + self.uptime = time() self.connection_time = 0 + self.time_last_connect = time() def at_server_connection(self): """ @@ -37,31 +45,62 @@ class PortalSessionHandler(SessionHandler): Server. At this point, the AMP connection is already established. """ - self.connection_time = time.time() + self.connection_time = time() def connect(self, session): """ Called by protocol at first connect. This adds a not-yet authenticated session using an ever-increasing counter for sessid. + + We implement a throttling mechanism here to limit the speed at which + new connections are accepted - this is both a stop against DoS attacks + as well as helps using the Dummyrunner tester with a large number of + connector dummies. + """ - self.latest_sessid += 1 - sessid = self.latest_sessid - session.sessid = sessid - sessdata = session.get_sync_data() - self.sessions[sessid] = session + session.server_connected = False + + if not session.sessid: + # only assign if we were not delayed + self.latest_sessid += 1 + session.sessid = self.latest_sessid + + now = time() + current_rate = 1.0 / (now - self.time_last_connect) + + if current_rate > _CONNECTION_RATE: + # we have too many connections per second. Delay. + #print " delaying connecting", session.sessid + reactor.callLater(_MIN_TIME_BETWEEN_CONNECTS, self.connect, session) + return + + if not self.portal.amp_protocol: + # if amp is not yet ready (usually because the server is + # booting up), try again a little later + reactor.callLater(0.5, self.connect, session) + return + # sync with server-side - if self.portal.amp_protocol: # this is a timing issue - self.portal.amp_protocol.call_remote_ServerAdmin(sessid, + + self.time_last_connect = now + sessdata = session.get_sync_data() + self.sessions[session.sessid] = session + session.server_connected = True + #print "connecting", session.sessid, " number:", len(self.sessions) + self.portal.amp_protocol.call_remote_ServerAdmin(session.sessid, operation=PCONN, data=sessdata) + def sync(self, session): """ Called by the protocol of an already connected session. This can be used to sync the session info in a delayed manner, such as when negotiation and handshakes are delayed. """ - if session.sessid: - # only use if session already has sessid (i.e. has already connected) + if session.sessid and session.server_connected: + # only use if session already has sessid and has already connected + # once to the server - if so we must re-sync woth the server, otherwise + # we skip this step. sessdata = session.get_sync_data() if self.portal.amp_protocol: # we only send sessdata that should not have changed @@ -100,7 +139,7 @@ class PortalSessionHandler(SessionHandler): protocol_path - full python path to the class factory for the protocol used, eg - 'src.server.portal.irc.IRCClientFactory' + 'evennia.server.portal.irc.IRCClientFactory' config - dictionary of configuration options, fed as **kwarg to protocol class' __init__ method. @@ -109,7 +148,7 @@ class PortalSessionHandler(SessionHandler): """ global _MOD_IMPORT if not _MOD_IMPORT: - from src.utils.utils import variable_from_module as _MOD_IMPORT + from evennia.utils.utils import variable_from_module as _MOD_IMPORT path, clsname = protocol_path.rsplit(".", 1) cls = _MOD_IMPORT(path, clsname) if not cls: @@ -179,16 +218,6 @@ class PortalSessionHandler(SessionHandler): return [sess for sess in self.get_sessions(include_unloggedin=True) if hasattr(sess, 'suid') and sess.suid == suid] - def data_in(self, session, text="", **kwargs): - """ - Called by portal sessions for relaying data coming - in from the protocol to the server. data is - serialized before passed on. - """ - self.portal.amp_protocol.call_remote_MsgPortal2Server(session.sessid, - msg=text, - data=kwargs) - def announce_all(self, message): """ Send message to all connection sessions @@ -196,13 +225,93 @@ class PortalSessionHandler(SessionHandler): for session in self.sessions.values(): session.data_out(message) + def oobstruct_parser(self, oobstruct): + """ + Helper method for each session to use to parse oob structures + (The 'oob' kwarg of the msg() method). + + Args: oobstruct (str or iterable): A structure representing + an oob command on one of the following forms: + - "cmdname" + - "cmdname", "cmdname" + - ("cmdname", arg) + - ("cmdname",(args)) + - ("cmdname",{kwargs} + - ("cmdname", (args), {kwargs}) + - (("cmdname", (args,), {kwargs}), ("cmdname", (args,), {kwargs})) + and any combination of argument-less commands or commands with only + args, only kwargs or both. + + Returns: + structure (tuple): A generic OOB structure on the form + `((cmdname, (args,), {kwargs}), ...)`, where the two last + args and kwargs may be empty + """ + def _parse(oobstruct): + slen = len(oobstruct) + if not oobstruct: + return tuple(None, (), {}) + elif not hasattr(oobstruct, "__iter__"): + # a singular command name, without arguments or kwargs + return (oobstruct, (), {}) + # regardless of number of args/kwargs, the first element must be + # the function name. We will not catch this error if not, but + # allow it to propagate. + if slen == 1: + return (oobstruct[0], (), {}) + elif slen == 2: + if isinstance(oobstruct[1], dict): + # (cmdname, {kwargs}) + return (oobstruct[0], (), dict(oobstruct[1])) + elif isinstance(oobstruct[1], (tuple, list)): + # (cmdname, (args,)) + return (oobstruct[0], tuple(oobstruct[1]), {}) + else: + # (cmdname, arg) + return (oobstruct[0], (oobstruct[1],), {}) + else: + # (cmdname, (args,), {kwargs}) + return (oobstruct[0], tuple(oobstruct[1]), dict(oobstruct[2])) + + if hasattr(oobstruct, "__iter__"): + # differentiate between (cmdname, cmdname), + # (cmdname, (args), {kwargs}) and ((cmdname,(args),{kwargs}), + # (cmdname,(args),{kwargs}), ...) + + if oobstruct and isinstance(oobstruct[0], basestring): + return (list(_parse(oobstruct)),) + else: + out = [] + for oobpart in oobstruct: + out.append(_parse(oobpart)) + return (list(out),) + return (_parse(oobstruct),) + + + def data_in(self, session, text="", **kwargs): + """ + Called by portal sessions for relaying data coming + in from the protocol to the server. data is + serialized before passed on. + + """ + self.portal.amp_protocol.call_remote_MsgPortal2Server(session.sessid, + msg=text, + data=kwargs) + def data_out(self, sessid, text=None, **kwargs): """ Called by server for having the portal relay messages and data - to the correct session protocol. + to the correct session protocol. We also convert oob input to + a generic form here. """ session = self.sessions.get(sessid, None) if session: + # convert oob to the generic format + if "oob" in kwargs: + #print "oobstruct_parser in:", kwargs["oob"] + kwargs["oob"] = self.oobstruct_parser(kwargs["oob"]) + #print "oobstruct_parser out:", kwargs["oob"] session.data_out(text=text, **kwargs) PORTAL_SESSIONS = PortalSessionHandler() diff --git a/src/server/portal/rss.py b/evennia/server/portal/rss.py similarity index 97% rename from src/server/portal/rss.py rename to evennia/server/portal/rss.py index 534050d5f..4f773759d 100644 --- a/src/server/portal/rss.py +++ b/evennia/server/portal/rss.py @@ -8,8 +8,8 @@ to the channel whenever the feed updates. from twisted.internet import task, threads from django.conf import settings -from src.server.session import Session -from src.utils import logger +from evennia.server.session import Session +from evennia.utils import logger RSS_ENABLED = settings.RSS_ENABLED #RETAG = re.compile(r'<[^>]*?>') diff --git a/src/server/portal/ssh.py b/evennia/server/portal/ssh.py similarity index 98% rename from src/server/portal/ssh.py rename to evennia/server/portal/ssh.py index 66d284492..73ed5ed28 100644 --- a/src/server/portal/ssh.py +++ b/evennia/server/portal/ssh.py @@ -24,9 +24,9 @@ from twisted.internet import defer from twisted.conch import interfaces as iconch from twisted.python import components from django.conf import settings -from src.server import session -from src.players.models import PlayerDB -from src.utils import ansi, utils +from evennia.server import session +from evennia.players.models import PlayerDB +from evennia.utils import ansi, utils ENCODINGS = settings.ENCODINGS @@ -343,4 +343,4 @@ def makeFactory(configdict): factory.portal.registerChecker(PlayerDBPasswordChecker(factory)) - return factory \ No newline at end of file + return factory diff --git a/src/server/portal/ssl.py b/evennia/server/portal/ssl.py similarity index 98% rename from src/server/portal/ssl.py rename to evennia/server/portal/ssl.py index 0ab535779..b17a29060 100644 --- a/src/server/portal/ssl.py +++ b/evennia/server/portal/ssl.py @@ -12,7 +12,7 @@ except ImportError: print " SSL_ENABLED requires PyOpenSSL." sys.exit(5) -from src.server.portal.telnet import TelnetProtocol +from evennia.server.portal.telnet import TelnetProtocol class SSLProtocol(TelnetProtocol): diff --git a/src/server/portal/telnet.py b/evennia/server/portal/telnet.py similarity index 90% rename from src/server/portal/telnet.py rename to evennia/server/portal/telnet.py index 503201aac..2ad8acfb6 100644 --- a/src/server/portal/telnet.py +++ b/evennia/server/portal/telnet.py @@ -9,11 +9,11 @@ sessions etc. import re from twisted.conch.telnet import Telnet, StatefulTelnetProtocol, IAC, LINEMODE, GA, WILL, WONT, ECHO -from src.server.session import Session -from src.server.portal import ttype, mssp, msdp, naws -from src.server.portal.mccp import Mccp, mccp_compress, MCCP -from src.server.portal.mxp import Mxp, mxp_parse -from src.utils import utils, ansi, logger +from evennia.server.session import Session +from evennia.server.portal import ttype, mssp, telnet_oob, naws +from evennia.server.portal.mccp import Mccp, mccp_compress, MCCP +from evennia.server.portal.mxp import Mxp, mxp_parse +from evennia.utils import utils, ansi, logger _RE_N = re.compile(r"\{n$") _RE_LEND = re.compile(r"\n$|\r$", re.MULTILINE) @@ -26,8 +26,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): """ def connectionMade(self): """ - This is called when the connection is first - established. + This is called when the connection is first established. """ # initialize the session self.iaw_mode = False @@ -35,7 +34,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): client_address = self.transport.client # this number is counted down for every handshake that completes. # when it reaches 0 the portal/server syncs their data - self.handshakes = 6 # naws, ttype, mccp, mssp, msdp, mxp + self.handshakes = 7 # naws, ttype, mccp, mssp, msdp, gmcp, mxp self.init_session("telnet", client_address, self.factory.sessionhandler) # negotiate client size @@ -47,8 +46,8 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): self.mccp = Mccp(self) # negotiate mssp (crawler communication) self.mssp = mssp.Mssp(self) - # msdp - self.msdp = msdp.Msdp(self) + # oob communication (MSDP, GMCP) - two handshake calls! + self.oob = telnet_oob.TelnetOOB(self) # mxp support self.mxp = Mxp(self) # keepalive watches for dead links @@ -58,7 +57,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): self.sessionhandler.connect(self) # timeout the handshakes in case the client doesn't reply at all - from src.utils.utils import delay + from evennia.utils.utils import delay delay(2, callback=self.handshake_done, retval=True) def handshake_done(self, force=False): @@ -67,7 +66,6 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): When all have reported, a sync with the server is performed. The system will force-call this sync after a small time to handle clients that don't reply to handshakes at all. - info - debug text from the protocol calling """ if self.handshakes > 0: if force: @@ -211,7 +209,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): through the telnet connection. valid telnet kwargs: - oob= - supply an Out-of-Band instruction. + oob=[(cmdname,args,kwargs), ...] - supply an Out-of-Band instruction. xterm256=True/False - enforce xterm256 setting. If not given, ttype result is used. If client does not suport xterm256, the @@ -236,14 +234,11 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): except Exception, e: self.sendLine(str(e)) return - if "oob" in kwargs: - oobstruct = self.sessionhandler.oobstruct_parser(kwargs.pop("oob")) - if "MSDP" in self.protocol_flags: - for cmdname, args, kwargs in oobstruct: - #print "cmdname, args, kwargs:", cmdname, args, kwargs - msdp_string = self.msdp.evennia_to_msdp(cmdname, *args, **kwargs) - #print "msdp_string:", msdp_string - self.msdp.data_out(msdp_string) + if "oob" in kwargs and "OOB" in self.protocol_flags: + # oob is a list of [(cmdname, arg, kwarg), ...] + for cmdname, args, okwargs in kwargs["oob"]: + #print "telnet oob data_out:", cmdname, args, kwargs + self.oob.data_out(cmdname, *args, **okwargs) # parse **kwargs, falling back to ttype if nothing is given explicitly ttype = self.protocol_flags.get('TTYPE', {}) diff --git a/evennia/server/portal/telnet_oob.py b/evennia/server/portal/telnet_oob.py new file mode 100644 index 000000000..9334ea8f7 --- /dev/null +++ b/evennia/server/portal/telnet_oob.py @@ -0,0 +1,256 @@ +""" + +Telnet OOB (Out of band communication) + +This implements the following telnet oob protocols: +MSDP (Mud Server Data Protocol) +GMCP (Generic Mud Communication Protocol) + +This implements the MSDP protocol as per +http://tintin.sourceforge.net/msdp/ and the GMCP protocol as per +http://www.ironrealms.com/rapture/manual/files/FeatGMCP-txt.html#Generic_MUD_Communication_Protocol%28GMCP%29 + +Following the lead of KaVir's protocol snippet, we first check if +client supports MSDP and if not, we fallback to GMCP with a MSDP +header where applicable. + +OOB manages out-of-band +communication between the client and server, for updating health bars +etc. See also GMCP which is another standard doing the same thing. + +""" +import re +import json +from evennia.utils.utils import to_str + +# MSDP-relevant telnet cmd/opt-codes +MSDP = chr(69) +MSDP_VAR = chr(1) +MSDP_VAL = chr(2) +MSDP_TABLE_OPEN = chr(3) +MSDP_TABLE_CLOSE = chr(4) +MSDP_ARRAY_OPEN = chr(5) +MSDP_ARRAY_CLOSE = chr(6) + +# GMCP +GMCP = chr(201) + +# General Telnet +IAC = chr(255) +SB = chr(250) +SE = chr(240) + +force_str = lambda inp: to_str(inp, force_string=True) + +# pre-compiled regexes +# returns 2-tuple +msdp_regex_array = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL, + MSDP_ARRAY_OPEN, + MSDP_ARRAY_CLOSE)) +# returns 2-tuple (may be nested) +msdp_regex_table = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL, + MSDP_TABLE_OPEN, + MSDP_TABLE_CLOSE)) +msdp_regex_var = re.compile(MSDP_VAR) +msdp_regex_val = re.compile(MSDP_VAL) + +# Msdp object handler + +class TelnetOOB(object): + """ + Implements the MSDP and GMCP protocols. + """ + + def __init__(self, protocol): + """ + Initiates by storing the protocol + on itself and trying to determine + if the client supports MSDP. + """ + self.protocol = protocol + self.protocol.protocol_flags['OOB'] = False + self.MSDP = False + self.GMCP = False + # ask for the available protocols and assign decoders + # (note that handshake_done() will be called twice!) + self.protocol.negotiationMap[MSDP] = self.decode_msdp + self.protocol.negotiationMap[GMCP] = self.decode_gmcp + self.protocol.will(MSDP).addCallbacks(self.do_msdp, self.no_msdp) + self.protocol.will(GMCP).addCallbacks(self.do_gmcp, self.no_gmcp) + self.oob_reported = {} + + def no_msdp(self, option): + "No msdp supported or wanted" + # no msdp, check GMCP + self.protocol.handshake_done() + + def do_msdp(self, option): + "MSDP supported by client" + self.MSDP = True + self.protocol.protocol_flags['OOB'] = True + self.protocol.handshake_done() + + def no_gmcp(self, option): + "Neither MSDP nor GMCP supported" + self.protocol.handshake_done() + + def do_gmcp(self, option): + """ + Called when client confirms that it can do MSDP or GMCP. + """ + self.GMCP = True + self.protocol.protocol_flags['OOB'] = True + self.protocol.handshake_done() + + # encoders + + def encode_msdp(self, cmdname, *args, **kwargs): + """ + handle return data from cmdname by converting it to + a proper msdp structure. These are the combinations we + support: + + cmdname string -> cmdname string + cmdname *args -> cmdname MSDP_ARRAY + cmdname **kwargs -> cmdname MSDP_TABLE + + # send 'raw' data structures + MSDP_ARRAY *args -> MSDP_ARRAY + MSDP_TABLE **kwargs -> MSDP_TABLE + + """ + msdp_string = "" + if args: + if cmdname == "MSDP_ARRAY": + msdp_string = "".join(["%s%s" % (MSDP_VAL, val) for val in args]) + else: + msdp_string = "%s%s%s" % (MSDP_VAR, cmdname, "".join( + "%s%s" % (MSDP_VAL, val) for val in args)) + elif kwargs: + if cmdname == "MSDP_TABLE": + msdp_string = "".join(["%s%s%s%s" % (MSDP_VAR, key, MSDP_VAL, val) + for key, val in kwargs.items()]) + else: + msdp_string = "%s%s%s" % (MSDP_VAR. cmdname, "".join( + ["%s%s%s%s" % (MSDP_VAR, key, MSDP_VAL, val) for key, val in kwargs.items()])) + #print "encode msdp result:", cmdname, args, kwargs, "->", msdp_string + return force_str(msdp_string) + + def encode_gmcp(self, cmdname, *args, **kwargs): + """ + Gmcp messages are on one of the following outgoing forms: + + cmdname string -> cmdname string + cmdname *args -> cmdname [arg, arg, arg, ...] + cmdname **kwargs -> cmdname {key:arg, key:arg, ...} + + cmdname is generally recommended to be a string on the form + Module.Submodule.Function + """ + if cmdname in ("SEND", "REPORT", "UNREPORT", "LIST"): + # we wrap the standard MSDP commands in a MSDP.submodule + # here as far as GMCP is concerned. + cmdname = "MSDP.%s" % cmdname + elif cmdname in ("MSDP_ARRAY", "MSDP_TABLE"): + # no cmdname should accompany these, just the MSDP wrapper + cmdname = "MSDP" + if args: + gmcp_string = "%s %s" % (cmdname, json.dumps(args)) + elif kwargs: + gmcp_string = "%s %s" % (cmdname, json.dumps(kwargs)) + #print "gmcp_encode", cmdname, args, kwargs, "->", gmcp_string + return force_str(gmcp_string).strip() + + def decode_msdp(self, data): + """ + Decodes incoming MSDP data + + cmdname var --> cmdname arg + cmdname array --> cmdname *args + cmdname table --> cmdname **kwargs + + """ + tables = {} + arrays = {} + variables = {} + + if hasattr(data, "__iter__"): + data = "".join(data) + + # decode + for key, table in msdp_regex_table.findall(data): + tables[key] = {} + for varval in msdp_regex_var.split(table): + parts = msdp_regex_val.split(varval) + tables[key].expand({parts[0]: tuple(parts[1:]) if len(parts) > 1 else ("",)}) + for key, array in msdp_regex_array.findall(data): + arrays[key] = [] + for val in msdp_regex_val.split(array): + arrays[key].append(val) + arrays[key] = tuple(arrays[key]) + for varval in msdp_regex_var.split(msdp_regex_array.sub("", msdp_regex_table.sub("", data))): + # get remaining varvals after cleaning away tables/arrays + parts = msdp_regex_val.split(varval) + variables[parts[0]] = tuple(parts[1:]) if len(parts) > 1 else ("", ) + + #print "OOB: MSDP decode:", data, "->", variables, arrays, tables + + # send to the sessionhandler + if data: + for varname, var in variables.items(): + # a simple function + argument + self.protocol.data_in(oob=(varname, var, {})) + for arrayname, array in arrays.items(): + # we assume the array are multiple arguments to the function + self.protocol.data_in(oob=(arrayname, array, {})) + for tablename, table in tables.items(): + # we assume tables are keyword arguments to the function + self.protocol.data_in(oob=(tablename, (), table)) + + def decode_gmcp(self, data): + """ + Decodes incoming GMCP data on the form 'varname ' + + cmdname string -> cmdname arg + cmdname [arg, arg,...] -> cmdname *args + cmdname {key:arg, key:arg, ...} -> cmdname **kwargs + + """ + if hasattr(data, "__iter__"): + data = "".join(data) + + #print "decode_gmcp:", data + if data: + splits = data.split(None, 1) + cmdname = splits[0] + if len(splits) < 2: + self.protocol.data_in(oob=(cmdname, (), {})) + elif splits[1]: + try: + struct = json.loads(splits[1]) + except ValueError: + struct = splits[1] + args, kwargs = (), {} + if hasattr(struct, "__iter__"): + if isinstance(struct, dict): + kwargs = struct + else: + args = tuple(struct) + else: + args = (struct,) + #print "gmcp decode:", data, "->", cmdname, args, kwargs + self.protocol.data_in(oob=(cmdname, args, kwargs)) + + # access methods + + def data_out(self, cmdname, *args, **kwargs): + """ + Return a msdp-valid subnegotiation across the protocol. + """ + #print "data_out:", encoded_oob + if self.MSDP: + encoded_oob = self.encode_msdp(cmdname, *args, **kwargs) + self.protocol._write(IAC + SB + MSDP + encoded_oob + IAC + SE) + if self.GMCP: + encoded_oob = self.encode_gmcp(cmdname, *args, **kwargs) + self.protocol._write(IAC + SB + GMCP + encoded_oob + IAC + SE) diff --git a/src/server/portal/ttype.py b/evennia/server/portal/ttype.py similarity index 100% rename from src/server/portal/ttype.py rename to evennia/server/portal/ttype.py diff --git a/src/server/portal/webclient.py b/evennia/server/portal/webclient.py similarity index 98% rename from src/server/portal/webclient.py rename to evennia/server/portal/webclient.py index 74c32c6a4..f9a12220d 100644 --- a/src/server/portal/webclient.py +++ b/evennia/server/portal/webclient.py @@ -26,9 +26,9 @@ from twisted.web import server, resource from django.utils.functional import Promise from django.utils.encoding import force_unicode from django.conf import settings -from src.utils import utils, logger -from src.utils.text2html import parse_html -from src.server import session +from evennia.utils import utils, logger +from evennia.utils.text2html import parse_html +from evennia.server import session SERVERNAME = settings.SERVERNAME ENCODINGS = settings.ENCODINGS diff --git a/src/server/portal/websocket_client.py b/evennia/server/portal/websocket_client.py similarity index 70% rename from src/server/portal/websocket_client.py rename to evennia/server/portal/websocket_client.py index f8b14f6c5..d0ddc1d25 100644 --- a/src/server/portal/websocket_client.py +++ b/evennia/server/portal/websocket_client.py @@ -3,7 +3,7 @@ Websocket-webclient This implements a webclient with WebSockets (http://en.wikipedia.org/wiki/WebSocket) by use of the txws implementation (https://github.com/MostAwesomeDude/txWS). It is -used together with src/web/media/javascript/evennia_websocket_webclient.js. +used together with evennia/web/media/javascript/evennia_websocket_webclient.js. Thanks to Ricard Pillosu whose Evennia plugin inspired this module. @@ -29,10 +29,10 @@ Example of call from a javascript client: """ import json from twisted.internet.protocol import Protocol -from src.server.session import Session -from src.utils.logger import log_trace -from src.utils.utils import to_str, make_iter -from src.utils.text2html import parse_html +from evennia.server.session import Session +from evennia.utils.logger import log_trace +from evennia.utils.utils import to_str, make_iter +from evennia.utils.text2html import parse_html class WebSocketClient(Protocol, Session): @@ -78,27 +78,48 @@ class WebSocketClient(Protocol, Session): OOB - This is an Out-of-band instruction. If so, the remaining string should be a json-packed string on the form {oobfuncname: [args, ], ...} - any other prefix (or lack of prefix) is considered - plain text data, to be treated like a game + CMD - plain text data, to be treated like a game input command. """ - if string[:3] == "OOB": - string = string[3:] - try: - oobdata = json.loads(string) - for (key, args) in oobdata.items(): - #print "oob data in:", (key, args) - self.data_in(text=None, oob=(key, make_iter(args))) - except Exception: - log_trace("Websocket malformed OOB request: %s" % string) - else: + mode = string[:3] + data = string[3:] + + if mode == "OOB": + # an out-of-band command + self.json_decode(data) + elif mode == "CMD": # plain text input - self.data_in(text=string) + self.data_in(text=data) def sendLine(self, line): "send data to client" return self.transport.write(line) + def json_decode(self, data): + """ + Decodes incoming data from the client + + [cmdname, [args],{kwargs}] -> cmdname *args **kwargs + + """ + try: + cmdname, args, kwargs = json.loads(data) + except Exception: + log_trace("Websocket malformed OOB request: %s" % data) + raise + self.sessionhandler.data_in(self, oob=(cmdname, args, kwargs)) + + def json_encode(self, cmdname, *args, **kwargs): + """ + Encode OOB data for sending to client + + cmdname *args -> cmdname [json array] + cmdname **kwargs -> cmdname {json object} + + """ + cmdtuple = [cmdname, list(args), kwargs] + self.sendLine("OOB" + json.dumps(cmdtuple)) + def data_in(self, text=None, **kwargs): """ Data Websocket -> Server @@ -121,15 +142,15 @@ class WebSocketClient(Protocol, Session): except Exception, e: self.sendLine(str(e)) if "oob" in kwargs: - oobstruct = self.sessionhandler.oobstruct_parser(kwargs.pop("oob")) - #print "oob data_out:", "OOB" + json.dumps(oobstruct) - self.sendLine("OOB" + json.dumps(oobstruct)) + for cmdname, args, okwargs in kwargs["oob"]: + self.json_encode(cmdname, *args, **okwargs) + raw = kwargs.get("raw", False) nomarkup = kwargs.get("nomarkup", False) if "prompt" in kwargs: self.sendLine("PROMPT" + parse_html(kwargs["prompt"], strip_ansi=nomarkup)) if raw: - self.sendLine(text) + self.sendLine("CMD" + text) else: - self.sendLine(parse_html(text, strip_ansi=nomarkup)) + self.sendLine("CMD" + parse_html(text, strip_ansi=nomarkup)) diff --git a/src/utils/dummyrunner/README.txt b/evennia/server/profiling/README.txt similarity index 100% rename from src/utils/dummyrunner/README.txt rename to evennia/server/profiling/README.txt diff --git a/src/web/__init__.py b/evennia/server/profiling/__init__.py similarity index 100% rename from src/web/__init__.py rename to evennia/server/profiling/__init__.py diff --git a/evennia/server/profiling/dummyrunner.py b/evennia/server/profiling/dummyrunner.py new file mode 100644 index 000000000..469747f5d --- /dev/null +++ b/evennia/server/profiling/dummyrunner.py @@ -0,0 +1,348 @@ +""" +Dummy client runner + +This module implements a stand-alone launcher for stress-testing +an Evennia game. It will launch any number of fake clients. These +clients will log into the server and start doing random operations. +Customizing and weighing these operations differently depends on +which type of game is tested. The module contains a testing module +for plain Evennia. + +Please note that you shouldn't run this on a production server! +Launch the program without any arguments or options to see a +full step-by-step setup help. + +Basically (for testing default Evennia): + + - Use an empty/testing database. + - set PERMISSION_PLAYER_DEFAULT = "Builders" + - start server, eventually with profiling active + - launch this client runner + +If you want to customize the runner's client actions +(because you changed the cmdset or needs to better +match your use cases or add more actions), you can +change which actions by adding a path to + + DUMMYRUNNER_ACTIONS_MODULE = + +in your settings. See utils.dummyrunner_actions.py +for instructions on how to define this module. + +""" + +import sys +import time +import random +from argparse import ArgumentParser +from twisted.conch import telnet +from twisted.internet import reactor, protocol +from twisted.internet.task import LoopingCall + +from django.conf import settings +from evennia.utils import mod_import, time_format + +# Load the dummyrunner settings module + +DUMMYRUNNER_SETTINGS = mod_import(settings.DUMMYRUNNER_SETTINGS_MODULE) +if not DUMMYRUNNER_SETTINGS: + raise IOError("Error: Dummyrunner could not find settings file at %s" % + settings.DUMMYRUNNER_SETTINGS_MODULE) + +DATESTRING = "%Y%m%d%H%M%S" + +# Settings + +# number of clients to launch if no input is given on command line +NCLIENTS = 1 +# time between each 'tick', in seconds, if not set on command +# line. All launched clients will be called upon to possibly do an +# action with this frequency. +TIMESTEP = DUMMYRUNNER_SETTINGS.TIMESTEP +# chance of a client performing an action, per timestep. This helps to +# spread out usage randomly, like it would be in reality. +CHANCE_OF_ACTION = DUMMYRUNNER_SETTINGS.CHANCE_OF_ACTION +# spread out the login action separately, having many players create accounts +# and connect simultaneously is generally unlikely. +CHANCE_OF_LOGIN = DUMMYRUNNER_SETTINGS.CHANCE_OF_LOGIN +# Port to use, if not specified on command line +TELNET_PORT = DUMMYRUNNER_SETTINGS.TELNET_PORT or settings.TELNET_PORTS[0] +# +NLOGGED_IN = 0 + + +# Messages + + +INFO_STARTING = \ + """ + Dummyrunner starting using {N} dummy player(s). If you don't see + any connection messages, make sure that the Evennia server is + running. + + Use Ctrl-C to stop/disconnect clients. + """ + +ERROR_NO_MIXIN = \ + """ + Error: Evennia is not set up for dummyrunner. Before starting the + server, make sure to include the following at *the end* of your + settings file (remove when not using dummyrunner!): + + from evennia.server.profiling.settings_mixin import * + + This will change the settings in the following way: + - change PERMISSION_PLAYER_DEFAULT to 'Immortals' to allow clients + to test all commands + - change PASSWORD_HASHERS to use a faster (but less safe) algorithm + when creating large numbers of accounts at the same time + + If you don't want to use the custom settings of the mixin for some + reason, you can change their values manually after the import, or + add DUMMYRUNNER_MIXIN=True to your settings file to avoid this + error completely. + + Warning: Don't run dummyrunner on a production database! It will + create a lot of spammy objects and player accounts! + """ + + +ERROR_FEW_ACTIONS = \ + """ + Dummyrunner settings error: The ACTIONS tuple is too short: it must + contain at least login- and logout functions. + """ + + +HELPTEXT = """ +DO NOT RUN THIS ON A PRODUCTION SERVER! USE A CLEAN/TESTING DATABASE! + +This stand-alone program launches dummy telnet clients against a +running Evennia server. The idea is to mimic real players logging in +and repeatedly doing resource-heavy commands so as to stress test the +game. It uses the default command set to log in and issue commands, so +if that was customized, some of the functionality will not be tested +(it will not fail, the commands will just not be recognized). The +running clients will create new objects and rooms all over the place +as part of their running, so using a clean/testing database is +strongly recommended. + +Setup: + 1) setup a fresh/clean database (if using sqlite, just safe-copy + away your real evennia.db3 file and create a new one with + manage.py) + 2) in server/conf/settings.py, add + + PERMISSION_PLAYER_DEFAULT="Builders" + + This is so that the dummy players can test building operations. + You can also customize the dummyrunner by modifying a setting + file specified by DUMMYRUNNER_SETTINGS_MODULE + + 3) Start Evennia like normal, optionally with profiling (--profile) + 4) Run this dummy runner via the evennia launcher: + + evennia --dummyrunner + + 5) Log on and determine if game remains responsive despite the + heavier load. Note that if you do profiling, there is an + additional overhead from the profiler too!j + 6) If you use profiling, let the game run long enough to gather + data, then stop the server cleanly using evennia stop or @shutdown. + @shutdown. The profile appears as + server/logs/server.prof/portal.prof (see Python's manual on + cProfiler). + +""" + +#------------------------------------------------------------ +# Helper functions +#------------------------------------------------------------ + + +ICOUNT = 0 +def idcounter(): + "makes unique ids" + global ICOUNT + ICOUNT += 1 + return str(ICOUNT) + + +GCOUNT = 0 +def gidcounter(): + "makes globally unique ids" + global GCOUNT + GCOUNT += 1 + return "%s-%s" % (time.strftime(DATESTRING), GCOUNT) + + +def makeiter(obj): + "makes everything iterable" + if not hasattr(obj, '__iter__'): + return [obj] + return obj + +#------------------------------------------------------------ +# Client classes +#------------------------------------------------------------ + +class DummyClient(telnet.StatefulTelnetProtocol): + """ + Handles connection to a running Evennia server, + mimicking a real player by sending commands on + a timer. + """ + + def connectionMade(self): + + # public properties + self.cid = idcounter() + self.key = "Dummy-%s" % self.cid + self.gid = "%s-%s" % (time.strftime(DATESTRING), self.cid) + self.istep = 0 + self.exits = [] # exit names created + self.objs = [] # obj names created + + self._connected = False + self._loggedin = False + self._logging_out = False + self._report = "" + self._cmdlist = [] # already stepping in a cmd definition + self._login = self.factory.actions[0] + self._logout = self.factory.actions[1] + self._actions = self.factory.actions[2:] + + reactor.addSystemEventTrigger('before', 'shutdown', self.logout) + + def dataReceived(self, data): + "Wait to start stepping until the server actually responds" + if not self._connected and not data.startswith(chr(255)): + # wait until we actually get text back (not just telnet + # negotiation) + self._connected = True + # start client tick + d = LoopingCall(self.step) + # dissipate exact step by up to +/- 0.5 second + timestep = TIMESTEP + (-0.5 + (random.random()*1.0)) + d.start(timestep, now=True).addErrback(self.error) + + def connectionLost(self, reason): + "loosing the connection" + if not self._logging_out: + print "client %s(%s) lost connection (%s)" % (self.key, self.cid, reason) + + def error(self, err): + "error callback" + print err + + def counter(self): + "produces a unique id, also between clients" + return gidcounter() + + def logout(self): + "Causes the client to log out of the server. Triggered by ctrl-c signal." + self._logging_out = True + cmd = self._logout(self) + print "client %s(%s) logout (%s actions)" % (self.key, self.cid, self.istep) + self.sendLine(cmd) + + def step(self): + """ + Perform a step. This is called repeatedly by the runner + and causes the client to issue commands to the server. + This holds all "intelligence" of the dummy client. + """ + global NLOGGED_IN + + rand = random.random() + + if not self._cmdlist: + # no commands ready. Load some. + + if not self._loggedin: + if rand < CHANCE_OF_LOGIN: + # get the login commands + self._cmdlist = list(makeiter(self._login(self))) + NLOGGED_IN += 1 # this is for book-keeping + print "connecting client %s (%i/%i)..." % (self.key, NLOGGED_IN, NCLIENTS) + self._loggedin = True + else: + # no login yet, so cmdlist not yet set + return + else: + # we always pick a cumulatively random function + crand = random.random() + cfunc = [func for (cprob, func) in self._actions if cprob >= crand][0] + self._cmdlist = list(makeiter(cfunc(self))) + + # at this point we always have a list of commands + if rand < CHANCE_OF_ACTION: + # send to the game + self.sendLine(str(self._cmdlist.pop(0))) + self.istep += 1 + + +class DummyFactory(protocol.ClientFactory): + protocol = DummyClient + def __init__(self, actions): + "Setup the factory base (shared by all clients)" + self.actions = actions + +#------------------------------------------------------------ +# Access method: +# Starts clients and connects them to a running server. +#------------------------------------------------------------ + +def start_all_dummy_clients(nclients): + + global NCLIENTS + NCLIENTS = int(nclients) + actions = DUMMYRUNNER_SETTINGS.ACTIONS + + if len(actions) < 2: + print ERROR_FEW_ACTIONS + return + + # make sure the probabilities add up to 1 + pratio = 1.0 / sum(tup[0] for tup in actions[2:]) + flogin, flogout, probs, cfuncs = actions[0], actions[1], [tup[0] * pratio for tup in actions[2:]], [tup[1] for tup in actions[2:]] + # create cumulative probabilies for the random actions + cprobs = [sum(v for i,v in enumerate(probs) if i<=k) for k in range(len(probs))] + # rebuild a new, optimized action structure + actions = (flogin, flogout) + tuple(zip(cprobs, cfuncs)) + + # setting up all clients (they are automatically started) + factory = DummyFactory(actions) + for i in range(NCLIENTS): + reactor.connectTCP("localhost", TELNET_PORT, factory) + # start reactor + reactor.run() + +#------------------------------------------------------------ +# Command line interface +#------------------------------------------------------------ + +if __name__ == '__main__': + + try: + settings.DUMMYRUNNER_MIXIN + except AttributeError: + print ERROR_NO_MIXIN + sys.exit() + + # parsing command line with default vals + parser = ArgumentParser(description=HELPTEXT) + parser.add_argument("-N", nargs=1, default=1, dest="nclients", + help="Number of clients to start") + + args = parser.parse_args() + + print INFO_STARTING.format(N=args.nclients[0]) + + # run the dummyrunner + t0 = time.time() + start_all_dummy_clients(nclients=args.nclients[0]) + ttot = time.time() - t0 + + # output runtime + print "... dummy client runner stopped after %s." % time_format(ttot, style=3) diff --git a/evennia/server/profiling/dummyrunner_settings.py b/evennia/server/profiling/dummyrunner_settings.py new file mode 100644 index 000000000..8f2dd013e --- /dev/null +++ b/evennia/server/profiling/dummyrunner_settings.py @@ -0,0 +1,267 @@ +""" +Settings and actions for the dummyrunner + +This module defines dummyrunner settings and sets up +the actions available to dummy players. + +The settings are global variables: + +TIMESTEP - time in seconds between each 'tick' +CHANCE_OF_ACTION - chance 0-1 of action happening +CHANCE_OF_LOGIN - chance 0-1 of login happening +TELNET_PORT - port to use, defaults to settings.TELNET_PORT +ACTIONS - see below + +ACTIONS is a tuple + +(login_func, logout_func, (0.3, func1), (0.1, func2) ... ) + +where the first entry is the function to call on first connect, with a +chance of occurring given by CHANCE_OF_LOGIN. This function is usually +responsible for logging in the player. The second entry is always +called when the dummyrunner disconnects from the server and should +thus issue a logout command. The other entries are tuples (chance, +func). They are picked randomly, their commonality based on the +cumulative chance given (the chance is normalized between all options +so if will still work also if the given chances don't add up to 1). +Since each function can return a list of game-command strings, each +function may result in multiple operations. + +An action-function is called with a "client" argument which is a +reference to the dummy client currently performing the action. It +returns a string or a list of command strings to execute. Use the +client object for optionally saving data between actions. + +The client object has the following relevant properties and methods: + key - an optional client key. This is only used for dummyrunner output. + Default is "Dummy-" + cid - client id + gid - globally unique id, hashed with time stamp + istep - the current step + exits - an empty list. Can be used to store exit names + objs - an empty list. Can be used to store object names + counter() - returns a unique increasing id, hashed with time stamp + to make it unique also between dummyrunner instances. + +The return should either be a single command string or a tuple of +command strings. This list of commands will always be executed every +TIMESTEP with a chance given by CHANCE_OF_ACTION by in the order given +(no randomness) and allows for setting up a more complex chain of +commands (such as creating an account and logging in). + +""" +# Dummy runner settings + +# Time between each dummyrunner "tick", in seconds. Each dummy +# will be called with this frequency. +TIMESTEP = 2 + +# Chance of a dummy actually performing an action on a given tick. +# This spreads out usage randomly, like it would be in reality. +CHANCE_OF_ACTION = 0.5 + +# Chance of a currently unlogged-in dummy performing its login +# action every tick. This emulates not all players logging in +# at exactly the same time. +CHANCE_OF_LOGIN = 1.0 + +# Which telnet port to connect to. If set to None, uses the first +# default telnet port of the running server. +TELNET_PORT = None + + +# Setup actions tuple + +# some convenient templates + +DUMMY_NAME = "Dummy-%s" +DUMMY_PWD = "password-%s" +START_ROOM = "testing_room_start_%s" +ROOM_TEMPLATE = "testing_room_%s" +EXIT_TEMPLATE = "exit_%s" +OBJ_TEMPLATE = "testing_obj_%s" +TOBJ_TEMPLATE = "testing_button_%s" +TOBJ_TYPECLASS = "contrib.tutorial_examples.red_button.RedButton" + + +# action function definitions (pick and choose from +# these to build a client "usage profile" + +# login/logout + +def c_login(client): + "logins to the game" + # we always use a new client name + cname = DUMMY_NAME % client.gid + cpwd = DUMMY_PWD % client.gid + + # set up for digging a first room (to move to and keep the + # login room clean) + roomname = ROOM_TEMPLATE % client.counter() + exitname1 = EXIT_TEMPLATE % client.counter() + exitname2 = EXIT_TEMPLATE % client.counter() + client.exits.extend([exitname1, exitname2]) + + cmds = ('create %s %s' % (cname, cpwd), + 'connect %s %s' % (cname, cpwd), + '@dig %s' % START_ROOM % client.gid, + '@teleport %s' % START_ROOM % client.gid, + '@dig %s = %s, %s' % (roomname, exitname1, exitname2) + ) + return cmds + +def c_login_nodig(client): + "logins, don't dig its own room" + cname = DUMMY_NAME % client.gid + cpwd = DUMMY_PWD % client.gid + + cmds = ('create %s %s' % (cname, cpwd), + 'connect %s %s' % (cname, cpwd)) + return cmds + +def c_logout(client): + "logouts of the game" + return "@quit" + +# random commands + +def c_looks(client): + "looks at various objects" + cmds = ["look %s" % obj for obj in client.objs] + if not cmds: + cmds = ["look %s" % exi for exi in client.exits] + if not cmds: + cmds = "look" + return cmds + +def c_examines(client): + "examines various objects" + cmds = ["examine %s" % obj for obj in client.objs] + if not cmds: + cmds = ["examine %s" % exi for exi in client.exits] + if not cmds: + cmds = "examine me" + return cmds + +def c_help(client): + "reads help files" + cmds = ('help', + 'help @teleport', + 'help look', + 'help @tunnel', + 'help @dig') + return cmds + +def c_digs(client): + "digs a new room, storing exit names on client" + roomname = ROOM_TEMPLATE % client.counter() + exitname1 = EXIT_TEMPLATE % client.counter() + exitname2 = EXIT_TEMPLATE % client.counter() + client.exits.extend([exitname1, exitname2]) + return '@dig/tel %s = %s, %s' % (roomname, exitname1, exitname2) + +def c_creates_obj(client): + "creates normal objects, storing their name on client" + objname = OBJ_TEMPLATE % client.counter() + client.objs.append(objname) + cmds = ('@create %s' % objname, + '@desc %s = "this is a test object' % objname, + '@set %s/testattr = this is a test attribute value.' % objname, + '@set %s/testattr2 = this is a second test attribute.' % objname) + return cmds + +def c_creates_button(client): + "creates example button, storing name on client" + objname = TOBJ_TEMPLATE % client.counter() + client.objs.append(objname) + cmds = ('@create %s:%s' % (objname, TOBJ_TYPECLASS), + '@desc %s = test red button!' % objname) + return cmds + +def c_socialize(client): + "socializechats on channel" + cmds = ('ooc Hello!', + 'ooc Testing ...', + 'ooc Testing ... times 2', + 'say Yo!', + 'emote stands looking around.') + return cmds + +def c_moves(client): + "moves to a previously created room, using the stored exits" + cmds = client.exits # try all exits - finally one will work + return "look" if not cmds else cmds + +def c_moves_n(client): + "move through north exit if available" + return "north" + +def c_moves_s(client): + "move through south exit if available" + return "south" + +# Action tuple (required) +# +# This is a tuple of client action functions. The first element is the +# function the client should use to log into the game and move to +# STARTROOM . The second element is the logout command, for cleanly +# exiting the mud. The following elements are 2-tuples of (probability, +# action_function). The probablities should normally sum up to 1, +# otherwise the system will normalize them. +# + +## "normal builder" definitionj +#ACTIONS = ( c_login, +# c_logout, +# (0.5, c_looks), +# (0.08, c_examines), +# (0.1, c_help), +# (0.01, c_digs), +# (0.01, c_creates_obj), +# (0.3, c_moves)) +## "heavy" builder definition +#ACTIONS = ( c_login, +# c_logout, +# (0.2, c_looks), +# (0.1, c_examines), +# (0.2, c_help), +# (0.1, c_digs), +# (0.1, c_creates_obj), +# #(0.01, c_creates_button), +# (0.2, c_moves)) +## "passive player" definition +#ACTIONS = ( c_login, +# c_logout, +# (0.7, c_looks), +# #(0.1, c_examines), +# (0.3, c_help)) +# #(0.1, c_digs), +# #(0.1, c_creates_obj), +# #(0.1, c_creates_button), +# #(0.4, c_moves)) +## "normal player" definition +ACTIONS = ( c_login, + c_logout, + (0.01, c_digs), + (0.39, c_looks), + (0.2, c_help), + (0.4, c_moves)) +# walking tester. This requires a pre-made +# "loop" of multiple rooms that ties back +# to limbo (using @tunnel and @open) +#ACTIONS = (c_login_nodig, +# c_logout, +# (1.0, c_moves_n)) +## "socializing heavy builder" definition +#ACTIONS = (c_login, +# c_logout, +# (0.1, c_socialize), +# (0.1, c_looks), +# (0.2, c_help), +# (0.1, c_creates_obj), +# (0.2, c_digs), +# (0.3, c_moves)) +## "heavy digger memory tester" definition +#ACTIONS = (c_login, +# c_logout, +# (1.0, c_digs)) diff --git a/src/utils/dummyrunner/memplot.py b/evennia/server/profiling/memplot.py similarity index 93% rename from src/utils/dummyrunner/memplot.py rename to evennia/server/profiling/memplot.py index a16c0e4b9..99aff4bbd 100644 --- a/src/utils/dummyrunner/memplot.py +++ b/evennia/server/profiling/memplot.py @@ -8,10 +8,11 @@ Call this module directly to plot the log (requires matplotlib and numpy). """ import os, sys import time -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) -os.environ['DJANGO_SETTINGS_MODULE'] = 'game.settings' +#TODO! +#sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +#os.environ['DJANGO_SETTINGS_MODULE'] = 'game.settings' import ev -from src.utils.idmapper import base as _idmapper +from evennia.utils.idmapper import base as _idmapper LOGFILE = "logs/memoryusage.log" INTERVAL = 30 # log every 30 seconds diff --git a/evennia/server/profiling/settings_mixin.py b/evennia/server/profiling/settings_mixin.py new file mode 100644 index 000000000..523eb893e --- /dev/null +++ b/evennia/server/profiling/settings_mixin.py @@ -0,0 +1,21 @@ +""" +Dummyrunner mixin. Add this at the end of the settings file before +running dummyrunner, like this: + + from evennia.server.profiling.settings_mixin import * + +Note that these mixin-settings are not suitable for production +servers! +""" + +# the dummyrunner will check this variable to make sure +# the mixin is present +DUMMYRUNNER_MIXIN = True +# a faster password hasher suitable for multiple simultaneous +# player creations. The default one is safer but deliberately +# very slow to make cracking harder. +PASSWORD_HASHERS = ( + 'django.contrib.auth.hashers.MD5PasswordHasher', + ) +# make dummy clients able to test all commands +PERMISSION_PLAYER_DEFAULT = "Immortals" diff --git a/src/utils/dummyrunner/test_queries.py b/evennia/server/profiling/test_queries.py similarity index 80% rename from src/utils/dummyrunner/test_queries.py rename to evennia/server/profiling/test_queries.py index c0f9e191a..d53db20e9 100644 --- a/src/utils/dummyrunner/test_queries.py +++ b/evennia/server/profiling/test_queries.py @@ -4,8 +4,8 @@ query as well as count them for optimization testing. """ import sys, os -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) -os.environ["DJANGO_SETTINGS_MODULE"] = "game.settings" +#sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +#os.environ["DJANGO_SETTINGS_MODULE"] = "game.settings" from django.db import connection @@ -31,7 +31,7 @@ if __name__ == "__main__": setup_string = \ """ -from src.objects.models import ObjectDB +from evennia.objects.models import ObjectDB g = ObjectDB.objects.get(db_key="Griatch") """ exec_string = \ diff --git a/src/server/server.py b/evennia/server/server.py similarity index 80% rename from src/server/server.py rename to evennia/server/server.py index a32b0d980..1a0ec9c65 100644 --- a/src/server/server.py +++ b/evennia/server/server.py @@ -10,49 +10,36 @@ by game/evennia.py). import time import sys import os -if os.name == 'nt': - # For Windows batchfile we need an extra path insertion here. - sys.path.insert(0, os.path.dirname(os.path.dirname( - os.path.dirname(os.path.abspath(__file__))))) + from twisted.web import server, static from twisted.application import internet, service from twisted.internet import reactor, defer import django django.setup() +import evennia +evennia._init() + from django.db import connection from django.conf import settings -from src.players.models import PlayerDB -from src.scripts.models import ScriptDB -from src.server.models import ServerConfig -from src.server import initial_setup +from evennia.players.models import PlayerDB +from evennia.scripts.models import ScriptDB +from evennia.server.models import ServerConfig +from evennia.server import initial_setup -from src.utils.utils import get_evennia_version, mod_import, make_iter -from src.comms import channelhandler -from src.server.sessionhandler import SESSIONS - -# setting up server-side field cache - -from django.db.models.signals import post_save -from src.server.caches import field_post_save -#pre_save.connect(field_pre_save, dispatch_uid="fieldcache") -post_save.connect(field_post_save, dispatch_uid="fieldcache") - -#from src.server.caches import post_attr_update -#from django.db.models.signals import m2m_changed - -# connect to attribute cache signal -#m2m_changed.connect(post_attr_update, sender=TypedObject.db_attributes.through) +from evennia.utils.utils import get_evennia_version, mod_import, make_iter +from evennia.comms import channelhandler +from evennia.server.sessionhandler import SESSIONS _SA = object.__setattr__ if os.name == 'nt': # For Windows we need to handle pid files manually. - SERVER_PIDFILE = os.path.join(settings.GAME_DIR, 'server.pid') + SERVER_PIDFILE = os.path.join(settings.GAME_DIR, "server", 'server.pid') # a file with a flag telling the server to restart after shutdown or not. -SERVER_RESTART = os.path.join(settings.GAME_DIR, 'server.restart') +SERVER_RESTART = os.path.join(settings.GAME_DIR, "server", 'server.restart') # module containing hook methods called during start_stop SERVER_STARTSTOP_MODULE = mod_import(settings.AT_SERVER_STARTSTOP_MODULE) @@ -103,7 +90,7 @@ class Evennia(object): application - an instantiated Twisted application """ - sys.path.append('.') + sys.path.insert(1, '.') # create a store of services self.services = service.IServiceCollection(application) @@ -169,24 +156,24 @@ class Evennia(object): if len(mismatches): # can't use any() since mismatches may be [0] which reads as False for any() # we have a changed default. Import relevant objects and # run the update - from src.objects.models import ObjectDB - from src.comms.models import ChannelDB - #from src.players.models import PlayerDB + from evennia.objects.models import ObjectDB + from evennia.comms.models import ChannelDB + #from evennia.players.models import PlayerDB for i, prev, curr in ((i, tup[0], tup[1]) for i, tup in enumerate(settings_compare) if i in mismatches): # update the database print " %s:\n '%s' changed to '%s'. Updating unchanged entries in database ..." % (settings_names[i], prev, curr) if i == 0: - [obj.__setattr__("cmdset_storage", curr) for obj in ObjectDB.objects.filter(db_cmdset_storage__exact=prev)] + ObjectDB.objects.filter(db_cmdset_storage__exact=prev).update(db_cmdset_storage=curr) if i == 1: - [ply.__setattr__("cmdset_storage", curr) for ply in PlayerDB.objects.filter(db_cmdset_storage__exact=prev)] + PlayerDB.objects.filter(db_cmdset_storage__exact=prev).update(db_cmdset_storage=curr) if i == 2: - [ply.__setattr__("typeclass_path", curr) for ply in PlayerDB.objects.filter(db_typeclass_path__exact=prev)] + PlayerDB.objects.filter(db_typeclass_path__exact=prev).update(db_typeclass_path=curr) if i in (3, 4, 5, 6): - [obj.__setattr__("typeclass_path", curr) for obj in ObjectDB.objects.filter(db_typeclass_path__exact=prev)] + ObjectDB.objects.filter(db_typeclass_path__exact=prev).update(db_typeclass_path=curr) if i == 7: - [scr.__setattr__("typeclass_path", curr) for scr in ScriptDB.objects.filter(db_typeclass_path__exact=prev)] + ScriptDB.objects.filter(db_typeclass_path__exact=prev).update(db_typeclass_path=curr) if i == 8: - [scr.__setattr__("typeclass_path", curr) for scr in ChannelDB.objects.filter(db_typeclass_path__exact=prev)] + ChannelDB.objects.filter(db_typeclass_path__exact=prev).update(db_typeclass_path=curr) # store the new default and clean caches ServerConfig.objects.conf(settings_names[i], curr) ObjectDB.flush_instance_cache() @@ -225,23 +212,23 @@ class Evennia(object): """ Called every server start """ - from src.objects.models import ObjectDB - #from src.players.models import PlayerDB + from evennia.objects.models import ObjectDB + #from evennia.players.models import PlayerDB #update eventual changed defaults self.update_defaults() #print "run_init_hooks:", ObjectDB.get_all_cached_instances() - [(o.typeclass, o.at_init()) for o in ObjectDB.get_all_cached_instances()] - [(p.typeclass, p.at_init()) for p in PlayerDB.get_all_cached_instances()] + [o.at_init() for o in ObjectDB.get_all_cached_instances()] + [p.at_init() for p in PlayerDB.get_all_cached_instances()] with open(SERVER_RESTART, 'r') as f: mode = f.read() if mode in ('True', 'reload'): - from src.server.oobhandler import OOB_HANDLER + from evennia.server.oobhandler import OOB_HANDLER OOB_HANDLER.restore() - from src.scripts.tickerhandler import TICKER_HANDLER + from evennia.scripts.tickerhandler import TICKER_HANDLER TICKER_HANDLER.restore() # call correct server hook based on start file value @@ -299,24 +286,22 @@ class Evennia(object): mode = self.set_restart_mode(mode) # call shutdown hooks on all cached objects - from src.objects.models import ObjectDB - #from src.players.models import PlayerDB - from src.server.models import ServerConfig + from evennia.objects.models import ObjectDB + #from evennia.players.models import PlayerDB + from evennia.server.models import ServerConfig if mode == 'reload': # call restart hooks - yield [(o.typeclass, o.at_server_reload()) - for o in ObjectDB.get_all_cached_instances()] - yield [(p.typeclass, p.at_server_reload()) - for p in PlayerDB.get_all_cached_instances()] - yield [(s.typeclass, s.pause(), s.at_server_reload()) + yield [o.at_server_reload() for o in ObjectDB.get_all_cached_instances()] + yield [p.at_server_reload() for p in PlayerDB.get_all_cached_instances()] + yield [(s.pause(), s.at_server_reload()) for s in ScriptDB.get_all_cached_instances()] yield self.sessions.all_sessions_portal_sync() ServerConfig.objects.conf("server_restart_mode", "reload") - from src.server.oobhandler import OOB_HANDLER + from evennia.server.oobhandler import OOB_HANDLER OOB_HANDLER.save() - from src.scripts.tickerhandler import TICKER_HANDLER + from evennia.scripts.tickerhandler import TICKER_HANDLER TICKER_HANDLER.save() self.at_server_reload_stop() @@ -325,26 +310,21 @@ class Evennia(object): if mode == 'reset': # don't unset the is_connected flag on reset, otherwise # same as shutdown - yield [(o.typeclass, o.at_server_shutdown()) - for o in ObjectDB.get_all_cached_instances()] - yield [(p.typeclass, p.at_server_shutdown()) - for p in PlayerDB.get_all_cached_instances()] + yield [o.at_server_shutdown() for o in ObjectDB.get_all_cached_instances()] + yield [p.at_server_shutdown() for p in PlayerDB.get_all_cached_instances()] else: # shutdown - yield [_SA(p, "is_connected", False) - for p in PlayerDB.get_all_cached_instances()] - yield [(o.typeclass, o.at_server_shutdown()) - for o in ObjectDB.get_all_cached_instances()] - yield [(p.typeclass, p.unpuppet_all(), p.at_server_shutdown()) + yield [_SA(p, "is_connected", False) for p in PlayerDB.get_all_cached_instances()] + yield [o.at_server_shutdown() for o in ObjectDB.get_all_cached_instances()] + yield [(p.unpuppet_all(), p.at_server_shutdown()) for p in PlayerDB.get_all_cached_instances()] - yield [(s.typeclass, s.at_server_shutdown()) - for s in ScriptDB.get_all_cached_instances()] + yield [s.at_server_shutdown() for s in ScriptDB.get_all_cached_instances()] yield ObjectDB.objects.clear_all_sessids() ServerConfig.objects.conf("server_restart_mode", "reset") self.at_server_cold_stop() # stopping time - from src.utils import gametime + from evennia.utils import gametime gametime.save() self.at_server_stop() @@ -446,7 +426,7 @@ if AMP_ENABLED: ifacestr = "-%s" % AMP_INTERFACE print ' amp (to Portal)%s: %s' % (ifacestr, AMP_PORT) - from src.server import amp + from evennia.server import amp factory = amp.AmpServerFactory(EVENNIA) amp_service = internet.TCPServer(AMP_PORT, factory, interface=AMP_INTERFACE) @@ -458,7 +438,7 @@ if WEBSERVER_ENABLED: # Start a django-compatible webserver. from twisted.python import threadpool - from src.server.webserver import DjangoWebRoot, WSGIWebServer + from evennia.server.webserver import DjangoWebRoot, WSGIWebServer # start a thread pool and define the root url (/) as a wsgi resource # recognized by Django diff --git a/src/server/serversession.py b/evennia/server/serversession.py similarity index 82% rename from src/server/serversession.py rename to evennia/server/serversession.py index 1a8896b75..9261ccbb1 100644 --- a/src/server/serversession.py +++ b/evennia/server/serversession.py @@ -7,26 +7,21 @@ It is stored on the Server side (as opposed to protocol-specific sessions which are stored on the Portal side) """ -import time +from time import time from datetime import datetime from django.conf import settings -#from src.scripts.models import ScriptDB -from src.comms.models import ChannelDB -from src.utils import logger, utils -from src.utils.inlinefunc import parse_inlinefunc -from src.utils.utils import make_iter -from src.commands.cmdhandler import cmdhandler -from src.commands.cmdsethandler import CmdSetHandler -from src.server.session import Session +from evennia.comms.models import ChannelDB +from evennia.utils import logger +from evennia.utils.inlinefunc import parse_inlinefunc +from evennia.utils.utils import make_iter +from evennia.commands.cmdhandler import cmdhandler +from evennia.commands.cmdsethandler import CmdSetHandler +from evennia.server.session import Session IDLE_COMMAND = settings.IDLE_COMMAND _GA = object.__getattribute__ _ObjectDB = None -_OOB_HANDLER = None -# load optional out-of-band function module (this acts as a verification) -OOB_PLUGIN_MODULES = [utils.mod_import(mod) - for mod in make_iter(settings.OOB_PLUGIN_MODULES) if mod] INLINEFUNC_ENABLED = settings.INLINEFUNC_ENABLED # i18n @@ -72,7 +67,7 @@ class ServerSession(Session): """ global _ObjectDB if not _ObjectDB: - from src.objects.models import ObjectDB as _ObjectDB + from evennia.objects.models import ObjectDB as _ObjectDB if not self.logged_in: # assign the unloggedin-command set. @@ -82,9 +77,16 @@ class ServerSession(Session): if self.puid: # reconnect puppet (puid is only set if we are coming - # back from a server reload) + # back from a server reload). This does all the steps + # done in the default @ic command but without any + # hooks, echoes or access checks. obj = _ObjectDB.objects.get(id=self.puid) - self.player.puppet_object(self.sessid, obj, normal_mode=False) + obj.sessid.add(self.sessid) + obj.player = self.player + self.puid = obj.id + self.puppet = obj + obj.scripts.validate() + obj.locks.cache_lock_bypass(obj) def at_login(self, player): """ @@ -96,7 +98,7 @@ class ServerSession(Session): self.uid = self.player.id self.uname = self.player.username self.logged_in = True - self.conn_time = time.time() + self.conn_time = time() self.puid = None self.puppet = None self.cmdset_storage = settings.CMDSET_SESSION @@ -115,18 +117,19 @@ class ServerSession(Session): if self.logged_in: sessid = self.sessid player = self.player - _GA(player.dbobj, "unpuppet_object")(sessid) - uaccount = player.dbobj + if self.puppet: + player.unpuppet_object(sessid) + uaccount = player uaccount.last_login = datetime.now() uaccount.save() # calling player hook - _GA(player.typeclass, "at_disconnect")() + player.at_disconnect() self.logged_in = False if not self.sessionhandler.sessions_from_player(player): # no more sessions connected to this player player.is_connected = False # this may be used to e.g. delete player after disconnection etc - _GA(player.typeclass, "at_post_disconnect")() + player.at_post_disconnect() def get_player(self): """ @@ -181,22 +184,20 @@ class ServerSession(Session): and command counters. """ # Store the timestamp of the user's last command. - self.cmd_last = time.time() if not idle: # Increment the user's command counter. self.cmd_total += 1 # Player-visible idle time, not used in idle timeout calcs. - self.cmd_last_visible = time.time() + self.cmd_last_visible = time() def data_in(self, text=None, **kwargs): """ Send User->Evennia. This will in effect execute a command string on the server. - Especially handled keywords: + Note that oob data is already sent to the + oobhandler at this point. - oob - this should hold a dictionary of oob command calls from - the oob-supporting protocol. """ #explicitly check for None since text can be an empty string, which is #also valid @@ -218,16 +219,6 @@ class ServerSession(Session): categories=("inputline", "channels"), include_player=False) cmdhandler(self, text, callertype="session", sessid=self.sessid) self.update_session_counters() - if "oob" in kwargs: - # handle oob instructions - global _OOB_HANDLER - if not _OOB_HANDLER: - from src.server.oobhandler import OOB_HANDLER as _OOB_HANDLER - oobstruct = self.sessionhandler.oobstruct_parser(kwargs.pop("oob", None)) - #print "session.data_in: oobstruct:",oobstruct - for (funcname, args, kwargs) in oobstruct: - if funcname: - _OOB_HANDLER.execute_cmd(self, funcname, *args, **kwargs) execute_cmd = data_in # alias @@ -238,7 +229,11 @@ class ServerSession(Session): text = text if text else "" if INLINEFUNC_ENABLED and not "raw" in kwargs: text = parse_inlinefunc(text, strip="strip_inlinefunc" in kwargs, session=self) - self.sessionhandler.data_out(self, text=text, **kwargs) + session = kwargs.pop('session', None) + session = session or self + self.sessionhandler.data_out(session, text=text, **kwargs) + # alias + msg = data_out def __eq__(self, other): return self.address == other.address @@ -266,18 +261,6 @@ class ServerSession(Session): """ return u"%s" % str(self) - # easy-access functions - - #def login(self, player): - # "alias for at_login" - # self.session_login(player) - #def disconnect(self): - # "alias for session_disconnect" - # self.session_disconnect() - def msg(self, text='', **kwargs): - "alias for at_data_out" - self.data_out(text=text, **kwargs) - # Dummy API hooks for use during non-loggedin operation def at_cmdset_get(self, **kwargs): diff --git a/src/server/session.py b/evennia/server/session.py similarity index 100% rename from src/server/session.py rename to evennia/server/session.py diff --git a/src/server/sessionhandler.py b/evennia/server/sessionhandler.py similarity index 76% rename from src/server/sessionhandler.py rename to evennia/server/sessionhandler.py index 046b4cc07..09e3bb257 100644 --- a/src/server/sessionhandler.py +++ b/evennia/server/sessionhandler.py @@ -12,11 +12,13 @@ There are two similar but separate stores of sessions: """ -import time +from time import time from django.conf import settings -from src.commands.cmdhandler import CMD_LOGINSTART -from src.utils.utils import variable_from_module, is_iter, \ - to_str, to_unicode, strip_control_sequences +from evennia.commands.cmdhandler import CMD_LOGINSTART +from evennia.utils.utils import variable_from_module, is_iter, \ + to_str, to_unicode, strip_control_sequences, make_iter +from evennia.utils import logger + try: import cPickle as pickle except ImportError: @@ -27,6 +29,7 @@ _PlayerDB = None _ServerSession = None _ServerConfig = None _ScriptDB = None +_OOB_HANDLER = None # AMP signals @@ -44,11 +47,15 @@ PCONNSYNC = chr(10) # portal post-syncing session # i18n from django.utils.translation import ugettext as _ -SERVERNAME = settings.SERVERNAME -MULTISESSION_MODE = settings.MULTISESSION_MODE -IDLE_TIMEOUT = settings.IDLE_TIMEOUT +_SERVERNAME = settings.SERVERNAME +_MULTISESSION_MODE = settings.MULTISESSION_MODE +_IDLE_TIMEOUT = settings.IDLE_TIMEOUT +_MAX_SERVER_COMMANDS_PER_SECOND = 100.0 +_MAX_SESSION_COMMANDS_PER_SECOND = 5.0 +_ERROR_COMMAND_OVERFLOW = "You entered commands too fast. Wait a moment and try again." + def delayed_import(): "Helper method for delayed import of all needed entities" global _ServerSession, _PlayerDB, _ServerConfig, _ScriptDB @@ -57,11 +64,11 @@ def delayed_import(): modulename, classname = settings.SERVER_SESSION_CLASS.rsplit(".", 1) _ServerSession = variable_from_module(modulename, classname) if not _PlayerDB: - from src.players.models import PlayerDB as _PlayerDB + from evennia.players.models import PlayerDB as _PlayerDB if not _ServerConfig: - from src.server.models import ServerConfig as _ServerConfig + from evennia.server.models import ServerConfig as _ServerConfig if not _ScriptDB: - from src.scripts.models import ScriptDB as _ScriptDB + from evennia.scripts.models import ScriptDB as _ScriptDB # including once to avoid warnings in Python syntax checkers _ServerSession, _PlayerDB, _ServerConfig, _ScriptDB @@ -102,63 +109,6 @@ class SessionHandler(object): """ return dict((sessid, sess.get_sync_data()) for sessid, sess in self.sessions.items()) - def oobstruct_parser(self, oobstruct): - """ - Helper method for each session to use to parse oob structures - (The 'oob' kwarg of the msg() method). - - Allowed input oob structures are: - cmdname - ((cmdname,), (cmdname,)) - (cmdname,(arg, )) - (cmdname,(arg1,arg2)) - (cmdname,{key:val,key2:val2}) - (cmdname, (args,), {kwargs}) - ((cmdname, (arg1,arg2)), cmdname, (cmdname, (arg1,))) - outputs an ordered structure on the form - ((cmdname, (args,), {kwargs}), ...), where the two last - parts of each tuple may be empty - """ - def _parse(oobstruct): - slen = len(oobstruct) - if not oobstruct: - return tuple(None, (), {}) - elif not hasattr(oobstruct, "__iter__"): - # a singular command name, without arguments or kwargs - return (oobstruct.lower(), (), {}) - # regardless of number of args/kwargs, the first element must be - # the function name. We will not catch this error if not, but - # allow it to propagate. - if slen == 1: - return (oobstruct[0].lower(), (), {}) - elif slen == 2: - if isinstance(oobstruct[1], dict): - # cmdname, {kwargs} - return (oobstruct[0].lower(), (), dict(oobstruct[1])) - elif isinstance(oobstruct[1], (tuple, list)): - # cmdname, (args,) - return (oobstruct[0].lower(), list(oobstruct[1]), {}) - else: - # cmdname, cmdname - return ((oobstruct[0].lower(), (), {}), (oobstruct[1].lower(), (), {})) - else: - # cmdname, (args,), {kwargs} - return (oobstruct[0].lower(), list(oobstruct[1]), dict(oobstruct[2])) - - if hasattr(oobstruct, "__iter__"): - # differentiate between (cmdname, cmdname), - # (cmdname, (args), {kwargs}) and ((cmdname,(args),{kwargs}), - # (cmdname,(args),{kwargs}), ...) - - if oobstruct and isinstance(oobstruct[0], basestring): - return (list(_parse(oobstruct)),) - else: - out = [] - for oobpart in oobstruct: - out.append(_parse(oobpart)) - return (list(out),) - return (_parse(oobstruct),) - #------------------------------------------------------------ # Server-SessionHandler class @@ -185,7 +135,7 @@ class ServerSessionHandler(SessionHandler): """ self.sessions = {} self.server = None - self.server_data = {"servername": SERVERNAME} + self.server_data = {"servername": _SERVERNAME} def portal_connect(self, portalsession): """ @@ -222,7 +172,7 @@ class ServerSessionHandler(SessionHandler): if session: # since some of the session properties may have had # a chance to change already before the portal gets here - # the portal doesn't send all sessiondata here, but only + # the portal doesn't send all sessiondata but only # ones which should only be changed from portal (like # protocol_flags etc) session.load_sync_data(portalsessiondata) @@ -285,7 +235,7 @@ class ServerSessionHandler(SessionHandler): This method allows the server-side to force the Portal to create a new bot session using the protocol specified by protocol_path, which should be the full python path to the class, including the - class name, like "src.server.portal.irc.IRCClient". + class name, like "evennia.server.portal.irc.IRCClient". The new session will use the supplied player-bot uid to initiate an already logged-in connection. The Portal will treat this as a normal connection and henceforth so will the @@ -334,7 +284,7 @@ class ServerSessionHandler(SessionHandler): player.at_pre_login() - if MULTISESSION_MODE == 0: + if _MULTISESSION_MODE == 0: # disconnect all previous sessions. self.disconnect_duplicate_sessions(session) @@ -414,20 +364,39 @@ class ServerSessionHandler(SessionHandler): Check all currently connected sessions (logged in and not) and see if any are dead or idle """ - tcurr = time.time() + tcurr = time() reason = _("Idle timeout exceeded, disconnecting.") for session in (session for session in self.sessions.values() - if session.logged_in and IDLE_TIMEOUT > 0 - and (tcurr - session.cmd_last) > IDLE_TIMEOUT): + if session.logged_in and _IDLE_TIMEOUT > 0 + and (tcurr - session.cmd_last) > _IDLE_TIMEOUT): self.disconnect(session, reason=reason) - def player_count(self): + def player_count(self, count=True): """ Get the number of connected players (not sessions since a player may have more than one session depending on settings). Only logged-in players are counted here. + + Args: + count (bool): If true, return a count of players, otherwise + return a list. + + Returns: + number (int): If count=True + players (list): I count=False + """ - return len(set(session.uid for session in self.sessions.values() if session.logged_in)) + players = set(session.uid for session in self.sessions.values() if session.logged_in) + if count: + return len(players) + return players + + def all_connected_players(self): + """ + Returns all conected players (not sessions, since a player may + have more than one session depending on sessions) + """ + return self.player_count(count=False) def session_from_sessid(self, sessid): """ @@ -455,14 +424,15 @@ class ServerSessionHandler(SessionHandler): uid = player.uid return [session for session in self.sessions.values() if session.logged_in and session.uid == uid] - def sessions_from_character(self, character): + def sessions_from_puppet(self, puppet): """ - Given a game character, return any matching sessions. + Given a puppeted object, return all controlling sessions. """ - sessid = character.sessid.get() + sessid = puppet.sessid.get() if is_iter(sessid): - return [self.sessions.get(sess) for sess in sessid if sessid in self.sessions] + return [self.sessions.get(sid) for sid in sessid if sid in self.sessions] return self.sessions.get(sessid) + sessions_from_character = sessions_from_puppet def announce_all(self, message): """ @@ -474,19 +444,73 @@ class ServerSessionHandler(SessionHandler): def data_out(self, session, text="", **kwargs): """ Sending data Server -> Portal + + Args: + session (Session): Session object + text (str, optional): text data to return + _nomulti (bool, optional): if given, only this + session will receive the rest of the data, + regardless of MULTISESSION_MODE. This is an + internal variable that will not be passed on. + This is ignored for MULTISESSION_MODE = 1, + since all messages are mirrored everywhere for + that. + _forced_nomulti (bool, optional): Like _nomulti, + but works even when MULTISESSION_MODE = 1. + Useful for connection handling messages. + """ + sessions = make_iter(session) + session = sessions[0] text = text and to_str(to_unicode(text), encoding=session.encoding) - self.server.amp_protocol.call_remote_MsgServer2Portal(sessid=session.sessid, - msg=text, - data=kwargs) + multi = not kwargs.pop("_nomulti", None) + forced_nomulti = kwargs.pop("_forced_nomulti", None) + # Mode 1 mirrors to all. + if _MULTISESSION_MODE == 1: + multi = True + # ...Unless we're absolutely sure. + if forced_nomulti: + multi = False + + if multi: + if _MULTISESSION_MODE == 1: + if session.player: + sessions = self.sessions_from_player(session.player) + if _MULTISESSION_MODE == 2: + if session.player: + sessions = self.sessions_from_player(session.player) + elif _MULTISESSION_MODE == 3: + if session.puppet: + sessions = self.sessions_from_puppet(session.puppet) + elif session.player: + sessions = self.sessions_from_player(session.player) + + # send to all found sessions + for session in sessions: + self.server.amp_protocol.call_remote_MsgServer2Portal(sessid=session.sessid, + msg=text, + data=kwargs) def data_in(self, sessid, text="", **kwargs): """ - Data Portal -> Server + Data Portal -> Server. + We also intercept OOB communication here. """ session = self.sessions.get(sessid, None) if session: text = text and to_unicode(strip_control_sequences(text), encoding=session.encoding) + if "oob" in kwargs: + # incoming data is always on the form (cmdname, args, kwargs) + global _OOB_HANDLER + if not _OOB_HANDLER: + from evennia.server.oobhandler import OOB_HANDLER as _OOB_HANDLER + funcname, args, kwargs = kwargs.pop("oob") + #print "OOB session.data_in:", funcname, args, kwargs + if funcname: + _OOB_HANDLER.execute_cmd(session, funcname, *args, **kwargs) + + # pass the rest off to the session session.data_in(text=text, **kwargs) -SESSIONS = ServerSessionHandler() +SESSION_HANDLER = ServerSessionHandler() +SESSIONS = SESSION_HANDLER # legacy diff --git a/evennia/server/tests.py b/evennia/server/tests.py new file mode 100644 index 000000000..59bd8b8eb --- /dev/null +++ b/evennia/server/tests.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- + +""" +Unit testing of the 'objects' Evennia component. + +Runs as part of the Evennia's test suite with 'manage.py test" + +Please add new tests to this module as needed. + +Guidelines: + A 'test case' is testing a specific component and is defined as a class + inheriting from unittest.TestCase. The test case class can have a method + setUp() that creates and sets up the testing environment. + All methods inside the test case class whose names start with 'test' are + used as test methods by the runner. Inside the test methods, special member + methods assert*() are used to test the behaviour. +""" + +import os +import sys +import glob + +try: + from django.utils.unittest import TestCase +except ImportError: + from django.test import TestCase +try: + from django.utils import unittest +except ImportError: + import unittest + +from django.test.runner import DiscoverRunner + + +class EvenniaTestSuiteRunner(DiscoverRunner): + """ + This test runner only runs tests on the apps specified in evennia/ + avoid running the large number of tests defined by Django + """ + def build_suite(self, test_labels, extra_tests=None, **kwargs): + """ + Build a test suite for Evennia. test_labels is a list of apps to test. + If not given, a subset of settings.INSTALLED_APPS will be used. + """ + import evennia + evennia._init() + return super(EvenniaTestSuiteRunner, self).build_suite(test_labels, extra_tests=extra_tests, **kwargs) diff --git a/src/server/webserver.py b/evennia/server/webserver.py similarity index 98% rename from src/server/webserver.py rename to evennia/server/webserver.py index defde838b..44e6796fb 100644 --- a/src/server/webserver.py +++ b/evennia/server/webserver.py @@ -20,9 +20,10 @@ from twisted.web.proxy import ReverseProxyResource from twisted.web.server import NOT_DONE_YET from twisted.web.wsgi import WSGIResource +from django.conf import settings from django.core.handlers.wsgi import WSGIHandler -from settings import UPSTREAM_IPS +UPSTREAM_IPS = settings.UPSTREAM_IPS # diff --git a/src/settings_default.py b/evennia/settings_default.py similarity index 84% rename from src/settings_default.py rename to evennia/settings_default.py index f9c282342..1a4724014 100644 --- a/src/settings_default.py +++ b/evennia/settings_default.py @@ -4,16 +4,17 @@ Master configuration file for Evennia. NOTE: NO MODIFICATIONS SHOULD BE MADE TO THIS FILE! All settings changes should be done by copy-pasting the variable and -its value to game/settings.py. An empty game/settings.py can be -auto-generated by running game/manage.py without any arguments. +its value to /conf/settings.py. Hint: Don't copy&paste over more from this file than you actually want to change. Anything you don't copy&paste will thus retain its default value - which may change as Evennia is developed. This way you can always be sure of what you have changed and what is default behaviour. + """ import os +import sys ###################################################################### # Evennia base server config @@ -96,14 +97,18 @@ WEBSOCKET_INTERFACES = ['0.0.0.0'] # This determine's whether Evennia's custom admin page is used, or if the # standard Django admin is used. EVENNIA_ADMIN = True -# The path that contains this settings.py file (no trailing slash). -BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -# Path to the src directory containing the bulk of the codebase's code. -SRC_DIR = os.path.join(BASE_PATH, 'src') +# Path to the lib directory containing the bulk of the codebase's code. +EVENNIA_DIR = os.path.dirname(os.path.abspath(__file__)) # Path to the game directory (containing the database file if using sqlite). -GAME_DIR = os.path.join(BASE_PATH, 'game') +if sys.argv[1] == 'test' if len(sys.argv)>1 else False: + # unittesting mode + GAME_DIR = os.getcwd() +else: + # Fallback location (will be replaced by the actual game dir at runtime) + GAME_DIR = os.path.join(EVENNIA_DIR, 'game_template') + # Place to put log files -LOG_DIR = os.path.join(GAME_DIR, 'logs') +LOG_DIR = os.path.join(GAME_DIR, 'server', 'logs') SERVER_LOG_FILE = os.path.join(LOG_DIR, 'server.log') PORTAL_LOG_FILE = os.path.join(LOG_DIR, 'portal.log') HTTP_LOG_FILE = os.path.join(LOG_DIR, 'http_requests.log') @@ -116,7 +121,7 @@ CYCLE_LOGFILES = True TIME_ZONE = 'UTC' # Authentication backends. This is the code used to authenticate a user. AUTHENTICATION_BACKENDS = ( - 'src.web.utils.backends.CaseInsensitiveModelBackend',) + 'evennia.web.utils.backends.CaseInsensitiveModelBackend',) # Language code for this installation. All choices can be found here: # http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes LANGUAGE_CODE = 'en-us' @@ -172,7 +177,7 @@ IDMAPPER_CACHE_MAXSIZE = 200 # (MB) # Evennia Database config ###################################################################### -# Database config syntax for Django 1.2+. +# Database config syntax: # ENGINE - path to the the database backend. Possible choices are: # 'django.db.backends.sqlite3', (default) # 'django.db.backends.mysql', @@ -186,7 +191,7 @@ IDMAPPER_CACHE_MAXSIZE = 200 # (MB) DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(GAME_DIR, 'evennia.db3'), + 'NAME': os.path.join(GAME_DIR, 'server', 'evennia.db3'), 'USER': '', 'PASSWORD': '', 'HOST': '', @@ -198,51 +203,54 @@ DATABASES = { ###################################################################### # Plugin modules extend Evennia in various ways. In the cases with no # existing default, there are examples of many of these modules -# in game/gamesrc/conf/examples. +# in contrib/examples. # The command parser module to use. See the default module for which # functions it must implement -COMMAND_PARSER = "src.commands.cmdparser.cmdparser" +COMMAND_PARSER = "evennia.commands.cmdparser.cmdparser" # The handler that outputs errors when searching # objects using object.search(). -SEARCH_AT_RESULT = "src.commands.cmdparser.at_search_result" +SEARCH_AT_RESULT = "evennia.commands.cmdparser.at_search_result" # The parser used in order to separate multiple # object matches (so you can separate between same-named # objects without using dbrefs). -SEARCH_AT_MULTIMATCH_INPUT = "src.commands.cmdparser.at_multimatch_input" +SEARCH_AT_MULTIMATCH_INPUT = "evennia.commands.cmdparser.at_multimatch_input" # The module holding text strings for the connection screen. # This module should contain one or more variables # with strings defining the look of the screen. -CONNECTION_SCREEN_MODULE = "src.commands.connection_screen" +CONNECTION_SCREEN_MODULE = "server.conf.connection_screens" # An optional module that, if existing, must hold a function # named at_initial_setup(). This hook method can be used to customize # the server's initial setup sequence (the very first startup of the system). # The check will fail quietly if module doesn't exist or fails to load. -AT_INITIAL_SETUP_HOOK_MODULE = "" +AT_INITIAL_SETUP_HOOK_MODULE = "server.conf.at_initial_setup_hook" # Module containing your custom at_server_start(), at_server_reload() and # at_server_stop() methods. These methods will be called every time # the server starts, reloads and resets/stops respectively. -AT_SERVER_STARTSTOP_MODULE = "" +AT_SERVER_STARTSTOP_MODULE = "server.conf.at_server_startstop" # List of one or more module paths to modules containing a function start_ # plugin_services(application). This module will be called with the main # Evennia Server application when the Server is initiated. # It will be called last in the startup sequence. -SERVER_SERVICES_PLUGIN_MODULES = [] +SERVER_SERVICES_PLUGIN_MODULES = ["server.conf.server_services_plugins"] # List of one or more module paths to modules containing a function # start_plugin_services(application). This module will be called with the # main Evennia Portal application when the Portal is initiated. # It will be called last in the startup sequence. -PORTAL_SERVICES_PLUGIN_MODULES = [] +PORTAL_SERVICES_PLUGIN_MODULES = ["server.conf.portal_services_plugins"] # Module holding MSSP meta data. This is used by MUD-crawlers to determine # what type of game you are running, how many players you have etc. -MSSP_META_MODULE = "" +MSSP_META_MODULE = "server.conf.mssp" # Tuple of modules implementing lock functions. All callable functions # inside these modules will be available as lock functions. -LOCK_FUNC_MODULES = ("src.locks.lockfuncs",) +LOCK_FUNC_MODULES = ("evennia.locks.lockfuncs", "server.conf.lockfuncs",) # Module holding OOB (Out of Band) hook objects. This allows for customization # and expansion of which hooks OOB protocols are allowed to call on the server # protocols for attaching tracker hooks for when various object field change -OOB_PLUGIN_MODULES = ["src.server.oob_cmds"] +OOB_PLUGIN_MODULES = ["evennia.server.oob_cmds", "server.conf.oobfuncs"] +# Module holding settings/actions for the dummyrunner program (see the +# dummyrunner for more information) +DUMMYRUNNER_SETTINGS_MODULE = "evennia.server.profiling.dummyrunner_settings" ###################################################################### # Default command sets @@ -251,57 +259,49 @@ OOB_PLUGIN_MODULES = ["src.server.oob_cmds"] # stored anywhere in the database), changing these paths will only affect # NEW created characters/objects, not those already in play. So if you plan to # change this, it's recommended you do it before having created a lot of objects -# (or simply reset the database after the change for simplicity). Remember -# that you should never edit things in src/. Instead copy out the examples -# in game/gamesrc/commands/examples up one level and re-point these settings -# to point to these copies instead - these you can then change as you please -# (or copy/paste from the default modules in src/ if you prefer). +# (or simply reset the database after the change for simplicity). # Command set used on session before player has logged in -CMDSET_UNLOGGEDIN = "src.commands.default.cmdset_unloggedin.UnloggedinCmdSet" +CMDSET_UNLOGGEDIN = "commands.default_cmdsets.UnloggedinCmdSet" # Command set used on the logged-in session -CMDSET_SESSION = "src.commands.default.cmdset_session.SessionCmdSet" +CMDSET_SESSION = "commands.default_cmdsets.SessionCmdSet" # Default set for logged in player with characters (fallback) -CMDSET_CHARACTER = "src.commands.default.cmdset_character.CharacterCmdSet" +CMDSET_CHARACTER = "commands.default_cmdsets.CharacterCmdSet" # Command set for players without a character (ooc) -CMDSET_PLAYER = "src.commands.default.cmdset_player.PlayerCmdSet" +CMDSET_PLAYER = "commands.default_cmdsets.PlayerCmdSet" # Location to search for cmdsets if full path not given -CMDSET_PATHS = ["game.gamesrc.commands"] +CMDSET_PATHS = ["commands"] ###################################################################### # Typeclasses and other paths ###################################################################### # Server-side session class used. -SERVER_SESSION_CLASS = "src.server.serversession.ServerSession" +SERVER_SESSION_CLASS = "evennia.server.serversession.ServerSession" # Base paths for typeclassed object classes. These paths must be # defined relatively to Evennia's root directory. They will be searched in # order to find relative typeclass paths. -OBJECT_TYPECLASS_PATHS = ["game.gamesrc.objects", - "game.gamesrc.objects.examples", - "contrib"] -SCRIPT_TYPECLASS_PATHS = ["game.gamesrc.scripts", - "game.gamesrc.scripts.examples", - "contrib"] -PLAYER_TYPECLASS_PATHS = ["game.gamesrc.objects", "contrib"] -CHANNEL_TYPECLASS_PATHS = ["game.gamesrc.conf", "contrib"] +OBJECT_TYPECLASS_PATHS = ["typeclasses", "evennia.contrib", "evennia.contrib.tutorial_examples"] +SCRIPT_TYPECLASS_PATHS = ["typeclasses", "evennia.contrib", "evennia.contrib.tutorial_examples"] +PLAYER_TYPECLASS_PATHS = ["typeclasses", "evennia.contrib", "evennia.contrib.tutorial_examples"] +CHANNEL_TYPECLASS_PATHS = ["typeclasses", "evennia.contrib", "evennia.contrib.tutorial_examples"] # Typeclass for player objects (linked to a character) (fallback) -BASE_PLAYER_TYPECLASS = "src.players.player.Player" +BASE_PLAYER_TYPECLASS = "typeclasses.players.Player" # Typeclass and base for all objects (fallback) -BASE_OBJECT_TYPECLASS = "src.objects.objects.Object" +BASE_OBJECT_TYPECLASS = "typeclasses.objects.Object" # Typeclass for character objects linked to a player (fallback) -BASE_CHARACTER_TYPECLASS = "src.objects.objects.Character" +BASE_CHARACTER_TYPECLASS = "typeclasses.characters.Character" # Typeclass for rooms (fallback) -BASE_ROOM_TYPECLASS = "src.objects.objects.Room" +BASE_ROOM_TYPECLASS = "typeclasses.rooms.Room" # Typeclass for Exit objects (fallback). -BASE_EXIT_TYPECLASS = "src.objects.objects.Exit" +BASE_EXIT_TYPECLASS = "typeclasses.exits.Exit" # Typeclass for Channel (fallback). -BASE_CHANNEL_TYPECLASS = "src.comms.comms.Channel" +BASE_CHANNEL_TYPECLASS = "typeclasses.channels.Channel" # Typeclass for Scripts (fallback). You usually don't need to change this # but create custom variations of scripts on a per-case basis instead. -BASE_SCRIPT_TYPECLASS = "src.scripts.scripts.DoNothing" +BASE_SCRIPT_TYPECLASS = "typeclasses.scripts.Script" # The default home location used for all objects. This is used as a # fallback if an object's normal home location is deleted. Default # is Limbo (#2). @@ -325,14 +325,14 @@ TYPECLASS_AGGRESSIVE_CACHE = True # Python path to a directory to be searched for batch scripts # for the batch processors (.ev and/or .py files). -BASE_BATCHPROCESS_PATHS = ['game.gamesrc.world', 'contrib'] +BASE_BATCHPROCESS_PATHS = ['world', 'evennia.contrib', 'evennia.contrib.tutorial_examples'] ###################################################################### # Game Time setup ###################################################################### # You don't actually have to use this, but it affects the routines in -# src.utils.gametime.py and allows for a convenient measure to +# evennia.utils.gametime.py and allows for a convenient measure to # determine the current in-game time. You can of course interpret # "week", "month" etc as your own in-game time units as desired. @@ -358,7 +358,7 @@ INLINEFUNC_ENABLED = False # Only functions defined globally (and not starting with '_') in # these modules will be considered valid inlinefuncs. The list # is loaded from left-to-right, same-named functions will overload -INLINEFUNC_MODULES = ["src.utils.inlinefunc"] +INLINEFUNC_MODULES = ["evennia.utils.inlinefunc", "server.conf.inlinefunc"] ###################################################################### # Default Player setup and access @@ -407,7 +407,7 @@ CLIENT_DEFAULT_HEIGHT = 45 # telnet standard is 24 but does anyone use such # This enables guest logins, by default via "connect guest" GUEST_ENABLED = False # Typeclass for guest player objects (linked to a character) -BASE_GUEST_TYPECLASS = "src.players.player.Guest" +BASE_GUEST_TYPECLASS = "typeclasses.players.Guest" # The permission given to guests PERMISSION_GUEST_DEFAULT = "Guests" # The default home location used for guests. @@ -424,19 +424,28 @@ GUEST_LIST = ["Guest" + str(s+1) for s in range(9)] # In-game Channels created from server start ###################################################################### -# Each default channel is defined by a tuple containing -# (name, aliases, description, locks) -# where aliases may be a tuple too, and locks is -# a valid lockstring definition. -# Default user channel for communication -CHANNEL_PUBLIC = ("Public", ('ooc',), 'Public discussion', - "control:perm(Wizards);listen:all();send:all()") -# General info about the server -CHANNEL_MUDINFO = ("MUDinfo", '', 'Informative messages', - "control:perm(Immortals);listen:perm(Immortals);send:false()") -# Channel showing when new people connecting -CHANNEL_CONNECTINFO = ("MUDconnections", '', 'Connection log', - "control:perm(Immortals);listen:perm(Wizards);send:false()") +# This is a list of global channels created by the +# initialization script the first time Evennia starts. +# The superuser (user #1) will be automatically subscribed +# to all channels in this list. Each channel is described by +# a dictionary keyed with the same keys valid as arguments +# to the evennia.create.create_channel() function. +# Note: Evennia will treat the first channel in this list as +# the general "public" channel and the second as the +# general "mud info" channel. Other channels beyond that +# are up to the admin to design and call appropriately. +DEFAULT_CHANNELS = [ + # public channel + {"key": "Public", + "aliases": ('ooc', 'pub'), + "desc": "Public discussion", + "locks": "control:perm(Wizards);listen:all();send:all()"}, + # connection/mud info + {"key": "MudInfo", + "aliases": "", + "desc": "Connection log", + "locks": "control:perm(Immortals);listen:perm(Wizards);send:false()"} + ] ###################################################################### # External Channel connections @@ -482,7 +491,6 @@ IMC2_PORT = 5000 # this is the imc2 port, not on localhost IMC2_CLIENT_PWD = "" IMC2_SERVER_PWD = "" - ###################################################################### # Django web features ###################################################################### @@ -502,7 +510,7 @@ ADMINS = () #'Your Name', 'your_email@domain.com'),) MANAGERS = ADMINS # Absolute path to the directory that holds file uploads from web apps. # Example: "/home/media/media.lawrence.com" -MEDIA_ROOT = os.path.join(GAME_DIR, "gamesrc", "web", "media") +MEDIA_ROOT = os.path.join(GAME_DIR, "web", "media") # It's safe to dis-regard this, as it's a Django feature we only half use as a # dependency, not actually what it's primarily meant for. SITE_ID = 1 @@ -522,13 +530,13 @@ SESSION_EXPIRE_AT_BROWSER_CLOSE = False # to load the internationalization machinery. USE_I18N = False # Where to find locales (no need to change this, most likely) -LOCALE_PATHS = ["../locale/"] +LOCALE_PATHS = [os.path.join(EVENNIA_DIR, "locale/")] # This should be turned off unless you want to do tests with Django's # development webserver (normally Evennia runs its own server) SERVE_MEDIA = False # The master urlconf file that contains all of the sub-branches to the # applications. Change this to add your own URLs to the website. -ROOT_URLCONF = 'src.web.urls' +ROOT_URLCONF = 'web.urls' #src.web.urls? # Where users are redirected after logging in via contrib.auth.login. LOGIN_REDIRECT_URL = '/' # Where to redirect users when using the @login_required decorator. @@ -543,12 +551,12 @@ MEDIA_URL = '/media/' # STATIC_URL/admin. STATIC_URL = '/static/' -STATIC_ROOT = os.path.join(GAME_DIR, "gamesrc", "web", "static") +STATIC_ROOT = os.path.join(GAME_DIR, "web", "static") -# Directories from which static files will be gathered from. +# Directories from which static files will be gathered. STATICFILES_DIRS = ( - os.path.join(GAME_DIR, "gamesrc", "web", "static_overrides"), - os.path.join(SRC_DIR, "web", "static"),) + os.path.join(GAME_DIR, "web", "static_overrides"), + os.path.join(EVENNIA_DIR, "web", "static"),) # Patterns of files in the static directories. Used here to make sure that # its readme file is preserved but unused. STATICFILES_IGNORE_PATTERNS = ('README.md',) @@ -557,9 +565,9 @@ STATICFILES_IGNORE_PATTERNS = ('README.md',) ACTIVE_TEMPLATE = 'prosimii' # We setup the location of the website template as well as the admin site. TEMPLATE_DIRS = ( - os.path.join(GAME_DIR, "gamesrc", "web", "template_overrides"), - os.path.join(SRC_DIR, "web", "templates", ACTIVE_TEMPLATE), - os.path.join(SRC_DIR, "web", "templates"),) + os.path.join(GAME_DIR, "web", "template_overrides"), + os.path.join(EVENNIA_DIR, "web", "templates", ACTIVE_TEMPLATE), + os.path.join(EVENNIA_DIR, "web", "templates"),) # List of callables that know how to import templates from various sources. TEMPLATE_LOADERS = ( 'django.template.loaders.filesystem.Loader', @@ -583,7 +591,7 @@ TEMPLATE_CONTEXT_PROCESSORS = ( 'django.contrib.auth.context_processors.auth', 'django.core.context_processors.media', 'django.core.context_processors.debug', - 'src.web.utils.general_context.general_context',) + 'evennia.web.utils.general_context.general_context',) ###################################################################### # Evennia components @@ -600,20 +608,21 @@ INSTALLED_APPS = ( 'django.contrib.admindocs', 'django.contrib.flatpages', 'django.contrib.staticfiles', - 'src.server', - 'src.typeclasses', - 'src.players', - 'src.objects', - 'src.comms', - 'src.help', - 'src.scripts', - 'src.web.webclient') + 'evennia.utils.idmapper', + 'evennia.server', + 'evennia.typeclasses', + 'evennia.players', + 'evennia.objects', + 'evennia.comms', + 'evennia.help', + 'evennia.scripts', + 'evennia.web.webclient') # The user profile extends the User object with more functionality; # This should usually not be changed. AUTH_USER_MODEL = "players.PlayerDB" -#AUTH_PROFILE_MODULE = "players.PlayerDB" + # Use a custom test runner that just tests Evennia-specific apps. -TEST_RUNNER = 'src.server.tests.EvenniaTestSuiteRunner' +TEST_RUNNER = 'evennia.server.tests.EvenniaTestSuiteRunner' ###################################################################### # Django extensions @@ -630,7 +639,9 @@ except ImportError: ####################################################################### # SECRET_KEY ####################################################################### -# This is the salt for cryptographic hashing used by Django. +# This is the signing key for the cookies generated by Evennia's +# web interface. +# # It is a fallback for the SECRET_KEY setting in settings.py, which # is randomly seeded when settings.py is first created. If copying # from here, make sure to change it! diff --git a/evennia/typeclasses/__init__.py b/evennia/typeclasses/__init__.py new file mode 100644 index 000000000..b72aa9ba3 --- /dev/null +++ b/evennia/typeclasses/__init__.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +""" +This sub-package defines the typeclass-system, a way to wrap database +access into almost-normal Python classes. Using typeclasses one can +work in normal Python while having the luxury of persistent data +storage at every turn. ObjectDB, ChannelDB, PlayerDB and ScriptDB all +inherit from the models in this package. Here is also were the +Attribute and Tag models are defined along with their handlers. + +""" diff --git a/src/typeclasses/admin.py b/evennia/typeclasses/admin.py similarity index 95% rename from src/typeclasses/admin.py rename to evennia/typeclasses/admin.py index 525d702f1..4571b3021 100644 --- a/src/typeclasses/admin.py +++ b/evennia/typeclasses/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin from django.contrib.admin import ModelAdmin from django.core.urlresolvers import reverse -from src.typeclasses.models import Attribute, Tag +from evennia.typeclasses.models import Attribute, Tag class TagAdmin(admin.ModelAdmin): @@ -65,4 +65,4 @@ class AttributeAdmin(ModelAdmin): return [] admin.site.register(Attribute, AttributeAdmin) -admin.site.register(Tag, TagAdmin) \ No newline at end of file +admin.site.register(Tag, TagAdmin) diff --git a/evennia/typeclasses/attributes.py b/evennia/typeclasses/attributes.py new file mode 100644 index 000000000..a51cb2ebc --- /dev/null +++ b/evennia/typeclasses/attributes.py @@ -0,0 +1,515 @@ +""" +Attributes are arbitrary data stored on objects. Attributes supports +both pure-string values and pickled arbitrary data. + +Attributes are also used to implement Nicks. This module also contains +the Attribute- and NickHandlers as well as the NAttributeHandler, +which is a non-db version of Attributes. + + +""" +import re +import weakref + +from django.db import models +from django.conf import settings +from django.utils.encoding import smart_str + +from evennia.locks.lockhandler import LockHandler +from evennia.utils.idmapper.models import SharedMemoryModel +from evennia.utils.dbserialize import to_pickle, from_pickle +from evennia.utils.picklefield import PickledObjectField +from evennia.utils.utils import lazy_property, to_str, make_iter + +_TYPECLASS_AGGRESSIVE_CACHE = settings.TYPECLASS_AGGRESSIVE_CACHE + +#------------------------------------------------------------ +# +# Attributes +# +#------------------------------------------------------------ + +class Attribute(SharedMemoryModel): + """ + Attributes are things that are specific to different types of objects. For + example, a drink container needs to store its fill level, whereas an exit + needs to store its open/closed/locked/unlocked state. These are done via + attributes, rather than making different classes for each object type and + storing them directly. The added benefit is that we can add/remove + attributes on the fly as we like. + The Attribute class defines the following properties: + key - primary identifier + lock_storage - perm strings + obj - which object the attribute is defined on + date_created - when the attribute was created. + value - the data stored in the attribute, in pickled form + using wrappers to be able to store/retrieve models. + strvalue - string-only data. This data is not pickled and is + thus faster to search for in the database. + category - optional character string for grouping the Attribute + + """ + + # + # Attribute Database Model setup + # + # These database fields are all set using their corresponding properties, + # named same as the field, but withtout the db_* prefix. + db_key = models.CharField('key', max_length=255, db_index=True) + db_value = PickledObjectField( + 'value', null=True, + help_text="The data returned when the attribute is accessed. Must be " + "written as a Python literal if editing through the admin " + "interface. Attribute values which are not Python literals " + "cannot be edited through the admin interface.") + db_strvalue = models.TextField( + 'strvalue', null=True, blank=True, + help_text="String-specific storage for quick look-up") + db_category = models.CharField( + 'category', max_length=128, db_index=True, blank=True, null=True, + help_text="Optional categorization of attribute.") + # Lock storage + db_lock_storage = models.TextField( + 'locks', blank=True, + help_text="Lockstrings for this object are stored here.") + db_model = models.CharField( + 'model', max_length=32, db_index=True, blank=True, null=True, + help_text="Which model of object this attribute is attached to (A " + "natural key like 'objects.dbobject'). You should not change " + "this value unless you know what you are doing.") + # subclass of Attribute (None or nick) + db_attrtype = models.CharField( + 'attrtype', max_length=16, db_index=True, blank=True, null=True, + help_text="Subclass of Attribute (None or nick)") + # time stamp + db_date_created = models.DateTimeField( + 'date_created', editable=False, auto_now_add=True) + + # Database manager + #objects = managers.AttributeManager() + + @lazy_property + def locks(self): + return LockHandler(self) + + class Meta: + "Define Django meta options" + verbose_name = "Evennia Attribute" + + # read-only wrappers + key = property(lambda self: self.db_key) + strvalue = property(lambda self: self.db_strvalue) + category = property(lambda self: self.db_category) + model = property(lambda self: self.db_model) + attrtype = property(lambda self: self.db_attrtype) + date_created = property(lambda self: self.db_date_created) + + def __lock_storage_get(self): + return self.db_lock_storage + def __lock_storage_set(self, value): + self.db_lock_storage = value + self.save(update_fields=["db_lock_storage"]) + def __lock_storage_del(self): + self.db_lock_storage = "" + self.save(update_fields=["db_lock_storage"]) + lock_storage = property(__lock_storage_get, __lock_storage_set, __lock_storage_del) + + # Wrapper properties to easily set database fields. These are + # @property decorators that allows to access these fields using + # normal python operations (without having to remember to save() + # etc). So e.g. a property 'attr' has a get/set/del decorator + # defined that allows the user to do self.attr = value, + # value = self.attr and del self.attr respectively (where self + # is the object in question). + + # value property (wraps db_value) + #@property + def __value_get(self): + """ + Getter. Allows for value = self.value. + We cannot cache here since it makes certain cases (such + as storing a dbobj which is then deleted elsewhere) out-of-sync. + The overhead of unpickling seems hard to avoid. + """ + return from_pickle(self.db_value, db_obj=self) + + #@value.setter + def __value_set(self, new_value): + """ + Setter. Allows for self.value = value. We cannot cache here, + see self.__value_get. + """ + self.db_value = to_pickle(new_value) + self.save(update_fields=["db_value"]) + + #@value.deleter + def __value_del(self): + "Deleter. Allows for del attr.value. This removes the entire attribute." + self.delete() + value = property(__value_get, __value_set, __value_del) + + # + # + # Attribute methods + # + # + + def __str__(self): + return smart_str("%s(%s)" % (self.db_key, self.id)) + + def __unicode__(self): + return u"%s(%s)" % (self.db_key,self.id) + + def access(self, accessing_obj, access_type='read', default=False, **kwargs): + """ + 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 + **kwargs - passed to at_access hook along with result. + """ + result = self.locks.check(accessing_obj, access_type=access_type, default=default) + #self.at_access(result, **kwargs) + return result + + +# +# Handlers making use of the Attribute model +# + +class AttributeHandler(object): + """ + Handler for adding Attributes to the object. + """ + _m2m_fieldname = "db_attributes" + _attrcreate = "attrcreate" + _attredit = "attredit" + _attrread = "attrread" + _attrtype = None + + def __init__(self, obj): + "Initialize handler" + self.obj = obj + self._objid = obj.id + self._model = to_str(obj.__dbclass__.__name__.lower()) + self._cache = None + + def _recache(self): + "Cache all attributes of this object" + query = {"%s__id" % self._model : self._objid, + "attribute__db_attrtype" : self._attrtype} + attrs = [conn.attribute for conn in getattr(self.obj, self._m2m_fieldname).through.objects.filter(**query)] + self._cache = dict(("%s-%s" % (to_str(attr.db_key).lower(), + attr.db_category.lower() if conn.attribute.db_category else None), + attr) for attr in attrs) + + def has(self, key, category=None): + """ + Checks if the given Attribute (or list of Attributes) exists on + the object. + + If an iterable is given, returns list of booleans. + """ + if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE: + self._recache() + key = [k.strip().lower() for k in make_iter(key) if k] + category = category.strip().lower() if category is not None else None + searchkeys = ["%s-%s" % (k, category) for k in make_iter(key)] + ret = [self._cache.get(skey) for skey in searchkeys if skey in self._cache] + return ret[0] if len(ret) == 1 else ret + + def get(self, key=None, category=None, default=None, return_obj=False, + strattr=False, raise_exception=False, accessing_obj=None, + default_access=True, not_found_none=False): + """ + Returns the value of the given Attribute or list of Attributes. + strattr will cause the string-only value field instead of the normal + pickled field data. Use to get back values from Attributes added with + the strattr keyword. + If return_obj=True, return the matching Attribute object + instead. Returns default if no matches (or [ ] if key was a list + with no matches). If raise_exception=True, failure to find a + match will raise AttributeError instead. + + If accessing_obj is given, its "attrread" permission lock will be + checked before displaying each looked-after Attribute. If no + accessing_obj is given, no check will be done. + """ + + class RetDefault(object): + "Holds default values" + def __init__(self): + self.value = default + self.strvalue = str(default) if default is not None else None + + if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE: + self._recache() + ret = [] + key = [k.strip().lower() for k in make_iter(key) if k] + category = category.strip().lower() if category is not None else None + #print "cache:", self._cache.keys(), key + if not key: + # return all with matching category (or no category) + catkey = "-%s" % category if category is not None else None + ret = [attr for key, attr in self._cache.items() if key and key.endswith(catkey)] + else: + for searchkey in ("%s-%s" % (k, category) for k in key): + attr_obj = self._cache.get(searchkey) + if attr_obj: + ret.append(attr_obj) + else: + if raise_exception: + raise AttributeError + else: + ret.append(RetDefault()) + if accessing_obj: + # check 'attrread' locks + ret = [attr for attr in ret if attr.access(accessing_obj, self._attrread, default=default_access)] + if strattr: + ret = ret if return_obj else [attr.strvalue for attr in ret if attr] + else: + ret = ret if return_obj else [attr.value for attr in ret if attr] + if not ret: + return ret if len(key) > 1 else default + return ret[0] if len(ret)==1 else ret + + + def add(self, key, value, category=None, lockstring="", + strattr=False, accessing_obj=None, default_access=True): + """ + Add attribute to object, with optional lockstring. + + If strattr is set, the db_strvalue field will be used (no pickling). + Use the get() method with the strattr keyword to get it back. + + If accessing_obj is given, self.obj's 'attrcreate' lock access + will be checked against it. If no accessing_obj is given, no check + will be done. + """ + if accessing_obj and not self.obj.access(accessing_obj, + self._attrcreate, default=default_access): + # check create access + return + if self._cache is None: + self._recache() + if not key: + return + + category = category.strip().lower() if category is not None else None + keystr = key.strip().lower() + cachekey = "%s-%s" % (keystr, category) + attr_obj = self._cache.get(cachekey) + + if attr_obj: + # update an existing attribute object + if strattr: + # store as a simple string (will not notify OOB handlers) + attr_obj.db_strvalue = value + attr_obj.save(update_fields=["db_strvalue"]) + else: + # store normally (this will also notify OOB handlers) + attr_obj.value = value + else: + # create a new Attribute (no OOB handlers can be notified) + kwargs = {"db_key" : keystr, "db_category" : category, + "db_model" : self._model, "db_attrtype" : self._attrtype, + "db_value" : None if strattr else to_pickle(value), + "db_strvalue" : value if strattr else None} + new_attr = Attribute(**kwargs) + new_attr.save() + getattr(self.obj, self._m2m_fieldname).add(new_attr) + self._cache[cachekey] = new_attr + + + def batch_add(self, key, value, category=None, lockstring="", + strattr=False, accessing_obj=None, default_access=True): + """ + Batch-version of add(). This is more efficient than + repeat-calling add. + + key and value must be sequences of the same length, each + representing a key-value pair. + + """ + if accessing_obj and not self.obj.access(accessing_obj, + self._attrcreate, default=default_access): + # check create access + return + if self._cache is None: + self._recache() + if not key: + return + + keys, values= make_iter(key), make_iter(value) + + if len(keys) != len(values): + raise RuntimeError("AttributeHandler.add(): key and value of different length: %s vs %s" % key, value) + category = category.strip().lower() if category is not None else None + new_attrobjs = [] + for ikey, keystr in enumerate(keys): + keystr = keystr.strip().lower() + new_value = values[ikey] + cachekey = "%s-%s" % (keystr, category) + attr_obj = self._cache.get(cachekey) + + if attr_obj: + # update an existing attribute object + if strattr: + # store as a simple string (will not notify OOB handlers) + attr_obj.db_strvalue = new_value + attr_obj.save(update_fields=["db_strvalue"]) + else: + # store normally (this will also notify OOB handlers) + attr_obj.value = new_value + else: + # create a new Attribute (no OOB handlers can be notified) + kwargs = {"db_key" : keystr, "db_category" : category, + "db_attrtype" : self._attrtype, + "db_value" : None if strattr else to_pickle(new_value), + "db_strvalue" : value if strattr else None} + new_attr = Attribute(**kwargs) + new_attr.save() + new_attrobjs.append(new_attr) + if new_attrobjs: + # Add new objects to m2m field all at once + getattr(self.obj, self._m2m_fieldname).add(*new_attrobjs) + self._recache() + + + def remove(self, key, raise_exception=False, category=None, + accessing_obj=None, default_access=True): + """Remove attribute or a list of attributes from object. + + If accessing_obj is given, will check against the 'attredit' lock. + If not given, this check is skipped. + """ + if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE: + self._recache() + key = [k.strip().lower() for k in make_iter(key) if k] + category = category.strip().lower() if category is not None else None + for searchstr in ("%s-%s" % (k, category) for k in key): + attr_obj = self._cache.get(searchstr) + if attr_obj: + if not (accessing_obj and not attr_obj.access(accessing_obj, + self._attredit, default=default_access)): + attr_obj.delete() + elif not attr_obj and raise_exception: + raise AttributeError + self._recache() + + def clear(self, category=None, accessing_obj=None, default_access=True): + """ + Remove all Attributes on this object. If accessing_obj is + given, check the 'attredit' lock on each Attribute before + continuing. If not given, skip check. + """ + if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE: + self._recache() + if accessing_obj: + [attr.delete() for attr in self._cache.values() + if attr.access(accessing_obj, self._attredit, default=default_access)] + else: + [attr.delete() for attr in self._cache.values()] + self._recache() + + def all(self, accessing_obj=None, default_access=True): + """ + Return all Attribute objects on this object. + + If accessing_obj is given, check the "attrread" lock on + each attribute before returning them. If not given, this + check is skipped. + """ + if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE: + self._recache() + attrs = sorted(self._cache.values(), key=lambda o: o.id) + if accessing_obj: + return [attr for attr in attrs + if attr.access(accessing_obj, self._attredit, default=default_access)] + else: + return attrs + + +class NickHandler(AttributeHandler): + """ + Handles the addition and removal of Nicks + (uses Attributes' strvalue and category fields) + + Nicks are stored as Attributes + with categories nick_ + """ + _attrtype = "nick" + + def has(self, key, category="inputline"): + return super(NickHandler, self).has(key, category=category) + + def get(self, key=None, category="inputline", **kwargs): + "Get the replacement value matching the given key and category" + return super(NickHandler, self).get(key=key, category=category, strattr=True, **kwargs) + + def add(self, key, replacement, category="inputline", **kwargs): + "Add a new nick" + super(NickHandler, self).add(key, replacement, category=category, strattr=True, **kwargs) + + def remove(self, key, category="inputline", **kwargs): + "Remove Nick with matching category" + super(NickHandler, self).remove(key, category=category, **kwargs) + + def nickreplace(self, raw_string, categories=("inputline", "channel"), include_player=True): + "Replace entries in raw_string with nick replacement" + raw_string + obj_nicks, player_nicks = [], [] + for category in make_iter(categories): + obj_nicks.extend([n for n in make_iter(self.get(category=category, return_obj=True)) if n]) + if include_player and self.obj.has_player: + for category in make_iter(categories): + player_nicks.extend([n for n in make_iter(self.obj.player.nicks.get(category=category, return_obj=True)) if n]) + for nick in obj_nicks + player_nicks: + # make a case-insensitive match here + match = re.match(re.escape(nick.db_key), raw_string, re.IGNORECASE) + if match: + raw_string = raw_string.replace(match.group(), nick.db_strvalue, 1) + break + return raw_string + + +class NAttributeHandler(object): + """ + This stand-alone handler manages non-database saving. + It is similar to AttributeHandler and is used + by the .ndb handler in the same way as .db does + for the AttributeHandler. + """ + def __init__(self, obj): + "initialized on the object" + self._store = {} + self.obj = weakref.proxy(obj) + + def has(self, key): + "Check if object has this attribute or not" + return key in self._store + + def get(self, key): + "Returns named key value" + return self._store.get(key, None) + + def add(self, key, value): + "Add new key and value" + self._store[key] = value + self.obj.set_recache_protection() + + def remove(self, key): + "Remove key from storage" + if key in self._store: + del self._store[key] + self.obj.set_recache_protection(self._store) + + def clear(self): + "Remove all nattributes from handler" + self._store = {} + + def all(self, return_tuples=False): + "List all keys or (keys, values) stored, except _keys" + if return_tuples: + return [(key, value) for (key, value) in self._store.items() if not key.startswith("_")] + return [key for key in self._store if not key.startswith("_")] diff --git a/evennia/typeclasses/django_new_patch.py b/evennia/typeclasses/django_new_patch.py new file mode 100644 index 000000000..9394bc44d --- /dev/null +++ b/evennia/typeclasses/django_new_patch.py @@ -0,0 +1,256 @@ +""" +This is a patch of django.db.models.base.py:__new__, to allow for the +proxy system to allow multiple inheritance when both parents are of +the same base model. + +This patch is implemented as per +https://code.djangoproject.com/ticket/11560 and will hopefully be +possibe to remove as it gets adde to django's main branch. +""" + +# django patch imports +import sys +import copy +import warnings +from django.apps import apps +from django.db.models.base import ModelBase, subclass_exception +from django.core.exceptions import ObjectDoesNotExist +from django.db.models.options import Options +from django.utils.deprecation import RemovedInDjango19Warning +from django.core.exceptions import MultipleObjectsReturned, FieldError +from django.apps.config import MODELS_MODULE_NAME +from django.db.models.fields.related import OneToOneField +#/ django patch imports + +def patched_new(cls, name, bases, attrs): + "Patched version of __new__" + + super_new = super(ModelBase, cls).__new__ + + # Also ensure initialization is only performed for subclasses of Model + # (excluding Model class itself). + parents = [b for b in bases if isinstance(b, ModelBase)] + if not parents: + return super_new(cls, name, bases, attrs) + + # Create the class. + module = attrs.pop('__module__') + new_class = super_new(cls, name, bases, {'__module__': module}) + attr_meta = attrs.pop('Meta', None) + abstract = getattr(attr_meta, 'abstract', False) + if not attr_meta: + meta = getattr(new_class, 'Meta', None) + else: + meta = attr_meta + base_meta = getattr(new_class, '_meta', None) + + # Look for an application configuration to attach the model to. + app_config = apps.get_containing_app_config(module) + + if getattr(meta, 'app_label', None) is None: + + if app_config is None: + # If the model is imported before the configuration for its + # application is created (#21719), or isn't in an installed + # application (#21680), use the legacy logic to figure out the + # app_label by looking one level up from the package or module + # named 'models'. If no such package or module exists, fall + # back to looking one level up from the module this model is + # defined in. + + # For 'django.contrib.sites.models', this would be 'sites'. + # For 'geo.models.places' this would be 'geo'. + + msg = ( + "Model class %s.%s doesn't declare an explicit app_label " + "and either isn't in an application in INSTALLED_APPS or " + "else was imported before its application was loaded. " % + (module, name)) + if abstract: + msg += "Its app_label will be set to None in Django 1.9." + else: + msg += "This will no longer be supported in Django 1.9." + warnings.warn(msg, RemovedInDjango19Warning, stacklevel=2) + + model_module = sys.modules[new_class.__module__] + package_components = model_module.__name__.split('.') + package_components.reverse() # find the last occurrence of 'models' + try: + app_label_index = package_components.index(MODELS_MODULE_NAME) + 1 + except ValueError: + app_label_index = 1 + kwargs = {"app_label": package_components[app_label_index]} + + else: + kwargs = {"app_label": app_config.label} + + else: + kwargs = {} + + new_class.add_to_class('_meta', Options(meta, **kwargs)) + if not abstract: + new_class.add_to_class( + 'DoesNotExist', + subclass_exception( + str('DoesNotExist'), + tuple(x.DoesNotExist for x in parents if hasattr(x, '_meta') and not x._meta.abstract) or (ObjectDoesNotExist,), + module, + attached_to=new_class)) + new_class.add_to_class( + 'MultipleObjectsReturned', + subclass_exception( + str('MultipleObjectsReturned'), + tuple(x.MultipleObjectsReturned for x in parents if hasattr(x, '_meta') and not x._meta.abstract) or (MultipleObjectsReturned,), + module, + attached_to=new_class)) + if base_meta and not base_meta.abstract: + # Non-abstract child classes inherit some attributes from their + # non-abstract parent (unless an ABC comes before it in the + # method resolution order). + if not hasattr(meta, 'ordering'): + new_class._meta.ordering = base_meta.ordering + if not hasattr(meta, 'get_latest_by'): + new_class._meta.get_latest_by = base_meta.get_latest_by + + is_proxy = new_class._meta.proxy + + # If the model is a proxy, ensure that the base class + # hasn't been swapped out. + if is_proxy and base_meta and base_meta.swapped: + raise TypeError("%s cannot proxy the swapped model '%s'." % (name, base_meta.swapped)) + + if getattr(new_class, '_default_manager', None): + if not is_proxy: + # Multi-table inheritance doesn't inherit default manager from + # parents. + new_class._default_manager = None + new_class._base_manager = None + else: + # Proxy classes do inherit parent's default manager, if none is + # set explicitly. + new_class._default_manager = new_class._default_manager._copy_to_model(new_class) + new_class._base_manager = new_class._base_manager._copy_to_model(new_class) + + # Add all attributes to the class. + for obj_name, obj in attrs.items(): + new_class.add_to_class(obj_name, obj) + + # All the fields of any type declared on this model + new_fields = ( + new_class._meta.local_fields + + new_class._meta.local_many_to_many + + new_class._meta.virtual_fields + ) + field_names = set(f.name for f in new_fields) + + # Basic setup for proxy models. + if is_proxy: + base = None + for parent in [kls for kls in parents if hasattr(kls, '_meta')]: + if parent._meta.abstract: + if parent._meta.fields: + raise TypeError("Abstract base class containing model fields not permitted for proxy model '%s'." % name) + else: + continue + #if base is not None: # patch + while parent._meta.proxy: # patch + parent = parent._meta.proxy_for_model # patch + if base is not None and base is not parent: # patch + raise TypeError("Proxy model '%s' has more than one non-abstract model base class." % name) + else: + base = parent + if base is None: + raise TypeError("Proxy model '%s' has no non-abstract model base class." % name) + new_class._meta.setup_proxy(base) + new_class._meta.concrete_model = base._meta.concrete_model + else: + new_class._meta.concrete_model = new_class + + # Collect the parent links for multi-table inheritance. + parent_links = {} + for base in reversed([new_class] + parents): + # Conceptually equivalent to `if base is Model`. + if not hasattr(base, '_meta'): + continue + # Skip concrete parent classes. + if base != new_class and not base._meta.abstract: + continue + # Locate OneToOneField instances. + for field in base._meta.local_fields: + if isinstance(field, OneToOneField): + parent_links[field.rel.to] = field + + # Do the appropriate setup for any model parents. + for base in parents: + original_base = base + if not hasattr(base, '_meta'): + # Things without _meta aren't functional models, so they're + # uninteresting parents. + continue + + parent_fields = base._meta.local_fields + base._meta.local_many_to_many + # Check for clashes between locally declared fields and those + # on the base classes (we cannot handle shadowed fields at the + # moment). + for field in parent_fields: + if field.name in field_names: + raise FieldError( + 'Local field %r in class %r clashes ' + 'with field of similar name from ' + 'base class %r' % (field.name, name, base.__name__) + ) + if not base._meta.abstract: + # Concrete classes... + base = base._meta.concrete_model + if base in parent_links: + field = parent_links[base] + elif not is_proxy: + attr_name = '%s_ptr' % base._meta.model_name + field = OneToOneField(base, name=attr_name, + auto_created=True, parent_link=True) + # Only add the ptr field if it's not already present; + # e.g. migrations will already have it specified + if not hasattr(new_class, attr_name): + new_class.add_to_class(attr_name, field) + else: + field = None + new_class._meta.parents[base] = field + else: + # .. and abstract ones. + for field in parent_fields: + new_class.add_to_class(field.name, copy.deepcopy(field)) + + # Pass any non-abstract parent classes onto child. + new_class._meta.parents.update(base._meta.parents) + + # Inherit managers from the abstract base classes. + new_class.copy_managers(base._meta.abstract_managers) + + # Proxy models inherit the non-abstract managers from their base, + # unless they have redefined any of them. + if is_proxy: + new_class.copy_managers(original_base._meta.concrete_managers) + + # Inherit virtual fields (like GenericForeignKey) from the parent + # class + for field in base._meta.virtual_fields: + if base._meta.abstract and field.name in field_names: + raise FieldError( + 'Local field %r in class %r clashes ' + 'with field of similar name from ' + 'abstract base class %r' % (field.name, name, base.__name__) + ) + new_class.add_to_class(field.name, copy.deepcopy(field)) + + if abstract: + # Abstract base models can't be instantiated and don't appear in + # the list of models for an app. We do the final setup for them a + # little differently from normal models. + attr_meta.abstract = False + new_class.Meta = attr_meta + return new_class + + new_class._prepare() + new_class._meta.apps.register_model(new_class._meta.app_label, new_class) + + return new_class diff --git a/src/typeclasses/managers.py b/evennia/typeclasses/managers.py similarity index 67% rename from src/typeclasses/managers.py rename to evennia/typeclasses/managers.py index c9f62f95b..0b07625b2 100644 --- a/src/typeclasses/managers.py +++ b/evennia/typeclasses/managers.py @@ -5,53 +5,47 @@ all Attributes and TypedObjects). """ from functools import update_wrapper from django.db.models import Q -from src.utils import idmapper -from src.utils.utils import make_iter, variable_from_module +from evennia.utils import idmapper +from evennia.utils.utils import make_iter, variable_from_module -__all__ = ("AttributeManager", "TypedObjectManager") +__all__ = ("TypedObjectManager", ) _GA = object.__getattribute__ _Tag = None # -# helper functions for the TypedObjectManager. +# Decorators # def returns_typeclass_list(method): """ - Decorator: Changes return of the decorated method (which are - TypeClassed objects) into object_classes(s) instead. Will always - return a list (may be empty). + Decorator: Always returns a list, even + if it is empty. """ def func(self, *args, **kwargs): - "decorator. Returns a list." self.__doc__ = method.__doc__ - matches = make_iter(method(self, *args, **kwargs)) - return [(hasattr(dbobj, "typeclass") and dbobj.typeclass) or dbobj - for dbobj in make_iter(matches)] + return list(method(self, *args, **kwargs)) return update_wrapper(func, method) def returns_typeclass(method): """ - Decorator: Will always return a single typeclassed result or None. + Decorator: Returns a single match or None """ def func(self, *args, **kwargs): - "decorator. Returns result or None." self.__doc__ = method.__doc__ - matches = method(self, *args, **kwargs) - dbobj = matches and make_iter(matches)[0] or None - if dbobj: - return (hasattr(dbobj, "typeclass") and dbobj.typeclass) or dbobj - return None + query = method(self, *args, **kwargs) + return query return update_wrapper(func, method) # Managers - class TypedObjectManager(idmapper.manager.SharedMemoryManager): """ Common ObjectManager for all dbobjects. """ + # common methods for all typed managers. These are used + # in other methods. Returns querysets. + # Attribute manager methods def get_attribute(self, key=None, category=None, value=None, strvalue=None, obj=None, attrtype=None): @@ -103,21 +97,38 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager): # Tag manager methods - def get_tag(self, key=None, category=None, obj=None, tagtype=None): + def get_tag(self, key=None, category=None, obj=None, tagtype=None, global_search=False): """ Return Tag objects by key, by category, by object (it is stored on) or with a combination of those criteria. tagtype - one of None (normal tags), "alias" or "permission" + global_search - include all possible tags, not just tags on + this object """ - query = [("tag__db_tagtype", tagtype)] - if obj: - query.append(("%s__id" % self.model.__name__.lower(), obj.id)) - if key: - query.append(("tag__db_key", key)) - if category: - query.append(("tag__db_category", category)) - return [th.tag for th in self.model.db_tags.through.objects.filter(**dict(query))] + global _Tag + if not _Tag: + from evennia.typeclasses.models import Tag as _Tag + if global_search: + # search all tags using the Tag model + query = [("db_tagtype", tagtype)] + if obj: + query.append(("id", obj.id)) + if key: + query.append(("db_key", key)) + if category: + query.append(("db_category", category)) + return _Tag.objects.filter(**dict(query)) + else: + # search only among tags stored on on this model + query = [("tag__db_tagtype", tagtype)] + if obj: + query.append(("%s__id" % self.model.__name__.lower(), obj.id)) + if key: + query.append(("tag__db_key", key)) + if category: + query.append(("tag__db_category", category)) + return [th.tag for th in self.model.db_tags.through.objects.filter(**dict(query))] def get_permission(self, key=None, category=None, obj=None): return self.get_tag(key=key, category=category, obj=obj, tagtype="permission") @@ -131,13 +142,18 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager): Return objects having tags with a given key or category or combination of the two. - tagtype = None, alias or permission + Args: + key (str, optional): Tag key. Not case sensitive. + category (str, optional): Tag category. Not case sensitive. + tagtype (str or None, optional): 'type' of Tag, by default + this is either `None` (a normal Tag), `alias` or + `permission`. """ query = [("db_tags__db_tagtype", tagtype)] if key: - query.append(("db_tags__db_key", key)) + query.append(("db_tags__db_key", key.lower())) if category: - query.append(("db_tags__db_category", category)) + query.append(("db_tags__db_category", category.lower())) return self.filter(**dict(query)) def get_by_permission(self, key=None, category=None): @@ -157,7 +173,8 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager): """ data = str(data) if data is not None else None # try to get old tag - tag = self.get_tag(key=key, category=category, tagtype=tagtype) + + tag = self.get_tag(key=key, category=category, tagtype=tagtype, global_search=True) if tag and data is not None: # overload data on tag tag.db_data = data @@ -166,7 +183,7 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager): # create a new tag global _Tag if not _Tag: - from src.typeclasses.models import Tag as _Tag + from evennia.typeclasses.models import Tag as _Tag tag = _Tag.objects.create( db_key=key.strip().lower() if key is not None else None, db_category=category.strip().lower() if category and key is not None else None, @@ -282,3 +299,69 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager): query = query | Q(db_typeclass_path__exact=parent.path) # actually query the database return self.filter(query) + + +class TypeclassManager(TypedObjectManager): + + def get(self, **kwargs): + """ + Overload the standard get. This will limit itself to only + return the current typeclass. + """ + kwargs.update({"db_typeclass_path":self.model.path}) + return super(TypedObjectManager, self).get(**kwargs) + + def filter(self, **kwargs): + """ + Overload of the standard filter function. This filter will + limit itself to only the current typeclass. + """ + kwargs.update({"db_typeclass_path":self.model.path}) + return super(TypedObjectManager, self).filter(**kwargs) + + def all(self, **kwargs): + """ + Overload method to return all matches, filtering for typeclass + """ + return super(TypedObjectManager, self).all(**kwargs).filter(db_typeclass_path=self.model.path) + + def _get_subclasses(self, cls): + """ + Recursively get all subclasses to a class + """ + all_subclasses = cls.__subclasses__() + for subclass in all_subclasses: + all_subclasses.extend(self._get_subclasses(subclass)) + return all_subclasses + + def get_family(self, **kwargs): + """ + Variation of get that not only returns the current + typeclass but also all subclasses of that typeclass. + """ + paths = [self.model.path] + ["%s.%s" % (cls.__module__, cls.__name__) + for cls in self._get_subclasses(self.model)] + kwargs.update({"db_typeclass_path__in":paths}) + return super(TypedObjectManager, self).get(**kwargs) + + def filter_family(self, **kwargs): + """ + Variation of filter that allows results both from typeclass + and from subclasses of typeclass + """ + # query, including all subclasses + paths = [self.model.path] + ["%s.%s" % (cls.__module__, cls.__name__) + for cls in self._get_subclasses(self.model)] + kwargs.update({"db_typeclass_path__in":paths}) + return super(TypedObjectManager, self).filter(**kwargs) + + def all_family(self, **kwargs): + """ + Return all matches, allowing matches from all subclasses of + the typeclass. + """ + paths = [self.model.path] + ["%s.%s" % (cls.__module__, cls.__name__) + for cls in self._get_subclasses(self.model)] + return super(TypedObjectManager, self).all(**kwargs).filter(db_typeclass_path__in=paths) + + diff --git a/src/typeclasses/migrations/0001_initial.py b/evennia/typeclasses/migrations/0001_initial.py similarity index 89% rename from src/typeclasses/migrations/0001_initial.py rename to evennia/typeclasses/migrations/0001_initial.py index 9cdc96589..cb26a7449 100644 --- a/src/typeclasses/migrations/0001_initial.py +++ b/evennia/typeclasses/migrations/0001_initial.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from django.db import models, migrations -import src.utils.picklefield +import evennia.utils.picklefield class Migration(migrations.Migration): @@ -16,7 +16,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('db_key', models.CharField(max_length=255, verbose_name=b'key', db_index=True)), - ('db_value', src.utils.picklefield.PickledObjectField(help_text=b'The data returned when the attribute is accessed. Must be written as a Python literal if editing through the admin interface. Attribute values which are not Python literals cannot be edited through the admin interface.', null=True, verbose_name=b'value')), + ('db_value', evennia.utils.picklefield.PickledObjectField(help_text=b'The data returned when the attribute is accessed. Must be written as a Python literal if editing through the admin interface. Attribute values which are not Python literals cannot be edited through the admin interface.', null=True, verbose_name=b'value')), ('db_strvalue', models.TextField(help_text=b'String-specific storage for quick look-up', null=True, verbose_name=b'strvalue', blank=True)), ('db_category', models.CharField(max_length=128, blank=True, help_text=b'Optional categorization of attribute.', null=True, verbose_name=b'category', db_index=True)), ('db_lock_storage', models.TextField(help_text=b'Lockstrings for this object are stored here.', verbose_name=b'locks', blank=True)), diff --git a/evennia/typeclasses/migrations/0002_auto_20150109_0913.py b/evennia/typeclasses/migrations/0002_auto_20150109_0913.py new file mode 100644 index 000000000..fbb7ac169 --- /dev/null +++ b/evennia/typeclasses/migrations/0002_auto_20150109_0913.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('typeclasses', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='attribute', + name='db_model', + field=models.CharField(max_length=32, blank=True, help_text=b"Which model of object this attribute is attached to (A natural key like 'objects.dbobject'). You should not change this value unless you know what you are doing.", null=True, verbose_name=b'model', db_index=True), + ), + ] diff --git a/game/gamesrc/objects/__init__.py b/evennia/typeclasses/migrations/__init__.py similarity index 100% rename from game/gamesrc/objects/__init__.py rename to evennia/typeclasses/migrations/__init__.py diff --git a/evennia/typeclasses/models.py b/evennia/typeclasses/models.py new file mode 100644 index 000000000..eb94c9e4f --- /dev/null +++ b/evennia/typeclasses/models.py @@ -0,0 +1,577 @@ +""" +This is the *abstract* django models for many of the database objects +in Evennia. A django abstract (obs, not the same as a Python metaclass!) is +a model which is not actually created in the database, but which only exists +for other models to inherit from, to avoid code duplication. Any model can +import and inherit from these classes. + +Attributes are database objects stored on other objects. The implementing +class needs to supply a ForeignKey field attr_object pointing to the kind +of object being mapped. Attributes storing iterables actually store special +types of iterables named PackedList/PackedDict respectively. These make +sure to save changes to them to database - this is criticial in order to +allow for obj.db.mylist[2] = data. Also, all dbobjects are saved as +dbrefs but are also aggressively cached. + +TypedObjects are objects 'decorated' with a typeclass - that is, the typeclass +(which is a normal Python class implementing some special tricks with its +get/set attribute methods, allows for the creation of all sorts of different +objects all with the same database object underneath. Usually attributes are +used to permanently store things not hard-coded as field on the database object. +The admin should usually not have to deal directly with the database object +layer. + +This module also contains the Managers for the respective models; inherit from +these to create custom managers. + +""" + +from django.db.models import signals + +from django.db import models +from django.core.exceptions import ObjectDoesNotExist +from django.conf import settings +from django.utils.encoding import smart_str + +from evennia.typeclasses.attributes import Attribute, AttributeHandler, NAttributeHandler +from evennia.typeclasses.tags import Tag, TagHandler, AliasHandler, PermissionHandler + +from evennia.utils.idmapper.models import SharedMemoryModel, SharedMemoryModelBase + +from evennia.typeclasses import managers +from evennia.locks.lockhandler import LockHandler +from evennia.utils.utils import ( + is_iter, inherits_from, lazy_property, + class_from_module) +from evennia.typeclasses.django_new_patch import patched_new + +__all__ = ("TypedObject", ) + +TICKER_HANDLER = None + +_PERMISSION_HIERARCHY = [p.lower() for p in settings.PERMISSION_HIERARCHY] +_TYPECLASS_AGGRESSIVE_CACHE = settings.TYPECLASS_AGGRESSIVE_CACHE +_GA = object.__getattribute__ +_SA = object.__setattr__ + +#------------------------------------------------------------ +# +# Typed Objects +# +#------------------------------------------------------------ + + +# +# Meta class for typeclasses +# + + +# signal receivers. Assigned in __new__ +def post_save(sender, instance, created, **kwargs): + """ + Receives a signal just after the object is saved. + """ + if created: + instance.at_first_save() + #TODO - put OOB handler here? + + +class TypeclassBase(SharedMemoryModelBase): + """ + Metaclass which should be set for the root of model proxies + that don't define any new fields, like Object, Script etc. This + is the basis for the typeclassing system. + """ + + def __new__(cls, name, bases, attrs): + """ + We must define our Typeclasses as proxies. We also store the + path directly on the class, this is required by managers. + """ + + # storage of stats + attrs["typename"] = name#cls.__name__ + attrs["path"] = "%s.%s" % (attrs["__module__"], name) + + # typeclass proxy setup + if not "Meta" in attrs: + class Meta: + proxy = True + attrs["Meta"] = Meta + attrs["Meta"].proxy = True + + # patch for django proxy multi-inheritance + # this is a copy of django.db.models.base.__new__ + # with a few lines changed as per + # https://code.djangoproject.com/ticket/11560 + new_class = patched_new(cls, name, bases, attrs) + + # attach signal + signals.post_save.connect(post_save, sender=new_class) + + return new_class + + +# +# Main TypedObject abstraction +# + + +class TypedObject(SharedMemoryModel): + """ + Abstract Django model. + + This is the basis for a typed object. It also contains all the + mechanics for managing connected attributes. + + The TypedObject has the following properties: + key - main name + name - alias for key + typeclass_path - the path to the decorating typeclass + typeclass - auto-linked typeclass + date_created - time stamp of object creation + permissions - perm strings + dbref - #id of object + db - persistent attribute storage + ndb - non-persistent attribute storage + + """ + + # + # TypedObject Database Model setup + # + # + # These databse fields are all accessed and set using their corresponding + # properties, named same as the field, but without the db_* prefix + # (no separate save() call is needed) + + # Main identifier of the object, for searching. Is accessed with self.key + # or self.name + db_key = models.CharField('key', max_length=255, db_index=True) + # This is the python path to the type class this object is tied to. The + # typeclass is what defines what kind of Object this is) + db_typeclass_path = models.CharField('typeclass', max_length=255, null=True, + help_text="this defines what 'type' of entity this is. This variable holds a Python path to a module with a valid Evennia Typeclass.") + # Creation date. This is not changed once the object is created. + db_date_created = models.DateTimeField('creation date', editable=False, auto_now_add=True) + # Lock storage + db_lock_storage = models.TextField('locks', blank=True, + help_text="locks limit access to an entity. A lock is defined as a 'lock string' on the form 'type:lockfunctions', defining what functionality is locked and how to determine access. Not defining a lock means no access is granted.") + # many2many relationships + db_attributes = models.ManyToManyField(Attribute, null=True, + help_text='attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).') + db_tags = models.ManyToManyField(Tag, null=True, + help_text='tags on this object. Tags are simple string markers to identify, group and alias objects.') + + # Database manager + objects = managers.TypedObjectManager() + + # quick on-object typeclass cache for speed + _cached_typeclass = None + + # typeclass mechanism + + def __init__(self, *args, **kwargs): + """ + This is the main function of the typeclass system - + to dynamically re-apply a class based on the + db_typeclass_path rather than use the one in the model. + """ + typeclass_path = kwargs.pop("typeclass", None) + super(TypedObject, self).__init__(*args, **kwargs) + if typeclass_path: + self.__class__ = class_from_module(typeclass_path) + self.db_typclass_path = typeclass_path + elif self.db_typeclass_path: + self.__class__ = class_from_module(self.db_typeclass_path) + else: + self.db_typeclass_path = "%s.%s" % (self.__module__, self.__class__.__name__) + # important to put this at the end since _meta is based on the set __class__ + self.__dbclass__ = self._meta.proxy_for_model or self.__class__ + + # initialize all handlers in a lazy fashion + @lazy_property + def attributes(self): + return AttributeHandler(self) + + @lazy_property + def locks(self): + return LockHandler(self) + + @lazy_property + def tags(self): + return TagHandler(self) + + @lazy_property + def aliases(self): + return AliasHandler(self) + + @lazy_property + def permissions(self): + return PermissionHandler(self) + + @lazy_property + def nattributes(self): + return NAttributeHandler(self) + + + class Meta: + """ + Django setup info. + """ + abstract = True + verbose_name = "Evennia Database Object" + ordering = ['-db_date_created', 'id', 'db_typeclass_path', 'db_key'] + + # wrapper + # Wrapper properties to easily set database fields. These are + # @property decorators that allows to access these fields using + # normal python operations (without having to remember to save() + # etc). So e.g. a property 'attr' has a get/set/del decorator + # defined that allows the user to do self.attr = value, + # value = self.attr and del self.attr respectively (where self + # is the object in question). + + # name property (alias to self.key) + def __name_get(self): + return self.key + + def __name_set(self, value): + self.key = value + + def __name_del(self): + raise Exception("Cannot delete name") + name = property(__name_get, __name_set, __name_del) + + # + # + # TypedObject main class methods and properties + # + # + + def __eq__(self, other): + return other and hasattr(other, 'dbid') and self.dbid == other.dbid + + def __str__(self): + return smart_str("%s" % self.db_key) + + def __unicode__(self): + return u"%s" % self.db_key + + #@property + def __dbid_get(self): + """ + Caches and returns the unique id of the object. + Use this instead of self.id, which is not cached. + """ + return self.id + + def __dbid_set(self, value): + raise Exception("dbid cannot be set!") + + def __dbid_del(self): + raise Exception("dbid cannot be deleted!") + dbid = property(__dbid_get, __dbid_set, __dbid_del) + + #@property + def __dbref_get(self): + """ + Returns the object's dbref on the form #NN. + """ + return "#%s" % self.id + + def __dbref_set(self): + raise Exception("dbref cannot be set!") + + def __dbref_del(self): + raise Exception("dbref cannot be deleted!") + dbref = property(__dbref_get, __dbref_set, __dbref_del) + + # + # Object manipulation methods + # + + def is_typeclass(self, typeclass, exact=True): + """ + Returns true if this object has this type OR has a typeclass + which is an subclass of the given typeclass. This operates on + the actually loaded typeclass (this is important since a + failing typeclass may instead have its default currently + loaded) typeclass - can be a class object or the python path + to such an object to match against. + + typeclass - a class or the full python path to the class + exact - returns true only + if the object's type is exactly this typeclass, ignoring + parents. + """ + if not isinstance(typeclass, basestring): + typeclass = typeclass.path + + if exact: + return typeclass == self.path + else: + # check parent chain + selfpath = self.path + return any(cls for cls in self.__class__.mro() if cls.path == selfpath) + + def swap_typeclass(self, new_typeclass, clean_attributes=False, + run_start_hooks=True, no_default=True): + """ + This performs an in-situ swap of the typeclass. This means + that in-game, this object will suddenly be something else. + Player will not be affected. To 'move' a player to a different + object entirely (while retaining this object's type), use + self.player.swap_object(). + + Note that this might be an error prone operation if the + old/new typeclass was heavily customized - your code + might expect one and not the other, so be careful to + bug test your code if using this feature! Often its easiest + to create a new object and just swap the player over to + that one instead. + + Arguments: + new_typeclass (path/classobj) - type to switch to + clean_attributes (bool/list) - will delete all attributes + stored on this object (but not any + of the database fields such as name or + location). You can't get attributes back, + but this is often the safest bet to make + sure nothing in the new typeclass clashes + with the old one. If you supply a list, + only those named attributes will be cleared. + run_start_hooks - trigger the start hooks of the object, as if + it was created for the first time. + no_default - if this is active, the swapper will not allow for + swapping to a default typeclass in case the given + one fails for some reason. Instead the old one + will be preserved. + Returns: + boolean True/False depending on if the swap worked or not. + + """ + + if not callable(new_typeclass): + # this is an actual class object - build the path + new_typeclass = class_from_module(new_typeclass) + + # if we get to this point, the class is ok. + + + if inherits_from(self, "evennia.scripts.models.ScriptDB"): + if self.interval > 0: + raise RuntimeError("Cannot use swap_typeclass on time-dependent " \ + "Script '%s'.\nStop and start a new Script of the " \ + "right type instead." % self.key) + + self.typeclass_path = new_typeclass.path + self.__class__ = new_typeclass + + if clean_attributes: + # Clean out old attributes + if is_iter(clean_attributes): + for attr in clean_attributes: + self.attributes.remove(attr) + for nattr in clean_attributes: + if hasattr(self.ndb, nattr): + self.nattributes.remove(nattr) + else: + #print "deleting attrs ..." + self.attributes.clear() + self.nattributes.clear() + + if run_start_hooks: + # fake this call to mimic the first save + self.at_first_save() + + # + # Lock / permission methods + # + + def access(self, accessing_obj, access_type='read', default=False, **kwargs): + """ + 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 + **kwargs - this is ignored, but is there to make the api consistent with the + object-typeclass method access, which use it to feed to its hook methods. + """ + return self.locks.check(accessing_obj, access_type=access_type, default=default) + + def check_permstring(self, permstring): + """ + This explicitly checks if we hold particular permission without + involving any locks. It does -not- trigger the at_access hook. + """ + if hasattr(self, "player"): + if self.player and self.player.is_superuser: + return True + else: + if self.is_superuser: + return True + + if not permstring: + return False + perm = permstring.lower() + perms = [p.lower() for p in self.permissions.all()] + if perm in perms: + # simplest case - we have a direct match + return True + if perm in _PERMISSION_HIERARCHY: + # check if we have a higher hierarchy position + ppos = _PERMISSION_HIERARCHY.index(perm) + return any(True for hpos, hperm in enumerate(_PERMISSION_HIERARCHY) + if hperm in perms and hpos > ppos) + return False + + # + # Deletion methods + # + + def _deleted(self, *args, **kwargs): + "Scrambling method for already deleted objects" + raise ObjectDoesNotExist("This object was already deleted!") + + def delete(self): + "Cleaning up handlers on the typeclass level" + global TICKER_HANDLER + if not TICKER_HANDLER: + from evennia.scripts.tickerhandler import TICKER_HANDLER + TICKER_HANDLER.remove(self) # removes objects' all ticker subscriptions + self.permissions.clear() + self.attributes.clear() + self.aliases.clear() + if hasattr(self, "nicks"): + self.nicks.clear() + + # scrambling properties + self.delete = self._deleted + super(TypedObject, self).delete() + + # + # Memory management + # + + def flush_from_cache(self): + """ + Flush this object instance from cache, forcing an object reload. + Note that this will kill all temporary attributes on this object + since it will be recreated as a new Typeclass instance. + """ + self.__class__.flush_cached_instance(self) + + # + # Attribute storage + # + + #@property db + def __db_get(self): + """ + Attribute handler wrapper. Allows for the syntax + obj.db.attrname = value + and + value = obj.db.attrname + and + del obj.db.attrname + and + all_attr = obj.db.all() (unless there is an attribute + named 'all', in which case that will be returned instead). + """ + try: + return self._db_holder + except AttributeError: + class DbHolder(object): + "Holder for allowing property access of attributes" + def __init__(self, obj): + _SA(self, "attrhandler", obj.attributes) + + def __getattribute__(self, attrname): + if attrname == 'all': + # we allow to overload our default .all + attr = _GA(self, "attrhandler").get("all") + if attr: + return attr + return self.all + return _GA(self, "attrhandler").get(attrname) + + def __setattr__(self, attrname, value): + _GA(self, "attrhandler").add(attrname, value) + + def __delattr__(self, attrname): + _GA(self, "attrhandler").remove(attrname) + + def get_all(self): + return _GA(self, "attrhandler").all() + all = property(get_all) + self._db_holder = DbHolder(self) + return self._db_holder + + #@db.setter + def __db_set(self, value): + "Stop accidentally replacing the db object" + string = "Cannot assign directly to db object! " + string += "Use db.attr=value instead." + raise Exception(string) + + #@db.deleter + def __db_del(self): + "Stop accidental deletion." + raise Exception("Cannot delete the db object!") + db = property(__db_get, __db_set, __db_del) + + # + # Non-persistent (ndb) storage + # + + #@property ndb + def __ndb_get(self): + """ + A non-attr_obj store (ndb: NonDataBase). Everything stored + to this is guaranteed to be cleared when a server is shutdown. + Syntax is same as for the _get_db_holder() method and + property, e.g. obj.ndb.attr = value etc. + """ + try: + return self._ndb_holder + except AttributeError: + class NDbHolder(object): + "Holder for allowing property access of attributes" + def __init__(self, obj): + _SA(self, "nattrhandler", obj.nattributes) + + def __getattribute__(self, attrname): + if attrname == 'all': + # we allow to overload our default .all + attr = _GA(self, "nattrhandler").get('all') + if attr: + return attr + return self.all + return _GA(self, "nattrhandler").get(attrname) + + def __setattr__(self, attrname, value): + _GA(self, "nattrhandler").add(attrname, value) + + def __delattr__(self, attrname): + _GA(self, "nattrhandler").remove(attrname) + + def get_all(self): + return _GA(self, "nattrhandler").all() + all = property(get_all) + self._ndb_holder = NDbHolder(self) + return self._ndb_holder + + #@db.setter + def __ndb_set(self, value): + "Stop accidentally replacing the ndb object" + string = "Cannot assign directly to ndb object! " + string += "Use ndb.attr=value instead." + raise Exception(string) + + #@db.deleter + def __ndb_del(self): + "Stop accidental deletion." + raise Exception("Cannot delete the ndb object!") + ndb = property(__ndb_get, __ndb_set, __ndb_del) + diff --git a/evennia/typeclasses/tags.py b/evennia/typeclasses/tags.py new file mode 100644 index 000000000..330b9e342 --- /dev/null +++ b/evennia/typeclasses/tags.py @@ -0,0 +1,199 @@ +""" +Tags are entities that are attached to objects like Attributes but +which are unique to an individual object - any number of objects +can have the same Tag attached to them. + +Tags are used for tagging, obviously, but the data structure +is also used for storing Aliases and Permissions. This module +contains the respective handlers. + +""" + +from django.conf import settings +from django.db import models +from evennia.utils.utils import to_str, make_iter + + +_TYPECLASS_AGGRESSIVE_CACHE = settings.TYPECLASS_AGGRESSIVE_CACHE + +#------------------------------------------------------------ +# +# Tags +# +#------------------------------------------------------------ + +class Tag(models.Model): + """ + Tags are quick markers for objects in-game. An typeobject + can have any number of tags, stored via its db_tags property. + Tagging similar objects will make it easier to quickly locate the + group later (such as when implementing zones). The main advantage + of tagging as opposed to using Attributes is speed; a tag is very + limited in what data it can hold, and the tag key+category is + indexed for efficient lookup in the database. Tags are shared between + objects - a new tag is only created if the key+category combination + did not previously exist, making them unsuitable for storing + object-related data (for this a full Attribute + should be used). + The 'db_data' field is intended as a documentation + field for the tag itself, such as to document what this tag+category + stands for and display that in a web interface or similar. + + The main default use for Tags is to implement Aliases for objects. + this uses the 'aliases' tag category, which is also checked by the + default search functions of Evennia to allow quick searches by alias. + """ + db_key = models.CharField('key', max_length=255, null=True, + help_text="tag identifier", db_index=True) + db_category = models.CharField('category', max_length=64, null=True, + help_text="tag category", db_index=True) + db_data = models.TextField('data', null=True, blank=True, + help_text="optional data field with extra information. This is not searched for.") + # this is "objectdb" etc. Required behind the scenes + db_model = models.CharField('model', max_length=32, null=True, help_text="database model to Tag", db_index=True) + # this is None, alias or permission + db_tagtype = models.CharField('tagtype', max_length=16, null=True, help_text="overall type of Tag", db_index=True) + + class Meta: + "Define Django meta options" + verbose_name = "Tag" + unique_together = (('db_key', 'db_category', 'db_tagtype'),) + index_together = (('db_key', 'db_category', 'db_tagtype'),) + + def __unicode__(self): + return u"%s" % self.db_key + + def __str__(self): + return str(self.db_key) + + +# +# Handlers making use of the Tags model +# + +class TagHandler(object): + """ + Generic tag-handler. Accessed via TypedObject.tags. + """ + _m2m_fieldname = "db_tags" + _tagtype = None + + def __init__(self, obj): + """ + Tags are stored internally in the TypedObject.db_tags m2m field + with an tag.db_model based on the obj the taghandler is stored on + and with a tagtype given by self.handlertype + """ + self.obj = obj + self._objid = obj.id + self._model = obj.__dbclass__.__name__.lower() + self._cache = None + + def _recache(self): + "Cache all tags of this object" + query = {"%s__id" % self._model : self._objid, + "tag__db_tagtype" : self._tagtype} + tagobjs = [conn.tag for conn in getattr(self.obj, self._m2m_fieldname).through.objects.filter(**query)] + self._cache = dict(("%s-%s" % (to_str(tagobj.db_key).lower(), + tagobj.db_category.lower() if tagobj.db_category else None), + tagobj) for tagobj in tagobjs) + + def add(self, tag=None, category=None, data=None): + "Add a new tag to the handler. Tag is a string or a list of strings." + if not tag: + return + for tagstr in make_iter(tag): + if not tagstr: + continue + tagstr = tagstr.strip().lower() + category = category.strip().lower() if category is not None else None + data = str(data) if data is not None else None + # this will only create tag if no matches existed beforehand (it + # will overload data on an existing tag since that is not + # considered part of making the tag unique) + tagobj = self.obj.__class__.objects.create_tag(key=tagstr, category=category, data=data, + tagtype=self._tagtype) + getattr(self.obj, self._m2m_fieldname).add(tagobj) + if self._cache is None: + self._recache() + cachestring = "%s-%s" % (tagstr, category) + self._cache[cachestring] = tagobj + + def get(self, key, category=None, return_tagobj=False): + """ + Get the tag for the given key or list of tags. If + return_data=True, return the matching Tag objects instead. + Returns a single tag if a unique match, otherwise a list + """ + if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE: + self._recache() + ret = [] + category = category.strip().lower() if category is not None else None + searchkey = ["%s-%s" % (key.strip().lower(), category) if key is not None else None for key in make_iter(key)] + ret = [val for val in (self._cache.get(keystr) for keystr in searchkey) if val] + ret = [to_str(tag.db_data) for tag in ret] if return_tagobj else ret + return ret[0] if len(ret) == 1 else ret + + def remove(self, key, category=None): + "Remove a tag from the handler based ond key and category." + for key in make_iter(key): + if not (key or key.strip()): # we don't allow empty tags + continue + tagstr = key.strip().lower() + category = category.strip().lower() if category is not None else None + + # This does not delete the tag object itself. Maybe it should do + # that when no objects reference the tag anymore (how to check)? + tagobj = self.obj.db_tags.filter(db_key=tagstr, db_category=category) + if tagobj: + getattr(self.obj, self._m2m_fieldname).remove(tagobj[0]) + self._recache() + + def clear(self, category=None): + """ + Remove all tags from the handle. Optionally, only remove those within + a certain category. + """ + if not category: + getattr(self.obj, self._m2m_fieldname).clear() + else: + getattr(self.obj, self._m2m_fieldname).filter(db_category=category).delete() + self._recache() + + def all(self, category=None, return_key_and_category=False): + """ + Get all tags in this handler. + If category is given, return only Tags with this category. If + return_keys_and_categories is set, return a list of tuples [(key, category), ...] + """ + if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE: + self._recache() + if category: + category = category.strip().lower() if category is not None else None + matches = [tag for tag in self._cache.values() if tag.db_category == category] + else: + matches = self._cache.values() + + if matches: + matches = sorted(matches, key=lambda o: o.id) + if return_key_and_category: + # return tuple (key, category) + return [(to_str(p.db_key), to_str(p.db_category)) for p in matches] + else: + return [to_str(p.db_key) for p in matches] + return [] + + def __str__(self): + return ",".join(self.all()) + + def __unicode(self): + return u",".join(self.all()) + + +class AliasHandler(TagHandler): + _tagtype = "alias" + + +class PermissionHandler(TagHandler): + _tagtype = "permission" + diff --git a/evennia/utils/__init__.py b/evennia/utils/__init__.py new file mode 100644 index 000000000..25eb6c1ab --- /dev/null +++ b/evennia/utils/__init__.py @@ -0,0 +1,13 @@ +""" +This sub-package holds the miscelaneous utilities used by other +modules in Evennia. It also holds the idmapper in-memory caching +functionality. + +""" +# simple check to determine if we are currently running under pypy. +try: + import __pypy__ as is_pypy +except ImportError: + is_pypy = False + +from utils import * diff --git a/src/utils/ansi.py b/evennia/utils/ansi.py similarity index 99% rename from src/utils/ansi.py rename to evennia/utils/ansi.py index 799eaf609..8ffcfae11 100644 --- a/src/utils/ansi.py +++ b/evennia/utils/ansi.py @@ -14,8 +14,8 @@ user. """ import re -from src.utils import utils -from src.utils.utils import to_str, to_unicode +from evennia.utils import utils +from evennia.utils.utils import to_str, to_unicode # ANSI definitions diff --git a/src/utils/batchprocessors.py b/evennia/utils/batchprocessors.py similarity index 93% rename from src/utils/batchprocessors.py rename to evennia/utils/batchprocessors.py index e6640880f..d07fb0553 100644 --- a/src/utils/batchprocessors.py +++ b/evennia/utils/batchprocessors.py @@ -90,7 +90,7 @@ It seems the bottom of the box is a bit loose. # close the @drop command since it's the end of the file) ------------------------- -An example batch file is game/gamesrc/commands/examples/batch_example.ev. +An example batch file is contrib/examples/batch_example.ev. ========================================================================== @@ -144,8 +144,8 @@ Example batch.py file import traceback from django.config import settings -from src.utils import create -from game.gamesrc.typeclasses import basetypes +from evennia.utils import create +from types import basetypes GOLD = 10 @@ -171,7 +171,7 @@ import traceback import sys #from traceback import format_exc from django.conf import settings -from src.utils import utils +from evennia.utils import utils #from game import settings as settings_module ENCODINGS = settings.ENCODINGS @@ -199,35 +199,30 @@ def read_batchfile(pythonpath, file_ending='.py'): """ # open the file - if pythonpath and not (pythonpath.startswith('src.') or pythonpath.startswith('game.') - or pythonpath.startswith('contrib.')): - abspaths = [] - for basepath in settings.BASE_BATCHPROCESS_PATHS: - abspaths.append(utils.pypath_to_realpath("%s.%s" % (basepath, pythonpath), file_ending)) - else: - abspaths = [utils.pypath_to_realpath(pythonpath, file_ending)] - text, fobj = None, None - fileerr, decoderr = [], [] + abspaths = [] + for basepath in settings.BASE_BATCHPROCESS_PATHS: + # note that pypath_to_realpath has already checked the file for existence + if basepath.startswith("evennia"): + basepath = basepath.split("evennia", 1)[-1] + abspaths.extend(utils.pypath_to_realpath("%s.%s" % (basepath, pythonpath), file_ending)) + if not abspaths: + raise IOError + text = None + decoderr = [] for abspath in abspaths: # try different paths, until we get a match # we read the file directly into unicode. for file_encoding in ENCODINGS: # try different encodings, in order try: - fobj = codecs.open(abspath, 'r', encoding=file_encoding) - text = fobj.read() - except IOError, e: - # could not find the file - fileerr.append(str(e)) - break + with codecs.open(abspath, 'r', encoding=file_encoding) as fobj: + text = fobj.read() except (ValueError, UnicodeDecodeError), e: # this means an encoding error; try another encoding decoderr.append(str(e)) continue break - if not fobj: - raise IOError("\n".join(fileerr)) - if not text: + if not text and decoderr: raise UnicodeDecodeError("\n".join(decoderr)) return text diff --git a/evennia/utils/create.py b/evennia/utils/create.py new file mode 100644 index 000000000..cb16a4cb8 --- /dev/null +++ b/evennia/utils/create.py @@ -0,0 +1,382 @@ +""" +This module gathers all the essential database-creation +functions for the game engine's various object types. + +Only objects created 'stand-alone' are in here, e.g. object Attributes +are always created directly through their respective objects. + +Each creation_* function also has an alias named for the entity being +created, such as create_object() and object(). This is for +consistency with the utils.search module and allows you to do the +shorter "create.object()". + +The respective object managers hold more methods for manipulating and +searching objects already existing in the database. + +Models covered: + Objects + Scripts + Help + Message + Channel + Players +""" +from django.conf import settings +from django.db import IntegrityError +from django.utils import timezone +from evennia.utils import logger +from evennia.utils.utils import make_iter, class_from_module, dbid_to_obj + +# delayed imports +_User = None +_ObjectDB = None +_Object = None +_Script = None +_ScriptDB = None +_HelpEntry = None +_Msg = None +_Player = None +_PlayerDB = None +_to_object = None +_ChannelDB = None +_channelhandler = None + + +# limit symbol import from API +__all__ = ("create_object", "create_script", "create_help_entry", + "create_message", "create_channel", "create_player") + +_GA = object.__getattribute__ + +# +# Game Object creation +# + +def create_object(typeclass=None, key=None, location=None, + home=None, permissions=None, locks=None, + aliases=None, destination=None, report_to=None, nohome=False): + """ + + Create a new in-game object. + + keywords: + typeclass - class or python path to a typeclass + key - name of the new object. If not set, a name of #dbref will be set. + home - obj or #dbref to use as the object's home location + permissions - a comma-separated string of permissions + locks - one or more lockstrings, separated by semicolons + aliases - a list of alternative keys + destination - obj or #dbref to use as an Exit's target + + nohome - this allows the creation of objects without a default home location; + only used when creating the default location itself or during unittests + """ + global _ObjectDB + if not _ObjectDB: + from evennia.objects.models import ObjectDB as _ObjectDB + + + typeclass = typeclass if typeclass else settings.BASE_OBJECT_TYPECLASS + + if isinstance(typeclass, basestring): + # a path is given. Load the actual typeclass + typeclass = class_from_module(typeclass, settings.OBJECT_TYPECLASS_PATHS) + + # Setup input for the create command. We use ObjectDB as baseclass here + # to give us maximum freedom (the typeclasses will load + # correctly when each object is recovered). + + location = dbid_to_obj(location, _ObjectDB) + destination = dbid_to_obj(destination, _ObjectDB) + home = dbid_to_obj(home, _ObjectDB) + if not home: + try: + home = dbid_to_obj(settings.DEFAULT_HOME, _ObjectDB) if not nohome else None + except _ObjectDB.DoesNotExist: + raise _ObjectDB.DoesNotExist("settings.DEFAULT_HOME (= '%s') does not exist, or the setting is malformed." % + settings.DEFAULT_HOME) + + # create new instance + new_object = typeclass(db_key=key, db_location=location, + db_destination=destination, db_home=home, + db_typeclass_path=typeclass.path) + # store the call signature for the signal + new_object._createdict = {"key":key, "location":location, "destination":destination, + "home":home, "typeclass":typeclass.path, "permissions":permissions, + "locks":locks, "aliases":aliases, "destination":destination, + "report_to":report_to, "nohome":nohome} + # this will trigger the save signal which in turn calls the + # at_first_save hook on the typeclass, where the _createdict can be + # used. + new_object.save() + return new_object + +#alias for create_object +object = create_object + + +# +# Script creation +# + +def create_script(typeclass, key=None, obj=None, player=None, locks=None, + interval=None, start_delay=None, repeats=None, + persistent=None, autostart=True, report_to=None): + """ + Create a new script. All scripts are a combination + of a database object that communicates with the + database, and an typeclass that 'decorates' the + database object into being different types of scripts. + It's behaviour is similar to the game objects except + scripts has a time component and are more limited in + scope. + + Argument 'typeclass' can be either an actual + typeclass object or a python path to such an object. + Only set key here if you want a unique name for this + particular script (set it in config to give + same key to all scripts of the same type). Set obj + to tie this script to a particular object. + + See evennia.scripts.manager for methods to manipulate existing + scripts in the database. + + report_to is an obtional object to receive error messages. + If report_to is not set, an Exception with the + error will be raised. If set, this method will + return None upon errors. + """ + global _ScriptDB + if not _ScriptDB: + from evennia.scripts.models import ScriptDB as _ScriptDB + + typeclass = typeclass if typeclass else settings.BASE_SCRIPT_TYPECLASS + + if isinstance(typeclass, basestring): + # a path is given. Load the actual typeclass + typeclass = class_from_module(typeclass, settings.SCRIPT_TYPECLASS_PATHS) + + # validate input + kwarg = {} + if key: kwarg["db_key"] = key + if player: kwarg["db_player"] = dbid_to_obj(player, _ScriptDB) + if obj: kwarg["db_obj"] = dbid_to_obj(obj, _ScriptDB) + if interval: kwarg["db_interval"] = interval + if start_delay: kwarg["db_start_delay"] = start_delay + if repeats: kwarg["db_repeats"] = repeats + if persistent: kwarg["db_persistent"] = persistent + + # create new instance + new_script = typeclass(**kwarg) + + # store the call signature for the signal + new_script._createdict = {"key":key, "obj":obj, "player":player, + "locks":locks, "interval":interval, + "start_delay":start_delay, "repeats":repeats, + "persistent":persistent, "autostart":autostart, + "report_to":report_to} + + # this will trigger the save signal which in turn calls the + # at_first_save hook on the tyepclass, where the _createdict + # can be used. + new_script.save() + return new_script + +#alias +script = create_script + + +# +# Help entry creation +# + +def create_help_entry(key, entrytext, category="General", locks=None): + """ + Create a static help entry in the help database. Note that Command + help entries are dynamic and directly taken from the __doc__ entries + of the command. The database-stored help entries are intended for more + general help on the game, more extensive info, in-game setting information + and so on. + """ + global _HelpEntry + if not _HelpEntry: + from evennia.help.models import HelpEntry as _HelpEntry + + try: + new_help = _HelpEntry() + new_help.key = key + new_help.entrytext = entrytext + new_help.help_category = category + if locks: + new_help.locks.add(locks) + new_help.save() + return new_help + except IntegrityError: + string = "Could not add help entry: key '%s' already exists." % key + logger.log_errmsg(string) + return None + except Exception: + logger.log_trace() + return None +# alias +help_entry = create_help_entry + + +# +# Comm system methods +# + +def create_message(senderobj, message, channels=None, + receivers=None, locks=None, header=None): + """ + Create a new communication message. Msgs are used for all + player-to-player communication, both between individual players + and over channels. + senderobj - the player sending the message. This must be the actual object. + message - text with the message. Eventual headers, titles etc + should all be included in this text string. Formatting + will be retained. + channels - a channel or a list of channels to send to. The channels + may be actual channel objects or their unique key strings. + receivers - a player to send to, or a list of them. May be Player objects + or playernames. + locks - lock definition string + header - mime-type or other optional information for the message + + The Comm system is created very open-ended, so it's fully possible + to let a message both go to several channels and to several receivers + at the same time, it's up to the command definitions to limit this as + desired. + """ + global _Msg + if not _Msg: + from evennia.comms.models import Msg as _Msg + if not message: + # we don't allow empty messages. + return + new_message = _Msg(db_message=message) + new_message.save() + for sender in make_iter(senderobj): + new_message.senders = sender + new_message.header = header + for channel in make_iter(channels): + new_message.channels = channel + for receiver in make_iter(receivers): + new_message.receivers = receiver + if locks: + new_message.locks.add(locks) + new_message.save() + return new_message +message = create_message + + +def create_channel(key, aliases=None, desc=None, + locks=None, keep_log=True, + typeclass=None): + """ + Create A communication Channel. A Channel serves as a central + hub for distributing Msgs to groups of people without + specifying the receivers explicitly. Instead players may + 'connect' to the channel and follow the flow of messages. By + default the channel allows access to all old messages, but + this can be turned off with the keep_log switch. + + key - this must be unique. + aliases - list of alternative (likely shorter) keynames. + locks - lock string definitions + """ + typeclass = typeclass if typeclass else settings.BASE_CHANNEL_TYPECLASS + + if isinstance(typeclass, basestring): + # a path is given. Load the actual typeclass + typeclass = class_from_module(typeclass, settings.CHANNEL_TYPECLASS_PATHS) + + # create new instance + new_channel = typeclass(db_key=key) + + # store call signature for the signal + new_channel._createdict = {"key":key, "aliases":aliases, + "desc":desc, "locks":locks, "keep_log":keep_log} + + # this will trigger the save signal which in turn calls the + # at_first_save hook on the typeclass, where the _createdict can be + # used. + new_channel.save() + return new_channel + +channel = create_channel + + + +# +# Player creation methods +# + +def create_player(key, email, password, + typeclass=None, + is_superuser=False, + locks=None, permissions=None, + report_to=None): + + """ + This creates a new player. + + key - the player's name. This should be unique. + email - email on valid addr@addr.domain form. + password - password in cleartext + is_superuser - wether or not this player is to be a superuser + locks - lockstring + permission - list of permissions + report_to - an object with a msg() method to report errors to. If + not given, errors will be logged. + + Will return the Player-typeclass or None/raise Exception if the + Typeclass given failed to load. + + Concerning is_superuser: + Usually only the server admin should need to be superuser, all + other access levels can be handled with more fine-grained + permissions or groups. A superuser bypasses all lock checking + operations and is thus not suitable for play-testing the game. + + """ + global _PlayerDB + if not _PlayerDB: + from evennia.players.models import PlayerDB as _PlayerDB + + typeclass = typeclass if typeclass else settings.BASE_PLAYER_TYPECLASS + + if isinstance(typeclass, basestring): + # a path is given. Load the actual typeclass. + typeclass = class_from_module(typeclass, settings.OBJECT_TYPECLASS_PATHS) + + # setup input for the create command. We use PlayerDB as baseclass + # here to give us maximum freedom (the typeclasses will load + # correctly when each object is recovered). + + if not email: + email = "dummy@dummy.com" + if _PlayerDB.objects.filter(username__iexact=key): + raise ValueError("A Player with the name '%s' already exists." % key) + + # this handles a given dbref-relocate to a player. + report_to = dbid_to_obj(report_to, _PlayerDB) + + # create the correct player entity, using the setup from + # base django auth. + now = timezone.now() + email = typeclass.objects.normalize_email(email) + new_player = typeclass(username=key, email=email, + is_staff=is_superuser, is_superuser=is_superuser, + last_login=now, date_joined=now) + new_player.set_password(password) + new_player._createdict = {"locks":locks, "permissions":permissions, + "report_to":report_to} + # saving will trigger the signal that calls the + # at_first_save hook on the typeclass, where the _createdict + # can be used. + new_player.save() + return new_player + +# alias +player = create_player diff --git a/src/utils/dbserialize.py b/evennia/utils/dbserialize.py similarity index 95% rename from src/utils/dbserialize.py rename to evennia/utils/dbserialize.py index 44eea29a1..4208f2428 100644 --- a/src/utils/dbserialize.py +++ b/evennia/utils/dbserialize.py @@ -28,9 +28,9 @@ except ImportError: from django.db import transaction from django.core.exceptions import ObjectDoesNotExist from django.contrib.contenttypes.models import ContentType -from src.server.models import ServerConfig -from src.utils.utils import to_str, uses_database -from src.utils import logger +from evennia.server.models import ServerConfig +from evennia.utils.utils import to_str, uses_database +from evennia.utils import logger __all__ = ("to_pickle", "from_pickle", "do_pickle", "do_unpickle") @@ -42,7 +42,6 @@ _GA = object.__getattribute__ _SA = object.__setattr__ _FROM_MODEL_MAP = None _TO_MODEL_MAP = None -_TO_TYPECLASS = lambda o: hasattr(o, 'typeclass') and o.typeclass or o _IS_PACKED_DBOBJ = lambda o: type(o) == tuple and len(o) == 4 and o[0] == '__packed_dbobj__' if uses_database("mysql") and ServerConfig.objects.get_mysql_db_version() < '5.6.4': # mysql <5.6.4 don't support millisecond precision @@ -214,9 +213,9 @@ def pack_dbobj(item): ("__packed_dbobj__", key, creation_time, id) """ _init_globals() - obj = hasattr(item, 'dbobj') and item.dbobj or item + obj = item natural_key = _FROM_MODEL_MAP[hasattr(obj, "id") and hasattr(obj, "db_date_created") and - hasattr(obj, '__class__') and obj.__class__.__name__.lower()] + hasattr(obj, '__dbclass__') and obj.__dbclass__.__name__.lower()] # build the internal representation as a tuple # ("__packed_dbobj__", key, creation_time, id) return natural_key and ('__packed_dbobj__', natural_key, @@ -232,16 +231,12 @@ def unpack_dbobj(item): """ _init_globals() try: - obj = item[3] and _TO_TYPECLASS(_TO_MODEL_MAP[item[1]].objects.get(id=item[3])) + obj = item[3] and _TO_MODEL_MAP[item[1]].objects.get(id=item[3]) except ObjectDoesNotExist: return None # even if we got back a match, check the sanity of the date (some # databases may 're-use' the id) - try: - dbobj = obj.dbobj - except AttributeError: - dbobj = obj - return _TO_DATESTRING(dbobj) == item[2] and obj or None + return _TO_DATESTRING(obj) == item[2] and obj or None # # Access methods diff --git a/src/utils/evennia-mode.el b/evennia/utils/evennia-mode.el similarity index 100% rename from src/utils/evennia-mode.el rename to evennia/utils/evennia-mode.el diff --git a/src/utils/evform.py b/evennia/utils/evform.py similarity index 98% rename from src/utils/evform.py rename to evennia/utils/evform.py index d25a1ef51..e441b7bab 100644 --- a/src/utils/evform.py +++ b/evennia/utils/evform.py @@ -131,9 +131,9 @@ form will raise an error. import re import copy -from src.utils.evtable import EvCell, EvTable -from src.utils.utils import all_from_module, to_str, to_unicode -from src.utils.ansi import ANSIString +from evennia.utils.evtable import EvCell, EvTable +from evennia.utils.utils import all_from_module, to_str, to_unicode +from evennia.utils.ansi import ANSIString # non-valid form-identifying characters (which can thus be # used as separators between forms without being detected @@ -410,7 +410,7 @@ class EvForm(object): def _test(): "test evform" - form = EvForm("src.utils.evform_test") + form = EvForm("evennia.utils.evform_test") # add data to each tagged form cell form.map(cells={1: "{gTom the Bouncer{n", diff --git a/src/utils/evform_test.py b/evennia/utils/evform_test.py similarity index 100% rename from src/utils/evform_test.py rename to evennia/utils/evform_test.py diff --git a/src/utils/evtable.py b/evennia/utils/evtable.py similarity index 99% rename from src/utils/evtable.py rename to evennia/utils/evtable.py index 8d51e979b..8b8847791 100644 --- a/src/utils/evtable.py +++ b/evennia/utils/evtable.py @@ -103,8 +103,8 @@ eventual colour outside #from textwrap import wrap from textwrap import TextWrapper from copy import deepcopy, copy -from src.utils.utils import to_unicode -from src.utils.ansi import ANSIString +from evennia.utils.utils import to_unicode +from evennia.utils.ansi import ANSIString def make_iter(obj): "Makes sure that the object is always iterable." diff --git a/src/utils/gametime.py b/evennia/utils/gametime.py similarity index 96% rename from src/utils/gametime.py rename to evennia/utils/gametime.py index f0c5575d7..0c96a31ce 100644 --- a/src/utils/gametime.py +++ b/evennia/utils/gametime.py @@ -8,8 +8,8 @@ total runtime of the server and the current uptime. from time import time from django.conf import settings -from src.scripts.scripts import Script -from src.utils.create import create_script +from evennia.scripts.scripts import DefaultScript +from evennia.utils.create import create_script GAMETIME_SCRIPT_NAME = "sys_game_time" @@ -40,7 +40,7 @@ SERVER_STARTTIME = time() SERVER_RUNTIME = 0.0 -class GameTime(Script): +class GameTime(DefaultScript): """ This script repeatedly saves server times so it can be retrieved after server downtime. @@ -75,12 +75,12 @@ class GameTime(Script): def save(): "Force save of time. This is called by server when shutting down/reloading." - from src.scripts.models import ScriptDB + from evennia.scripts.models import ScriptDB try: script = ScriptDB.objects.get(db_key=GAMETIME_SCRIPT_NAME) script.at_repeat() except Exception: - from src.utils import logger + from evennia.utils import logger logger.log_trace() def _format(seconds, *divisors) : diff --git a/evennia/utils/idmapper/LICENSE.md b/evennia/utils/idmapper/LICENSE.md new file mode 100644 index 000000000..63990df4e --- /dev/null +++ b/evennia/utils/idmapper/LICENSE.md @@ -0,0 +1,24 @@ +Copyright (c) 2009, David Cramer All rights +reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. * +Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/utils/idmapper/EVENNIA.txt b/evennia/utils/idmapper/README_evennia.md similarity index 72% rename from src/utils/idmapper/EVENNIA.txt rename to evennia/utils/idmapper/README_evennia.md index ae70a38e3..a436c4ff6 100644 --- a/src/utils/idmapper/EVENNIA.txt +++ b/evennia/utils/idmapper/README_evennia.md @@ -1,6 +1,5 @@ -IDMAPPER --------- +# IDMAPPER https://github.com/dcramer/django-idmapper @@ -10,9 +9,8 @@ not only lowers memory consumption but most importantly allows for semi-persistance of properties on database model instances (something not guaranteed for normal Django models). -Evennia makes a few modifications to the original IDmapper routines -(we try to limit our modifications in order to make it easy to update -it from upstream down the line). +Evennia makes extensive modifications to the original IDmapper +routines: - We change the caching from a WeakValueDictionary to a normal dictionary. This is done because we use the models as semi- @@ -21,4 +19,9 @@ it from upstream down the line). then allowed them to be garbage collected. With this change they are guaranteed to remain (which is good for persistence but potentially bad for memory consumption). -- We add some caching/reset hooks called from the server side. +- We change the save and init code to allow for typeclass hook loading + and subprocessor checks. +- We add caching/reset hooks called from the server side. +- We add dynamic field wrappers for all fields named db_* + + diff --git a/evennia/utils/idmapper/README_orig.rst b/evennia/utils/idmapper/README_orig.rst new file mode 100644 index 000000000..43ec578d5 --- /dev/null +++ b/evennia/utils/idmapper/README_orig.rst @@ -0,0 +1,56 @@ +This fork of django-idmapper fixes some bugs that prevented the +idmapper from being used in many instances. In particular, the caching +manager is now inherited by SharedMemoryManager subclasses, and it is +used when Django uses an automatic manager (see +http://docs.djangoproject.com/en/dev/topics/db/managers/#controlling-automatic-manager-types). +This means access through foreign keys now uses identity mapping. + +Tested with Django version 1.2 alpha 1 SVN-12375. + +My modifications are usually accompanied by comments marked with "CL:". + +Django Identity Mapper +====================== + +A pluggable Django application which allows you to explicitally mark +your models to use an identity mapping pattern. This will share +instances of the same model in memory throughout your interpreter. + +Please note, that deserialization (such as from the cache) will *not* use the identity mapper. + +Usage +----- To use the shared memory model you simply need to inherit from +it (instead of models.Model). This enable all queries (and relational +queries) to this model to use the shared memory instance cache, +effectively creating a single instance for each unique row (based on +primary key) in the queryset. + +For example, if you want to simply mark all of your models as a +SharedMemoryModel, you might as well just import it as models. +:: + + from idmapper import models + + class MyModel(models.SharedMemoryModel): + name = models.CharField(...) + +Because the system is isolated, you may mix and match +SharedMemoryModels with regular Models. The module idmapper.models +imports everything from django.db.models and only adds +SharedMemoryModel, so you can simply replace your import of models +from django.db. +:: + + from idmapper import models + + class MyModel(models.SharedMemoryModel): + name = models.CharField(...) + fkey = models.ForeignKey('Other') + + class Other(models.Model): + name = models.CharField(...) + +References +---------- + +Original code and concept: http://code.djangoproject.com/ticket/17 diff --git a/src/web/utils/__init__.py b/evennia/utils/idmapper/__init__.py similarity index 100% rename from src/web/utils/__init__.py rename to evennia/utils/idmapper/__init__.py diff --git a/src/utils/idmapper/manager.py b/evennia/utils/idmapper/manager.py old mode 100755 new mode 100644 similarity index 63% rename from src/utils/idmapper/manager.py rename to evennia/utils/idmapper/manager.py index 2c017b3af..dec3deb63 --- a/src/utils/idmapper/manager.py +++ b/evennia/utils/idmapper/manager.py @@ -1,26 +1,13 @@ +""" +IDmapper extension to the default manager. +""" from django.db.models.manager import Manager -try: - from django.db import router -except: - pass - class SharedMemoryManager(Manager): # CL: this ensures our manager is used when accessing instances via # ForeignKey etc. (see docs) use_for_related_fields = True - # CL: in the dev version of django, ReverseSingleRelatedObjectDescriptor - # will call us as: - # rel_obj = rel_mgr.using(db).get(**params) - # We need to handle using, or the get method will be called on a vanilla - # queryset, and we won't get a change to use the cache. - def using(self, alias): - if alias == router.db_for_read(self.model): - return self - else: - return super(SharedMemoryManager, self).using(alias) - # TODO: improve on this implementation # We need a way to handle reverse lookups so that this model can # still use the singleton cache, but the active model isn't required diff --git a/src/utils/idmapper/base.py b/evennia/utils/idmapper/models.py old mode 100755 new mode 100644 similarity index 77% rename from src/utils/idmapper/base.py rename to evennia/utils/idmapper/models.py index 122c5b7d2..128fac388 --- a/src/utils/idmapper/base.py +++ b/evennia/utils/idmapper/models.py @@ -13,10 +13,11 @@ import os, threading, gc, time from weakref import WeakValueDictionary from twisted.internet.reactor import callFromThread from django.core.exceptions import ObjectDoesNotExist, FieldError +from django.db.models.signals import post_save from django.db.models.base import Model, ModelBase -from django.db.models.signals import post_save, pre_delete, post_syncdb -from src.utils import logger -from src.utils.utils import dbref, get_evennia_pids, to_str +from django.db.models.signals import pre_delete, post_syncdb +from evennia.utils import logger +from evennia.utils.utils import dbref, get_evennia_pids, to_str from manager import SharedMemoryManager @@ -26,6 +27,7 @@ _GA = object.__getattribute__ _SA = object.__setattr__ _DA = object.__delattr__ + # References to db-updated objects are stored here so the # main process can be informed to re-cache itself. PROC_MODIFIED_COUNT = 0 @@ -63,16 +65,25 @@ class SharedMemoryModelBase(ModelBase): cached_instance = cls.get_cached_instance(instance_key) if cached_instance is None: cached_instance = new_instance() - cls.cache_instance(cached_instance) + cls.cache_instance(cached_instance, new=True) return cached_instance def _prepare(cls): - cls.__instance_cache__ = {} - cls._idmapper_recache_protection = False + """ + Prepare the cache, making sure that proxies of the same db base + share the same cache. + """ + # the dbmodel is either the proxy base or ourselves + dbmodel = cls._meta.proxy_for_model if cls._meta.proxy else cls + cls.__dbclass__ = dbmodel + dbmodel._idmapper_recache_protection = False + if not hasattr(dbmodel, "__instance_cache__"): + # we store __instance_cache__ only on the dbmodel base + dbmodel.__instance_cache__ = {} super(SharedMemoryModelBase, cls)._prepare() - def __new__(cls, classname, bases, classdict, *args, **kwargs): + def __new__(cls, name, bases, attrs): """ Field shortcut creation: Takes field names db_* and creates property wrappers named without the db_ prefix. So db_key -> key @@ -81,6 +92,11 @@ class SharedMemoryModelBase(ModelBase): document this auto-wrapping in the class header, this could seem very much like magic to the user otherwise. """ + attrs["typename"] = cls.__name__ + attrs["path"] = "%s.%s" % (attrs["__module__"], name) + attrs["_is_deleted"] = False + + # set up the typeclass handling only if a variable _is_typeclass is set on the class def create_wrapper(cls, fieldname, wrappername, editable=True, foreignkey=False): "Helper method to create property wrappers with unique names (must be in separate call)" def _get(cls, fname): @@ -93,12 +109,7 @@ class SharedMemoryModelBase(ModelBase): "Wrapper for returing foreignkey fields" if _GA(cls, "_is_deleted"): raise ObjectDoesNotExist("Cannot access %s: Hosting object was already deleted." % fname) - value = _GA(cls, fieldname) - #print "_get_foreign:value:", value - try: - return _GA(value, "typeclass") - except: - return value + return _GA(cls, fieldname) def _set_nonedit(cls, fname, value): "Wrapper for blocking editing of field" raise FieldError("Field %s cannot be edited." % fname) @@ -158,26 +169,31 @@ class SharedMemoryModelBase(ModelBase): fset = lambda cls, val: _set(cls, fieldname, val) fdel = lambda cls: _del(cls, fieldname) if editable else _del_nonedit(cls,fieldname) # assigning - classdict[wrappername] = property(fget, fset, fdel) + attrs[wrappername] = property(fget, fset, fdel) #type(cls).__setattr__(cls, wrappername, property(fget, fset, fdel))#, doc)) # exclude some models that should not auto-create wrapper fields if cls.__name__ in ("ServerConfig", "TypeNick"): return # dynamically create the wrapper properties for all fields not already handled (manytomanyfields are always handlers) - for fieldname, field in ((fname, field) for fname, field in classdict.items() + for fieldname, field in ((fname, field) for fname, field in attrs.items() if fname.startswith("db_") and type(field).__name__ != "ManyToManyField"): foreignkey = type(field).__name__ == "ForeignKey" #print fieldname, type(field).__name__, field wrappername = "dbid" if fieldname == "id" else fieldname.replace("db_", "", 1) - if wrappername not in classdict: + #print fieldname, wrappername + if wrappername not in attrs: # makes sure not to overload manually created wrappers on the model #print "wrapping %s -> %s" % (fieldname, wrappername) create_wrapper(cls, fieldname, wrappername, editable=field.editable, foreignkey=foreignkey) - return super(SharedMemoryModelBase, cls).__new__(cls, classname, bases, classdict, *args, **kwargs) + + return super(SharedMemoryModelBase, cls).__new__(cls, name, bases, attrs) class SharedMemoryModel(Model): + """ + Base class for idmapped objects. Inherit from this. + """ # CL: setting abstract correctly to allow subclasses to inherit the default # manager. __metaclass__ = SharedMemoryModelBase @@ -187,10 +203,7 @@ class SharedMemoryModel(Model): class Meta: abstract = True - #def __init__(cls, *args, **kwargs): - # super(SharedMemoryModel, cls).__init__(*args, **kwargs) - # cls._idmapper_recache_protection = False - + @classmethod def _get_cache_key(cls, args, kwargs): """ This method is used by the caching subsystem to infer the PK value from the constructor arguments. @@ -219,40 +232,56 @@ class SharedMemoryModel(Model): # if the pk value happens to be a model instance (which can happen wich a FK), we'd rather use its own pk as the key result = result._get_pk_val() return result - _get_cache_key = classmethod(_get_cache_key) + #_get_cache_key = classmethod(_get_cache_key) + + + @classmethod def get_cached_instance(cls, id): """ Method to retrieve a cached instance by pk value. Returns None when not found (which will always be the case when caching is disabled for this class). Please note that the lookup will be done even when instance caching is disabled. """ - return cls.__instance_cache__.get(id) - get_cached_instance = classmethod(get_cached_instance) + return cls.__dbclass__.__instance_cache__.get(id) - def cache_instance(cls, instance): + @classmethod + def cache_instance(cls, instance, new=False): """ Method to store an instance in the cache. + + Args: + instance (Class instance): the instance to cache + new (bool, optional): this is the first time this + instance is cached (i.e. this is not an update + operation). + """ if instance._get_pk_val() is not None: + cls.__dbclass__.__instance_cache__[instance._get_pk_val()] = instance + if new: + try: + # trigger the at_init hook only + # at first initialization + instance.at_init() + except AttributeError: + pass - cls.__instance_cache__[instance._get_pk_val()] = instance - cache_instance = classmethod(cache_instance) - + @classmethod def get_all_cached_instances(cls): "return the objects so far cached by idmapper for this class." - return cls.__instance_cache__.values() - get_all_cached_instances = classmethod(get_all_cached_instances) + return cls.__dbclass__.__instance_cache__.values() + @classmethod def _flush_cached_by_key(cls, key, force=True): "Remove the cached reference." try: if force or not cls._idmapper_recache_protection: - del cls.__instance_cache__[key] + del cls.__dbclass__.__instance_cache__[key] except KeyError: pass - _flush_cached_by_key = classmethod(_flush_cached_by_key) + @classmethod def flush_cached_instance(cls, instance, force=True): """ Method to flush an instance from the cache. The instance will @@ -262,45 +291,93 @@ class SharedMemoryModel(Model): """ cls._flush_cached_by_key(instance._get_pk_val(), force=force) - flush_cached_instance = classmethod(flush_cached_instance) - - # per-instance methods - - def set_recache_protection(cls, mode=True): - "set if this instance should be allowed to be recached." - cls._idmapper_recache_protection = bool(mode) + #flush_cached_instance = classmethod(flush_cached_instance) + @classmethod def flush_instance_cache(cls, force=False): """ This will clean safe objects from the cache. Use force keyword to remove all objects, safe or not. """ if force: - cls.__instance_cache__ = {} + cls.__dbclass__.__instance_cache__ = {} else: - cls.__instance_cache__ = dict((key, obj) for key, obj in cls.__instance_cache__.items() + cls.__dbclass__.__instance_cache__ = dict((key, obj) for key, obj in cls.__dbclass__.__instance_cache__.items() if obj._idmapper_recache_protection) - flush_instance_cache = classmethod(flush_instance_cache) + #flush_instance_cache = classmethod(flush_instance_cache) - def save(cls, *args, **kwargs): - "save method tracking process/thread issues" + # per-instance methods + + def flush_from_cache(self, force=False): + """ + Flush this instance from the instance cache. Use + force to override recache_protection for the object. + """ + if self.pk and (force or not self._idmapper_recache_protection): + self.__class__.__dbclass__.__instance_cache__.pop(self.pk, None) + + def set_recache_protection(self, mode=True): + "set if this instance should be allowed to be recached." + self._idmapper_recache_protection = bool(mode) + + def delete(self, *args, **kwargs): + """ + Delete the object, clearing cache + """ + self.flush_from_cache() + self._is_deleted = True + super(SharedMemoryModel, self).delete(*args, **kwargs) + + def save(self, *args, **kwargs): + """ + Central database save operation. + + Arguments as per django documentation + + Calls: + self.at__postsave(new) + # this is a wrapper set by oobhandler: + self._oob_at__postsave() + + """ if _IS_SUBPROCESS: # we keep a store of objects modified in subprocesses so # we know to update their caches in the central process global PROC_MODIFIED_COUNT, PROC_MODIFIED_OBJS PROC_MODIFIED_COUNT += 1 - PROC_MODIFIED_OBJS[PROC_MODIFIED_COUNT] = cls + PROC_MODIFIED_OBJS[PROC_MODIFIED_COUNT] = self if _IS_MAIN_THREAD: # in main thread - normal operation - super(SharedMemoryModel, cls).save(*args, **kwargs) + super(SharedMemoryModel, self).save(*args, **kwargs) else: # in another thread; make sure to save in reactor thread def _save_callback(cls, *args, **kwargs): super(SharedMemoryModel, cls).save(*args, **kwargs) - #blockingCallFromThread(reactor, _save_callback, cls, *args, **kwargs) - callFromThread(_save_callback, cls, *args, **kwargs) + callFromThread(_save_callback, self, *args, **kwargs) + + # update field-update hooks and eventual OOB watchers + new = False + if "update_fields" in kwargs and kwargs["update_fields"]: + # get field objects from their names + update_fields = (self._meta.get_field_by_name(field)[0] + for field in kwargs.get("update_fields")) + else: + # meta.fields are already field objects; get them all + new =True + update_fields = self._meta.fields + for field in update_fields: + fieldname = field.name + # if a hook is defined it must be named exactly on this form + hookname = "at_%s_postsave" % fieldname + if hasattr(self, hookname) and callable(_GA(self, hookname)): + _GA(self, hookname)(new) + # if a trackerhandler is set on this object, update it with the + # fieldname and the new value + fieldtracker = "_oob_at_%s_postsave" % fieldname + if hasattr(self, fieldtracker): + _GA(self, fieldtracker)(fieldname) class WeakSharedMemoryModelBase(SharedMemoryModelBase): @@ -309,7 +386,7 @@ class WeakSharedMemoryModelBase(SharedMemoryModelBase): """ def _prepare(cls): super(WeakSharedMemoryModelBase, cls)._prepare() - cls.__instance_cache__ = WeakValueDictionary() + cls.__dbclass__.__instance_cache__ = WeakValueDictionary() cls._idmapper_recache_protection = False @@ -341,7 +418,9 @@ def flush_cache(**kwargs): else: yield cls + #print "start flush ..." for cls in class_hierarchy([SharedMemoryModel]): + #print cls cls.flush_instance_cache() # run the python garbage collector return gc.collect() diff --git a/src/utils/idmapper/tests.py b/evennia/utils/idmapper/tests.py old mode 100755 new mode 100644 similarity index 84% rename from src/utils/idmapper/tests.py rename to evennia/utils/idmapper/tests.py index 789fe3d9e..fcad2a18a --- a/src/utils/idmapper/tests.py +++ b/evennia/utils/idmapper/tests.py @@ -1,6 +1,6 @@ from django.test import TestCase -from base import SharedMemoryModel +from models import SharedMemoryModel from django.db import models class Category(SharedMemoryModel): @@ -21,16 +21,17 @@ class RegularArticle(models.Model): class SharedMemorysTest(TestCase): # TODO: test for cross model relation (singleton to regular) - + def setUp(self): + super(SharedMemorysTest, self).setUp() n = 0 category = Category.objects.create(name="Category %d" % (n,)) regcategory = RegularCategory.objects.create(name="Category %d" % (n,)) - + for n in xrange(0, 10): Article.objects.create(name="Article %d" % (n,), category=category, category2=regcategory) RegularArticle.objects.create(name="Article %d" % (n,), category=category, category2=regcategory) - + def testSharedMemoryReferences(self): article_list = Article.objects.all().select_related('category') last_article = article_list[0] @@ -52,19 +53,19 @@ class SharedMemorysTest(TestCase): self.assertEquals(article.category is last_article.category, True) last_article = article - article_list = Article.objects.all().select_related('category') - last_article = article_list[0] - for article in article_list[1:]: - self.assertEquals(article.category2 is last_article.category2, False) - last_article = article - + #article_list = Article.objects.all().select_related('category') + #last_article = article_list[0] + #for article in article_list[1:]: + # self.assertEquals(article.category2 is last_article.category2, False) + # last_article = article + def testObjectDeletion(self): # This must execute first so its guaranteed to be in memory. - article_list = list(Article.objects.all().select_related('category')) - + list(Article.objects.all().select_related('category')) + article = Article.objects.all()[0:1].get() pk = article.pk article.delete() self.assertEquals(pk not in Article.__instance_cache__, True) - - \ No newline at end of file + + diff --git a/src/utils/inlinefunc.py b/evennia/utils/inlinefunc.py similarity index 99% rename from src/utils/inlinefunc.py rename to evennia/utils/inlinefunc.py index ec4dba0cf..ed929aeb7 100644 --- a/src/utils/inlinefunc.py +++ b/evennia/utils/inlinefunc.py @@ -45,7 +45,7 @@ should be returned. The inlinefunc should never cause a traceback. import re from django.conf import settings -from src.utils import utils +from evennia.utils import utils # inline functions diff --git a/src/utils/logger.py b/evennia/utils/logger.py similarity index 83% rename from src/utils/logger.py rename to evennia/utils/logger.py index b2a8341f0..bfad6b42b 100644 --- a/src/utils/logger.py +++ b/evennia/utils/logger.py @@ -1,26 +1,25 @@ """ Logging facilities -These are thin wrappers on top of Twisted's -logging facilities; logs are all directed -either to stdout (if Evennia is running in +These are thin wrappers on top of Twisted's logging facilities; logs +are all directed either to stdout (if Evennia is running in interactive mode) or to game/logs. -The log_file() function uses its own threading -system to log to arbitrary files in game/logs. +The log_file() function uses its own threading system to log to +arbitrary files in game/logs. -Note: -All logging functions have two aliases, -log_type() and log_typemsg(). This is for -historical, back-compatible reasons. +Note: All logging functions have two aliases, log_type() and +log_typemsg(). This is for historical, back-compatible reasons. """ +import os from datetime import datetime from traceback import format_exc from twisted.python import log from twisted.internet.threads import deferToThread +_LOGDIR = None def log_trace(errmsg=None): """ @@ -112,10 +111,14 @@ LOG_FILE_HANDLES = {} # holds open log handles def log_file(msg, filename="game.log"): """ Arbitrary file logger using threads. Filename defaults to - 'game.log'. All logs will appear in game/logs directory and log + 'game.log'. All logs will appear in the logs directory and log entries will start on new lines following datetime info. """ - global LOG_FILE_HANDLES + global LOG_FILE_HANDLES, _LOGDIR + + if not _LOGDIR: + from django.conf import settings + _LOGDIR = settings.LOG_DIR def callback(filehandle, msg): "Writing to file and flushing result" @@ -129,8 +132,8 @@ def log_file(msg, filename="game.log"): "Catching errors to normal log" log_trace() - # save to game/logs/ directory - filename = "logs/" + filename + # save to server/logs/ directory + filename = os.path.join(_LOGDIR, filename) if filename in LOG_FILE_HANDLES: filehandle = LOG_FILE_HANDLES[filename] diff --git a/src/utils/picklefield.py b/evennia/utils/picklefield.py similarity index 98% rename from src/utils/picklefield.py rename to evennia/utils/picklefield.py index 49da9f0eb..8bc8576de 100644 --- a/src/utils/picklefield.py +++ b/evennia/utils/picklefield.py @@ -43,7 +43,7 @@ from django.forms import CharField, Textarea from django.forms.util import flatatt from django.utils.html import format_html -from src.utils.dbserialize import from_pickle, to_pickle +from evennia.utils.dbserialize import from_pickle, to_pickle try: from django.utils.encoding import force_text @@ -276,4 +276,4 @@ try: except ImportError: pass else: - add_introspection_rules([], [r"^src\.utils\.picklefield\.PickledObjectField"]) + add_introspection_rules([], [r"^evennia\.utils\.picklefield\.PickledObjectField"]) diff --git a/src/utils/prettytable.py b/evennia/utils/prettytable.py similarity index 99% rename from src/utils/prettytable.py rename to evennia/utils/prettytable.py index 1b8dfe7c6..800afb3d7 100644 --- a/src/utils/prettytable.py +++ b/evennia/utils/prettytable.py @@ -40,7 +40,7 @@ import textwrap import itertools import unicodedata -from src.utils.ansi import parse_ansi +from evennia.utils.ansi import parse_ansi py3k = sys.version_info[0] >= 3 if py3k: diff --git a/src/utils/search.py b/evennia/utils/search.py similarity index 96% rename from src/utils/search.py rename to evennia/utils/search.py index 71711d44d..3cb71b754 100644 --- a/src/utils/search.py +++ b/evennia/utils/search.py @@ -5,7 +5,7 @@ search methods for the various database tables. It is intended to be used e.g. as -> from src.utils import search +> from evennia.utils import search > match = search.objects(...) Note that this is not intended to be a complete listing of all search @@ -17,9 +17,9 @@ Also remember that all commands in this file return lists (also if there is only one match) unless noted otherwise. Example: To reach the search method 'get_object_with_player' - in src/objects/managers.py: + in evennia/objects/managers.py: -> from src.objects.models import ObjectDB +> from evennia.objects.models import ObjectDB > match = Object.objects.get_object_with_player(...) @@ -174,10 +174,11 @@ channels = search_channels # category - limit the search to a particular help topic # """ -search_help_entry = HelpEntry.objects.search_help -search_help_entries = search_help_entry -help_entry_search = search_help_entry -help_entries = search_help_entries +search_help = HelpEntry.objects.search_help +search_help_entry = search_help +search_help_entries = search_help +help_entry_search = search_help +help_entries = search_help # Locate Attributes diff --git a/src/utils/spawner.py b/evennia/utils/spawner.py similarity index 87% rename from src/utils/spawner.py rename to evennia/utils/spawner.py index b4bbdc311..d25d5080b 100644 --- a/src/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -9,7 +9,7 @@ The main function is spawn(*prototype), where the prototype is a dictionary like this: GOBLIN = { - "typeclass": "game.gamesrc.objects.objects.Monster", + "typeclass": "types.objects.Monster", "key": "goblin grunt", "health": lambda: randint(20,30), "resists": ["cold", "poison"], @@ -69,19 +69,19 @@ many traits with a normal goblin. """ -import os, sys, copy -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) -os.environ['DJANGO_SETTINGS_MODULE'] = 'game.settings' +import copy +#TODO +#sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +#os.environ['DJANGO_SETTINGS_MODULE'] = 'game.settings' from django.conf import settings from random import randint -from src.objects.models import ObjectDB -from src.utils.create import handle_dbref -from src.utils.utils import make_iter, all_from_module +from evennia.objects.models import ObjectDB +from evennia.utils.utils import make_iter, all_from_module, dbid_to_obj _CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination") -_handle_dbref = lambda inp: handle_dbref(inp, ObjectDB) +_handle_dbref = lambda inp: dbid_to_obj(inp, ObjectDB) def _validate_prototype(key, prototype, protparents, visited): @@ -128,42 +128,29 @@ def _batch_create_object(*objparams): creation/add handlers in the following order: (create, permissions, locks, aliases, nattributes, attributes) Returns: - A list of created objects + objects (list): A list of created objects + """ # bulk create all objects in one go - dbobjs = [ObjectDB(**objparam[0]) for objparam in objparams] - # unfortunately this doesn't work since bulk_create don't creates pks; + + # unfortunately this doesn't work since bulk_create doesn't creates pks; # the result are double objects at the next stage #dbobjs = _ObjectDB.objects.bulk_create(dbobjs) + dbobjs = [ObjectDB(**objparam[0]) for objparam in objparams] objs = [] - for iobj, dbobj in enumerate(dbobjs): + for iobj, obj in enumerate(dbobjs): # call all setup hooks on each object objparam = objparams[iobj] - obj = dbobj.typeclass # this saves dbobj if not done already - obj.basetype_setup() - obj.at_object_creation() - - if objparam[1]: - # permissions - obj.permissions.add(objparam[1]) - if objparam[2]: - # locks - obj.locks.add(objparam[2]) - if objparam[3]: - # aliases - obj.aliases.add(objparam[3]) - if objparam[4]: - # nattributes - for key, value in objparam[4].items(): - obj.nattributes.add(key, value) - if objparam[5]: - # attributes - keys, values = objparam[5].keys(), objparam[5].values() - obj.attributes.batch_add(keys, values) - - obj.basetype_posthook_setup() + # setup + obj._createdict = {"permissions": objparam[1], + "locks": objparam[2], + "aliases": objparam[3], + "nattributes": objparam[4], + "attributes": objparam[5]} + # this triggers all hooks + obj.save() objs.append(obj) return objs diff --git a/evennia/utils/test_resources.py b/evennia/utils/test_resources.py new file mode 100644 index 000000000..27083126f --- /dev/null +++ b/evennia/utils/test_resources.py @@ -0,0 +1,60 @@ +from django.conf import settings +from django.test import TestCase +from mock import Mock +from evennia.objects import DefaultObject, DefaultCharacter, DefaultRoom +from evennia.players import DefaultPlayer +from evennia.scripts import DefaultScript +from evennia.server.serversession import ServerSession +from evennia.server.sessionhandler import SESSIONS +from evennia.utils import create +from evennia.utils.idmapper.models import flush_cache + + +SESSIONS.data_out = Mock() +SESSIONS.disconnect = Mock() + + +class EvenniaTest(TestCase): + """ + Base test for Evennia, sets up a basic environment. + """ + player_typeclass = DefaultPlayer + object_typeclass = DefaultObject + character_typeclass = DefaultCharacter + room_typeclass = DefaultRoom + script_typeclass = DefaultScript + + def setUp(self): + """ + Sets up testing environment + """ + self.player = create.create_player("TestPlayer", email="test@test.com", password="testpassword", typeclass=self.player_typeclass) + self.player2 = create.create_player("TestPlayer2", email="test@test.com", password="testpassword", typeclass=self.player_typeclass) + self.room1 = create.create_object(self.room_typeclass, key="Room", nohome=True) + self.room1.db.desc = "room_desc" + settings.DEFAULT_HOME = "#%i" % self.room1.id # we must have a default home + self.room2 = create.create_object(self.room_typeclass, key="Room2") + self.obj1 = create.create_object(self.object_typeclass, key="Obj", location=self.room1, home=self.room1) + self.obj2 = create.create_object(self.object_typeclass, key="Obj2", location=self.room1, home=self.room1) + self.char1 = create.create_object(self.character_typeclass, key="Char", location=self.room1, home=self.room1) + self.char1.permissions.add("Immortals") + self.char2 = create.create_object(self.character_typeclass, key="Char2", location=self.room1, home=self.room1) + self.char1.player = self.player + self.player.db._last_puppet = self.char1 + self.char2.player = self.player2 + self.player2.db._last_puppet = self.char2 + self.script = create.create_script(self.script_typeclass, key="Script") + self.player.permissions.add("Immortals") + + # set up a fake session + + session = ServerSession() + session.init_session("telnet", ("localhost", "testmode"), SESSIONS) + session.sessid = 1 + SESSIONS.portal_connect(session.get_sync_data()) + SESSIONS.login(SESSIONS.session_from_sessid(1), self.player, testmode=True) + self.session = session + + def tearDown(self): + flush_cache() + del SESSIONS.sessions[self.session.sessid] diff --git a/src/utils/tests.py b/evennia/utils/tests.py similarity index 65% rename from src/utils/tests.py rename to evennia/utils/tests.py index 9bb9c53de..463639b52 100644 --- a/src/utils/tests.py +++ b/evennia/utils/tests.py @@ -6,6 +6,7 @@ except ImportError: from django.test import TestCase from ansi import ANSIString +import utils class ANSIStringTestCase(TestCase): @@ -118,4 +119,61 @@ class ANSIStringTestCase(TestCase): """ target = ANSIString('{gtest{n') result = u'\x1b[1m\x1b[32mTest\x1b[0m' - self.checker(target.capitalize(), result, u'Test') \ No newline at end of file + self.checker(target.capitalize(), result, u'Test') + + +class TestIsIter(TestCase): + def test_is_iter(self): + self.assertEqual(True, utils.is_iter([1,2,3,4])) + self.assertEqual(False, utils.is_iter("This is not an iterable")) + + +class TestCrop(TestCase): + def test_crop(self): + # No text, return no text + self.assertEqual("", utils.crop("", width=10, suffix="[...]")) + # Input length equal to max width, no crop + self.assertEqual("0123456789", utils.crop("0123456789", width=10, suffix="[...]")) + # Input length greater than max width, crop (suffix included in width) + self.assertEqual("0123[...]", utils.crop("0123456789", width=9, suffix="[...]")) + # Input length less than desired width, no crop + self.assertEqual("0123", utils.crop("0123", width=9, suffix="[...]")) + # Width too small or equal to width of suffix + self.assertEqual("012", utils.crop("0123", width=3, suffix="[...]")) + self.assertEqual("01234", utils.crop("0123456", width=5, suffix="[...]")) + + +class TestDedent(TestCase): + def test_dedent(self): + #print "Did TestDedent run?" + # Empty string, return empty string + self.assertEqual("", utils.dedent("")) + # No leading whitespace + self.assertEqual("TestDedent", utils.dedent("TestDedent")) + # Leading whitespace, single line + self.assertEqual("TestDedent", utils.dedent(" TestDedent")) + # Leading whitespace, multi line + input_string = " hello\n world" + expected_string = "hello\nworld" + self.assertEqual(expected_string, utils.dedent(input_string)) + + +class TestListToString(TestCase): + """ + Default function header from utils.py: + list_to_string(inlist, endsep="and", addquote=False) + + Examples: + no endsep: + [1,2,3] -> '1, 2, 3' + with endsep=='and': + [1,2,3] -> '1, 2 and 3' + with addquote and endsep + [1,2,3] -> '"1", "2" and "3"' + """ + def test_list_to_string(self): + self.assertEqual('1, 2, 3', utils.list_to_string([1,2,3], endsep="")) + self.assertEqual('"1", "2", "3"', utils.list_to_string([1,2,3], endsep="", addquote=True)) + self.assertEqual('1, 2 and 3', utils.list_to_string([1,2,3])) + self.assertEqual('"1", "2" and "3"', utils.list_to_string([1,2,3], endsep="and", addquote=True)) + diff --git a/src/utils/text2html.py b/evennia/utils/text2html.py similarity index 100% rename from src/utils/text2html.py rename to evennia/utils/text2html.py diff --git a/src/utils/txws.py b/evennia/utils/txws.py similarity index 100% rename from src/utils/txws.py rename to evennia/utils/txws.py diff --git a/src/utils/utils.py b/evennia/utils/utils.py similarity index 85% rename from src/utils/utils.py rename to evennia/utils/utils.py index 1930b65e5..29506b462 100644 --- a/src/utils/utils.py +++ b/evennia/utils/utils.py @@ -16,7 +16,8 @@ import textwrap import datetime import random import traceback -from inspect import ismodule +from importlib import import_module +from inspect import ismodule, trace from collections import defaultdict from twisted.internet import threads, defer, reactor from django.conf import settings @@ -176,7 +177,8 @@ def time_format(seconds, style=0): Style 0: 1d 08:30 Style 1: 1d - Style 2: 1 day, 8 hours, 30 minutes, 10 seconds + Style 2: 1 day, 8 hours, 30 minutes + Style 3: 1 day, 8 hours, 30 minutes, 10 seconds """ if seconds < 0: seconds = 0 @@ -261,7 +263,7 @@ def time_format(seconds, style=0): seconds_str = '%i seconds ' % seconds retval = '%s%s%s%s' % (days_str, hours_str, minutes_str, seconds_str) - return retval + return retval.strip() def datetime_format(dtobj): @@ -303,38 +305,35 @@ def host_os_is(osname): def get_evennia_version(): - """ - Check for the evennia version info. - """ - try: - f = open(settings.BASE_PATH + os.sep + "VERSION.txt", 'r') - return "%s-%s" % (f.read().strip(), os.popen("git rev-parse --short HEAD").read().strip()) - except IOError: - return "Unknown version" + import evennia + return evennia.__version__ def pypath_to_realpath(python_path, file_ending='.py'): """ - Converts a path on dot python form (e.g. 'src.objects.models') to - a system path ($BASE_PATH/src/objects/models.py). Calculates all - paths as absoulte paths starting from the evennia main directory. + Converts a dotted Python path to an absolute path under the + Evennia library directory or under the current game directory. + + Args: + python_path (str): a dot-python path + file_ending (str): a file ending, including the period. + + Returns: + abspaths (list of str): The two absolute paths created by prepending + EVENNIA_DIR and GAME_DIR respectively. These are checked for + existence before being returned, so this may be an empty list. - Since it seems to be a common mistake to include the file ending - when entering filename for things like batchprocess, we handle the - case of erroneously adding the file ending too. """ pathsplit = python_path.strip().split('.') - if python_path.endswith(file_ending): - # this is actually a malformed path ... - pathsplit = pathsplit[:-1] - if not pathsplit: - return python_path - path = settings.BASE_PATH - for directory in pathsplit: - path = os.path.join(path, directory) + paths = [os.path.join(settings.EVENNIA_DIR, *pathsplit), + os.path.join(settings.GAME_DIR, *pathsplit)] if file_ending: - return "%s%s" % (path, file_ending) - return path + # attach file ending to the paths if not already set (a common mistake) + file_ending = ".%s" % file_ending if not file_ending.startswith(".") else file_ending + paths = ["%s%s" % (p, file_ending) if not p.endswith(file_ending) else p + for p in paths] + # check so the paths actually exists before returning + return [p for p in paths if os.path.isfile(p)] def dbref(dbref, reqhash=True): @@ -346,16 +345,43 @@ def dbref(dbref, reqhash=True): Output is the integer part. """ if reqhash: - return (int(dbref.lstrip('#')) if (isinstance(dbref, basestring) and + num = (int(dbref.lstrip('#')) if (isinstance(dbref, basestring) and dbref.startswith("#") and dbref.lstrip('#').isdigit()) else None) + return num if num > 0 else None elif isinstance(dbref, basestring): dbref = dbref.lstrip('#') - return int(dbref) if dbref.isdigit() else None - return dbref if isinstance(dbref, int) else None + return int(dbref) if dbref.isdigit() and int(dbref) > 0 else None + else: + return dbref if isinstance(dbref, int) else None +def dbid_to_obj(inp, objclass, raise_errors=True): + """ + Convert a #dbid to a valid object of objclass. objclass + should be a valid object class to filter against (objclass.filter ...) + If not raise_errors is set, this will swallow errors of non-existing + objects. + """ + dbid = dbref(inp) + if not dbid: + # we only convert #dbrefs + return inp + try: + if int(inp) < 0: + return None + except ValueError: + return None + + # if we get to this point, inp is an integer dbref; get the matching object + try: + return objclass.objects.get(id=inp) + except Exception: + if raise_errors: + raise + return inp + def to_unicode(obj, encoding='utf-8', force_string=False): """ This decodes a suitable object to the unicode format. Note that @@ -495,7 +521,7 @@ def server_services(): since services are launced in memory, this function will only return any results if called from inside the game. """ - from src.server.sessionhandler import SESSIONS + from evennia.server.sessionhandler import SESSIONS if hasattr(SESSIONS, "server") and hasattr(SESSIONS.server, "services"): server = SESSIONS.server.services.namedServices else: @@ -549,14 +575,13 @@ def clean_object_caches(obj): """ global _TYPECLASSMODELS, _OBJECTMODELS if not _TYPECLASSMODELS: - from src.typeclasses import models as _TYPECLASSMODELS + from evennia.typeclasses import models as _TYPECLASSMODELS #if not _OBJECTMODELS: - # from src.objects import models as _OBJECTMODELS + # from evennia.objects import models as _OBJECTMODELS #print "recaching:", obj if not obj: return - obj = hasattr(obj, "dbobj") and obj.dbobj or obj # contents cache try: _SA(obj, "_contents_cache", None) @@ -724,9 +749,9 @@ def mod_import(module): Args: module - this can be either a Python path (dot-notation like - src.objects.models), an absolute path - (e.g. /home/eve/evennia/src/objects.models.py) - or an already import module object (e.g. models) + evennia.objects.models), an absolute path + (e.g. /home/eve/evennia/evennia/objects.models.py) + or an already imported module object (e.g. models) Returns: an imported module. If the input argument was already a model, this is returned as-is, otherwise the path is parsed and imported. @@ -871,6 +896,79 @@ def random_string_from_module(module): """ return random.choice(string_from_module(module)) +def fuzzy_import_from_module(path, variable, default=None, defaultpaths=None): + """ + Import a variable based on a fuzzy path. First the literal + path will be tried, then all given defaultdirs will be + prepended to see a match is found. + + path - full or partial python path + variable - name of variable to import from module + defaultpaths - an iterable of python paths to attempt + in order if importing directly from + path does not work. + """ + paths = [path] + make_iter(defaultpaths) + for modpath in paths: + try: + mod = import_module(path) + except ImportError, ex: + if not str(ex).startswith ("No module named %s" % path): + # this means the module was found but it + # triggers an ImportError on import. + raise ex + return getattr(mod, variable, default) + return default + +def class_from_module(path, defaultpaths=None): + """ + Return a class from a module, given the module's path. This is + primarily used to convert db_typeclass_path:s to classes. + + if a list of defaultpaths is given, try subsequent runs by + prepending those paths to the given path. + """ + cls = None + if defaultpaths: + paths = [path] + ["%s.%s" % (dpath, path) for dpath in make_iter(defaultpaths)] if defaultpaths else [] + else: + paths = [path] + + for testpath in paths: + if "." in path: + testpath, clsname = testpath.rsplit(".", 1) + else: + raise ImportError("the path '%s' is not on the form modulepath.Classname." % path) + try: + mod = import_module(testpath, package="evennia") + except ImportError: + if len(trace()) > 2: + # this means the error happened within the called module and + # we must not hide it. + exc = sys.exc_info() + raise exc[1], None, exc[2] + else: + # otherwise, try the next suggested path + continue + try: + cls = getattr(mod, clsname) + break + except AttributeError: + if len(trace()) > 2: + # AttributeError within the module, don't hide it + exc = sys.exc_info() + raise exc[1], None, exc[2] + if not cls: + err = "Could not load typeclass '%s'" % path + if defaultpaths: + err += "\nPaths searched:\n %s" % "\n ".join(paths) + else: + err += "." + raise ImportError(err) + return cls +# alias +object_from_module = class_from_module + def init_new_player(player): """ Helper method to call all hooks, set flags etc on a newly created @@ -977,7 +1075,7 @@ def string_partial_matching(alternatives, inp, ret_index=True): def format_table(table, extra_space=1): """ - Note: src.utils.prettytable is more powerful than this, but this + Note: evennia.utils.prettytable is more powerful than this, but this function can be useful when the number of columns and rows are unknown and must be calculated on the fly. @@ -1013,7 +1111,6 @@ def format_table(table, extra_space=1): for icol, col in enumerate(table)]) return ftable - def get_evennia_pids(): """ Get the currently valids PIDs (Process IDs) of the Portal and Server @@ -1113,5 +1210,23 @@ def strip_control_sequences(string): """ global _STRIP_ANSI if not _STRIP_ANSI: - from src.utils.ansi import strip_raw_ansi as _STRIP_ANSI + from evennia.utils.ansi import strip_raw_ansi as _STRIP_ANSI return _RE_CONTROL_CHAR.sub('', _STRIP_ANSI(string)) + +def calledby(callerdepth=1): + """ + Only to be used for debug purposes. + Insert this debug function in another function; it will print + which function called it. With callerdepth > 1, it will print the + caller of the caller etc. + """ + import inspect, os + stack = inspect.stack() + # we must step one extra level back in stack since we don't want + # to include the call of this function itself. + callerdepth = min(max(2, callerdepth + 1), len(stack)-1) + frame = inspect.stack()[callerdepth] + path = os.path.sep.join(frame[1].rsplit(os.path.sep, 2)[-2:]) + return "[called by '%s': %s:%s %s]" % (frame[3], path, frame[2], frame[4]) + + diff --git a/evennia/web/__init__.py b/evennia/web/__init__.py new file mode 100644 index 000000000..03578813e --- /dev/null +++ b/evennia/web/__init__.py @@ -0,0 +1,7 @@ +""" +This sub-package holds the web presence of Evennia, using normal +Django to relate the database contents to web pages. Also the basic +webclient and the website are defined in here (the webserver itself is +found under the `server` package). +""" + diff --git a/src/web/static/evennia_general/css/prosimii-print.css b/evennia/web/static/evennia_general/css/prosimii-print.css similarity index 100% rename from src/web/static/evennia_general/css/prosimii-print.css rename to evennia/web/static/evennia_general/css/prosimii-print.css diff --git a/src/web/static/evennia_general/css/prosimii-screen-alt.css b/evennia/web/static/evennia_general/css/prosimii-screen-alt.css similarity index 100% rename from src/web/static/evennia_general/css/prosimii-screen-alt.css rename to evennia/web/static/evennia_general/css/prosimii-screen-alt.css diff --git a/src/web/static/evennia_general/css/prosimii-screen.css b/evennia/web/static/evennia_general/css/prosimii-screen.css similarity index 100% rename from src/web/static/evennia_general/css/prosimii-screen.css rename to evennia/web/static/evennia_general/css/prosimii-screen.css diff --git a/src/web/static/evennia_general/images/LICENCE b/evennia/web/static/evennia_general/images/LICENCE similarity index 100% rename from src/web/static/evennia_general/images/LICENCE rename to evennia/web/static/evennia_general/images/LICENCE diff --git a/src/web/static/evennia_general/images/evennia_logo.png b/evennia/web/static/evennia_general/images/evennia_logo.png similarity index 100% rename from src/web/static/evennia_general/images/evennia_logo.png rename to evennia/web/static/evennia_general/images/evennia_logo.png diff --git a/src/web/static/evennia_general/images/favicon.ico b/evennia/web/static/evennia_general/images/favicon.ico similarity index 100% rename from src/web/static/evennia_general/images/favicon.ico rename to evennia/web/static/evennia_general/images/favicon.ico diff --git a/src/web/templates/evennia_general/evennia_admin.html b/evennia/web/templates/evennia_general/evennia_admin.html similarity index 100% rename from src/web/templates/evennia_general/evennia_admin.html rename to evennia/web/templates/evennia_general/evennia_admin.html diff --git a/src/web/templates/evennia_general/index.html b/evennia/web/templates/evennia_general/index.html similarity index 100% rename from src/web/templates/evennia_general/index.html rename to evennia/web/templates/evennia_general/index.html diff --git a/src/web/templates/evennia_general/tbi.html b/evennia/web/templates/evennia_general/tbi.html similarity index 100% rename from src/web/templates/evennia_general/tbi.html rename to evennia/web/templates/evennia_general/tbi.html diff --git a/src/web/templates/prosimii/404.html b/evennia/web/templates/prosimii/404.html similarity index 100% rename from src/web/templates/prosimii/404.html rename to evennia/web/templates/prosimii/404.html diff --git a/src/web/templates/prosimii/500.html b/evennia/web/templates/prosimii/500.html similarity index 100% rename from src/web/templates/prosimii/500.html rename to evennia/web/templates/prosimii/500.html diff --git a/src/web/templates/prosimii/base.html b/evennia/web/templates/prosimii/base.html similarity index 100% rename from src/web/templates/prosimii/base.html rename to evennia/web/templates/prosimii/base.html diff --git a/src/web/templates/prosimii/flatpages/default.html b/evennia/web/templates/prosimii/flatpages/default.html similarity index 100% rename from src/web/templates/prosimii/flatpages/default.html rename to evennia/web/templates/prosimii/flatpages/default.html diff --git a/src/web/templates/prosimii/registration/logged_out.html b/evennia/web/templates/prosimii/registration/logged_out.html similarity index 100% rename from src/web/templates/prosimii/registration/logged_out.html rename to evennia/web/templates/prosimii/registration/logged_out.html diff --git a/src/web/templates/prosimii/registration/login.html b/evennia/web/templates/prosimii/registration/login.html similarity index 100% rename from src/web/templates/prosimii/registration/login.html rename to evennia/web/templates/prosimii/registration/login.html diff --git a/src/web/urls.py b/evennia/web/urls.py similarity index 83% rename from src/web/urls.py rename to evennia/web/urls.py index 02299ac57..9c3bb1387 100755 --- a/src/web/urls.py +++ b/evennia/web/urls.py @@ -30,7 +30,7 @@ urlpatterns = [ url(r'^accounts/logout', 'django.contrib.auth.views.logout', name="logout"), # Page place-holder for things that aren't implemented yet. - url(r'^tbi/', 'src.web.views.to_be_implemented', name='to_be_implemented'), + url(r'^tbi/', 'evennia.web.views.to_be_implemented', name='to_be_implemented'), # Admin interface url(r'^admin/doc/', include('django.contrib.admindocs.urls')), @@ -39,19 +39,19 @@ urlpatterns = [ url(r'^favicon\.ico$', RedirectView.as_view(url='/media/images/favicon.ico')), # ajax stuff - url(r'^webclient/', include('src.web.webclient.urls', namespace='webclient', app_name='webclient')), + url(r'^webclient/', include('evennia.web.webclient.urls', namespace='webclient', app_name='webclient')), # Front page - url(r'^$', 'src.web.views.page_index', name="index"), + url(r'^$', 'evennia.web.views.page_index', name="index"), # Django original admin page. Make this URL is always available, whether # we've chosen to use Evennia's custom admin or not. - url(r'django_admin/', 'src.web.views.admin_wrapper', name="django_admin")] + url(r'django_admin/', 'evennia.web.views.admin_wrapper', name="django_admin")] if settings.EVENNIA_ADMIN: urlpatterns += [ # Our override for the admin. - url('^admin/$', 'src.web.views.evennia_admin', name="evennia_admin"), + url('^admin/$', 'evennia.web.views.evennia_admin', name="evennia_admin"), # Makes sure that other admin pages get loaded. url(r'^admin/', include(admin.site.urls))] diff --git a/src/web/webclient/__init__.py b/evennia/web/utils/__init__.py similarity index 100% rename from src/web/webclient/__init__.py rename to evennia/web/utils/__init__.py diff --git a/src/web/utils/apache_wsgi.conf b/evennia/web/utils/apache_wsgi.conf similarity index 100% rename from src/web/utils/apache_wsgi.conf rename to evennia/web/utils/apache_wsgi.conf diff --git a/src/web/utils/backends.py b/evennia/web/utils/backends.py similarity index 100% rename from src/web/utils/backends.py rename to evennia/web/utils/backends.py diff --git a/src/web/utils/evennia_modpy_apache.conf b/evennia/web/utils/evennia_modpy_apache.conf similarity index 100% rename from src/web/utils/evennia_modpy_apache.conf rename to evennia/web/utils/evennia_modpy_apache.conf diff --git a/src/web/utils/evennia_wsgi_apache.conf b/evennia/web/utils/evennia_wsgi_apache.conf similarity index 100% rename from src/web/utils/evennia_wsgi_apache.conf rename to evennia/web/utils/evennia_wsgi_apache.conf diff --git a/src/web/utils/general_context.py b/evennia/web/utils/general_context.py similarity index 96% rename from src/web/utils/general_context.py rename to evennia/web/utils/general_context.py index b901e856e..dfd35772c 100644 --- a/src/web/utils/general_context.py +++ b/evennia/web/utils/general_context.py @@ -7,7 +7,7 @@ # from django.conf import settings -from src.utils.utils import get_evennia_version +from evennia.utils.utils import get_evennia_version # Determine the site name and server version diff --git a/src/web/views.py b/evennia/web/views.py similarity index 89% rename from src/web/views.py rename to evennia/web/views.py index 127b61360..a985f289a 100644 --- a/src/web/views.py +++ b/evennia/web/views.py @@ -10,8 +10,9 @@ from django.conf import settings from django.contrib.admin.views.decorators import staff_member_required from django.shortcuts import render -from src.objects.models import ObjectDB -from src.players.models import PlayerDB +from evennia import SESSION_HANDLER +from evennia.objects.models import ObjectDB +from evennia.players.models import PlayerDB _BASE_CHAR_TYPECLASS = settings.BASE_CHARACTER_TYPECLASS @@ -29,7 +30,8 @@ def page_index(request): nplyrs_conn_recent = len(recent_users) or "none" nplyrs = PlayerDB.objects.num_total_players() or "none" nplyrs_reg_recent = len(PlayerDB.objects.get_recently_created_players()) or "none" - nsess = len(PlayerDB.objects.get_connected_players()) or "no one" + nsess = SESSION_HANDLER.player_count() + # nsess = len(PlayerDB.objects.get_connected_players()) or "no one" nobjs = ObjectDB.objects.all().count() nrooms = ObjectDB.objects.filter(db_location__isnull=True).exclude(db_typeclass_path=_BASE_CHAR_TYPECLASS).count() @@ -81,4 +83,4 @@ def admin_wrapper(request): """ Wrapper that allows us to properly use the base Django admin site, if needed. """ - return staff_member_required(site.index)(request) \ No newline at end of file + return staff_member_required(site.index)(request) diff --git a/game/logs/.empty b/evennia/web/webclient/__init__.py similarity index 100% rename from game/logs/.empty rename to evennia/web/webclient/__init__.py diff --git a/src/web/webclient/models.py b/evennia/web/webclient/models.py similarity index 100% rename from src/web/webclient/models.py rename to evennia/web/webclient/models.py diff --git a/src/web/webclient/static/webclient/css/webclient.css b/evennia/web/webclient/static/webclient/css/webclient.css similarity index 100% rename from src/web/webclient/static/webclient/css/webclient.css rename to evennia/web/webclient/static/webclient/css/webclient.css diff --git a/src/web/webclient/static/webclient/js/evennia_ajax_webclient.js b/evennia/web/webclient/static/webclient/js/evennia_ajax_webclient.js similarity index 97% rename from src/web/webclient/static/webclient/js/evennia_ajax_webclient.js rename to evennia/web/webclient/static/webclient/js/evennia_ajax_webclient.js index d80c45cab..e2f2d1f5c 100644 --- a/src/web/webclient/static/webclient/js/evennia_ajax_webclient.js +++ b/evennia/web/webclient/static/webclient/js/evennia_ajax_webclient.js @@ -5,7 +5,7 @@ Evennia ajax webclient (javascript component) The client is composed of several parts: templates/webclient.html - the main page webclient/views.py - the django view serving the template (based on urls.py pattern) - src/server/webclient.py - the server component receiving requests from the client + evennia/server/webclient.py - the server component receiving requests from the client this file - the javascript component handling dynamic ajax content This implements an ajax mud client for use with Evennia, using jQuery @@ -159,7 +159,7 @@ function webclient_init(){ }, 10000); // Report success - msg_display('sys',"Connected to " + data.msg + "."); + msg_display('sys',"Connected to " + data.msg + ". Websockets not available: Using ajax client without OOB support."); // Wait for input webclient_receive(); diff --git a/src/web/webclient/static/webclient/js/evennia_websocket_webclient.js b/evennia/web/webclient/static/webclient/js/evennia_websocket_webclient.js similarity index 68% rename from src/web/webclient/static/webclient/js/evennia_websocket_webclient.js rename to evennia/web/webclient/static/webclient/js/evennia_websocket_webclient.js index ac50691f0..caa53470a 100644 --- a/src/web/webclient/static/webclient/js/evennia_websocket_webclient.js +++ b/evennia/web/webclient/static/webclient/js/evennia_websocket_webclient.js @@ -3,7 +3,7 @@ Evennia websocket webclient (javascript component) The client is composed of two parts: - src/server/portal/websocket_client.py - the portal-side component + /server/portal/websocket_client.py - the portal-side component this file - the javascript component handling dynamic content messages sent to the client is one of two modes: @@ -16,7 +16,7 @@ messages sent to the client is one of two modes: // If on, allows client user to send OOB messages to server by // prepending with ##OOB{}, for example ##OOB{"echo":[1,2,3,4]} -var OOB_debug = true +var DEBUG = true // // Custom OOB functions @@ -36,14 +36,14 @@ function list (args, kwargs) { function send (args, kwargs) { // show in main window. SEND returns kwargs {name:value}. - for (sendvalue in kwargs) { + for (var sendvalue in kwargs) { doShow("out", sendvalue + " = " + kwargs[sendvalue]);} } function report (args, kwargs) { // show in main window. REPORT returns kwargs // {attrfieldname:value} - for (name in kwargs) { + for (var name in kwargs) { doShow("out", name + " = " + kwargs[name]) } } @@ -55,6 +55,8 @@ function err (args, kwargs) { // display error doShow("err", args) } +// Map above functions with oob command names +var CMD_MAP = {"echo":echo, "LIST":list, "SEND":send, "REPORT":report, "error":err}; // // Webclient code @@ -86,34 +88,54 @@ function onClose(evt) { } function onMessage(evt) { - // called when the Evennia is sending data to client - var inmsg = evt.data - if (inmsg.length > 3 && inmsg.substr(0, 3) == "OOB") { - // dynamically call oob methods, if available + // called when the Evennia is sending data to client. + // Such data is always prepended by a 3-letter marker + // OOB, PRT or CMD, defining its operation + var inmsg = evt.data; + if (inmsg.length < 4) return; + var mode = inmsg.substr(0, 3); + var message = inmsg.slice(3); + if (mode == "OOB") { + // dynamically call oob methods if available + // The incoming data is on the form [cmdname, [args], {kwargs}] try { - var oobarray = JSON.parse(inmsg.slice(3));} // everything after OOB } - catch(err) { - // not JSON packed - a normal text - doShow('out', inmsg); - return; - } - if (typeof oobarray != "undefined") { - for (var ind in oobarray) { - try { - window[oobarray[ind][0]](oobarray[ind][1], oobarray[ind][2]) } - catch(err) { - doShow("err", "Could not execute js OOB function '" + oobarray[ind][0] + "(" + oobarray[ind][1] + oobarray[ind][2] + ")'") } + if (message.length < 1) { + throw "Usage: ##OOB [[commandname, [args], {kwargs}], ...]" + } + var oobcmd = JSON.parse(message); + doShow("debug", "Received OOB: " + message + " parsed: " + oobcmd); + // call each command tuple in turn + var cmdname = oobcmd[0]; + var args = oobcmd[1]; + var kwargs = oobcmd[2]; + // match cmdname with a command existing in the + // CMD_MAP mapping + if (cmdname in CMD_MAP == false) { + throw "oob command " + cmdname + " is not supported by client."; + } + // we have a matching oob command in CMD_MAP. + // Prepare the error message beforehand + // Execute + try { + CMD_MAP[cmdname](args, kwargs); + } + catch(error) { + doShow("err", "Client could not execute OOB function" + "cmdname" + "(" + args + kwargs + ")."); } } + catch(error) { + doShow("err", error); + } } - else if (inmsg.length >= 6 && inmsg.substr(0, 6) == "PROMPT") { + else if (mode == "PRT") { // handle prompt - var game_prompt = inmsg.slice(6); - doPrompt("prompt", game_prompt); + doPrompt("prompt", message); } - else { + else if (mode == "CMD") { + // normal command operation // normal message - doShow('out', inmsg); } + doShow('out', message); + } } function onError(evt) { @@ -123,47 +145,86 @@ function onError(evt) { function doSend(){ // relays data from client to Evennia. - // If OOB_debug is set, allows OOB test data on the - // form ##OOB{func:args} + // If OOB_debug is set, allows OOB test data using the syntax + // ##OOB[funcname, args, kwargs] outmsg = $("#inputfield").val(); history_add(outmsg); HISTORY_POS = 0; $('#inputform')[0].reset(); // clear input field - if (OOB_debug && outmsg.length > 4 && outmsg.substr(0, 5) == "##OOB") { - if (outmsg == "##OOBUNITTEST") { + if (outmsg.length > 4 && outmsg.substr(0, 5) == "##OOB") { + // OOB direct input + var outmsg = outmsg.slice(5); + if (outmsg == "UNITTEST") { // unittest mode doShow("out", "OOB testing mode ..."); - doOOB(JSON.parse('{"ECHO":"Echo test"}')); - doOOB(JSON.parse('{"LIST":"COMMANDS"}')); - doOOB(JSON.parse('{"SEND":"CHARACTER_NAME"}')); - doOOB(JSON.parse('{"REPORT":"TEST"}')); - doOOB(JSON.parse('{"UNREPORT":"TEST"}')); - doOOB(JSON.parse('{"REPEAT": 1}')); - doOOB(JSON.parse('{"UNREPEAT": 1}')); + doOOB(["ECHO", ["Echo test"]]); + doOOB(["LIST", ["COMMANDS"]]); + doOOB(["SEND", ["CHARACTER_NAME"]]); + doOOB(["REPORT", ["TEST"]]); + doOOB(["UNREPORT", ["TEST"]]); + doOOB(["REPEAT", [1, "ECHO"]]); + doOOB(["UNREPEAT", [1, "ECHO"]]); doShow("out", "... OOB testing mode done."); return } - // test OOB messaging + // send a manual OOB instruction try { - doShow("out", "OOB input: " + outmsg.slice(5)); - if (outmsg.length == 5) { - doShow("err", "OOB testing syntax: ##OOB{\"cmdname:args, ...}"); } + doShow("debug", "OOB input: " + outmsg); + if (outmsg.length == 0) { + throw "Usage: ##OOB [[commandname, [args], {kwargs}], ...]"; + } else { - doOOB(JSON.parse(outmsg.slice(5))); } } + doOOB(outmsg); + } + } catch(err) { - doShow("err", err) } + doShow("err", err) + } } else { // normal output - websocket.send(outmsg); } + websocket.send("CMD" + outmsg); } } -function doOOB(oobdict){ +function doOOB(cmdstring){ // Send OOB data from client to Evennia. - // Takes input on form {funcname:[args], funcname: [args], ... } - var oobmsg = JSON.stringify(oobdict); - websocket.send("OOB" + oobmsg); + // Takes input strings with syntax ["cmdname", args, kwargs] + doShow("debug", "into doOOB... " + cmdstring) + try { + var cmdtuple = JSON.parse(cmdstring); + var oobmsg = ""; + if (cmdtuple instanceof Array == false) { + // a single command instruction without arguments + oobmsg = [cmdtuple, [], {}]; + } + else { + switch (cmdtuple.length) { + case 0: + throw "No command given"; + case 1: + // [cmdname] + oobmsg = [cmdtuple[0], [], {}]; + break; + case 2: + // [cmdname, args] + oobmsg = [cmdtuple[0], cmdtuple[1], {}]; + break; + case 3: + // [cmdname, args, kwargs] + oobmsg = [cmdtuple[0], cmdtuple[1], cmdtuple[2]]; + break; + default: + throw "Malformed OOB instruction: " + cmdstring; + } + // convert to string and send it to the server + oobmsg = JSON.stringify(oobmsg); + websocket.send("OOB" + oobmsg); + } + } + catch(error) { + doShow("err", "OOB output " + cmdtuple + " is not on the right form: " + error); + } } function doShow(type, msg){ @@ -171,6 +232,16 @@ function doShow(type, msg){ // type gives the class of div to use. // The default types are // "out" (normal output) or "err" (red error message) + if (type == "debug") { + if (DEBUG) { + type = "out"; + msg = "DEBUG: " + msg; + } + else { + return; + } + } + // output $("#messagewindow").append( "
"+ msg +"
"); // scroll message window to bottom @@ -352,6 +423,6 @@ $(document).ready(function(){ }, 500); // set an idle timer to avoid proxy servers to time out on us (every 3 minutes) setInterval(function() { - websocket.send("idle"); + doSend("idle") }, 60000*3); }); diff --git a/src/web/webclient/templates/webclient.html b/evennia/web/webclient/templates/webclient.html similarity index 100% rename from src/web/webclient/templates/webclient.html rename to evennia/web/webclient/templates/webclient.html diff --git a/src/web/webclient/urls.py b/evennia/web/webclient/urls.py similarity index 64% rename from src/web/webclient/urls.py rename to evennia/web/webclient/urls.py index ba5cdf818..a802fa426 100644 --- a/src/web/webclient/urls.py +++ b/evennia/web/webclient/urls.py @@ -5,4 +5,4 @@ webpage 'application'. from django.conf.urls import * urlpatterns = [ - url(r'^$', 'src.web.webclient.views.webclient', name="index")] + url(r'^$', 'evennia.web.webclient.views.webclient', name="index")] diff --git a/src/web/webclient/views.py b/evennia/web/webclient/views.py similarity index 94% rename from src/web/webclient/views.py rename to evennia/web/webclient/views.py index c8ea3c82a..4fd388f93 100644 --- a/src/web/webclient/views.py +++ b/evennia/web/webclient/views.py @@ -6,7 +6,7 @@ page and serve it eventual static content. """ from django.shortcuts import render -from src.players.models import PlayerDB +from evennia.players.models import PlayerDB def webclient(request): diff --git a/game/evennia.py b/game/evennia.py deleted file mode 100755 index 2abd00b52..000000000 --- a/game/evennia.py +++ /dev/null @@ -1,539 +0,0 @@ -#!/usr/bin/env python -""" -EVENNIA SERVER STARTUP SCRIPT - -This is the start point for running Evennia. - -Sets the appropriate environmental variables and launches the server -and portal through the runner. Run without arguments to get a -menu. Run the script with the -h flag to see usage information. - -""" -import os -import sys -import signal -from optparse import OptionParser -from subprocess import Popen -import django - -# Set the Python path up so we can get to settings.py from here. -from django.core import management - -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -os.environ['DJANGO_SETTINGS_MODULE'] = 'game.settings' - -if not os.path.exists('settings.py'): - # make sure we have a settings.py file. - print " No settings.py file found. launching manage.py ..." - - # this triggers the settings file creation. - import game.manage - sys.exit() - -# required since django1.7. -django.setup() - -# signal processing -SIG = signal.SIGINT - - -CMDLINE_HELP = \ -""" -Main Evennia launcher. When starting in interactive (-i) mode, only -the Server will do so since this is the most commonly useful setup. To -activate interactive mode also for the Portal, use the menu or launch -the two services one after the other as two separate calls to this -program. -""" - - -VERSION_INFO = \ -""" - Evennia {version} - {about} - OS: {os} - Python: {python} - Twisted: {twisted} - Django: {django} - {south} -""" - -ABOUT_INFO= \ -""" - MUD/MUX/MU* development system - - Licence: BSD 3-Clause Licence - Web: http://www.evennia.com - Irc: #evennia on FreeNode - Forum: http://www.evennia.com/discussions - Maintainer (2010-): Griatch (griatch AT gmail DOT com) - Maintainer (2006-10): Greg Taylor -""" - -HELP_ENTRY = \ -""" - (version %s) - -All launcher functionality can be accessed directly from the command -line. See python evennia.py -h for options. - -Evennia has two parts that both must run: - -Portal - the connection to the outside world (via telnet, web, ssh - etc). This is normally running as a daemon and don't need to - be reloaded unless you are debugging a new connection - protocol. -Server - the game server itself. This will often need to be reloaded - as you develop your game. The Portal will auto-connect to the - Server whenever the Server activates. - -Use option (1) in a production environment. During development (2) is -usually enough, portal debugging is usually only useful if you are -adding new protocols or are debugging an Evennia bug. - -Reload with (5) to update the server with your changes without -disconnecting any players. - -Reload and stop are sometimes poorly supported in Windows. If you have -issues, log into the game to stop or restart the server instead. -""" - -MENU = \ -""" -+----Evennia Launcher-------------------------------------------------------+ -| | -+--- Starting --------------------------------------------------------------+ -| | -| 1) (default): All output to logfiles. | -| 2) (game debug): Server outputs to terminal instead of to logfile. | -| 3) (portal debug): Portal outputs to terminal instead of to logfile. | -| 4) (full debug): Both Server and Portal output to terminal | -| | -+--- Restarting ------------------------------------------------------------+ -| | -| 5) Reload the Server | -| 6) Reload the Portal (only works with portal/full debug) | -| | -+--- Stopping --------------------------------------------------------------+ -| | -| 7) Stopping both Portal and Server. | -| 8) Stopping only Server. | -| 9) Stopping only Portal. | -| | -+---------------------------------------------------------------------------+ -| h) Help i) About info q) Abort | -+---------------------------------------------------------------------------+ -""" - - -# -# System Configuration and setup -# - -SERVER_PIDFILE = "server.pid" -PORTAL_PIDFILE = "portal.pid" - -SERVER_RESTART = "server.restart" -PORTAL_RESTART = "portal.restart" - -# Get the settings -from django.conf import settings - -from src.utils.utils import get_evennia_version -EVENNIA_VERSION = get_evennia_version() - -# Setup access of the evennia server itself -SERVER_PY_FILE = os.path.join(settings.SRC_DIR, 'server/server.py') -PORTAL_PY_FILE = os.path.join(settings.SRC_DIR, 'server/portal.py') - -# Get logfile names -SERVER_LOGFILE = settings.SERVER_LOG_FILE -PORTAL_LOGFILE = settings.PORTAL_LOG_FILE - -# Check so a database exists and is accessible -from django.db import DatabaseError -from src.players.models import PlayerDB -try: - superuser = PlayerDB.objects.get(id=1) -except DatabaseError, e: - print """ - Your database does not seem to be set up correctly. - (error was '%s') - - Please run: - - python manage.py syncdb - - When you have a database set up, rerun evennia.py. - """ % e - sys.exit() -except PlayerDB.DoesNotExist: - # no superuser yet. We need to create it. - from django.core.management import call_command - print "\nCreate a superuser below. The superuser is Player #1, the 'owner' account of the server.\n" - call_command("createsuperuser", interactive=True) - -# Add this to the environmental variable for the 'twistd' command. -currpath = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -if 'PYTHONPATH' in os.environ: - os.environ['PYTHONPATH'] += (":%s" % currpath) -else: - os.environ['PYTHONPATH'] = currpath - -TWISTED_BINARY = 'twistd' -if os.name == 'nt': - # Windows needs more work to get the correct binary - try: - # Test for for win32api - import win32api - except ImportError: - print """ - ERROR: Unable to import win32api, which Twisted requires to run. - You may download it from: - - http://sourceforge.net/projects/pywin32 - or - http://starship.python.net/crew/mhammond/win32/Downloads.html""" - sys.exit() - - if not os.path.exists('twistd.bat'): - # Test for executable twisted batch file. This calls the twistd.py - # executable that is usually not found on the path in Windows. - # It's not enough to locate scripts.twistd, what we want is the - # executable script C:\PythonXX/Scripts/twistd.py. Alas we cannot - # hardcode this location since we don't know if user has Python - # in a non-standard location, so we try to figure it out. - from twisted.scripts import twistd - twistd_path = os.path.abspath( - os.path.join(os.path.dirname(twistd.__file__), - os.pardir, os.pardir, os.pardir, os.pardir, - 'scripts', 'twistd.py')) - bat_file = open('twistd.bat', 'w') - bat_file.write("@\"%s\" \"%s\" %%*" % (sys.executable, twistd_path)) - bat_file.close() - print """ - INFO: Since you are running Windows, a file 'twistd.bat' was - created for you. This is a simple batch file that tries to call - the twisted executable. Evennia determined this to be: - - %(twistd_path)s - - If you run into errors at startup you might need to edit - twistd.bat to point to the actual location of the Twisted - executable (usually called twistd.py) on your machine. - - This procedure is only done once. Run evennia.py again when you - are ready to start the server. - """ % {'twistd_path': twistd_path} - sys.exit() - - TWISTED_BINARY = 'twistd.bat' - - -# Functions - -def get_pid(pidfile): - """ - Get the PID (Process ID) by trying to access - an PID file. - """ - pid = None - if os.path.exists(pidfile): - f = open(pidfile, 'r') - pid = f.read() - return pid - - -def del_pid(pidfile): - """ - The pidfile should normally be removed after a process has finished, but - when sending certain signals they remain, so we need to clean them manually. - """ - if os.path.exists(pidfile): - os.remove(pidfile) - - -def kill(pidfile, signal=SIG, succmsg="", errmsg="", restart_file=SERVER_RESTART, restart="reload"): - """ - Send a kill signal to a process based on PID. A customized success/error - message will be returned. If clean=True, the system will attempt to manually - remove the pid file. - """ - pid = get_pid(pidfile) - if pid: - if os.name == 'nt': - if sys.version < "2.7": - print "Windows requires Python 2.7 or higher for this operation." - return - os.remove(pidfile) - # set restart/norestart flag - if restart == 'reload': - management.call_command('collectstatic', interactive=False, verbosity=0) - f = open(restart_file, 'w') - f.write(str(restart)) - f.close() - try: - os.kill(int(pid), signal) - except OSError: - print "Process %(pid)s could not be signalled. The PID file '%(pidfile)s' seems stale. Try removing it." % {'pid': pid, 'pidfile': pidfile} - return - print "Evennia:", succmsg - return - print "Evennia:", errmsg - -def show_version_info(about=False): - """ - Display version info - """ - import os, sys - import twisted - import django - try: - import south - sversion = "South %s" % south.__version__ - except ImportError: - sversion = "South " - - return VERSION_INFO.format(version=EVENNIA_VERSION, - about=ABOUT_INFO if about else "", - os=os.name, python=sys.version.split()[0], - twisted=twisted.version.short(), - django=django.get_version(), - south=sversion) - -def run_menu(): - """ - This launches an interactive menu. - """ - - cmdstr = [sys.executable, "runner.py"] - - while True: - # menu loop - - print MENU - inp = raw_input(" option > ") - - # quitting and help - if inp.lower() == 'q': - sys.exit() - elif inp.lower() == 'h': - print HELP_ENTRY % EVENNIA_VERSION - raw_input("press to continue ...") - continue - elif inp.lower() in ('v', 'i', 'a'): - print show_version_info(about=True) - raw_input("press to continue ...") - continue - - # options - try: - inp = int(inp) - except ValueError: - print "Not a valid option." - continue - errmsg = "The %s does not seem to be running." - if inp < 5: - if inp == 1: - pass # default operation - elif inp == 2: - cmdstr.extend(['--iserver']) - elif inp == 3: - cmdstr.extend(['--iportal']) - elif inp == 4: - cmdstr.extend(['--iserver', '--iportal']) - return cmdstr - elif inp < 10: - if inp == 5: - if os.name == 'nt': - print "This operation is not supported under Windows. Log into the game to restart/reload the server." - return - kill(SERVER_PIDFILE, SIG, "Server reloaded.", errmsg % "Server", restart="reload") - elif inp == 6: - if os.name == 'nt': - print "This operation is not supported under Windows." - return - kill(PORTAL_PIDFILE, SIG, "Portal reloaded (or stopped if in daemon mode).", errmsg % "Portal", restart=True) - elif inp == 7: - kill(SERVER_PIDFILE, SIG, "Stopped Portal.", errmsg % "Portal", PORTAL_RESTART, restart=False) - kill(PORTAL_PIDFILE, SIG, "Stopped Server.", errmsg % "Server", restart="shutdown") - elif inp == 8: - kill(PORTAL_PIDFILE, SIG, "Stopped Server.", errmsg % "Server", restart="shutdown") - elif inp == 9: - kill(SERVER_PIDFILE, SIG, "Stopped Portal.", errmsg % "Portal", PORTAL_RESTART, restart=False) - return - else: - print "Not a valid option." - return None - - -def handle_args(options, mode, service): - """ - Handle argument options given on the command line. - - options - parsed object for command line - mode - str; start/stop etc - service - str; server, portal or all - """ - - inter = options.interactive - cmdstr = [sys.executable, "runner.py"] - errmsg = "The %s does not seem to be running." - - if mode == 'start': - - # launch the error checker. Best to catch the errors already here. - error_check_python_modules() - - # starting one or many services - if service == 'server': - if inter: - cmdstr.append('--iserver') - cmdstr.append('--noportal') - elif service == 'portal': - if inter: - cmdstr.append('--iportal') - cmdstr.append('--noserver') - management.call_command('collectstatic', verbosity=1, interactive=False) - else: # all - # for convenience we don't start logging of - # portal, only of server with this command. - if inter: - cmdstr.extend(['--iserver']) - management.call_command('collectstatic', verbosity=1, interactive=False) - return cmdstr - - elif mode == 'reload': - # restarting services - if os.name == 'nt': - print "Restarting from command line is not supported under Windows. Log into the game to restart." - return - if service == 'server': - kill(SERVER_PIDFILE, SIG, "Server reloaded.", errmsg % 'Server', restart="reload") - elif service == 'portal': - print """ - Note: Portal usually don't need to be reloaded unless you are debugging in interactive mode. - If Portal was running in default Daemon mode, it cannot be restarted. In that case you have - to restart it manually with 'evennia.py start portal' - """ - kill(PORTAL_PIDFILE, SIG, "Portal reloaded (or stopped, if it was in daemon mode).", errmsg % 'Portal', PORTAL_RESTART) - else: # all - # default mode, only restart server - kill(SERVER_PIDFILE, SIG, "Server reload.", errmsg % 'Server', restart="reload") - - elif mode == 'stop': - # stop processes, avoiding reload - if service == 'server': - kill(SERVER_PIDFILE, SIG, "Server stopped.", errmsg % 'Server', restart="shutdown") - elif service == 'portal': - kill(PORTAL_PIDFILE, SIG, "Portal stopped.", errmsg % 'Portal', PORTAL_RESTART, restart=False) - else: - kill(PORTAL_PIDFILE, SIG, "Portal stopped.", errmsg % 'Portal', PORTAL_RESTART, restart=False) - kill(SERVER_PIDFILE, SIG, "Server stopped.", errmsg % 'Server', restart="shutdown") - return None - - -def error_check_python_modules(): - """ - Import settings modules in settings. This will raise exceptions on - pure python-syntax issues which are hard to catch gracefully - with exceptions in the engine (since they are formatting errors in - the python source files themselves). Best they fail already here - before we get any further. - """ - def imp(path, split=True): - mod, fromlist = path, "None" - if split: - mod, fromlist = path.rsplit('.', 1) - __import__(mod, fromlist=[fromlist]) - - # core modules - imp(settings.COMMAND_PARSER) - imp(settings.SEARCH_AT_RESULT) - imp(settings.SEARCH_AT_MULTIMATCH_INPUT) - imp(settings.CONNECTION_SCREEN_MODULE, split=False) - #imp(settings.AT_INITIAL_SETUP_HOOK_MODULE, split=False) - for path in settings.LOCK_FUNC_MODULES: - imp(path, split=False) - # cmdsets - - deprstring = "settings.%s should be renamed to %s. If defaults are used, " \ - "their path/classname must be updated (see src/settings_default.py)." - if hasattr(settings, "CMDSET_DEFAULT"): - raise DeprecationWarning(deprstring % ("CMDSET_DEFAULT", "CMDSET_CHARACTER")) - if hasattr(settings, "CMDSET_OOC"): - raise DeprecationWarning(deprstring % ("CMDSET_OOC", "CMDSET_PLAYER")) - if settings.WEBSERVER_ENABLED and not isinstance(settings.WEBSERVER_PORTS[0], tuple): - raise DeprecationWarning("settings.WEBSERVER_PORTS must be on the form [(proxyport, serverport), ...]") - if hasattr(settings, "BASE_COMM_TYPECLASS"): - raise DeprecationWarning(deprstring % ("BASE_COMM_TYPECLASS", "BASE_CHANNEL_TYPECLASS")) - if hasattr(settings, "COMM_TYPECLASS_PATHS"): - raise DeprecationWarning(deprstring % ("COMM_TYPECLASS_PATHS", "CHANNEL_TYPECLASS_PATHS")) - if hasattr(settings, "CHARACTER_DEFAULT_HOME"): - raise DeprecationWarning("settings.CHARACTER_DEFAULT_HOME should be renamed to DEFAULT_HOME. " \ - "See also settings.START_LOCATION (see src/settings_default.py).") - - from src.commands import cmdsethandler - if not cmdsethandler.import_cmdset(settings.CMDSET_UNLOGGEDIN, None): print "Warning: CMDSET_UNLOGGED failed to load!" - if not cmdsethandler.import_cmdset(settings.CMDSET_CHARACTER, None): print "Warning: CMDSET_CHARACTER failed to load" - if not cmdsethandler.import_cmdset(settings.CMDSET_PLAYER, None): print "Warning: CMDSET_PLAYER failed to load" - # typeclasses - imp(settings.BASE_PLAYER_TYPECLASS) - imp(settings.BASE_OBJECT_TYPECLASS) - imp(settings.BASE_CHARACTER_TYPECLASS) - imp(settings.BASE_ROOM_TYPECLASS) - imp(settings.BASE_EXIT_TYPECLASS) - imp(settings.BASE_SCRIPT_TYPECLASS) - - -def main(): - """ - This handles command line input. - """ - - parser = OptionParser(usage="%prog [-i] start|stop|reload|menu [server|portal]", - description=CMDLINE_HELP) - parser.add_option('-i', '--interactive', action='store_true', - dest='interactive', default=False, - help="Start given processes in interactive mode.") - parser.add_option('-v', '--version', action='store_true', - dest='show_version', default=False, - help="Show version info.") - - options, args = parser.parse_args() - - if not args: - if options.show_version: - print show_version_info() - return - mode = "menu" - service = 'all' - if args: - mode = args[0] - service = "all" - if len(args) > 1: - service = args[1] - - if mode not in ['menu', 'start', 'reload', 'stop']: - print "mode should be none, 'menu', 'start', 'reload' or 'stop'." - sys.exit() - if service not in ['server', 'portal', 'all']: - print "service should be none, 'server', 'portal' or 'all'." - sys.exit() - - if mode == 'menu': - # launch menu - cmdstr = run_menu() - else: - # handle command-line arguments - cmdstr = handle_args(options, mode, service) - if cmdstr: - # call the runner. - cmdstr.append('start') - Popen(cmdstr) - -if __name__ == '__main__': - # start Evennia - from src.utils.utils import check_evennia_dependencies - if check_evennia_dependencies(): - main() diff --git a/game/gamesrc/commands/examples/cmdset.py b/game/gamesrc/commands/examples/cmdset.py deleted file mode 100644 index 88e7c621b..000000000 --- a/game/gamesrc/commands/examples/cmdset.py +++ /dev/null @@ -1,121 +0,0 @@ -""" -Example command set template module. - -To create new commands to populate the cmdset, see -examples/command.py. - -To extend the character command set: - - copy this file up one level to gamesrc/commands and name it - something fitting. - - change settings.CMDSET_CHARACTER to point to the new module's - CharacterCmdSet class - - import/add commands at the end of CharacterCmdSet's add() method. - -To extend Player cmdset: - - like character set, but point settings.PLAYER on your new cmdset. - -To extend Unloggedin cmdset: - - like default set, but point settings.CMDSET_UNLOGGEDIN on your new cmdset. - -To add a wholly new command set: - - copy this file up one level to gamesrc/commands and name it - something fitting. - - add a new cmdset class - - add it to objects e.g. with obj.cmdset.add(path.to.the.module.and.class) - -""" - -from ev import CmdSet, Command -from ev import default_cmds - -#from contrib import menusystem, lineeditor -#from contrib import misc_commands -#from contrib import chargen - - -class ExampleCmdSet(CmdSet): - """ - Implements an empty, example cmdset. - """ - - key = "ExampleSet" - - def at_cmdset_creation(self): - """ - This is the only method defined in a cmdset, called during - its creation. It should populate the set with command instances. - - As and example we just add the empty base Command object. - It prints some info. - """ - self.add(Command()) - - -class CharacterCmdSet(default_cmds.CharacterCmdSet): - """ - This is an example of how to overload the default command - set defined in src/commands/default/cmdset_character.py. - - Here we copy everything by calling the parent, but you can - copy&paste any combination of the default command to customize - your default set. Next you change settings.CMDSET_CHARACTER to point - to this class. - """ - key = "DefaultCharacter" - - def at_cmdset_creation(self): - """ - Populates the cmdset - """ - # calling setup in src.commands.default.cmdset_character - super(CharacterCmdSet, self).at_cmdset_creation() - - # - # any commands you add below will overload the default ones. - # - #self.add(menusystem.CmdMenuTest()) - #self.add(lineeditor.CmdEditor()) - #self.add(misc_commands.CmdQuell()) - - -class UnloggedinCmdSet(default_cmds.UnloggedinCmdSet): - """ - This is an example of how to overload the command set of the - unloggedin commands, defined in - src/commands/default/cmdset_unloggedin.py. - - Here we copy everything by calling the parent, but you can - copy&paste any combination of the default command to customize - your default set. Next you change settings.CMDSET_UNLOGGEDIN to - point to this class. - """ - key = "DefaultUnloggedin" - - def at_cmdset_creation(self): - """ - Populates the cmdset - """ - # calling setup in src.commands.default.cmdset_unloggedin - super(UnloggedinCmdSet, self).at_cmdset_creation() - - # - # any commands you add below will overload the default ones. - # - - -class PlayerCmdSet(default_cmds.PlayerCmdSet): - """ - This is set is available to the player when they have no - character connected to them (i.e. they are out-of-character, ooc). - """ - key = "DefaultPlayer" - - def at_cmdset_creation(self): - """ - Populates the cmdset - """ - # calling setup in src.commands.default.cmdset_ooc - super(PlayerCmdSet, self).at_cmdset_creation() - # - # any commands you add below will overload the default ones. - # diff --git a/game/gamesrc/conf/examples/connection_screens.py b/game/gamesrc/conf/examples/connection_screens.py deleted file mode 100644 index bc66875b7..000000000 --- a/game/gamesrc/conf/examples/connection_screens.py +++ /dev/null @@ -1,58 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Connect screen module template - -Copy this module one level up, to gamesrc/conf/, name it what -you want and modify it to your liking. - -Then you set settings.CONNECTION_SCREEN_MODULE to point to your -new module. - - - This module holds textual connection screen definitions. All global - string variables (only) in this module are read by Evennia and - assumed to define a Connection screen. - - The names of the string variables doesn't matter (but names starting - with an underscore will be ignored), but each should hold a string - defining a connection screen - as seen when first connecting to the - game (before having logged in). - - OBS - If there are more than one global string variable in this - module, a random one is picked! - - After adding new connection screens to this module you must either - reboot or reload the server to make them available. - -""" - -# comment this out if wanting to completely remove the default screen -from src.commands.connection_screen import DEFAULT_SCREEN - -## uncomment these for showing the name and version -# from django.conf import settings -# from src.utils import utils - -## A copy of the default screen to modify - -# CUSTOM_SCREEN = \ -#"""{b=============================================================={n -# Welcome to {g%s{n, version %s! -# -# If you have an existing account, connect to it by typing: -# {wconnect {n -# If you need to create an account, type (without the <>'s): -# {wcreate {n -# -# If you have spaces in your username, enclose it in quotes. -# Enter {whelp{n for more info. {wlook{n will re-show this screen. -#{b=============================================================={n""" \ -# % (settings.SERVERNAME, utils.get_evennia_version()) - -## Minimal header for use with contrib/menu_login.py - -# MENU_SCREEN = \ -# """{b=============================================================={n -# Welcome to {g%s{n, version %s! -# {b=============================================================={n""" \ -# % (settings.SERVERNAME, utils.get_evennia_version()) diff --git a/game/gamesrc/conf/examples/lockfuncs.py b/game/gamesrc/conf/examples/lockfuncs.py deleted file mode 100644 index 5693f9988..000000000 --- a/game/gamesrc/conf/examples/lockfuncs.py +++ /dev/null @@ -1,33 +0,0 @@ -""" - -Lockfuncs module template - -Copy this module one level up, to gamesrc/conf/, name it what -you will and edit it to your liking. - -Then add the new module's path to the end of the tuple -defined in settings.LOCK_FUNC_MODULES. - -All functions defined globally in this module are assumed to be -available for use in lockstrings to determine access. See -http://code.google.com/p/evennia/wiki/Locks - -A lock function is always called with two arguments, accessing_obj and -accessed_obj, followed by any number of arguments. All possible -arguments should be handled (excess ones calling magic (*args, -**kwargs) to avoid errors). The lock function should handle all -eventual tracebacks by logging the error and returning False. - -See many more examples of lock functions in src.locks.lockfuncs. - -""" - - -def myfalse(accessing_obj, accessed_obj, *args, **kwargs): - """ - called in lockstring with myfalse(). - A simple logger that always returns false. Prints to stdout - for simplicity, should use utils.logger for real operation. - """ - print "%s tried to access %s. Access denied." % (accessing_obj, accessed_obj) - return False diff --git a/game/gamesrc/conf/examples/oobfuncs.py b/game/gamesrc/conf/examples/oobfuncs.py deleted file mode 100644 index 8547ed325..000000000 --- a/game/gamesrc/conf/examples/oobfuncs.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -OOB configuration. - -This module should be included in (or replace) the -default module set in settings.OOB_PLUGIN_MODULES - -All functions defined in this module are made available -to be called by the OOB handler. - -See src/server/oob_msdp.py for more information. - - function execution - the oob protocol can execute a function directly on - the server. The available functions must be defined - as global functions via settings.OOB_PLUGIN_MODULES. - repeat func execution - the oob protocol can request a given function be - executed repeatedly at a regular interval. This - uses an internal script pool. - tracking - the oob protocol can request Evennia to track changes to - fields on objects, as well as changes in Attributes. This is - done by dynamically adding tracker-objects on entities. The - behaviour of those objects can be customized via - settings.OOB_PLUGIN_MODULES. - -oob functions have the following call signature: - function(caller, session, *args, **kwargs) - -oob trackers should inherit from the OOBTracker class in src/server.oob_msdp.py - and implement a minimum of the same functionality. - -a global function oob_error will be used as optional error management. - -""" - -# import the contents of the default msdp module -from src.server.oob_cmds import * - diff --git a/game/gamesrc/conf/examples/portal_services_plugin.py b/game/gamesrc/conf/examples/portal_services_plugin.py deleted file mode 100644 index 294c6d03d..000000000 --- a/game/gamesrc/conf/examples/portal_services_plugin.py +++ /dev/null @@ -1,24 +0,0 @@ -""" - -This plugin module can define user-created services for the Server to start. - -To use, copy this module up one level to game/gamesrc/conf/ and set -settings.SERVER_SERVICES_PLUGIN_MODULE to point to this module. - -This module must handle all imports and setups required to start a twisted -services (see examples in src/server/server.py). It must also contain a -function start_plugin_services(application). Evennia will call this function -with the main Server application (so your services can be added to it). The -function should not return anything. Plugin services are started last in -the Server startup process. - -""" - - -def start_plugin_services(server): - """ - This hook is called by Evennia, last in the Server startup process. - - server - a reference to the main server application. - """ - pass diff --git a/game/gamesrc/conf/examples/server_services_plugin.py b/game/gamesrc/conf/examples/server_services_plugin.py deleted file mode 100644 index a585c6770..000000000 --- a/game/gamesrc/conf/examples/server_services_plugin.py +++ /dev/null @@ -1,24 +0,0 @@ -""" - -This plugin module can define user-created services for the Portal to start. - -To use, copy this module up one level to game/gamesrc/conf/ and set -settings.PORTAL_SERVICES_PLUGIN_MODULE to point to this module. - -This module must handle all imports and setups required to start a twisted -service (see examples in src/server/server.py). It must also contain a -function start_plugin_services(application). Evennia will call this function -with the main Portal application (so your services can be added to it). The -function should not return anything. Plugin services are started last in -the Portal startup process. - -""" - - -def start_plugin_services(portal): - """ - This hook is called by Evennia, last in the Portal startup process. - - portal - a reference to the main portal application. - """ - pass diff --git a/game/gamesrc/objects/examples/__init__.py b/game/gamesrc/objects/examples/__init__.py deleted file mode 100644 index 40a96afc6..000000000 --- a/game/gamesrc/objects/examples/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/game/gamesrc/objects/examples/room.py b/game/gamesrc/objects/examples/room.py deleted file mode 100644 index 01bb16d90..000000000 --- a/game/gamesrc/objects/examples/room.py +++ /dev/null @@ -1,33 +0,0 @@ -""" - -Template module for Rooms - -Copy this module up one level and name it as you like, then -use it as a template to create your own Objects. - -To make the default commands (such as @dig) default to creating rooms -of your new type, change settings.BASE_ROOM_TYPECLASS to point to -your new class, e.g. - -settings.BASE_ROOM_TYPECLASS = "game.gamesrc.objects.myroom.MyRoom" - -Note that objects already created in the database will not notice -this change, you have to convert them manually e.g. with the -@typeclass command. - -""" - -from ev import Room as DefaultRoom - - -class Room(DefaultRoom): - """ - Rooms are like any Object, except their location is None - (which is default). They also use basetype_setup() to - add locks so they cannot be puppeted or picked up. - (to change that, use at_object_creation instead) - - See examples/object.py for a list of - properties and methods available on all Objects. - """ - pass diff --git a/game/gamesrc/scripts/__init__.py b/game/gamesrc/scripts/__init__.py deleted file mode 100644 index 40a96afc6..000000000 --- a/game/gamesrc/scripts/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/game/gamesrc/scripts/examples/__init__.py b/game/gamesrc/scripts/examples/__init__.py deleted file mode 100644 index 40a96afc6..000000000 --- a/game/gamesrc/scripts/examples/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/game/gamesrc/web/examples/__init__.py b/game/gamesrc/web/examples/__init__.py deleted file mode 100644 index cdcf9604a..000000000 --- a/game/gamesrc/web/examples/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__author__ = 'kelketek' diff --git a/game/gamesrc/web/static_overrides/README.md b/game/gamesrc/web/static_overrides/README.md deleted file mode 100644 index 0bc428b14..000000000 --- a/game/gamesrc/web/static_overrides/README.md +++ /dev/null @@ -1,7 +0,0 @@ -If you want to override one of the static files (such as a CSS or JS file) used by Evennia or a Django app installed in your Evennia project, copy it into this folder, and it will be placed in the static folder when you run: - - python manage.py collectstatic - -...or when you reload the server via the command line. - -Do note you may have to reproduce any preceeding directory structures for the file to end up in the right place. diff --git a/game/gamesrc/web/template_overrides/README.md b/game/gamesrc/web/template_overrides/README.md deleted file mode 100644 index 8fb5f1fdb..000000000 --- a/game/gamesrc/web/template_overrides/README.md +++ /dev/null @@ -1,4 +0,0 @@ -Place your own version of templates into this file to override the default ones. -For instance, if there's a template at: `src/web/templates/evennia_general/index.html` -and you want to replace it, create the file `game/gamesrc/web/template_overrides/evennia_general/index.html` -and it will be loaded instead. \ No newline at end of file diff --git a/game/gamesrc/world/__init__.py b/game/gamesrc/world/__init__.py deleted file mode 100644 index 40a96afc6..000000000 --- a/game/gamesrc/world/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/game/gamesrc/world/examples/prototypes.py b/game/gamesrc/world/examples/prototypes.py deleted file mode 100644 index 537616f10..000000000 --- a/game/gamesrc/world/examples/prototypes.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -Example prototypes read by the @spawn command but is also easily -available to use from code. Each prototype should be a dictionary. Use -the same name as the variable to refer to other prototypes. - -Possible keywords are: - prototype - string pointing to parent prototype of this structure - key - string, the main object identifier - typeclass - string, if not set, will use settings.BASE_OBJECT_TYPECLASS - location - this should be a valid object or #dbref - home - valid object or #dbref - destination - only valid for exits (object or dbref) - - permissions - string or list of permission strings - locks - a lock-string - aliases - string or list of strings - - ndb_ - value of a nattribute (ndb_ is stripped) - any other keywords are interpreted as Attributes and their values. - -See the @spawn command and src.utils.spawner for more info. - -""" - -from random import randint - -NOBODY = {} - -GOBLIN = { - "key": "goblin grunt", - "health": lambda: randint(20,30), - "resists": ["cold", "poison"], - "attacks": ["fists"], - "weaknesses": ["fire", "light"] - } - -GOBLIN_WIZARD = { - "prototype": "GOBLIN", - "key": "goblin wizard", - "spells": ["fire ball", "lighting bolt"] - } - -GOBLIN_ARCHER = { - "prototype": "GOBLIN", - "key": "goblin archer", - "attacks": ["short bow"] -} - -ARCHWIZARD = { - "attacks": ["archwizard staff"], -} - -GOBLIN_ARCHWIZARD = { - "key": "goblin archwizard", - "prototype" : ("GOBLIN_WIZARD", "ARCHWIZARD") -} diff --git a/game/logs/README b/game/logs/README deleted file mode 100644 index 0c62c5425..000000000 --- a/game/logs/README +++ /dev/null @@ -1,8 +0,0 @@ -This directory holds log files for the server. - -portal.log - logs from Portal (if run in daemon mode) -server.log - logs from Server (if run in daemon mode) -http_requests.log - http request info (if running webserver/client) - -All log files are rotated to their *.old version when the server is fully -stopped, then restarted again (i.e. not on normal reloads). diff --git a/game/manage.py b/game/manage.py deleted file mode 100755 index f4f69e2b0..000000000 --- a/game/manage.py +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env python -""" -Set up the evennia system. A first startup consists of giving -the command './manage syncdb' to setup the system and create -the database. -""" - -import sys -import os - -# Tack on the root evennia directory to the python path. -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -#------------------------------------------------------------ -# Get Evennia version -#------------------------------------------------------------ -try: - f = open(os.pardir + os.sep + 'VERSION.txt', 'r''') - VERSION = "%s" % f.read().strip() - f.close() -except IOError: - VERSION = "Unknown version" - -#------------------------------------------------------------ -# Check so session file exists in the current dir- if not, create it. -#------------------------------------------------------------ - -_CREATED_SETTINGS = False -if not os.path.exists('settings.py'): - # If settings.py doesn't already exist, create it and populate it with some - # basic stuff. - - # make random secret_key. - import random - import string - secret_key = list((string.letters + - string.digits + string.punctuation).replace("\\", "").replace("'", '"')) - random.shuffle(secret_key) - secret_key = "".join(secret_key[:40]) - - settings_file = open('settings.py', 'w') - _CREATED_SETTINGS = True - - string = \ - """ -###################################################################### -# Evennia MU* server configuration file -# -# You may customize your setup by copy&pasting the variables you want -# to change from the master config file src/settings_default.py to -# this file. Try to *only* copy over things you really need to customize -# and do *not* make any changes to src/settings_default.py directly. -# This way you'll always have a sane default to fall back on -# (also, the master config file may change with server updates). -# -###################################################################### - -from src.settings_default import * - -###################################################################### -# Custom settings -###################################################################### - - -###################################################################### -# SECRET_KEY was randomly seeded when settings.py was first created. -# Don't share this with anybody. It is used by Evennia to handle -# cryptographic hashing for things like cookies on the web side. -###################################################################### -SECRET_KEY = '%s' - -""" % secret_key - - settings_file.write(string) - settings_file.close() - - # obs - this string cannot be under i18n since settings didn't exist yet. - print """ - Welcome to Evennia! - - This looks like your first startup, so we created a fresh - game/settings.py file for you. No database has yet been created. - You may edit the settings file now if you like, but you don't - have to touch anything if you just want to quickly get started. - - Once you are ready to continue, run - - python manage.py migrate - - This will initialize the database. When that is done you can - start Evennia itself with - - python evennia.py -i start - - The first time the server starts it will set things up for you. - Make sure to create a superuser when asked. The superuser's - email-address does not have to exist. - """ - - -#------------------------------------------------------------ -# Test the import of the settings file -#------------------------------------------------------------ -try: - from game import settings -except Exception: - import traceback - string = "\n" + traceback.format_exc() - - # note - if this fails, ugettext will also fail, so we cannot translate this string. - - string += """\n - Error: Couldn't import the file 'settings.py' in the directory containing %(file)r. - There are usually two reasons for this: - 1) The settings module contains errors. Review the traceback above to resolve the - problem, then try again. - 2) If you get errors on finding DJANGO_SETTINGS_MODULE you might have set up django - wrong in some way. If you run a virtual machine, it might be worth to restart it - to see if this resolves the issue. Evennia should not require you to define any - environment variables manually. - """ % {'file': __file__} - print string - sys.exit(1) - -os.environ['DJANGO_SETTINGS_MODULE'] = 'game.settings' - -#------------------------------------------------------------ -# This is run only if the module is called as a program -#------------------------------------------------------------ -if __name__ == "__main__": - - if _CREATED_SETTINGS: - # if settings were created, info has already been printed. - sys.exit() - - # run the standard django manager, if dependencies match - from src.utils.utils import check_evennia_dependencies - if check_evennia_dependencies(): - if len(sys.argv) > 1 and sys.argv[1] in ('runserver', 'testserver'): - print """ - WARNING: There is no need to run the Django development - webserver to test out Evennia web features (the web client - will in fact not work since the Django test server knows - nothing about MUDs). Instead, just start Evennia with the - webserver component active (this is the default). - """ - from django.core.management import execute_from_command_line - execute_from_command_line(sys.argv) diff --git a/requirements.txt b/requirements.txt index a65b79783..c1e31a901 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ django >= 1.7 twisted >= 12.0 +mock >= 1.0.1 pillow diff --git a/setup.py b/setup.py new file mode 100644 index 000000000..b0057fff9 --- /dev/null +++ b/setup.py @@ -0,0 +1,77 @@ +import os +import sys +from setuptools import setup, find_packages + +os.chdir(os.path.dirname(os.path.realpath(__file__))) + +VERSION_PATH = os.path.join('evennia', 'VERSION.txt') + + +def get_requirements(): + """ + To update the requirements for Evennia, edit the requirements.txt file. + """ + req_lines = open('requirements.txt', 'r').readlines() + reqs = [] + for line in req_lines: + # Avoid adding comments. + line = line.split('#')[0].strip() + if line: + reqs.append(line) + return reqs + + +def get_scripts(): + """ + Determine which executable scripts should be added. For Windows, + this means creating a .bat file. + """ + if os.name == "nt": + batpath = os.path.join("bin", "windows", "evennia.bat") + scriptpath = os.path.join(sys.prefix, "Scripts", "evennia_launcher.py") + with open(batpath, "w") as batfile: + batfile.write("@\"%s\" \"%s\" %%*" % (sys.executable, scriptpath)) + return [batpath, os.path.join("bin", "windows", "evennia_launcher.py")] + else: + return [os.path.join("bin", "unix", "evennia")] + + +def get_version(): + """ + When updating the Evennia package for release, remember to increment the + version number in evennia/VERSION.txt + """ + return open(VERSION_PATH).read().strip() + + +def package_data(): + """ + By default, the distribution tools ignore all non-python files. + + Make sure we get everything. + """ + file_set = [] + for root, dirs, files in os.walk('evennia'): + for f in files: + if '.git' in f.split(os.path.normpath(os.path.join(root, f))): + # Prevent the repo from being added. + continue + file_name = os.path.relpath(os.path.join(root, f), 'evennia') + file_set.append(file_name) + return file_set + +# setup the package +setup( + name='evennia', + version=get_version(), + author = "Evennia community", + maintainer = "Griatch", + maintainer_email = "griatch AT gmail DOT com", + url = "http://www.evennia.com", + description='A full-featured MUD building toolkit.', + packages=find_packages(), + scripts=get_scripts(), + install_requires=get_requirements(), + package_data={'': package_data()}, + zip_safe=False +) diff --git a/src/commands/__init__.py b/src/commands/__init__.py deleted file mode 100644 index 40a96afc6..000000000 --- a/src/commands/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/src/commands/default/tests.py b/src/commands/default/tests.py deleted file mode 100644 index 05ef9b60c..000000000 --- a/src/commands/default/tests.py +++ /dev/null @@ -1,286 +0,0 @@ -# -*- 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 -src/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 django.utils.unittest import TestCase -from src.server.serversession import ServerSession -from src.objects.objects import Object, Character -from src.players.player import Player -from src.utils import create, ansi -from src.server.sessionhandler import SESSIONS - -from django.db.models.signals import post_save -from src.server.caches import field_post_save -post_save.connect(field_post_save, dispatch_uid="fieldcache") - -# set up signal here since we are not starting the server - -_RE = re.compile(r"^\+|-+\+|\+-+|--*|\|", re.MULTILINE) - -#------------------------------------------------------------ -# Command testing -# ------------------------------------------------------------ - - -def dummy(self, *args, **kwargs): - pass - -SESSIONS.data_out = dummy -SESSIONS.disconnect = dummy - - -class TestObjectClass(Object): - def msg(self, text="", **kwargs): - "test message" - pass - - -class TestCharacterClass(Character): - def msg(self, text="", **kwargs): - "test message" - if self.player: - self.player.msg(text=text, **kwargs) - else: - if not self.ndb.stored_msg: - self.ndb.stored_msg = [] - self.ndb.stored_msg.append(text) - - -class TestPlayerClass(Player): - def msg(self, text="", **kwargs): - "test message" - if not self.ndb.stored_msg: - self.ndb.stored_msg = [] - self.ndb.stored_msg.append(text) - - def _get_superuser(self): - "test with superuser flag" - return self.ndb.is_superuser - is_superuser = property(_get_superuser) - - -class CommandTest(TestCase): - """ - Tests a command - """ - CID = 0 # we must set a different CID in every test to avoid unique-name collisions creating the objects - def setUp(self): - "sets up testing environment" - #print "creating player %i: %s" % (self.CID, self.__class__.__name__) - self.player = create.create_player("TestPlayer%i" % self.CID, "test@test.com", "testpassword", typeclass=TestPlayerClass) - self.player2 = create.create_player("TestPlayer%ib" % self.CID, "test@test.com", "testpassword", typeclass=TestPlayerClass) - self.room1 = create.create_object("src.objects.objects.Room", key="Room%i"%self.CID, nohome=True) - self.room1.db.desc = "room_desc" - settings.DEFAULT_HOME = "#%i" % self.room1.id # we must have a default home - self.room2 = create.create_object("src.objects.objects.Room", key="Room%ib" % self.CID) - self.obj1 = create.create_object(TestObjectClass, key="Obj%i" % self.CID, location=self.room1, home=self.room1) - self.obj2 = create.create_object(TestObjectClass, key="Obj%ib" % self.CID, location=self.room1, home=self.room1) - self.char1 = create.create_object(TestCharacterClass, key="Char%i" % self.CID, location=self.room1, home=self.room1) - self.char1.permissions.add("Immortals") - self.char2 = create.create_object(TestCharacterClass, key="Char%ib" % self.CID, location=self.room1, home=self.room1) - self.char1.player = self.player - self.char2.player = self.player2 - self.script = create.create_script("src.scripts.scripts.Script", key="Script%i" % self.CID) - self.player.permissions.add("Immortals") - - # set up a fake session - - global SESSIONS - session = ServerSession() - session.init_session("telnet", ("localhost", "testmode"), SESSIONS) - session.sessid = self.CID - SESSIONS.portal_connect(session.get_sync_data()) - SESSIONS.login(SESSIONS.session_from_sessid(self.CID), self.player, testmode=True) - - - def call(self, cmdobj, args, msg=None, cmdset=None, noansi=True, caller=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 - """ - cmdobj.caller = caller if caller else self.char1 - #print "call:", cmdobj.key, cmdobj.caller, caller if caller else cmdobj.caller.player - #print "perms:", cmdobj.caller.permissions.all() - cmdobj.cmdstring = cmdobj.key - cmdobj.args = args - cmdobj.cmdset = cmdset - cmdobj.sessid = self.CID - cmdobj.session = SESSIONS.session_from_sessid(self.CID) - cmdobj.player = self.player - cmdobj.raw_string = cmdobj.key + " " + args - cmdobj.obj = caller if caller else self.char1 - # test - self.char1.player.ndb.stored_msg = [] - cmdobj.at_pre_cmd() - cmdobj.parse() - cmdobj.func() - cmdobj.at_post_cmd() - # clean out prettytable sugar - stored_msg = self.char1.player.ndb.stored_msg if self.char1.player else self.char1.ndb.stored_msg - returned_msg = "|".join(_RE.sub("", mess) for mess in stored_msg) - #returned_msg = "|".join(self.char1.player.ndb.stored_msg) - returned_msg = ansi.parse_ansi(returned_msg, strip_ansi=noansi).strip() - if msg != 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) - -#------------------------------------------------------------ -# Individual module Tests -#------------------------------------------------------------ - -from src.commands.default import general -class TestGeneral(CommandTest): - CID = 1 - - def test_cmds(self): - self.call(general.CmdLook(), "here", "Room1\n room_desc") - self.call(general.CmdHome(), "", "You are already home") - self.call(general.CmdInventory(), "", "You are not carrying anything.") - self.call(general.CmdPose(), "looks around", "Char1 looks around") - self.call(general.CmdHome(), "", "You are already home") - 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")) - self.call(general.CmdGet(), "Obj1", "You pick up Obj1.") - self.call(general.CmdDrop(), "Obj1", "You drop Obj1.") - self.call(general.CmdSay(), "Testing", "You say, \"Testing\"") - self.call(general.CmdAccess(), "", "Permission Hierarchy (climbing):") - - -from src.commands.default import help -from src.commands.default.cmdset_character import CharacterCmdSet -class TestHelp(CommandTest): - CID = 2 - def test_cmds(self): - self.call(help.CmdHelp(), "", "Command help entries", cmdset=CharacterCmdSet()) - 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()) - - -from src.commands.default import system -class TestSystem(CommandTest): - CID = 3 - def test_cmds(self): - # we are not testing CmdReload, CmdReset and CmdShutdown, CmdService or CmdTime - # since the server is not running during these tests. - self.call(system.CmdPy(), "1+2", ">>> 1+2|<<< 3") - self.call(system.CmdScripts(), "", "dbref ") - self.call(system.CmdObjects(), "", "Object subtype totals") - self.call(system.CmdAbout(), "", None) - self.call(system.CmdServerLoad(), "", "Server CPU and Memory load:") - - -from src.commands.default import admin -class TestAdmin(CommandTest): - CID = 4 - def test_cmds(self): - # not testing CmdBoot, CmdDelPlayer, CmdNewPassword - self.call(admin.CmdEmit(), "Char4b = Test", "Emitted to Char4b:\nTest") - self.call(admin.CmdPerm(), "Obj4 = Builders", "Permission 'Builders' given to Obj4 (the Object/Character).") - self.call(admin.CmdWall(), "Test", "Announcing to all connected players ...") - self.call(admin.CmdPerm(), "Char4b = Builders","Permission 'Builders' given to Char4b (the Object/Character).") - self.call(admin.CmdBan(), "Char4", "NameBan char4 was added.") - - -from src.commands.default import player -class TestPlayer(CommandTest): - CID = 5 - def test_cmds(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 TestPlayer5 (you are OutofCharacter)", caller=self.player) - self.call(player.CmdOOC(), "", "You are already", caller=self.player) - self.call(player.CmdIC(), "Char5","You become Char5.", caller=self.player) - self.call(player.CmdPassword(), "testpassword = testpassword", "Password changed.", caller=self.player) - self.call(player.CmdEncoding(), "", "Default encoding:", caller=self.player) - self.call(player.CmdWho(), "", "Players:", caller=self.player) - self.call(player.CmdQuit(), "", "Quitting. Hope to see you soon again.", caller=self.player) - self.call(player.CmdSessions(), "", "Your current session(s):", caller=self.player) - self.call(player.CmdColorTest(), "ansi", "ANSI colors:", caller=self.player) - self.call(player.CmdCharCreate(), "Test1=Test char","Created new character Test1. Use @ic Test1 to enter the game", caller=self.player) - self.call(player.CmdQuell(), "", "Quelling to current puppet's permissions (immortals).", caller=self.player) - - -from src.commands.default import building -class TestBuilding(CommandTest): - CID = 6 - def test_cmds(self): - self.call(building.CmdCreate(), "/drop TestObj1", "You create a new Object: TestObj1.") - self.call(building.CmdExamine(), "TestObj1", "Name/key: TestObj1") - self.call(building.CmdSetObjAlias(), "TestObj1 = TestObj1b","Alias(es) for 'TestObj1' set to testobj1b.") - self.call(building.CmdCopy(), "TestObj1 = TestObj2;TestObj2b, TestObj3;TestObj3b", "Copied TestObj1 to 'TestObj3' (aliases: ['TestObj3b']") - self.call(building.CmdSetAttribute(), "Obj6/test1=\"value1\"", "Created attribute Obj6/test1 = \"value1\"") - self.call(building.CmdSetAttribute(), "Obj6b/test2=\"value2\"", "Created attribute Obj6b/test2 = \"value2\"") - self.call(building.CmdMvAttr(), "Obj6b/test2 = Obj6/test3", "Moving Obj6b/test2 (with value value2) ...\nMoved Obj6b.test2") - self.call(building.CmdCpAttr(), "Obj6/test1 = Obj6b/test3", "Copying Obj6/test1 (with value value1) ...\nCopied Obj6.test1") - self.call(building.CmdName(), "Obj6b=Obj6c", "Object's name changed to 'Obj6c'.") - self.call(building.CmdDesc(), "Obj6c=TestDesc", "The description was set on Obj6c.") - self.call(building.CmdWipe(), "Obj6c/test2/test3", "Wiped attributes test2,test3 on Obj6c.") - self.call(building.CmdDestroy(), "TestObj1","TestObj1 was destroyed.") - self.call(building.CmdDig(), "TestRoom1=testroom;tr,back;b", "Created room TestRoom1") - self.call(building.CmdTunnel(), "n = TestRoom2;test2", "Created room TestRoom2") - self.call(building.CmdOpen(), "TestExit1=Room6b", "Created new Exit 'TestExit1' from Room6 to Room6b") - self.call(building.CmdLink(),"TestExit1 = TestRoom1","Link created TestExit1 > TestRoom1 (one way).") - self.call(building.CmdUnLink(), "TestExit1", "Former exit TestExit1 no longer links anywhere.") - self.call(building.CmdSetHome(), "Obj6 = Room6b", "Obj6's home location was changed from Room6") - self.call(building.CmdListCmdSets(), "", ":") - self.call(building.CmdTypeclass(), "Obj6 = src.objects.objects.Exit", - "Obj6 changed typeclass from src.commands.default.tests.TestObjectClass to src.objects.objects.Exit") - self.call(building.CmdLock(), "Obj6 = test:perm(Immortals)", "Added lock 'test:perm(Immortals)' to Obj6.") - self.call(building.CmdFind(), "TestRoom1", "One Match") - self.call(building.CmdScript(), "Obj6 = src.scripts.scripts.Script", "Script src.scripts.scripts.Script successfully added") - self.call(building.CmdTeleport(), "TestRoom1", "TestRoom1\nExits: back|Teleported to TestRoom1.") - - -from src.commands.default import comms -class TestComms(CommandTest): - CID = 7 - def test_cmds(self): - # not testing the irc/imc2/rss commands here since testing happens offline - self.call(comms.CmdChannelCreate(), "testchan;test=Test Channel", "Created channel testchan and connected to it.") - self.call(comms.CmdAddCom(), "tc = testchan", "You are already connected to channel testchan. You can now") - self.call(comms.CmdDelCom(), "tc", "Your alias 'tc' for channel testchan was cleared.") - self.call(comms.CmdChannels(), "" ,"Available channels (use comlist,addcom and delcom to manage") - self.call(comms.CmdAllCom(), "", "Available channels (use comlist,addcom and delcom to manage") - self.call(comms.CmdClock(), "testchan=send:all()", "Lock(s) applied. Current locks on testchan:") - self.call(comms.CmdCdesc(), "testchan = Test Channel", "Description of channel 'testchan' set to 'Test Channel'.") - self.call(comms.CmdCemit(), "testchan = Test Message", "[testchan] Test Message|Sent to channel testchan: Test Message") - self.call(comms.CmdCWho(), "testchan", "Channel subscriptions\ntestchan:\n TestPlayer7") - self.call(comms.CmdPage(), "TestPlayer7b = Test", "TestPlayer7b is offline. They will see your message if they list their pages later.|You paged TestPlayer7b with: 'Test'.") - self.call(comms.CmdCBoot(), "", "Usage: @cboot[/quiet] = [:reason]") # noone else connected to boot - self.call(comms.CmdCdestroy(), "testchan" ,"[testchan] TestPlayer7: testchan is being destroyed. Make sure to change your aliases.|Channel 'testchan' was destroyed.") - - -from src.commands.default import batchprocess -class TestBatchProcess(CommandTest): - CID = 8 - def test_cmds(self): - # cannot test batchcode here, it must run inside the server process - self.call(batchprocess.CmdBatchCommands(), "examples.batch_cmds", "Running Batchcommand processor Automatic mode for examples.batch_cmds") - #self.call(batchprocess.CmdBatchCode(), "examples.batch_code", "") diff --git a/src/comms/__init__.py b/src/comms/__init__.py deleted file mode 100644 index 721f85bbb..000000000 --- a/src/comms/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -Makes it easier to import by grouping all relevant things already at this -level. - -You can henceforth import most things directly from src.comms -Also, the initiated object manager is available as src.comms.msgmanager and -src.comms.channelmanager. - -""" - -from src.comms.models import * - -msgmanager = Msg.objects -channelmanager = ChannelDB.objects diff --git a/src/comms/migrations/__init__.py b/src/comms/migrations/__init__.py deleted file mode 100644 index 40a96afc6..000000000 --- a/src/comms/migrations/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/src/help/__init__.py b/src/help/__init__.py deleted file mode 100644 index e1f630d05..000000000 --- a/src/help/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -Makes it easier to import by grouping all relevant things already at this level. - -You can henceforth import most things directly from src.help -Also, the initiated object manager is available as src.help.manager. - -""" - -from src.help.models import * - -manager = HelpEntry.objects diff --git a/src/help/migrations/__init__.py b/src/help/migrations/__init__.py deleted file mode 100644 index 40a96afc6..000000000 --- a/src/help/migrations/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/src/help/models.py b/src/help/models.py deleted file mode 100644 index d81f7a284..000000000 --- a/src/help/models.py +++ /dev/null @@ -1,198 +0,0 @@ -""" -Models for the help system. - -The database-tied help system is only half of Evennia's help -functionality, the other one being the auto-generated command help -that is created on the fly from each command's __doc__ string. The -persistent database system defined here is intended for all other -forms of help that do not concern commands, like information about the -game world, policy info, rules and similar. - -""" -from django.db import models -from src.utils.idmapper.models import SharedMemoryModel -from src.help.manager import HelpEntryManager -from src.typeclasses.models import Tag, TagHandler -from src.locks.lockhandler import LockHandler -from src.utils.utils import lazy_property -__all__ = ("HelpEntry",) - - -#------------------------------------------------------------ -# -# HelpEntry -# -#------------------------------------------------------------ - -class HelpEntry(SharedMemoryModel): - """ - A generic help entry. - - An HelpEntry object has the following properties defined: - key - main name of entry - help_category - which category entry belongs to (defaults to General) - entrytext - the actual help text - permissions - perm strings - - Method: - access - - """ - - # - # HelpEntry Database Model setup - # - # - # These database fields are all set using their corresponding properties, - # named same as the field, but withtout the db_* prefix. - - # title of the help entry - db_key = models.CharField('help key', max_length=255, unique=True, help_text='key to search for') - # help category - db_help_category = models.CharField("help category", max_length=255, default="General", - help_text='organizes help entries in lists') - # the actual help entry text, in any formatting. - db_entrytext = models.TextField('help entry', blank=True, help_text='the main body of help text') - # lock string storage - db_lock_storage = models.TextField('locks', blank=True, help_text='normally view:all().') - # tags are primarily used for permissions - db_tags = models.ManyToManyField(Tag, null=True, - help_text='tags on this object. Tags are simple string markers to identify, group and alias objects.') - # (deprecated, only here to allow MUX helpfile load (don't use otherwise)). - # TODO: remove this when not needed anymore. - db_staff_only = models.BooleanField(default=False) - - # Database manager - objects = HelpEntryManager() - _is_deleted = False - - # lazy-loaded handlers - - @lazy_property - def locks(self): - return LockHandler(self) - - @lazy_property - def tags(self): - return TagHandler(self) - - - class Meta: - "Define Django meta options" - verbose_name = "Help Entry" - verbose_name_plural = "Help Entries" - - # Wrapper properties to easily set database fields. These are - # @property decorators that allows to access these fields using - # normal python operations (without having to remember to save() - # etc). So e.g. a property 'attr' has a get/set/del decorator - # defined that allows the user to do self.attr = value, - # value = self.attr and del self.attr respectively (where self - # is the object in question). - - # key property (wraps db_key) - #@property - #def __key_get(self): - # "Getter. Allows for value = self.key" - # return self.db_key - ##@key.setter - #def __key_set(self, value): - # "Setter. Allows for self.key = value" - # self.db_key = value - # self.save() - ##@key.deleter - #def __key_del(self): - # "Deleter. Allows for del self.key. Deletes entry." - # self.delete() - #key = property(__key_get, __key_set, __key_del) - - ## help_category property (wraps db_help_category) - ##@property - #def __help_category_get(self): - # "Getter. Allows for value = self.help_category" - # return self.db_help_category - ##@help_category.setter - #def __help_category_set(self, value): - # "Setter. Allows for self.help_category = value" - # self.db_help_category = value - # self.save() - ##@help_category.deleter - #def __help_category_del(self): - # "Deleter. Allows for del self.help_category" - # self.db_help_category = "General" - # self.save() - #help_category = property(__help_category_get, __help_category_set, __help_category_del) - - ## entrytext property (wraps db_entrytext) - ##@property - #def __entrytext_get(self): - # "Getter. Allows for value = self.entrytext" - # return self.db_entrytext - ##@entrytext.setter - #def __entrytext_set(self, value): - # "Setter. Allows for self.entrytext = value" - # self.db_entrytext = value - # self.save() - ##@entrytext.deleter - #def __entrytext_del(self): - # "Deleter. Allows for del self.entrytext" - # self.db_entrytext = "" - # self.save() - #entrytext = property(__entrytext_get, __entrytext_set, __entrytext_del) - - ## permissions property - ##@property - #def __permissions_get(self): - # "Getter. Allows for value = self.permissions. Returns a list of permissions." - # return [perm.strip() for perm in self.db_permissions.split(',')] - ##@permissions.setter - #def __permissions_set(self, value): - # "Setter. Allows for self.permissions = value. Stores as a comma-separated string." - # if is_iter(value): - # value = ",".join([str(val).strip().lower() for val in value]) - # self.db_permissions = value - # self.save() - ##@permissions.deleter - #def __permissions_del(self): - # "Deleter. Allows for del self.permissions" - # self.db_permissions = "" - # self.save() - #permissions = property(__permissions_get, __permissions_set, __permissions_del) - - # lock_storage property (wraps db_lock_storage) - ##@property - #def __lock_storage_get(self): - # "Getter. Allows for value = self.lock_storage" - # return self.db_lock_storage - ##@nick.setter - #def __lock_storage_set(self, value): - # """Saves the lock_storagetodate. This is usually not called directly, but through self.lock()""" - # self.db_lock_storage = value - # self.save() - ##@nick.deleter - #def __lock_storage_del(self): - # "Deleter is disabled. Use the lockhandler.delete (self.lock.delete) instead""" - # logger.log_errmsg("Lock_Storage (on %s) cannot be deleted. Use obj.lock.delete() instead." % self) - #lock_storage = property(__lock_storage_get, __lock_storage_set, __lock_storage_del) - - - # - # - # HelpEntry main class methods - # - # - - def __str__(self): - return self.key - - def __unicode__(self): - return u'%s' % self.key - - def access(self, accessing_obj, access_type='read', 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) diff --git a/src/locks/__init__.py b/src/locks/__init__.py deleted file mode 100644 index 40a96afc6..000000000 --- a/src/locks/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/src/objects/__init__.py b/src/objects/__init__.py deleted file mode 100644 index 4931eaa39..000000000 --- a/src/objects/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -""" -Makes it easier to import by grouping all relevant things already at this level. - -You can henceforth import most things directly from src.objects -Also, the initiated object manager is available as src.objects.manager. - -""" - -from src.objects.objects import * -from src.objects.models import ObjectDB - -manager = ObjectDB.objects diff --git a/src/objects/models.py b/src/objects/models.py deleted file mode 100644 index b5e1302b7..000000000 --- a/src/objects/models.py +++ /dev/null @@ -1,850 +0,0 @@ -""" -This module defines the database models for all in-game objects, that -is, all objects that has an actual existence in-game. - -Each database object is 'decorated' with a 'typeclass', a normal -python class that implements all the various logics needed by the game -in question. Objects created of this class transparently communicate -with its related database object for storing all attributes. The -admin should usually not have to deal directly with this database -object layer. - -Attributes are separate objects that store values persistently onto -the database object. Like everything else, they can be accessed -transparently through the decorating TypeClass. -""" - -import traceback -from django.db import models -from django.conf import settings -from django.core.exceptions import ObjectDoesNotExist - -from src.typeclasses.models import TypedObject, NickHandler -from src.objects.manager import ObjectManager -from src.players.models import PlayerDB -from src.commands.cmdsethandler import CmdSetHandler -from src.commands import cmdhandler -from src.scripts.scripthandler import ScriptHandler -from src.utils import logger -from src.utils.utils import (make_iter, to_str, to_unicode, lazy_property, - variable_from_module, dbref) - -MULTISESSION_MODE = settings.MULTISESSION_MODE -from django.utils.translation import ugettext as _ - -#__all__ = ("ObjectDB", ) - -_ScriptDB = None -_AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1)) -_SESSIONS = None - -_GA = object.__getattribute__ -_SA = object.__setattr__ -_DA = object.__delattr__ - -# the sessid_max is based on the length of the db_sessid csv field (excluding commas) -_SESSID_MAX = 16 if MULTISESSION_MODE in (1, 3) else 1 - -class SessidHandler(object): - """ - Handles the get/setting of the sessid - comma-separated integer field - """ - def __init__(self, obj): - self.obj = obj - self._cache = set() - self._recache() - - def _recache(self): - self._cache = list(set(int(val) for val in (_GA(self.obj, "db_sessid") or "").split(",") if val)) - - def get(self): - "Returns a list of one or more session ids" - return self._cache - - def add(self, sessid): - "Add sessid to handler" - _cache = self._cache - if sessid not in _cache: - if len(_cache) >= _SESSID_MAX: - return - _cache.append(sessid) - _SA(self.obj, "db_sessid", ",".join(str(val) for val in _cache)) - _GA(self.obj, "save")(update_fields=["db_sessid"]) - - def remove(self, sessid): - "Remove sessid from handler" - _cache = self._cache - if sessid in _cache: - _cache.remove(sessid) - _SA(self.obj, "db_sessid", ",".join(str(val) for val in _cache)) - _GA(self.obj, "save")(update_fields=["db_sessid"]) - - def clear(self): - "Clear sessids" - self._cache = [] - _SA(self.obj, "db_sessid", None) - _GA(self.obj, "save")(update_fields=["db_sessid"]) - - def count(self): - "Return amount of sessions connected" - return len(self._cache) - - -#------------------------------------------------------------ -# -# ObjectDB -# -#------------------------------------------------------------ - -class ObjectDB(TypedObject): - """ - All objects in the game use the ObjectDB model to store - data in the database. This is handled transparently through - the typeclass system. - - Note that the base objectdb is very simple, with - few defined fields. Use attributes to extend your - type class with new database-stored variables. - - The TypedObject supplies the following (inherited) properties: - key - main name - name - alias for key - typeclass_path - the path to the decorating typeclass - typeclass - auto-linked typeclass - date_created - time stamp of object creation - permissions - perm strings - locks - lock definitions (handler) - dbref - #id of object - db - persistent attribute storage - ndb - non-persistent attribute storage - - The ObjectDB adds the following properties: - player - optional connected player (always together with sessid) - sessid - optional connection session id (always together with player) - location - in-game location of object - home - safety location for object (handler) - - scripts - scripts assigned to object (handler from typeclass) - cmdset - active cmdset on object (handler from typeclass) - aliases - aliases for this object (property) - nicks - nicknames for *other* things in Evennia (handler) - sessions - sessions connected to this object (see also player) - has_player - bool if an active player is currently connected - contents - other objects having this object as location - exits - exits from this object - """ - - # - # ObjectDB Database model setup - # - # - # inherited fields (from TypedObject): - # db_key (also 'name' works), db_typeclass_path, db_date_created, - # db_permissions - # - # These databse fields (including the inherited ones) should normally be - # managed by their corresponding wrapper properties, named same as the - # field, but without the db_* prefix (e.g. the db_key field is set with - # self.key instead). The wrappers are created at the metaclass level and - # will automatically save and cache the data more efficiently. - - # If this is a character object, the player is connected here. - db_player = models.ForeignKey("players.PlayerDB", null=True, verbose_name='player', on_delete=models.SET_NULL, - help_text='a Player connected to this object, if any.') - # the session id associated with this player, if any - db_sessid = models.CommaSeparatedIntegerField(null=True, max_length=32, verbose_name="session id", - help_text="csv list of session ids of connected Player, if any.") - # The location in the game world. Since this one is likely - # to change often, we set this with the 'location' property - # to transparently handle Typeclassing. - db_location = models.ForeignKey('self', related_name="locations_set", db_index=True, on_delete=models.SET_NULL, - blank=True, null=True, verbose_name='game location') - # a safety location, this usually don't change much. - db_home = models.ForeignKey('self', related_name="homes_set", on_delete=models.SET_NULL, - blank=True, null=True, verbose_name='home location') - # destination of this object - primarily used by exits. - db_destination = models.ForeignKey('self', related_name="destinations_set", db_index=True, on_delete=models.SET_NULL, - blank=True, null=True, verbose_name='destination', - help_text='a destination, used only by exit objects.') - # database storage of persistant cmdsets. - db_cmdset_storage = models.CharField('cmdset', max_length=255, null=True, blank=True, - help_text="optional python path to a cmdset class.") - - # Database manager - objects = ObjectManager() - - # caches for quick lookups of typeclass loading. - _typeclass_paths = settings.OBJECT_TYPECLASS_PATHS - _default_typeclass_path = settings.BASE_OBJECT_TYPECLASS or "src.objects.objects.Object" - - # lazy-load handlers - @lazy_property - def cmdset(self): - return CmdSetHandler(self, True) - - @lazy_property - def scripts(self): - return ScriptHandler(self) - - @lazy_property - def nicks(self): - return NickHandler(self) - - @lazy_property - def sessid(self): - return SessidHandler(self) - - def _at_db_player_postsave(self): - """ - This hook is called automatically after the player field is saved. - """ - # we need to re-cache this for superusers to bypass. - self.locks.cache_lock_bypass(self) - - # cmdset_storage property. We use a custom wrapper to manage this. This also - # seems very sensitive to caching, so leaving it be for now. /Griatch - #@property - def __cmdset_storage_get(self): - """ - Getter. Allows for value = self.name. - Returns a list of cmdset_storage. - """ - storage = _GA(self, "db_cmdset_storage") - # we need to check so storage is not None - return [path.strip() for path in storage.split(',')] if storage else [] - #@cmdset_storage.setter - def __cmdset_storage_set(self, value): - """ - Setter. Allows for self.name = value. - Stores as a comma-separated string. - """ - _SA(self, "db_cmdset_storage", ",".join(str(val).strip() for val in make_iter(value))) - _GA(self, "save")() - #@cmdset_storage.deleter - def __cmdset_storage_del(self): - "Deleter. Allows for del self.name" - _SA(self, "db_cmdset_storage", None) - _GA(self, "save")() - cmdset_storage = property(__cmdset_storage_get, __cmdset_storage_set, __cmdset_storage_del) - - # location getsetter - def __location_get(self): - "Get location" - loc = _GA(_GA(self, "dbobj"), "db_location") - return _GA(loc, "typeclass") if loc else loc - - def __location_set(self, location): - "Set location, checking for loops and allowing dbref" - if isinstance(location, (basestring, int)): - # allow setting of #dbref - dbid = dbref(location, reqhash=False) - if dbid: - try: - location = ObjectDB.objects.get(id=dbid) - except ObjectDoesNotExist: - # maybe it is just a name that happens to look like a dbid - pass - try: - def is_loc_loop(loc, depth=0): - "Recursively traverse target location, trying to catch a loop." - if depth > 10: - return - elif loc == self: - raise RuntimeError - elif loc == None: - raise RuntimeWarning - return is_loc_loop(_GA(_GA(loc, "dbobj"), "db_location"), depth + 1) - try: - is_loc_loop(location) - except RuntimeWarning: - pass - # actually set the field - _SA(_GA(self, "dbobj"), "db_location", _GA(location, "dbobj") if location else location) - _GA(_GA(self, "dbobj"), "save")(update_fields=["db_location"]) - except RuntimeError: - errmsg = "Error: %s.location = %s creates a location loop." % (self.key, location) - logger.log_errmsg(errmsg) - raise RuntimeError(errmsg) - except Exception, e: - errmsg = "Error (%s): %s is not a valid location." % (str(e), location) - logger.log_errmsg(errmsg) - raise Exception(errmsg) - - def __location_del(self): - "Cleanly delete the location reference" - _SA(_GA(self, "dbobj"), "db_location", None) - _GA(_GA(self, "dbobj"), "save")(upate_fields=["db_location"]) - location = property(__location_get, __location_set, __location_del) - - class Meta: - "Define Django meta options" - verbose_name = "Object" - verbose_name_plural = "Objects" - - # - # ObjectDB class access methods/properties - # - - #@property - def __sessions_get(self): - """ - Retrieve sessions connected to this object. - """ - # if the player is not connected, this will simply be an empty list. - if _GA(self, "db_player"): - return _GA(_GA(self, "db_player"), "get_all_sessions")() - return [] - sessions = property(__sessions_get) - - #@property - def __has_player_get(self): - """ - Convenience function for checking if an active player is - currently connected to this object - """ - return any(_GA(self, "sessions")) - has_player = property(__has_player_get) - is_player = property(__has_player_get) - - #@property - def __is_superuser_get(self): - "Check if user has a player, and if so, if it is a superuser." - return (_GA(self, "db_player") and _GA(_GA(self, "db_player"), "is_superuser") - and not _GA(_GA(self, "db_player"), "attributes").get("_quell")) - is_superuser = property(__is_superuser_get) - - # contents - - def contents_get(self, exclude=None): - """ - Returns the contents of this object, i.e. all - objects that has this object set as its location. - This should be publically available. - - exclude is one or more objects to not return - """ - if exclude: - exclude = [obj.dbobj for obj in make_iter(exclude)] - return ObjectDB.objects.get_contents(self, excludeobj=exclude) - return ObjectDB.objects.get_contents(self) - contents = property(contents_get) - - #@property - def __exits_get(self): - """ - Returns all exits from this object, i.e. all objects - at this location having the property destination != None. - """ - return [exi for exi in _GA(self, "contents") - if exi.destination] - exits = property(__exits_get) - - # - # Main Search method - # - - def search(self, searchdata, - global_search=False, - use_nicks=True, # should this default to off? - typeclass=None, - location=None, - attribute_name=None, - quiet=False, - exact=False): - """ - Returns the typeclass of an Object matching a search string/condition - - Perform a standard object search in the database, handling - multiple results and lack thereof gracefully. By default, only - objects in self's current location or inventory is searched. - Note: to find Players, use eg. ev.player_search. - - Inputs: - - searchdata (str or obj): Primary search criterion. Will be matched - against object.key (with object.aliases second) unless - the keyword attribute_name specifies otherwise. - Special strings: - # - search by unique dbref. This is always - a global search. - me,self - self-reference to this object - - - can be used to differentiate - between multiple same-named matches - global_search (bool): Search all objects globally. This is overruled - by "location" keyword. - use_nicks (bool): Use nickname-replace (nicktype "object") on the - search string - typeclass (str or Typeclass, or list of either): Limit search only - to Objects with this typeclass. May be a list of typeclasses - for a broader search. - location (Object): Specify a location to search, if different from the - self's given location plus its contents. This can also - be a list of locations. - attribute_name (str): Define which property to search. If set, no - key+alias search will be performed. This can be used to - search database fields (db_ will be automatically - appended), and if that fails, it will try to return - objects having Attributes with this name and value - equal to searchdata. A special use is to search for - "key" here if you want to do a key-search without - including aliases. - quiet (bool) - don't display default error messages - this tells the - search method that the user wants to handle all errors - themselves. It also changes the return value type, see - below. - exact (bool) - if unset (default) - prefers to match to beginning of - string rather than not matching at all. If set, requires - exact mathing of entire string. - - Returns: - quiet=False (default): - no match or multimatch: - auto-echoes errors to self.msg, then returns None - (results are handled by settings.SEARCH_AT_RESULT - and settings.SEARCH_AT_MULTIMATCH_INPUT) - match: - a unique object match - quiet=True: - returns a list of 0, 1 or more matches - - """ - is_string = isinstance(searchdata, basestring) - - if use_nicks: - # do nick-replacement on search - searchdata = self.nicks.nickreplace(searchdata, categories=("object", "player"), include_player=True) - - candidates=None - if(global_search or (is_string and searchdata.startswith("#") and - len(searchdata) > 1 and searchdata[1:].isdigit())): - # only allow exact matching if searching the entire database - # or unique #dbrefs - exact = True - elif location: - # location(s) were given - candidates = [] - for obj in make_iter(location): - candidates.extend([o.dbobj for o in obj.contents]) - else: - # local search. Candidates are self.contents, self.location - # and self.location.contents - location = self.location - candidates = self.contents - if location: - candidates = candidates + [location] + location.contents - else: - # normally we are included in location.contents - candidates.append(self) - # db manager expects database objects - candidates = [obj.dbobj for obj in candidates] - - results = ObjectDB.objects.object_search(searchdata, - attribute_name=attribute_name, - typeclass=typeclass, - candidates=candidates, - exact=exact) - if quiet: - return results - return _AT_SEARCH_RESULT(self, searchdata, results, global_search) - - def search_player(self, searchdata, quiet=False): - """ - Simple shortcut wrapper to search for players, not characters. - - searchdata - search criterion - the key or dbref of the player - to search for. If this is "here" or "me", search - for the player connected to this object. - quiet - return the results as a list rather than echo eventual - standard error messages. - - Returns: - quiet=False (default): - no match or multimatch: - auto-echoes errors to self.msg, then returns None - (results are handled by settings.SEARCH_AT_RESULT - and settings.SEARCH_AT_MULTIMATCH_INPUT) - match: - a unique player match - quiet=True: - no match or multimatch: - returns None or list of multi-matches - match: - a unique object match - """ - results = PlayerDB.objects.player_search(searchdata) - if quiet: - return results - return _AT_SEARCH_RESULT(self, searchdata, results, global_search=True) - - # - # Execution/action methods - # - - def execute_cmd(self, raw_string, sessid=None, **kwargs): - """ - Do something as this object. This method is a copy of the execute_ - cmd method on the session. This is never called normally, it's only - used when wanting specifically to let an object be the caller of a - command. It makes use of nicks of eventual connected players as well. - - Argument: - raw_string (string) - raw command input - sessid (int) - optional session id to return results to - **kwargs - other keyword arguments will be added to the found command - object instace as variables before it executes. This is - unused by default Evennia but may be used to set flags and - change operating paramaters for commands at run-time. - - Returns Deferred - this is an asynchronous Twisted object that will - not fire until the command has actually finished executing. To - overload this one needs to attach callback functions to it, with - addCallback(function). This function will be called with an - eventual return value from the command execution. - - This return is not used at all by Evennia by default, but might - be useful for coders intending to implement some sort of nested - command structure. - """ - # nick replacement - we require full-word matching. - - # do text encoding conversion - raw_string = to_unicode(raw_string) - raw_string = self.nicks.nickreplace(raw_string, - categories=("inputline", "channel"), include_player=True) - return cmdhandler.cmdhandler(_GA(self, "typeclass"), raw_string, callertype="object", sessid=sessid, **kwargs) - - def msg(self, text=None, from_obj=None, sessid=0, **kwargs): - """ - Emits something to a session attached to the object. - - message (str): The message to send - from_obj (obj): object that is sending. - data (object): an optional data object that may or may not - be used by the protocol. - sessid (int): sessid to relay to, if any. - If set to 0 (default), use either from_obj.sessid (if set) or self.sessid automatically - If None, echo to all connected sessions - - When this message is called, from_obj.at_msg_send and self.at_msg_receive are called. - - """ - global _SESSIONS - if not _SESSIONS: - from src.server.sessionhandler import SESSIONS as _SESSIONS - - text = to_str(text, force_string=True) if text else "" - - if "data" in kwargs: - # deprecation warning - logger.log_depmsg("ObjectDB.msg(): 'data'-dict keyword is deprecated. Use **kwargs instead.") - data = kwargs.pop("data") - if isinstance(data, dict): - kwargs.update(data) - - if from_obj: - # call hook - try: - _GA(from_obj, "at_msg_send")(text=text, to_obj=_GA(self, "typeclass"), **kwargs) - except Exception: - logger.log_trace() - try: - if not _GA(_GA(self, "typeclass"), "at_msg_receive")(text=text, **kwargs): - # if at_msg_receive returns false, we abort message to this object - return - except Exception: - logger.log_trace() - - sessions = _SESSIONS.session_from_sessid([sessid] if sessid else make_iter(_GA(self, "sessid").get())) - for session in sessions: - session.msg(text=text, **kwargs) - - def msg_contents(self, message, exclude=None, from_obj=None, **kwargs): - """ - Emits something to all objects inside an object. - - exclude is a list of objects not to send to. See self.msg() for - more info. - """ - contents = _GA(self, "contents") - if exclude: - exclude = make_iter(exclude) - contents = [obj for obj in contents if obj not in exclude] - for obj in contents: - obj.msg(message, from_obj=from_obj, **kwargs) - - def move_to(self, destination, quiet=False, - emit_to_obj=None, use_destination=True, to_none=False): - """ - Moves this object to a new location. - - Moves this object to a new location. Note that if is an - exit object (i.e. it has "destination"!=None), the move_to will - happen to this destination and -not- into the exit object itself, unless - use_destination=False. Note that no lock checks are done by this - function, such things are assumed to have been handled before calling - move_to. - - destination: (Object) Reference to the object to move to. This - can also be an exit object, in which case the destination - property is used as destination. - quiet: (bool) If true, don't emit left/arrived messages. - emit_to_obj: (Object) object to receive error messages - use_destination (bool): Default is for objects to use the "destination" - property of destinations as the target to move to. - Turning off this keyword allows objects to move - "inside" exit objects. - to_none - allow destination to be None. Note that no hooks are run when - moving to a None location. If you want to run hooks, - run them manually (and make sure they can manage None - locations). - - Returns True/False depending on if there were problems with the move. - This method may also return various error messages to the - emit_to_obj. - """ - def logerr(string=""): - trc = traceback.format_exc() - errstring = "%s%s" % (trc, string) - logger.log_trace() - _GA(self, "msg")(errstring) - - errtxt = _("Couldn't perform move ('%s'). Contact an admin.") - if not emit_to_obj: - emit_to_obj = self - - if not destination: - if to_none: - # immediately move to None. There can be no hooks called since - # there is no destination to call them with. - self.location = None - return True - emit_to_obj.msg(_("The destination doesn't exist.")) - return - if destination.destination and use_destination: - # traverse exits - destination = destination.destination - - # Before the move, call eventual pre-commands. - try: - if not self.at_before_move(_GA(destination, "typeclass")): - return - except Exception: - logerr(errtxt % "at_before_move()") - #emit_to_obj.msg(errtxt % "at_before_move()") - #logger.log_trace() - return False - - # Save the old location - source_location = _GA(self, "location") - if not source_location: - # there was some error in placing this room. - # we have to set one or we won't be able to continue - if _GA(self, "home"): - source_location = _GA(self, "home") - else: - default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME) - source_location = default_home - - # Call hook on source location - try: - source_location.at_object_leave(_GA(self, "typeclass"), _GA(destination, "typeclass")) - except Exception: - logerr(errtxt % "at_object_leave()") - #emit_to_obj.msg(errtxt % "at_object_leave()") - #logger.log_trace() - return False - - if not quiet: - #tell the old room we are leaving - try: - self.announce_move_from(_GA(destination, "typeclass")) - except Exception: - logerr(errtxt % "at_announce_move()") - #emit_to_obj.msg(errtxt % "at_announce_move()" ) - #logger.log_trace() - return False - - # Perform move - try: - #print "move_to location:", destination - _SA(self, "location", destination) - except Exception: - emit_to_obj.msg(errtxt % "location change") - logger.log_trace() - return False - - if not quiet: - # Tell the new room we are there. - try: - self.announce_move_to(_GA(source_location, "typeclass")) - except Exception: - logerr(errtxt % "announce_move_to()") - #emit_to_obj.msg(errtxt % "announce_move_to()") - #logger.log_trace() - return False - - # Perform eventual extra commands on the receiving location - # (the object has already arrived at this point) - try: - destination.at_object_receive(_GA(self, "typeclass"), _GA(source_location, "typeclass")) - except Exception: - logerr(errtxt % "at_object_receive()") - #emit_to_obj.msg(errtxt % "at_object_receive()") - #logger.log_trace() - return False - - # Execute eventual extra commands on this object after moving it - # (usually calling 'look') - try: - self.at_after_move(_GA(source_location, "typeclass")) - except Exception: - logerr(errtxt % "at_after_move") - #emit_to_obj.msg(errtxt % "at_after_move()") - #logger.log_trace() - return False - return True - - # - # Object Swap, Delete and Cleanup methods - # - - def clear_exits(self): - """ - Destroys all of the exits and any exits pointing to this - object as a destination. - """ - for out_exit in [exi for exi in ObjectDB.objects.get_contents(self) if exi.db_destination]: - out_exit.delete() - for in_exit in ObjectDB.objects.filter(db_destination=self): - in_exit.delete() - - def clear_contents(self): - """ - Moves all objects (players/things) to their home - location or to default home. - """ - # Gather up everything that thinks this is its location. - objs = ObjectDB.objects.filter(db_location=self) - default_home_id = int(settings.DEFAULT_HOME.lstrip("#")) - try: - default_home = ObjectDB.objects.get(id=default_home_id) - if default_home.dbid == _GA(self, "dbid"): - # we are deleting default home! - default_home = None - except Exception: - string = _("Could not find default home '(#%d)'.") - logger.log_errmsg(string % default_home_id) - default_home = None - - for obj in objs: - home = obj.home - # Obviously, we can't send it back to here. - if not home or (home and home.dbid == _GA(self, "dbid")): - obj.home = default_home - home = default_home - - # If for some reason it's still None... - if not home: - string = "Missing default home, '%s(#%d)' " - string += "now has a null location." - obj.location = None - obj.msg(_("Something went wrong! You are dumped into nowhere. Contact an admin.")) - logger.log_errmsg(string % (obj.name, obj.dbid)) - return - - if obj.has_player: - if home: - string = "Your current location has ceased to exist," - string += " moving you to %s(#%d)." - obj.msg(_(string) % (home.name, home.dbid)) - else: - # Famous last words: The player should never see this. - string = "This place should not exist ... contact an admin." - obj.msg(_(string)) - obj.move_to(home) - - def copy(self, new_key=None): - """ - Makes an identical copy of this object. If you want to customize the - copy by changing some settings, use ObjectDB.object.copy_object() - directly. - - new_key (string) - new key/name of copied object. If new_key is not - specified, the copy will be named _copy - by default. - Returns: Object (copy of this one) - """ - def find_clone_key(): - """ - Append 01, 02 etc to obj.key. Checks next higher number in the - same location, then adds the next number available - - returns the new clone name on the form keyXX - """ - key = _GA(self, "key") - num = 1 - for obj in (obj for obj in self.location.contents - if obj.key.startswith(key) and - obj.key.lstrip(key).isdigit()): - num += 1 - return "%s%03i" % (key, num) - new_key = new_key or find_clone_key() - return ObjectDB.objects.copy_object(self, new_key=new_key) - - delete_iter = 0 - def delete(self): - """ - Deletes this object. - Before deletion, this method makes sure to move all contained - objects to their respective home locations, as well as clean - up all exits to/from the object. - """ - global _ScriptDB - if not _ScriptDB: - from src.scripts.models import ScriptDB as _ScriptDB - - if _GA(self, "delete_iter") > 0: - # make sure to only call delete once on this object - # (avoid recursive loops) - return False - - if not self.at_object_delete(): - # this is an extra pre-check - # run before deletion mechanism - # is kicked into gear. - _SA(self, "delete_iter", 0) - return False - - self.delete_iter += 1 - - # See if we need to kick the player off. - - for session in _GA(self, "sessions"): - session.msg(_("Your character %s has been destroyed.") % _GA(self, "key")) - # no need to disconnect, Player just jumps to OOC mode. - # sever the connection (important!) - if _GA(self, 'player'): - _SA(_GA(self, "player"), "character", None) - _SA(self, "player", None) - - for script in _ScriptDB.objects.get_all_scripts_on_obj(self): - script.stop() - #for script in _GA(self, "scripts").all(): - # script.stop() - - # if self.player: - # self.player.user.is_active = False - # self.player.user.save( - - # Destroy any exits to and from this room, if any - _GA(self, "clear_exits")() - # Clear out any non-exit objects located within the object - _GA(self, "clear_contents")() - _GA(self, "attributes").clear() - _GA(self, "nicks").clear() - _GA(self, "aliases").clear() - - # Perform the deletion of the object - super(ObjectDB, self).delete() - return True diff --git a/src/objects/objects.py b/src/objects/objects.py deleted file mode 100644 index 56c50a3a9..000000000 --- a/src/objects/objects.py +++ /dev/null @@ -1,1138 +0,0 @@ -""" -This is the basis of the typeclass system. - -The idea is have the object as a normal class with the -database-connection tied to itself through a property. - -The instances of all the different object types are all tied to their -own database object stored in the 'dbobj' property. All attribute -get/set operations are channeled transparently to the database object -as desired. You should normally never have to worry about the database -abstraction, just do everything on the TypeClass object. - -That an object is controlled by a player/user is just defined by its -'user' property being set. This means a user may switch which object -they control by simply linking to a new object's user property. -""" - -from django.conf import settings -from src.typeclasses.typeclass import TypeClass -from src.commands import cmdset, command -from src.utils.logger import log_depmsg - -__all__ = ("Object", "Character", "Room", "Exit") - -_GA = object.__getattribute__ -_SA = object.__setattr__ -_DA = object.__delattr__ - - -# -# Base class to inherit from. -# - -class Object(TypeClass): - """ - This is the base class for all in-game objects. Inherit from this - to create different types of objects in the game. - """ - # __init__ is only defined here in order to present docstring to API. - def __init__(self, dbobj): - """ - This is the root typeclass object, representing all entities - that have an actual presence in-game. Objects generally have a - location. They can also be manipulated and looked at. Most - game entities you define should inherit from Object at some distance. - Evennia defines some important subclasses of Object by default, namely - Characters, Exits and Rooms (see the bottom of this module). - - Note that all new Objects and their subclasses *must* always be - created using the ev.create_object() function. This is so the - typeclass system can be correctly initiated behind the scenes. - - - Object Typeclass API: - - * Available properties (only available on *initiated* typeclass objects) - - key (string) - name of object - name (string) - same as key - aliases (list of strings) - aliases to the object. Will be saved to - database as AliasDB entries but returned as strings. - dbref (int, read-only) - unique #id-number. Also "id" can be used. - dbobj (Object, read-only) - link to database model. dbobj.typeclass - points back to this class - typeclass (Object, read-only) - this links back to this class as an - identified only. Use self.swap_typeclass() to switch. - date_created (string) - time stamp of object creation - permissions (list of strings) - list of permission strings - - player (Player) - controlling player (if any, only set together with - sessid below) - sessid (int, read-only) - session id (if any, only set together with - player above) - location (Object) - current location. Is None if this is a room - home (Object) - safety start-location - sessions (list of Sessions, read-only) - returns all sessions - connected to this object - has_player (bool, read-only)- will only return *connected* players - contents (list of Objects, read-only) - returns all objects inside - this object (including exits) - exits (list of Objects, read-only) - returns all exits from this - object, if any - destination (Object) - only set if this object is an exit. - is_superuser (bool, read-only) - True/False if this user is a superuser - - * Handlers available - - locks - lock-handler: use locks.add() to add new lock strings - db - attribute-handler: store/retrieve database attributes on this - self.db.myattr=val, val=self.db.myattr - ndb - non-persistent attribute handler: same as db but does not - create a database entry when storing data - scripts - script-handler. Add new scripts to object with scripts.add() - cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object - nicks - nick-handler. New nicks with nicks.add(). - - * Helper methods (see src.objects.objects.py for full headers) - - search(ostring, global_search=False, use_nicks=True, - typeclass=None, - attribute_name=None, use_nicks=True, location=None, - quiet=False, exact=False) - execute_cmd(raw_string) - msg(text=None, from_obj=None, sessid=0, **kwargs) - msg_contents(message, exclude=None, from_obj=None, **kwargs) - move_to(destination, quiet=False, emit_to_obj=None, - use_destination=True, to_none=False) - copy(new_key=None) - delete() - is_typeclass(typeclass, exact=False) - swap_typeclass(new_typeclass, clean_attributes=False, no_default=True) - access(accessing_obj, access_type='read', default=False) - check_permstring(permstring) - - * Hook methods - - basetype_setup() - only called once, used for behind-the-scenes - setup. Normally not modified. - basetype_posthook_setup() - customization in basetype, after the - object has been created; Normally not modified. - - at_object_creation() - only called once, when object is first created. - Object customizations go here. - at_object_delete() - called just before deleting an object. If - returning False, deletion is aborted. Note that - all objects inside a deleted object are - automatically moved to their , they don't - need to be removed here. - - at_init() called whenever typeclass is cached from - memory, at least once every server restart/reload - at_cmdset_get(**kwargs) - this is called just before the command - handler requests a cmdset from this object, usually - without any kwargs - at_pre_puppet(player)- (player-controlled objects only) called just - before puppeting - at_post_puppet() - (player-controlled objects only) called just - after completing connection player<->object - at_pre_unpuppet() - (player-controlled objects only) called just - before un-puppeting - at_post_unpuppet(player) (player-controlled objects only) called - just after disconnecting player<->object link - at_server_reload() - called before server is reloaded - at_server_shutdown() - called just before server is fully shut down - - at_before_move(destination) called just before moving - object to the destination. If returns - False, move is cancelled. - announce_move_from(destination) - called in old location, just before - move, if obj.move_to() has - quiet=False - announce_move_to(source_location) - called in new location, - just after move, if obj.move_to() - has quiet=False - at_after_move(source_location) - always called after a move - has been successfully performed. - at_object_leave(obj, target_location) - called when an object leaves - this object in any fashion - at_object_receive(obj, source_location) - called when this object - receives another object - at_access(result, **kwargs) - this is called with the result of an - access call, along with any kwargs used - for that call. The return of this - method does not affect the result of the - lock check. - at_before_traverse(traversing_object) - (exit-objects only) called - just before an object - traverses this object - at_after_traverse(traversing_object, source_location) - (exit-objects - only) called just after a traversal has happened. - at_failed_traverse(traversing_object) - (exit-objects only) called - if traversal fails and property err_traverse is not defined. - - at_msg_receive(self, msg, from_obj=None, data=None) - called when a - message (via self.msg()) is sent to this obj. - If returns false, aborts send. - at_msg_send(self, msg, to_obj=None, data=None) - called when this - objects sends a message to someone via self.msg(). - - return_appearance(looker) - describes this object. Used by "look" - command by default - at_desc(looker=None) - called by 'look' whenever the appearance - is requested. - at_get(getter) - called after object has been picked up. - Does not stop pickup. - at_drop(dropper) - called when this object has been dropped. - at_say(speaker, message) - by default, called if an object inside - this object speaks - - """ - super(Object, self).__init__(dbobj) - - ## methods inherited from the database object (overload them here) - - def search(self, searchdata, - global_search=False, - use_nicks=True, - typeclass=None, - location=None, - attribute_name=None, - quiet=False, - exact=False): - """ - Returns the typeclass of an Object matching a search string/condition - - Perform a standard object search in the database, handling - multiple results and lack thereof gracefully. By default, only - objects in self's current location or inventory is searched. - Note: to find Players, use eg. ev.player_search. - - Inputs: - - searchdata (str): Primary search criterion. Will be matched against - object.key (with object.aliases second) - unless the keyword attribute_name specifies otherwise. - Special strings: - # - search by unique dbref. This is always a - global search. - me,self - self-reference to this object - here - current location - - - can be used to differentiate between - multiple same-named matches - global_search (bool): Search all objects globally. This is overruled - by "location" keyword. - use_nicks (bool): Use nickname-replace (nicktype "object") on the - search string - typeclass (str or Typeclass): Limit search only to Objects with this - typeclass. May be a list of typeclasses for a - broader search. - location (Object): Specify a location to search, if different from the - self's given location - plus its contents. This can also be a list of locations. - attribute_name (str): Use this named Attribute to match ostring against, - instead of object.key. - quiet (bool) - don't display default error messages - return multiple - matches as a list and no matches as None. If not - set (default), will echo error messages and return None. - exact (bool) - if unset (default) - prefers to match to beginning of - string rather than not matching at all. If set, - requires exact mathing of entire string. - - Returns: - - quiet=False (default): - no match or multimatch: - auto-echoes errors to self.msg, then returns None - (results are handled by settings.SEARCH_AT_RESULT - and settings.SEARCH_AT_MULTIMATCH_INPUT) - match: - a unique object match - quiet=True: - no match or multimatch: - returns None or list of multi-matches - match: - a unique object match - - """ - if isinstance(searchdata, basestring): - # searchdata is a string; wrap some common self-references - if searchdata.lower() in ("here", ): - return self.location - if searchdata.lower() in ("me", "self",): - return self - - return self.dbobj.search(searchdata, - global_search=global_search, - use_nicks=use_nicks, - typeclass=typeclass, - location=location, - attribute_name=attribute_name, - quiet=quiet, - exact=exact) - - def search_player(self, searchdata, quiet=False): - """ - Simple shortcut wrapper to search for players, not characters. - - searchdata - search criterion - the key or dbref of the player - to search for. If this is "here" or "me", search - for the player connected to this object. - quiet - return the results as a list rather than echo eventual - standard error messages. - - Returns: - quiet=False (default): - no match or multimatch: - auto-echoes errors to self.msg, then returns None - (results are handled by settings.SEARCH_AT_RESULT - and settings.SEARCH_AT_MULTIMATCH_INPUT) - match: - a unique player match - quiet=True: - no match or multimatch: - returns None or list of multi-matches - match: - a unique object match - """ - if isinstance(searchdata, basestring): - # searchdata is a string; wrap some common self-references - if searchdata.lower() in ("me", "self",): - return self.player - return self.dbobj.search_player(searchdata, quiet=quiet) - - def execute_cmd(self, raw_string, sessid=None, **kwargs): - """ - Do something as this object. This command transparently - lets its typeclass execute the command. This method is - never called normally, it is only called explicitly in - code. - - Argument: - raw_string (string) - raw command input - sessid (int) - id of session executing the command. This sets the - sessid property on the command. - **kwargs - other keyword arguments will be added to the found command - object instace as variables before it executes. This is - unused by default Evennia but may be used to set flags and - change operating paramaters for commands at run-time. - - Returns Deferred - this is an asynchronous Twisted object that will - not fire until the command has actually finished executing. To - overload this one needs to attach callback functions to it, with - addCallback(function). This function will be called with an - eventual return value from the command execution. - - This return is not used at all by Evennia by default, but might be - useful for coders intending to implement some sort of nested - command structure. - """ - return self.dbobj.execute_cmd(raw_string, sessid=sessid, **kwargs) - - def msg(self, text=None, from_obj=None, sessid=None, **kwargs): - """ - Emits something to any sessions attached to the object. - - message (str): The message to send - from_obj (obj): object that is sending. - data (object): an optional data object that may or may not - be used by the protocol. - sessid: optional session target. If sessid=0, the session will - default to self.sessid or from_obj.sessid. - """ - - self.dbobj.msg(text=text, from_obj=from_obj, sessid=sessid, **kwargs) - - def msg_contents(self, text=None, exclude=None, from_obj=None, **kwargs): - """ - Emits something to all objects inside an object. - - exclude is a list of objects not to send to. See self.msg() for - more info. - """ - self.dbobj.msg_contents(text, exclude=exclude, - from_obj=from_obj, **kwargs) - - def move_to(self, destination, quiet=False, - emit_to_obj=None, use_destination=True, to_none=False): - """ - Moves this object to a new location. Note that if is an - exit object (i.e. it has "destination"!=None), the move_to will - happen to this destination and -not- into the exit object itself, - unless use_destination=False. Note that no lock checks are done by - this function, such things are assumed to have been handled before - calling move_to. - - destination: (Object) Reference to the object to move to. This - can also be an exit object, in which case the destination - property is used as destination. - quiet: (bool) If true, don't emit left/arrived messages. - emit_to_obj: (Object) object to receive error messages - use_destination (bool): Default is for objects to use the "destination" - property of destinations as the target to move to. - Turning off this keyword allows objects to move - "inside" exit objects. - to_none - allow destination to be None. Note that no hooks are run - when moving to a None location. If you want to run hooks, run - them manually (and make sure the hooks can handle a None - location). - Returns True/False depending on if there were problems with the move. - This method may also return various error messages to the - emit_to_obj. - - """ - return self.dbobj.move_to(destination, quiet=quiet, - emit_to_obj=emit_to_obj, - use_destination=use_destination) - - def copy(self, new_key=None): - """ - Makes an identical copy of this object. If you want to customize the - copy by changing some settings, use ObjectDB.object.copy_object() - directly. - - new_key (string) - new key/name of copied object. If new_key is not - specified, the copy will be named - _copy by default. - Returns: Object (copy of this one) - """ - return self.dbobj.copy(new_key=new_key) - - def delete(self): - """ - Deletes this object. - Before deletion, this method makes sure to move all contained - objects to their respective home locations, as well as clean - up all exits to/from the object. - - Returns: boolean True if deletion succeded, False if there - were errors during deletion or deletion otherwise - failed. - """ - return self.dbobj.delete() - - # methods inherited from the typeclass system - - def is_typeclass(self, typeclass, exact=False): - """ - Returns true if this object has this type - OR has a typeclass which is an subclass of - the given typeclass. - - typeclass - can be a class object or the - python path to such an object to match against. - - exact - returns true only if the object's - type is exactly this typeclass, ignoring - parents. - - Returns: Boolean - """ - return self.dbobj.is_typeclass(typeclass, exact=exact) - - def swap_typeclass(self, new_typeclass, clean_attributes=False, no_default=True): - """ - This performs an in-situ swap of the typeclass. This means - that in-game, this object will suddenly be something else. - Player will not be affected. To 'move' a player to a different - object entirely (while retaining this object's type), use - self.player.swap_object(). - - Note that this might be an error prone operation if the - old/new typeclass was heavily customized - your code - might expect one and not the other, so be careful to - bug test your code if using this feature! Often its easiest - to create a new object and just swap the player over to - that one instead. - - Arguments: - new_typeclass (path/classobj) - type to switch to - clean_attributes (bool/list) - will delete all attributes - stored on this object (but not any - of the database fields such as name or - location). You can't get attributes back, - but this is often the safest bet to make - sure nothing in the new typeclass clashes - with the old one. If you supply a list, - only those named attributes will be cleared. - no_default - if this is active, the swapper will not allow for - swapping to a default typeclass in case the given - one fails for some reason. Instead the old one - will be preserved. - Returns: - boolean True/False depending on if the swap worked or not. - - - """ - return self.dbobj.swap_typeclass(new_typeclass, - clean_attributes=clean_attributes, no_default=no_default) - - def access(self, accessing_obj, access_type='read', default=False, **kwargs): - """ - Determines if another object has permission to access this object in - whatever way. - - accessing_obj (Object)- object trying to access this one - access_type (string) - type of access sought - default (bool) - what to return if no lock of access_type was found - **kwargs - passed to at_access hook along with result,accessing_obj and access_type - """ - result = self.dbobj.access(accessing_obj, access_type=access_type, default=default) - self.at_access(result, accessing_obj, access_type, **kwargs) - return result - - # OBS: DEPRECATED! - if result: - self.at_access_success(accessing_obj, access_type) - return True - else: - self.at_access_failure(accessing_obj, access_type) - return False - - def check_permstring(self, permstring): - """ - This explicitly checks the given string against this object's - 'permissions' property without involving any locks. - - permstring (string) - permission string that need to match a - permission on the object. - (example: 'Builders') - """ - return self.dbobj.check_permstring(permstring) - - def __eq__(self, other): - """ - Checks for equality against an id string or another object or user. - - This has be located at this level, having it in the - parent doesn't work. - """ - try: - return _GA(_GA(self, "dbobj"), "dbid") == _GA(_GA(other, "dbobj"), "dbid") - except AttributeError: - # compare players instead - try: - return _GA(_GA(_GA(self, "dbobj"), "player"), "uid") == _GA(_GA(other, "player"), "uid") - except AttributeError: - return False - - ## hooks called by the game engine - - def basetype_setup(self): - """ - This sets up the default properties of an Object, - just before the more general at_object_creation. - - You normally don't need to change this unless you change some - fundamental things like names of permission groups. - """ - # the default security setup fallback for a generic - # object. Overload in child for a custom setup. Also creation - # commands may set this (create an item and you should be its - # controller, for example) - - self.locks.add(";".join([ - "control:perm(Immortals)", # edit locks/permissions, delete - "examine:perm(Builders)", # examine properties - "view:all()", # look at object (visibility) - "edit:perm(Wizards)", # edit properties/attributes - "delete:perm(Wizards)", # delete object - "get:all()", # pick up object - "call:true()", # allow to call commands on this object - "tell:perm(Wizards)", # allow emits to this object - "puppet:pperm(Immortals)"])) # lock down puppeting only to staff by default - - def basetype_posthook_setup(self): - """ - Called once, after basetype_setup and at_object_creation. This should - generally not be overloaded unless you are redefining how a - room/exit/object works. It allows for basetype-like setup after the - object is created. An example of this is EXITs, who need to know keys, - aliases, locks etc to set up their exit-cmdsets. - """ - pass - - def at_object_creation(self): - """ - Called once, when this object is first created. - """ - pass - - def at_object_delete(self): - """ - Called just before the database object is - permanently delete()d from the database. If - this method returns False, deletion is aborted. - """ - return True - - def at_init(self): - """ - This is always called whenever this object is initiated -- - that is, whenever it its typeclass is cached from memory. This - happens on-demand first time the object is used or activated - in some way after being created but also after each server - restart or reload. - """ - pass - - - def at_cmdset_get(self, **kwargs): - """ - Called just before cmdsets on this object are requested by the - command handler. If changes need to be done on the fly to the - cmdset before passing them on to the cmdhandler, this is the - place to do it. This is called also if the object currently - have no cmdsets. **kwargs are usually not set but could be - used e.g. to force rebuilding of a dynamically created cmdset - or similar. - """ - pass - - def at_pre_puppet(self, player, sessid=None): - """ - Called just before a Player connects to this object - to puppet it. - - player - connecting player object - sessid - session id controlling the connection - """ - pass - - def at_post_puppet(self): - """ - Called just after puppeting has been completed and - all Player<->Object links have been established. - """ - pass - - def at_pre_unpuppet(self): - """ - Called just before beginning to un-connect a puppeting - from this Player. - """ - pass - - def at_post_unpuppet(self, player, sessid=None): - """ - Called just after the Player successfully disconnected - from this object, severing all connections. - - player - the player object that just disconnected from - this object. - sessid - session id controlling the connection - """ - pass - - def at_server_reload(self): - """ - This hook is called whenever the server is shutting down for - restart/reboot. If you want to, for example, save non-persistent - properties across a restart, this is the place to do it. - """ - pass - - def at_server_shutdown(self): - """ - This hook is called whenever the server is shutting down fully - (i.e. not for a restart). - """ - pass - - def at_access(self, result, accessing_obj, access_type, **kwargs): - """ - This is called with the result of an access call, along with - any kwargs used for that call. The return of this method does - not affect the result of the lock check. It can be used e.g. to - customize error messages in a central location or other effects - based on the access result. - """ - pass - - def at_access_success(self, accessing_obj, access_type): - """ - OBS: DEPRECATED. Use at_access instead - - This hook is called whenever accessing_obj succeed a lock check of - type access_type on this object, for whatever reason. The return value - of this hook is not used, the lock will still pass regardless of what - this hook does (use lockstring/funcs to tweak the lock result). - """ - log_depmsg("at_access_success is deprecated. Use at_access(result,**kwargs) instead.") - pass - - def at_access_failure(self, accessing_obj, access_type): - """ - OBS: DEPRECATED. Use at_access instead - - This hook is called whenever accessing_obj fails a lock check of type - access_type on this object, for whatever reason. The return value of - this hook is not used, the lock will still fail regardless of what - this hook does (use lockstring/funcs to tweak the lock result). - """ - log_depmsg("at_access_failure is deprecated. Use at_access(result,**kwargs) instead.") - pass - - # hooks called when moving the object - - def at_before_move(self, destination): - """ - Called just before starting to move - this object to destination. - - destination - the object we are moving to - - If this method returns False/None, the move - is cancelled before it is even started. - """ - #return has_perm(self, destination, "can_move") - return True - - def announce_move_from(self, destination): - """ - Called if the move is to be announced. This is - called while we are still standing in the old - location. - - destination - the place we are going to. - """ - if not self.location: - return - name = self.name - loc_name = "" - loc_name = self.location.name - dest_name = destination.name - string = "%s is leaving %s, heading for %s." - self.location.msg_contents(string % (name, loc_name, dest_name), exclude=self) - - def announce_move_to(self, source_location): - """ - Called after the move if the move was not quiet. At this - point we are standing in the new location. - - source_location - the place we came from - """ - - name = self.name - if not source_location and self.location.has_player: - # This was created from nowhere and added to a player's - # inventory; it's probably the result of a create command. - string = "You now have %s in your possession." % name - self.location.msg(string) - return - - src_name = "nowhere" - loc_name = self.location.name - if source_location: - src_name = source_location.name - string = "%s arrives to %s from %s." - self.location.msg_contents(string % (name, loc_name, src_name), exclude=self) - - def at_after_move(self, source_location): - """ - Called after move has completed, regardless of quiet mode or not. - Allows changes to the object due to the location it is now in. - - source_location - where we came from. This may be None. - """ - pass - - def at_object_leave(self, moved_obj, target_location): - """ - Called just before an object leaves from inside this object - - moved_obj - the object leaving - target_location - where the object is going. - """ - pass - - def at_object_receive(self, moved_obj, source_location): - """ - Called after an object has been moved into this object. - - moved_obj - the object moved into this one - source_location - where moved_object came from. - """ - pass - - def at_before_traverse(self, traversing_object): - """ - Called just before an object uses this object to - traverse to another object (i.e. this object is a type of Exit) - - The target location should normally be available as self.destination. - """ - pass - - def at_traverse(self, traversing_object, target_location): - """ - This hook is responsible for handling the actual traversal, normally - by calling traversing_object.move_to(target_location). It is normally - only implemented by Exit objects. If it returns False (usually because - move_to returned False), at_after_traverse below should not be called - and instead at_failed_traverse should be called. - """ - pass - - def at_after_traverse(self, traversing_object, source_location): - """ - Called just after an object successfully used this object to - traverse to another object (i.e. this object is a type of Exit) - - The target location should normally be available as self.destination. - """ - pass - - def at_failed_traverse(self, traversing_object): - """ - This is called if an object fails to traverse this object for some - reason. It will not be called if the attribute err_traverse is defined, - that attribute will then be echoed back instead. - """ - pass - - def at_msg_receive(self, text=None, **kwargs): - """ - This hook is called whenever someone - sends a message to this object. - - Note that from_obj may be None if the sender did - not include itself as an argument to the obj.msg() - call - so you have to check for this. . - - Consider this a pre-processing method before - msg is passed on to the user sesssion. If this - method returns False, the msg will not be - passed on. - Input: - msg = the message received - from_obj = the one sending the message - Output: - boolean True/False - """ - return True - - def at_msg_send(self, text=None, to_obj=None, **kwargs): - """ - This is a hook that is called when /this/ object - sends a message to another object with obj.msg() - while also specifying that it is the one sending. - - Note that this method is executed on the object - passed along with the msg() function (i.e. using - obj.msg(msg, from_obj=caller) will then launch caller.at_msg()) - and if no object was passed, it will never be called. - """ - pass - - # hooks called by the default cmdset. - - def return_appearance(self, pobject): - """ - This is a convenient hook for a 'look' - command to call. - """ - if not pobject: - return - # get and identify all objects - visible = (con for con in self.contents if con != pobject and - con.access(pobject, "view")) - exits, users, things = [], [], [] - for con in visible: - key = con.key - if con.destination: - exits.append(key) - elif con.has_player: - users.append("{c%s{n" % key) - else: - things.append(key) - # get description, build string - string = "{c%s{n" % self.key - desc = self.db.desc - if desc: - string += "\n %s" % desc - if exits: - string += "\n{wExits:{n " + ", ".join(exits) - if users or things: - string += "\n{wYou see:{n " + ", ".join(users + things) - return string - - def at_desc(self, looker=None): - """ - This is called whenever someone looks - at this object. Looker is the looking - object. - """ - pass - - def at_get(self, getter): - """ - Called when this object has been picked up. Obs- - this method cannot stop the pickup - use permissions - for that! - - getter - the object getting this object. - """ - pass - - def at_drop(self, dropper): - """ - Called when this object has been dropped. - - dropper - the object which just dropped this object. - """ - pass - - def at_say(self, speaker, message): - """ - Called on this object if an object inside this object speaks. - The string returned from this method is the final form - of the speech. Obs - you don't have to add things like - 'you say: ' or similar, that is handled by the say command. - - speaker - the object speaking - message - the words spoken. - """ - return message - -# -# Base Character object -# - -class Character(Object): - """ - This is just like the Object except it implements its own - version of the at_object_creation to set up the script - that adds the default cmdset to the object. - """ - - def basetype_setup(self): - """ - Setup character-specific security - - You should normally not need to overload this, but if you do, make - sure to reproduce at least the two last commands in this method (unless - you want to fundamentally change how a Character object works). - - """ - super(Character, self).basetype_setup() - self.locks.add(";".join(["get:false()", # noone can pick up the character - "call:false()"])) # no commands can be called on character from outside - # add the default cmdset - self.cmdset.add_default(settings.CMDSET_CHARACTER, permanent=True) - - def at_object_creation(self): - """ - All this does (for now) is to add the default cmdset. Since - the script is permanently stored to this object (the permanent - keyword creates a script to do this), we should never need to - do this again for as long as this object exists. - """ - pass - - def at_after_move(self, source_location): - "Default is to look around after a move." - self.execute_cmd('look') - - def at_pre_puppet(self, player, sessid=None): - """ - This recovers the character again after having been "stoved away" - at the unpuppet - """ - if self.db.prelogout_location: - # try to recover - self.location = self.db.prelogout_location - if self.location is None: - # make sure location is never None (home should always exist) - self.location = self.home - if self.location: - # save location again to be sure - self.db.prelogout_location = self.location - self.location.at_object_receive(self, self.location) - else: - player.msg("{r%s has no location and no home is set.{n" % self, sessid=sessid) - - def at_post_puppet(self): - """ - Called just after puppeting has completed. - """ - self.msg("\nYou become {c%s{n.\n" % self.name) - self.execute_cmd("look") - if self.location: - self.location.msg_contents("%s has entered the game." % self.name, exclude=[self]) - - def at_post_unpuppet(self, player, sessid=None): - """ - We stove away the character when the player goes ooc/logs off, - otherwise the character object will remain in the room also after the - player logged off ("headless", so to say). - """ - if self.location: # have to check, in case of multiple connections closing - self.location.msg_contents("%s has left the game." % self.name, exclude=[self]) - self.db.prelogout_location = self.location - self.location = None - -# -# Base Room object -# - -class Room(Object): - """ - This is the base room object. It's just like any Object except its - location is None. - """ - def basetype_setup(self): - """ - Simple setup, shown as an example - (since default is None anyway) - """ - - super(Room, self).basetype_setup() - self.locks.add(";".join(["get:false()", - "puppet:false()"])) # would be weird to puppet a room ... - self.location = None - - -# -# Base Exit object -# - -class Exit(Object): - """ - This is the base exit object - it connects a location to another. - This is done by the exit assigning a "command" on itself with the - same name as the exit object (to do this we need to remember to - re-create the command when the object is cached since it must be - created dynamically depending on what the exit is called). This - command (which has a high priority) will thus allow us to traverse - exits simply by giving the exit-object's name on its own. - """ - - # Helper classes and methods to implement the Exit. These need not - # be overloaded unless one want to change the foundation for how - # Exits work. See the end of the class for hook methods to overload. - - def create_exit_cmdset(self, exidbobj): - """ - Helper function for creating an exit command set + command. - - The command of this cmdset has the same name as the Exit object - and allows the exit to react when the player enter the exit's name, - triggering the movement between rooms. - - Note that exitdbobj is an ObjectDB instance. This is necessary - for handling reloads and avoid tracebacks if this is called while - the typeclass system is rebooting. - """ - class ExitCommand(command.Command): - """ - This is a command that simply cause the caller - to traverse the object it is attached to. - """ - obj = None - - def func(self): - "Default exit traverse if no syscommand is defined." - - if self.obj.access(self.caller, 'traverse'): - # we may traverse the exit. - self.obj.at_traverse(self.caller, self.obj.destination) - else: - # exit is locked - if self.obj.db.err_traverse: - # if exit has a better error message, let's use it. - self.caller.msg(self.obj.db.err_traverse) - else: - # No shorthand error message. Call hook. - self.obj.at_failed_traverse(self.caller) - - # create an exit command. We give the properties here, - # to always trigger metaclass preparations - cmd = ExitCommand(key=exidbobj.db_key.strip().lower(), - aliases=exidbobj.aliases.all(), - locks=str(exidbobj.locks), - auto_help=False, - destination=exidbobj.db_destination, - arg_regex=r"^$", - is_exit=True, - obj=exidbobj) - # create a cmdset - exit_cmdset = cmdset.CmdSet(None) - exit_cmdset.key = '_exitset' - exit_cmdset.priority = 101 - exit_cmdset.duplicates = True - # add command to cmdset - exit_cmdset.add(cmd) - return exit_cmdset - - # Command hooks - def basetype_setup(self): - """ - Setup exit-security - - You should normally not need to overload this - if you do make sure you - include all the functionality in this method. - """ - super(Exit, self).basetype_setup() - - # setting default locks (overload these in at_object_creation() - self.locks.add(";".join(["puppet:false()", # would be weird to puppet an exit ... - "traverse:all()", # who can pass through exit by default - "get:false()"])) # noone can pick up the exit - - # an exit should have a destination (this is replaced at creation time) - if self.dbobj.location: - self.destination = self.dbobj.location - - def at_cmdset_get(self, **kwargs): - """ - Called when the cmdset is requested from this object, just before the - cmdset is actually extracted. If no Exit-cmdset is cached, create - it now. - - kwargs: - force_init=True - force a re-build of the cmdset (for example to update aliases) - """ - - if "force_init" in kwargs or not self.cmdset.has_cmdset("_exitset", must_be_default=True): - # we are resetting, or no exit-cmdset was set. Create one dynamically. - self.cmdset.add_default(self.create_exit_cmdset(self.dbobj), permanent=False) - - # this and other hooks are what usually can be modified safely. - - def at_object_creation(self): - "Called once, when object is first created (after basetype_setup)." - pass - - def at_traverse(self, traversing_object, target_location): - """ - This implements the actual traversal. The traverse lock has already been - checked (in the Exit command) at this point. - """ - source_location = traversing_object.location - if traversing_object.move_to(target_location): - self.at_after_traverse(traversing_object, source_location) - else: - if self.db.err_traverse: - # if exit has a better error message, let's use it. - self.caller.msg(self.db.err_traverse) - else: - # No shorthand error message. Call hook. - self.at_failed_traverse(traversing_object) - - def at_after_traverse(self, traversing_object, source_location): - """ - Called after a successful traverse. - """ - pass - - def at_failed_traverse(self, traversing_object): - """ - This is called if an object fails to traverse this object for some - reason. It will not be called if the attribute "err_traverse" is - defined, that attribute will then be echoed back instead as a - convenient shortcut. - - (See also hooks at_before_traverse and at_after_traverse). - """ - traversing_object.msg("You cannot go there.") diff --git a/src/players/__init__.py b/src/players/__init__.py deleted file mode 100644 index b49e80468..000000000 --- a/src/players/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -""" -Makes it easier to import by grouping all relevant things already at this -level. - -You can henceforth import most things directly from src.player -Also, the initiated object manager is available as src.players.manager. - -""" - -from src.players.player import * -from src.players.models import PlayerDB - -manager = PlayerDB.objects diff --git a/src/players/migrations/__init__.py b/src/players/migrations/__init__.py deleted file mode 100644 index 40a96afc6..000000000 --- a/src/players/migrations/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/src/players/models.py b/src/players/models.py deleted file mode 100644 index d8c341b81..000000000 --- a/src/players/models.py +++ /dev/null @@ -1,484 +0,0 @@ -""" -Player - -The player class is an extension of the default Django user class, -and is customized for the needs of Evennia. - -We use the Player to store a more mud-friendly style of permission -system as well as to allow the admin more flexibility by storing -attributes on the Player. Within the game we should normally use the -Player manager's methods to create users so that permissions are set -correctly. - -To make the Player model more flexible for your own game, it can also -persistently store attributes of its own. This is ideal for extra -account info and OOC account configuration variables etc. - -""" - -from django.conf import settings -from django.db import models -from django.contrib.auth.models import AbstractUser -from django.utils.encoding import smart_str - -from src.players import manager -from src.scripts.models import ScriptDB -from src.typeclasses.models import (TypedObject, NickHandler) -from src.scripts.scripthandler import ScriptHandler -from src.commands.cmdsethandler import CmdSetHandler -from src.commands import cmdhandler -from src.utils import utils, logger -from src.utils.utils import to_str, make_iter, lazy_property - -from django.utils.translation import ugettext as _ - -__all__ = ("PlayerDB",) - -#_ME = _("me") -#_SELF = _("self") - -_SESSIONS = None -_AT_SEARCH_RESULT = utils.variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1)) -_MULTISESSION_MODE = settings.MULTISESSION_MODE - -_GA = object.__getattribute__ -_SA = object.__setattr__ -_DA = object.__delattr__ - -_TYPECLASS = None - - -#------------------------------------------------------------ -# -# PlayerDB -# -#------------------------------------------------------------ - -class PlayerDB(TypedObject, AbstractUser): - """ - This is a special model using Django's 'profile' functionality - and extends the default Django User model. It is defined as such - by use of the variable AUTH_PROFILE_MODULE in the settings. - One accesses the fields/methods. We try use this model as much - as possible rather than User, since we can customize this to - our liking. - - The TypedObject supplies the following (inherited) properties: - key - main name - typeclass_path - the path to the decorating typeclass - typeclass - auto-linked typeclass - date_created - time stamp of object creation - permissions - perm strings - dbref - #id of object - db - persistent attribute storage - ndb - non-persistent attribute storage - - The PlayerDB adds the following properties: - user - Connected User object. django field, needs to be save():d. - name - alias for user.username - sessions - sessions connected to this player - is_superuser - bool if this player is a superuser - is_bot - bool if this player is a bot and not a real player - - """ - - # - # PlayerDB Database model setup - # - # inherited fields (from TypedObject): - # db_key, db_typeclass_path, db_date_created, db_permissions - - # store a connected flag here too, not just in sessionhandler. - # This makes it easier to track from various out-of-process locations - db_is_connected = models.BooleanField(default=False, - verbose_name="is_connected", - help_text="If player is connected to game or not") - # database storage of persistant cmdsets. - db_cmdset_storage = models.CharField('cmdset', max_length=255, null=True, - help_text="optional python path to a cmdset class. If creating a Character, this will default to settings.CMDSET_CHARACTER.") - # marks if this is a "virtual" bot player object - db_is_bot = models.BooleanField(default=False, verbose_name="is_bot", help_text="Used to identify irc/imc2/rss bots") - - # Database manager - objects = manager.PlayerManager() - - # caches for quick lookups - _typeclass_paths = settings.PLAYER_TYPECLASS_PATHS - _default_typeclass_path = settings.BASE_PLAYER_TYPECLASS or "src.players.player.Player" - - class Meta: - app_label = 'players' - verbose_name = 'Player' - - # lazy-loading of handlers - @lazy_property - def cmdset(self): - return CmdSetHandler(self, True) - - @lazy_property - def scripts(self): - return ScriptHandler(self) - - @lazy_property - def nicks(self): - return NickHandler(self) - - - # alias to the objs property - def __characters_get(self): - return self.objs - - def __characters_set(self, value): - self.objs = value - - def __characters_del(self): - raise Exception("Cannot delete name") - characters = property(__characters_get, __characters_set, __characters_del) - - # cmdset_storage property - # This seems very sensitive to caching, so leaving it be for now /Griatch - #@property - def cmdset_storage_get(self): - """ - Getter. Allows for value = self.name. Returns a list of cmdset_storage. - """ - storage = _GA(self, "db_cmdset_storage") - # we need to check so storage is not None - return [path.strip() for path in storage.split(',')] if storage else [] - - #@cmdset_storage.setter - def cmdset_storage_set(self, value): - """ - Setter. Allows for self.name = value. Stores as a comma-separated - string. - """ - _SA(self, "db_cmdset_storage", ",".join(str(val).strip() for val in make_iter(value))) - _GA(self, "save")() - - #@cmdset_storage.deleter - def cmdset_storage_del(self): - "Deleter. Allows for del self.name" - _SA(self, "db_cmdset_storage", None) - _GA(self, "save")() - cmdset_storage = property(cmdset_storage_get, cmdset_storage_set, cmdset_storage_del) - - class Meta: - "Define Django meta options" - verbose_name = "Player" - verbose_name_plural = "Players" - - # - # PlayerDB main class properties and methods - # - - def __str__(self): - return smart_str("%s(player %s)" % (_GA(self, "name"), _GA(self, "dbid"))) - - def __unicode__(self): - return u"%s(player#%s)" % (_GA(self, "name"), _GA(self, "dbid")) - - #@property - def __username_get(self): - return _GA(self, "username") - - def __username_set(self, value): - _SA(self, "username", value) - - def __username_del(self): - _DA(self, "username") - # aliases - name = property(__username_get, __username_set, __username_del) - key = property(__username_get, __username_set, __username_del) - - #@property - def __uid_get(self): - "Getter. Retrieves the user id" - return self.id - - def __uid_set(self, value): - raise Exception("User id cannot be set!") - - def __uid_del(self): - raise Exception("User id cannot be deleted!") - uid = property(__uid_get, __uid_set, __uid_del) - - #@property - #def __is_superuser_get(self): - # "Superusers have all permissions." - # return self.db_is_superuser - # #is_suser = get_prop_cache(self, "_is_superuser") - # #if is_suser == None: - # # is_suser = _GA(self, "user").is_superuser - # # set_prop_cache(self, "_is_superuser", is_suser) - # #return is_suser - #is_superuser = property(__is_superuser_get) - - # - # PlayerDB class access methods - # - - def msg(self, text=None, from_obj=None, sessid=None, **kwargs): - """ - Evennia -> User - This is the main route for sending data back to the user from the - server. - - outgoing_string (string) - text data to send - from_obj (Object/Player) - source object of message to send. Its - at_msg_send() hook will be called. - sessid - the session id of the session to send to. If not given, return - to all sessions connected to this player. This is usually only - relevant when using msg() directly from a player-command (from - a command on a Character, the character automatically stores - and handles the sessid). Can also be a list of sessids. - kwargs (dict) - All other keywords are parsed as extra data. - """ - if "data" in kwargs: - # deprecation warning - logger.log_depmsg("PlayerDB:msg() 'data'-dict keyword is deprecated. Use **kwargs instead.") - data = kwargs.pop("data") - if isinstance(data, dict): - kwargs.update(data) - - text = to_str(text, force_string=True) if text else "" - if from_obj: - # call hook - try: - _GA(from_obj, "at_msg_send")(text=text, to_obj=_GA(self, "typeclass"), **kwargs) - except Exception: - pass - sessions = _MULTISESSION_MODE > 1 and sessid and _GA(self, "get_session")(sessid) or None - if sessions: - for session in make_iter(sessions): - obj = session.puppet - if obj and not obj.at_msg_receive(text=text, **kwargs): - # if hook returns false, cancel send - continue - session.msg(text=text, **kwargs) - else: - # if no session was specified, send to them all - for sess in _GA(self, 'get_all_sessions')(): - sess.msg(text=text, **kwargs) - - # session-related methods - - def get_session(self, sessid): - """ - Return session with given sessid connected to this player. - note that the sessionhandler also accepts sessid as an iterable. - """ - global _SESSIONS - if not _SESSIONS: - from src.server.sessionhandler import SESSIONS as _SESSIONS - return _SESSIONS.session_from_player(self, sessid) - - def get_all_sessions(self): - "Return all sessions connected to this player" - global _SESSIONS - if not _SESSIONS: - from src.server.sessionhandler import SESSIONS as _SESSIONS - return _SESSIONS.sessions_from_player(self) - sessions = property(get_all_sessions) # alias shortcut - - def disconnect_session_from_player(self, sessid): - """ - Access method for disconnecting a given session from the player - (connection happens automatically in the sessionhandler) - """ - # this should only be one value, loop just to make sure to - # clean everything - sessions = (session for session in self.get_all_sessions() - if session.sessid == sessid) - for session in sessions: - # this will also trigger unpuppeting - session.sessionhandler.disconnect(session) - - # puppeting operations - - def puppet_object(self, sessid, obj, normal_mode=True): - """ - Use the given session to control (puppet) the given object (usually - a Character type). Note that we make no puppet checks here, that must - have been done before calling this method. - - sessid - session id of session to connect - obj - the object to connect to - normal_mode - trigger hooks and extra checks - this is turned off when - the server reloads, to quickly re-connect puppets. - - returns True if successful, False otherwise - """ - session = self.get_session(sessid) - if not session: - return False - if normal_mode and session.puppet: - # cleanly unpuppet eventual previous object puppeted by this session - self.unpuppet_object(sessid) - if obj.player and obj.player.is_connected and obj.player != self: - # we don't allow to puppet an object already controlled by an active - # player. To kick a player, call unpuppet_object on them explicitly. - return - # if we get to this point the character is ready to puppet or it - # was left with a lingering player/sessid reference from an unclean - # server kill or similar - - if normal_mode: - _GA(obj.typeclass, "at_pre_puppet")(_GA(self, "typeclass"), sessid=sessid) - # do the connection - obj.sessid.add(sessid) - obj.player = self - session.puid = obj.id - session.puppet = obj - # validate/start persistent scripts on object - ScriptDB.objects.validate(obj=obj) - if normal_mode: - _GA(obj.typeclass, "at_post_puppet")() - return True - - def unpuppet_object(self, sessid): - """ - Disengage control over an object - - sessid - the session id to disengage - - returns True if successful - """ - session = self.get_session(sessid) - if not session: - return False - obj = hasattr(session, "puppet") and session.puppet or None - if not obj: - return False - # do the disconnect, but only if we are the last session to puppet - _GA(obj.typeclass, "at_pre_unpuppet")() - obj.dbobj.sessid.remove(sessid) - if not obj.dbobj.sessid.count(): - del obj.dbobj.player - _GA(obj.typeclass, "at_post_unpuppet")(_GA(self, "typeclass"), sessid=sessid) - session.puppet = None - session.puid = None - return True - - def unpuppet_all(self): - """ - Disconnect all puppets. This is called by server - before a reset/shutdown. - """ - for session in self.get_all_sessions(): - self.unpuppet_object(session.sessid) - - def get_puppet(self, sessid, return_dbobj=False): - """ - Get an object puppeted by this session through this player. This is - the main method for retrieving the puppeted object from the - player's end. - - sessid - return character connected to this sessid, - character - return character if connected to this player, else None. - - """ - session = self.get_session(sessid) - if not session: - return None - if return_dbobj: - return session.puppet - return session.puppet and session.puppet.typeclass or None - - def get_all_puppets(self, return_dbobj=False): - """ - Get all currently puppeted objects as a list - """ - puppets = [session.puppet for session in self.get_all_sessions() - if session.puppet] - if return_dbobj: - return puppets - return [puppet.typeclass for puppet in puppets] - - def __get_single_puppet(self): - """ - This is a legacy convenience link for users of - MULTISESSION_MODE 0 or 1. It will return - only the first puppet. For mode 2, this returns - a list of all characters. - """ - puppets = self.get_all_puppets() - if _MULTISESSION_MODE in (0, 1): - return puppets and puppets[0] or None - return puppets - character = property(__get_single_puppet) - puppet = property(__get_single_puppet) - - # utility methods - - def delete(self, *args, **kwargs): - """ - Deletes the player permanently. - """ - for session in self.get_all_sessions(): - # unpuppeting all objects and disconnecting the user, if any - # sessions remain (should usually be handled from the - # deleting command) - self.unpuppet_object(session.sessid) - session.sessionhandler.disconnect(session, reason=_("Player being deleted.")) - self.scripts.stop() - _GA(self, "attributes").clear() - _GA(self, "nicks").clear() - _GA(self, "aliases").clear() - super(PlayerDB, self).delete(*args, **kwargs) - - def execute_cmd(self, raw_string, sessid=None, **kwargs): - """ - Do something as this player. This method is never called normally, - but only when the player object itself is supposed to execute the - command. It takes player nicks into account, but not nicks of - eventual puppets. - - raw_string - raw command input coming from the command line. - sessid - the optional session id to be responsible for the command-send - **kwargs - other keyword arguments will be added to the found command - object instace as variables before it executes. This is - unused by default Evennia but may be used to set flags and - change operating paramaters for commands at run-time. - """ - raw_string = utils.to_unicode(raw_string) - raw_string = self.nicks.nickreplace(raw_string, - categories=("inputline", "channel"), include_player=False) - if not sessid and _MULTISESSION_MODE in (0, 1): - # in this case, we should either have only one sessid, or the sessid - # should not matter (since the return goes to all of them we can - # just use the first one as the source) - try: - sessid = self.get_all_sessions()[0].sessid - except IndexError: - # this can happen for bots - sessid = None - return cmdhandler.cmdhandler(self.typeclass, raw_string, - callertype="player", sessid=sessid, **kwargs) - - def search(self, searchdata, return_puppet=False, **kwargs): - """ - This is similar to the ObjectDB search method but will search for - Players only. Errors will be echoed, and None returned if no Player - is found. - searchdata - search criterion, the Player's key or dbref to search for - return_puppet - will try to return the object the player controls - instead of the Player object itself. If no - puppeted object exists (since Player is OOC), None will - be returned. - Extra keywords are ignored, but are allowed in call in order to make - API more consistent with objects.models.TypedObject.search. - """ - #TODO deprecation - if "return_character" in kwargs: - logger.log_depmsg("Player.search's 'return_character' keyword is deprecated. Use the return_puppet keyword instead.") - return_puppet = kwargs.get("return_character") - - matches = _GA(self, "__class__").objects.player_search(searchdata) - matches = _AT_SEARCH_RESULT(_GA(self, "typeclass"), searchdata, matches, global_search=True) - if matches and return_puppet: - try: - return _GA(matches, "puppet") - except AttributeError: - return None - return matches - diff --git a/src/players/player.py b/src/players/player.py deleted file mode 100644 index 4ce3c37be..000000000 --- a/src/players/player.py +++ /dev/null @@ -1,468 +0,0 @@ -""" -Typeclass for Player objects - -Note that this object is primarily intended to -store OOC information, not game info! This -object represents the actual user (not their -character) and has NO actual precence in the -game world (this is handled by the associated -character object, so you should customize that -instead for most things). - -""" - -import datetime -from django.conf import settings -from src.typeclasses.typeclass import TypeClass -from src.comms.models import ChannelDB -from src.utils import logger -__all__ = ("Player",) - -_MULTISESSION_MODE = settings.MULTISESSION_MODE -_CMDSET_PLAYER = settings.CMDSET_PLAYER -_CONNECT_CHANNEL = None - - -class Player(TypeClass): - """ - Base typeclass for all Players. - """ - def __init__(self, dbobj): - """ - This is the base Typeclass for all Players. Players represent - the person playing the game and tracks account info, password - etc. They are OOC entities without presence in-game. A Player - can connect to a Character Object in order to "enter" the - game. - - Player Typeclass API: - - * Available properties (only available on initiated typeclass objects) - - key (string) - name of player - name (string)- wrapper for user.username - aliases (list of strings) - aliases to the object. Will be saved to - database as AliasDB entries but returned as strings. - dbref (int, read-only) - unique #id-number. Also "id" can be used. - dbobj (Player, read-only) - link to database model. dbobj.typeclass - points back to this class - typeclass (Player, read-only) - this links back to this class as an - identified only. Use self.swap_typeclass() to switch. - date_created (string) - time stamp of object creation - permissions (list of strings) - list of permission strings - - user (User, read-only) - django User authorization object - obj (Object) - game object controlled by player. 'character' can also - be used. - sessions (list of Sessions) - sessions connected to this player - is_superuser (bool, read-only) - if the connected user is a superuser - - * Handlers - - locks - lock-handler: use locks.add() to add new lock strings - db - attribute-handler: store/retrieve database attributes on this - self.db.myattr=val, val=self.db.myattr - ndb - non-persistent attribute handler: same as db but does not - create a database entry when storing data - scripts - script-handler. Add new scripts to object with scripts.add() - cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object - nicks - nick-handler. New nicks with nicks.add(). - - * Helper methods - - msg(outgoing_string, from_obj=None, **kwargs) - swap_character(new_character, delete_old_character=False) - execute_cmd(raw_string) - search(ostring, global_search=False, attribute_name=None, - use_nicks=False, location=None, - ignore_errors=False, player=False) - is_typeclass(typeclass, exact=False) - swap_typeclass(new_typeclass, clean_attributes=False, no_default=True) - access(accessing_obj, access_type='read', default=False) - check_permstring(permstring) - - * Hook methods - - basetype_setup() - at_player_creation() - - - note that the following hooks are also found on Objects and are - usually handled on the character level: - - at_init() - at_access() - at_cmdset_get(**kwargs) - at_first_login() - at_post_login(sessid=None) - at_disconnect() - at_message_receive() - at_message_send() - at_server_reload() - at_server_shutdown() - - """ - super(Player, self).__init__(dbobj) - - ## methods inherited from database model - - def msg(self, text=None, from_obj=None, sessid=None, **kwargs): - """ - Evennia -> User - This is the main route for sending data back to the user from - the server. - - text (string) - text data to send - from_obj (Object/Player) - source object of message to send - sessid - the session id of the session to send to. If not given, - return to all sessions connected to this player. This is usually only - relevant when using msg() directly from a player-command (from - a command on a Character, the character automatically stores and - handles the sessid). - kwargs - extra data to send through protocol - """ - self.dbobj.msg(text=text, from_obj=from_obj, sessid=sessid, **kwargs) - - def swap_character(self, new_character, delete_old_character=False): - """ - Swaps the character controlled by this Player, if possible. - - new_character (Object) - character/object to swap to - delete_old_character (bool) - delete the old character when swapping - - Returns: True/False depending on if swap suceeded or not. - """ - return self.dbobj.swap_character(new_character, delete_old_character=delete_old_character) - - def execute_cmd(self, raw_string, sessid=None, **kwargs): - """ - Do something as this object. This command transparently - lets its typeclass execute the command. This method - is -not- called by Evennia normally, it is here to be - called explicitly in code. - - Argument: - raw_string (string) - raw command input - sessid (int) - id of session executing the command. This sets the - sessid property on the command - **kwargs - other keyword arguments will be added to the found command - object instace as variables before it executes. This is - unused by default Evennia but may be used to set flags and - change operating paramaters for commands at run-time. - - Returns Deferred - this is an asynchronous Twisted object that will - not fire until the command has actually finished executing. To - overload this one needs to attach callback functions to it, with - addCallback(function). This function will be called with an - eventual return value from the command execution. - - This return is not used at all by Evennia by default, but might - be useful for coders intending to implement some sort of nested - command structure. - """ - return self.dbobj.execute_cmd(raw_string, sessid=sessid, **kwargs) - - def search(self, searchdata, return_puppet=False, **kwargs): - """ - This is similar to the Object search method but will search for - Players only. Errors will be echoed, and None returned if no Player - is found. - searchdata - search criterion, the Player's key or dbref to search for - return_puppet - will try to return the object the player controls - instead of the Player object itself. If no - puppeted object exists (since Player is OOC), None will - be returned. - Extra keywords are ignored, but are allowed in call in order to make - API more consistent with objects.models.TypedObject.search. - """ - # handle me, self and *me, *self - if isinstance(searchdata, basestring): - # handle wrapping of common terms - if searchdata.lower() in ("me", "*me", "self", "*self",): - return self - return self.dbobj.search(searchdata, return_puppet=return_puppet, **kwargs) - - def is_typeclass(self, typeclass, exact=False): - """ - Returns true if this object has this type - OR has a typeclass which is an subclass of - the given typeclass. - - typeclass - can be a class object or the - python path to such an object to match against. - - exact - returns true only if the object's - type is exactly this typeclass, ignoring - parents. - - Returns: Boolean - """ - return self.dbobj.is_typeclass(typeclass, exact=exact) - - def swap_typeclass(self, new_typeclass, clean_attributes=False, no_default=True): - """ - This performs an in-situ swap of the typeclass. This means - that in-game, this object will suddenly be something else. - Player will not be affected. To 'move' a player to a different - object entirely (while retaining this object's type), use - self.player.swap_object(). - - Note that this might be an error prone operation if the - old/new typeclass was heavily customized - your code - might expect one and not the other, so be careful to - bug test your code if using this feature! Often its easiest - to create a new object and just swap the player over to - that one instead. - - Arguments: - new_typeclass (path/classobj) - type to switch to - clean_attributes (bool/list) - will delete all attributes - stored on this object (but not any - of the database fields such as name or - location). You can't get attributes back, - but this is often the safest bet to make - sure nothing in the new typeclass clashes - with the old one. If you supply a list, - only those named attributes will be cleared. - no_default - if this is active, the swapper will not allow for - swapping to a default typeclass in case the given - one fails for some reason. Instead the old one - will be preserved. - Returns: - boolean True/False depending on if the swap worked or not. - - """ - self.dbobj.swap_typeclass(new_typeclass, - clean_attributes=clean_attributes, no_default=no_default) - - def access(self, accessing_obj, access_type='read', default=False, **kwargs): - """ - Determines if another object has permission to access this object - in whatever way. - - accessing_obj (Object)- object trying to access this one - access_type (string) - type of access sought - default (bool) - what to return if no lock of access_type was found - **kwargs - passed to the at_access hook along with the result. - """ - result = self.dbobj.access(accessing_obj, access_type=access_type, default=default) - self.at_access(result, accessing_obj, access_type, **kwargs) - return result - - def check_permstring(self, permstring): - """ - This explicitly checks the given string against this object's - 'permissions' property without involving any locks. - - permstring (string) - permission string that need to match a permission - on the object. (example: 'Builders') - Note that this method does -not- call the at_access hook. - """ - return self.dbobj.check_permstring(permstring) - - ## player hooks - - def basetype_setup(self): - """ - This sets up the basic properties for a player. - Overload this with at_player_creation rather than - changing this method. - - """ - # A basic security setup - lockstring = "examine:perm(Wizards);edit:perm(Wizards);delete:perm(Wizards);boot:perm(Wizards);msg:all()" - self.locks.add(lockstring) - - # The ooc player cmdset - self.cmdset.add_default(_CMDSET_PLAYER, permanent=True) - - def at_player_creation(self): - """ - This is called once, the very first time - the player is created (i.e. first time they - register with the game). It's a good place - to store attributes all players should have, - like configuration values etc. - """ - # set an (empty) attribute holding the characters this player has - lockstring = "attrread:perm(Admins);attredit:perm(Admins);attrcreate:perm(Admins)" - self.attributes.add("_playable_characters", [], lockstring=lockstring) - - def at_init(self): - """ - This is always called whenever this object is initiated -- - that is, whenever it its typeclass is cached from memory. This - happens on-demand first time the object is used or activated - in some way after being created but also after each server - restart or reload. In the case of player objects, this usually - happens the moment the player logs in or reconnects after a - reload. - """ - pass - - # Note that the hooks below also exist in the character object's - # typeclass. You can often ignore these and rely on the character - # ones instead, unless you are implementing a multi-character game - # and have some things that should be done regardless of which - # character is currently connected to this player. - - def at_access(self, result, accessing_obj, access_type, **kwargs): - """ - This is called with the result of an access call, along with - any kwargs used for that call. The return of this method does - not affect the result of the lock check. It can be used e.g. to - customize error messages in a central location or other effects - based on the access result. - """ - pass - - def at_cmdset_get(self, **kwargs): - """ - Called just before cmdsets on this player are requested by the - command handler. If changes need to be done on the fly to the - cmdset before passing them on to the cmdhandler, this is the - place to do it. This is called also if the player currently - have no cmdsets. kwargs are usually not used unless the - cmdset is generated dynamically. - """ - pass - - def at_first_login(self): - """ - Only called once, the very first - time the user logs in. - """ - pass - - def at_pre_login(self): - """ - Called every time the user logs in, just before the actual - login-state is set. - """ - pass - - def _send_to_connect_channel(self, message): - "Helper method for loading the default comm channel" - global _CONNECT_CHANNEL - if not _CONNECT_CHANNEL: - try: - _CONNECT_CHANNEL = ChannelDB.objects.filter(db_key=settings.CHANNEL_CONNECTINFO[0])[0] - except Exception: - logger.log_trace() - now = datetime.datetime.now() - now = "%02i-%02i-%02i(%02i:%02i)" % (now.year, now.month, - now.day, now.hour, now.minute) - if _CONNECT_CHANNEL: - _CONNECT_CHANNEL.tempmsg("[%s, %s]: %s" % (_CONNECT_CHANNEL.key, now, message)) - else: - logger.log_infomsg("[%s]: %s" % (now, message)) - - def at_post_login(self, sessid=None): - """ - Called at the end of the login process, just before letting - them loose. This is called before an eventual Character's - at_post_login hook. - """ - self._send_to_connect_channel("{G%s connected{n" % self.key) - if _MULTISESSION_MODE == 0: - # in this mode we should have only one character available. We - # try to auto-connect to it by calling the @ic command - # (this relies on player.db._last_puppet being set) - self.execute_cmd("@ic", sessid=sessid) - elif _MULTISESSION_MODE == 1: - # in this mode the first session to connect acts like mode 0, - # the following sessions "share" the same view and should - # not perform any actions - if not self.get_all_puppets(): - self.execute_cmd("@ic", sessid=sessid) - elif _MULTISESSION_MODE in (2, 3): - # In this mode we by default end up at a character selection - # screen. We execute look on the player. - self.execute_cmd("look", sessid=sessid) - - def at_disconnect(self, reason=None): - """ - Called just before user is disconnected. - """ - reason = reason and "(%s)" % reason or "" - self._send_to_connect_channel("{R%s disconnected %s{n" % (self.key, reason)) - - def at_post_disconnect(self): - """ - This is called after disconnection is complete. No messages - can be relayed to the player from here. After this call, the - player should not be accessed any more, making this a good - spot for deleting it (in the case of a guest player account, - for example). - """ - pass - - def at_message_receive(self, message, from_obj=None): - """ - Called when any text is emitted to this - object. If it returns False, no text - will be sent automatically. - """ - return True - - def at_message_send(self, message, to_object): - """ - Called whenever this object tries to send text - to another object. Only called if the object supplied - itself as a sender in the msg() call. - """ - pass - - def at_server_reload(self): - """ - This hook is called whenever the server is shutting down for - restart/reboot. If you want to, for example, save non-persistent - properties across a restart, this is the place to do it. - """ - pass - - def at_server_shutdown(self): - """ - This hook is called whenever the server is shutting down fully - (i.e. not for a restart). - """ - pass - -class Guest(Player): - """ - This class is used for guest logins. Unlike Players, Guests and their - characters are deleted after disconnection. - """ - def at_post_login(self, sessid=None): - """ - In theory, guests only have one character regardless of which - MULTISESSION_MODE we're in. They don't get a choice. - """ - self._send_to_connect_channel("{G%s connected{n" % self.key) - self.execute_cmd("@ic", sessid=sessid) - - def at_disconnect(self): - """ - A Guest's characters aren't meant to linger on the server. When a - Guest disconnects, we remove its character. - """ - super(Guest, self).at_disconnect() - characters = self.db._playable_characters - for character in filter(None, characters): - character.delete() - - def at_server_shutdown(self): - """ - We repeat at_disconnect() here just to be on the safe side. - """ - super(Guest, self).at_server_shutdown() - characters = self.db._playable_characters - for character in filter(None, characters): - character.delete() - - def at_post_disconnect(self): - """ - Guests aren't meant to linger on the server, either. We need to wait - until after the Guest disconnects to delete it, though. - """ - super(Guest, self).at_post_disconnect() - self.delete() diff --git a/src/scripts/__init__.py b/src/scripts/__init__.py deleted file mode 100644 index 54d7d20b3..000000000 --- a/src/scripts/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -""" -Makes it easier to import by grouping all relevant things already at this -level. - -You can henceforth import most things directly from src.scripts -Also, the initiated object manager is available as src.scripts.manager. - -""" - -from src.scripts.scripts import * -from src.scripts.models import ScriptDB - -manager = ScriptDB.objects diff --git a/src/scripts/migrations/__init__.py b/src/scripts/migrations/__init__.py deleted file mode 100644 index 40a96afc6..000000000 --- a/src/scripts/migrations/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/src/server/__init__.py b/src/server/__init__.py deleted file mode 100644 index cfe668ac7..000000000 --- a/src/server/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -Makes it easier to import by grouping all relevant things already at this level. - -You can henceforth import most things directly from src.server -Also, the initiated object manager is available as src.server.manager. - -""" - -from src.server.models import * -manager = ServerConfig.objects diff --git a/src/server/caches.py b/src/server/caches.py deleted file mode 100644 index c55d50df3..000000000 --- a/src/server/caches.py +++ /dev/null @@ -1,193 +0,0 @@ -""" -Central caching module. - -""" - -from sys import getsizeof -import os -import threading -from collections import defaultdict - -from src.server.models import ServerConfig -from src.utils.utils import uses_database, to_str, get_evennia_pids - -_GA = object.__getattribute__ -_SA = object.__setattr__ -_DA = object.__delattr__ - -_IS_SUBPROCESS = os.getpid() in get_evennia_pids() -_IS_MAIN_THREAD = threading.currentThread().getName() == "MainThread" - -# -# Set up the cache stores -# - -_ATTR_CACHE = {} -_PROP_CACHE = defaultdict(dict) - -#------------------------------------------------------------ -# Cache key hash generation -#------------------------------------------------------------ - -if uses_database("mysql") and ServerConfig.objects.get_mysql_db_version() < '5.6.4': - # mysql <5.6.4 don't support millisecond precision - _DATESTRING = "%Y:%m:%d-%H:%M:%S:000000" -else: - _DATESTRING = "%Y:%m:%d-%H:%M:%S:%f" - - -def hashid(obj, suffix=""): - """ - Returns a per-class unique hash that combines the object's - class name with its idnum and creation time. This makes this id unique also - between different typeclassed entities such as scripts and - objects (which may still have the same id). - """ - if not obj: - return obj - try: - hid = _GA(obj, "_hashid") - except AttributeError: - try: - date, idnum = _GA(obj, "db_date_created").strftime(_DATESTRING), _GA(obj, "id") - except AttributeError: - try: - # maybe a typeclass, try to go to dbobj - obj = _GA(obj, "dbobj") - date, idnum = _GA(obj, "db_date_created").strftime(_DATESTRING), _GA(obj, "id") - except AttributeError: - # this happens if hashing something like ndb. We have to - # rely on memory adressing in this case. - date, idnum = "InMemory", id(obj) - if not idnum or not date: - # this will happen if setting properties on an object which - # is not yet saved - return None - # we have to remove the class-name's space, for eventual use - # of memcached - hid = "%s-%s-#%s" % (_GA(obj, "__class__"), date, idnum) - hid = hid.replace(" ", "") - # we cache the object part of the hashid to avoid too many - # object lookups - _SA(obj, "_hashid", hid) - # build the complete hashid - hid = "%s%s" % (hid, suffix) - return to_str(hid) - - -#------------------------------------------------------------ -# Cache callback handlers -#------------------------------------------------------------ - -# callback to field pre_save signal (connected in src.server.server) -#def field_pre_save(sender, instance=None, update_fields=None, raw=False, **kwargs): -# """ -# Called at the beginning of the field save operation. The save method -# must be called with the update_fields keyword in order to be most efficient. -# This method should NOT save; rather it is the save() that triggers this -# function. Its main purpose is to allow to plug-in a save handler and oob -# handlers. -# """ -# if raw: -# return -# if update_fields: -# # this is a list of strings at this point. We want field objects -# update_fields = (_GA(_GA(instance, "_meta"), "get_field_by_name")(field)[0] for field in update_fields) -# else: -# # meta.fields are already field objects; get them all -# update_fields = _GA(_GA(instance, "_meta"), "fields") -# for field in update_fields: -# fieldname = field.name -# handlername = "_at_%s_presave" % fieldname -# handler = _GA(instance, handlername) if handlername in _GA(sender, '__dict__') else None -# if callable(handler): -# handler() - - -def field_post_save(sender, instance=None, update_fields=None, raw=False, **kwargs): - """ - Called at the beginning of the field save operation. The save method - must be called with the update_fields keyword in order to be most efficient. - This method should NOT save; rather it is the save() that triggers this - function. Its main purpose is to allow to plug-in a save handler and oob - handlers. - """ - if raw: - return - if update_fields: - # this is a list of strings at this point. We want field objects - update_fields = (_GA(_GA(instance, "_meta"), "get_field_by_name")(field)[0] for field in update_fields) - else: - # meta.fields are already field objects; get them all - update_fields = _GA(_GA(instance, "_meta"), "fields") - for field in update_fields: - fieldname = field.name - handlername = "_at_%s_postsave" % fieldname - handler = _GA(instance, handlername) if handlername in _GA(sender, '__dict__') else None - if callable(handler): - handler() - trackerhandler = _GA(instance, "_trackerhandler") if "_trackerhandler" in _GA(instance, '__dict__') else None - if trackerhandler: - trackerhandler.update(fieldname, _GA(instance, fieldname)) - -#------------------------------------------------------------ -# Attribute lookup cache -#------------------------------------------------------------ - -def get_attr_cache(obj): - "Retrieve lookup cache" - hid = hashid(obj) - return _ATTR_CACHE.get(hid, None) - - -def set_attr_cache(obj, store): - "Set lookup cache" - global _ATTR_CACHE - hid = hashid(obj) - _ATTR_CACHE[hid] = store - -#------------------------------------------------------------ -# Property cache - this is a generic cache for properties stored on models. -#------------------------------------------------------------ - -# access methods - -def get_prop_cache(obj, propname): - "retrieve data from cache" - hid = hashid(obj, "-%s" % propname) - return _PROP_CACHE[hid].get(propname, None) if hid else None - - -def set_prop_cache(obj, propname, propvalue): - "Set property cache" - hid = hashid(obj, "-%s" % propname) - if hid: - _PROP_CACHE[hid][propname] = propvalue - - -def del_prop_cache(obj, propname): - "Delete element from property cache" - hid = hashid(obj, "-%s" % propname) - if hid: - if propname in _PROP_CACHE[hid]: - del _PROP_CACHE[hid][propname] - - -def flush_prop_cache(): - "Clear property cache" - global _PROP_CACHE - _PROP_CACHE = defaultdict(dict) - - -def get_cache_sizes(): - """ - Get cache sizes, expressed in number of objects and memory size in MB - """ - global _ATTR_CACHE, _PROP_CACHE - attr_n = len(_ATTR_CACHE) - attr_mb = sum(getsizeof(obj) for obj in _ATTR_CACHE) / 1024.0 - prop_n = sum(len(dic) for dic in _PROP_CACHE.values()) - prop_mb = sum(sum([getsizeof(obj) for obj in dic.values()]) for dic in _PROP_CACHE.values()) / 1024.0 - return (attr_n, attr_mb), (prop_n, prop_mb) - - diff --git a/src/server/migrations/__init__.py b/src/server/migrations/__init__.py deleted file mode 100644 index 40a96afc6..000000000 --- a/src/server/migrations/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/src/server/oob_cmds.py b/src/server/oob_cmds.py deleted file mode 100644 index 42a762c3b..000000000 --- a/src/server/oob_cmds.py +++ /dev/null @@ -1,221 +0,0 @@ -""" -Out-of-band default plugin commands available for OOB handler. - -This module implements commands as defined by the MSDP standard -(http://tintin.sourceforge.net/msdp/), but is independent of the -actual transfer protocol (webclient, MSDP, GMCP etc). - -This module is pointed to by settings.OOB_PLUGIN_MODULES. All functions -(not classes) defined globally in this module will be made available -to the oob mechanism. - -oob functions have the following call signature: - function(oobhandler, session, *args, **kwargs) - -where oobhandler is a back-reference to the central OOB_HANDLER -instance and session is the active session to get return data. - -The function names are not case-sensitive (this allows for names -like "LIST" which would otherwise collide with Python builtins). - -A function named OOB_ERROR will retrieve error strings if it is -defined. It will get the error message as its 3rd argument. - -Data is usually returned via - session.msg(oob=(cmdname, (args,), {kwargs})) -Note that args, kwargs must be iterable/dict, non-iterables will -be interpreted as a new command name. - -""" - -from django.conf import settings -_GA = object.__getattribute__ -_SA = object.__setattr__ -_NA_SEND = lambda o: "N/A" - -#------------------------------------------------------------ -# All OOB commands must be on the form -# cmdname(oobhandler, session, *args, **kwargs) -#------------------------------------------------------------ - -def OOB_ERROR(oobhandler, session, errmsg, *args, **kwargs): - """ - A function with this name is special and is called by the oobhandler when an error - occurs already at the execution stage (such as the oob function - not being recognized or having the wrong args etc). - """ - session.msg(oob=("err", ("ERROR " + errmsg,))) - - -def ECHO(oobhandler, session, *args, **kwargs): - "Test/debug function, simply returning the args and kwargs" - session.msg(oob=("echo", args, kwargs)) - -##OOB{"SEND":"CHARACTER_NAME"} -def SEND(oobhandler, session, *args, **kwargs): - """ - This function directly returns the value of the given variable to the - session. - """ - obj = session.get_puppet_or_player() - ret = {} - if obj: - for name in (a.upper() for a in args if a): - try: - value = OOB_SENDABLE.get(name, _NA_SEND)(obj) - ret[name] = value - except Exception, e: - ret[name] = str(e) - session.msg(oob=("send", ret)) - else: - session.msg(oob=("err", ("You must log in first.",))) - -##OOB{"REPORT":"TEST"} -def REPORT(oobhandler, session, *args, **kwargs): - """ - This creates a tracker instance to track the data given in *args. - - The tracker will return with a oob structure - oob={"report":["attrfieldname", (args,), {kwargs}} - - Note that the data name is assumed to be a field is it starts with db_* - and an Attribute otherwise. - - "Example of tracking changes to the db_key field and the desc" Attribite: - REPORT(oobhandler, session, "CHARACTER_NAME", ) - """ - obj = session.get_puppet_or_player() - if obj: - for name in (a.upper() for a in args if a): - trackname = OOB_REPORTABLE.get(name, None) - if not trackname: - session.msg(oob=("err", ("No Reportable property '%s'. Use LIST REPORTABLE_VARIABLES." % trackname,))) - elif trackname.startswith("db_"): - oobhandler.track_field(obj, session.sessid, trackname) - else: - oobhandler.track_attribute(obj, session.sessid, trackname) - else: - session.msg(oob=("err", ("You must log in first.",))) - - -##OOB{"UNREPORT": "TEST"} -def UNREPORT(oobhandler, session, *args, **kwargs): - """ - This removes tracking for the given data given in *args. - """ - obj = session.get_puppet_or_player() - if obj: - for name in (a.upper() for a in args if a): - trackname = OOB_REPORTABLE.get(name, None) - if not trackname: - session.msg(oob=("err", ("No Un-Reportable property '%s'. Use LIST REPORTED_VALUES." % name,))) - elif trackname.startswith("db_"): - oobhandler.untrack_field(obj, session.sessid, trackname) - else: # assume attribute - oobhandler.untrack_attribute(obj, session.sessid, trackname) - else: - session.msg(oob=("err", ("You must log in first.",))) - - -##OOB{"LIST":"COMMANDS"} -def LIST(oobhandler, session, mode, *args, **kwargs): - """ - List available properties. Mode is the type of information - desired: - "COMMANDS" Request an array of commands supported - by the server. - "LISTS" Request an array of lists supported - by the server. - "CONFIGURABLE_VARIABLES" Request an array of variables the client - can configure. - "REPORTABLE_VARIABLES" Request an array of variables the server - will report. - "REPORTED_VARIABLES" Request an array of variables currently - being reported. - "SENDABLE_VARIABLES" Request an array of variables the server - will send. - """ - mode = mode.upper() - if mode == "COMMANDS": - session.msg(oob=("list", ("COMMANDS", - "LIST", - "REPORT", - "UNREPORT", - # "RESET", - "SEND"))) - elif mode == "LISTS": - session.msg(oob=("list", ("LISTS", - "REPORTABLE_VARIABLES", - "REPORTED_VARIABLES", - # "CONFIGURABLE_VARIABLES", - "SENDABLE_VARIABLES"))) - elif mode == "REPORTABLE_VARIABLES": - session.msg(oob=("list", ("REPORTABLE_VARIABLES",) + - tuple(key for key in OOB_REPORTABLE.keys()))) - elif mode == "REPORTED_VARIABLES": - # we need to check so as to use the right return value depending on if it is - # an Attribute (identified by tracking the db_value field) or a normal database field - reported = oobhandler.get_all_tracked(session) - reported = [stored[2] if stored[2] != "db_value" else stored[4][0] for stored in reported] - session.msg(oob=("list", ["REPORTED_VARIABLES"] + reported)) - elif mode == "SENDABLE_VARIABLES": - session.msg(oob=("list", ("SENDABLE_VARIABLES",) + - tuple(key for key in OOB_REPORTABLE.keys()))) - elif mode == "CONFIGURABLE_VARIABLES": - # Not implemented (game specific) - pass - else: - session.msg(oob=("err", ("LIST", "Unsupported mode",))) - -def _repeat_callback(oobhandler, session, *args, **kwargs): - "Set up by REPEAT" - session.msg(oob=("repeat", ("Repeat!",))) - -##OOB{"REPEAT":10} -def REPEAT(oobhandler, session, interval, *args, **kwargs): - """ - Test command for the repeat functionality. Note that the args/kwargs - must not be db objects (or anything else non-picklable), rather use - dbrefs if so needed. The callback must be defined globally and - will be called as - callback(oobhandler, session, *args, **kwargs) - """ - oobhandler.repeat(None, session.sessid, interval, _repeat_callback, *args, **kwargs) - - -##OOB{"UNREPEAT":10} -def UNREPEAT(oobhandler, session, interval): - """ - Disable repeating callback - """ - oobhandler.unrepeat(None, session.sessid, interval) - - -# Mapping for how to retrieve each property name. -# Each entry should point to a callable that gets the interesting object as -# input and returns the relevant value. - -# MSDP recommends the following standard name mappings for general compliance: -# "CHARACTER_NAME", "SERVER_ID", "SERVER_TIME", "AFFECTS", "ALIGNMENT", "EXPERIENCE", "EXPERIENCE_MAX", "EXPERIENCE_TNL", -# "HEALTH", "HEALTH_MAX", "LEVEL", "RACE", "CLASS", "MANA", "MANA_MAX", "WIMPY", "PRACTICE", "MONEY", "MOVEMENT", -# "MOVEMENT_MAX", "HITROLL", "DAMROLL", "AC", "STR", "INT", "WIS", "DEX", "CON", "OPPONENT_HEALTH", "OPPONENT_HEALTH_MAX", -# "OPPONENT_LEVEL", "OPPONENT_NAME", "AREA_NAME", "ROOM_EXITS", "ROOM_VNUM", "ROOM_NAME", "WORLD_TIME", "CLIENT_ID", -# "CLIENT_VERSION", "PLUGIN_ID", "ANSI_COLORS", "XTERM_256_COLORS", "UTF_8", "SOUND", "MXP", "BUTTON_1", "BUTTON_2", -# "BUTTON_3", "BUTTON_4", "BUTTON_5", "GAUGE_1", "GAUGE_2","GAUGE_3", "GAUGE_4", "GAUGE_5" - -OOB_SENDABLE = { - "CHARACTER_NAME": lambda o: o.key, - "SERVER_ID": lambda o: settings.SERVERNAME, - "ROOM_NAME": lambda o: o.db_location.key, - "ANSI_COLORS": lambda o: True, - "XTERM_256_COLORS": lambda o: True, - "UTF_8": lambda o: True - } - -# mapping for which properties may be tracked. Each value points either to a database field -# (starting with db_*) or an Attribute name. -OOB_REPORTABLE = { - "CHARACTER_NAME": "db_key", - "ROOM_NAME": "db_location", - "TEST" : "test" - } diff --git a/src/server/oobhandler.py b/src/server/oobhandler.py deleted file mode 100644 index 1dc4aca88..000000000 --- a/src/server/oobhandler.py +++ /dev/null @@ -1,426 +0,0 @@ -""" -OOBHandler - Out Of Band Handler - -The OOBHandler.execute_cmd is called by the sessionhandler when it detects -an OOB instruction (exactly how this looked depends on the protocol; at this -point all oob calls should look the same) - -The handler pieces of functionality: - - function execution - the oob protocol can execute a function directly on - the server. The available functions must be defined - as global functions in settings.OOB_PLUGIN_MODULES. - repeat func execution - the oob protocol can request a given function be - executed repeatedly at a regular interval. This - uses an internal script pool. - tracking - the oob protocol can request Evennia to track changes to - fields on objects, as well as changes in Attributes. This is - done by dynamically adding tracker-objects on entities. The - behaviour of those objects can be customized by adding new - tracker classes in settings.OOB_PLUGIN_MODULES. - -What goes into the OOB_PLUGIN_MODULES is a (list of) modules that contains -the working server-side code available to the OOB system: oob functions and -tracker classes. - -oob functions have the following call signature: - function(caller, session, *args, **kwargs) - -oob trackers should inherit from the OOBTracker class (in this - module) and implement a minimum of the same functionality. - -If a function named "oob_error" is given, this will be called with error -messages. - -""" - -from inspect import isfunction -from twisted.internet.defer import inlineCallbacks -from django.conf import settings -from src.server.models import ServerConfig -from src.server.sessionhandler import SESSIONS -#from src.scripts.scripts import Script -#from src.utils.create import create_script -from src.scripts.tickerhandler import Ticker, TickerPool, TickerHandler -from src.utils.dbserialize import dbserialize, dbunserialize, pack_dbobj, unpack_dbobj -from src.utils import logger -from src.utils.utils import all_from_module, make_iter, to_str - -_SA = object.__setattr__ -_GA = object.__getattribute__ -_DA = object.__delattr__ - -# load resources from plugin module -_OOB_FUNCS = {} -for mod in make_iter(settings.OOB_PLUGIN_MODULES): - _OOB_FUNCS.update(dict((key.lower(), func) for key, func in all_from_module(mod).items() if isfunction(func))) - -# get custom error method or use the default -_OOB_ERROR = _OOB_FUNCS.get("oob_error", None) -if not _OOB_ERROR: - # create default oob error message function - def oob_error(oobhandler, session, errmsg, *args, **kwargs): - "Error wrapper" - session.msg(oob=("err", ("ERROR ", errmsg))) - _OOB_ERROR = oob_error - - -# -# TrackerHandler is assigned to objects that should notify themselves to -# the OOB system when some property changes. This is never assigned manually -# but automatically through the OOBHandler. -# - -class TrackerHandler(object): - """ - This object is dynamically assigned to objects whenever one of its fields - are to be tracked. It holds an internal dictionary mapping to the fields - on that object. Each field can be tracked by any number of trackers (each - tied to a different callback). - """ - def __init__(self, obj): - """ - This is initiated and stored on the object as a - property _trackerhandler. - """ - try: - obj = obj.dbobj - except AttributeError: - pass - self.obj = obj - self.ntrackers = 0 - # initiate store only with valid on-object fieldnames - self.tracktargets = dict((key, {}) - for key in _GA(_GA(self.obj, "_meta"), "get_all_field_names")()) - - def add(self, fieldname, tracker): - """ - Add tracker to the handler. Raises KeyError if fieldname - does not exist. - """ - trackerkey = tracker.__class__.__name__ - self.tracktargets[fieldname][trackerkey] = tracker - self.ntrackers += 1 - - def remove(self, fieldname, trackerclass, *args, **kwargs): - """ - Remove identified tracker from TrackerHandler. - Raises KeyError if tracker is not found. - """ - trackerkey = trackerclass.__name__ - tracker = self.tracktargets[fieldname][trackerkey] - try: - tracker.at_remove(*args, **kwargs) - except Exception: - logger.log_trace() - del self.tracktargets[fieldname][trackerkey] - self.ntrackers -= 1 - if self.ntrackers <= 0: - # if there are no more trackers, clean this handler - del self - - def update(self, fieldname, new_value): - """ - Called by the field when it updates to a new value - """ - for tracker in self.tracktargets[fieldname].values(): - try: - tracker.update(new_value) - except Exception: - logger.log_trace() - - -# On-object Trackers to load with TrackerHandler - -class TrackerBase(object): - """ - Base class for OOB Tracker objects. Inherit from this - to define custom trackers. - """ - def __init__(self, *args, **kwargs): - pass - - def update(self, *args, **kwargs): - "Called by tracked objects" - pass - - def at_remove(self, *args, **kwargs): - "Called when tracker is removed" - pass - - -class ReportFieldTracker(TrackerBase): - """ - Tracker that passively sends data to a stored sessid whenever - a named database field changes. The TrackerHandler calls this with - the correct arguments. - """ - def __init__(self, oobhandler, fieldname, sessid, *args, **kwargs): - """ - name - name of entity to track, such as "db_key" - sessid - sessid of session to report to - """ - self.oobhandler = oobhandler - self.fieldname = fieldname - self.sessid = sessid - - def update(self, new_value, *args, **kwargs): - "Called by cache when updating the tracked entitiy" - # use oobhandler to relay data - try: - # we must never relay objects across the amp, only text data. - new_value = new_value.key - except AttributeError: - new_value = to_str(new_value, force_string=True) - kwargs[self.fieldname] = new_value - # this is a wrapper call for sending oob data back to session - self.oobhandler.msg(self.sessid, "report", *args, **kwargs) - - -class ReportAttributeTracker(TrackerBase): - """ - Tracker that passively sends data to a stored sessid whenever - the Attribute updates. Since the field here is always "db_key", - we instead store the name of the attribute to return. - """ - def __init__(self, oobhandler, fieldname, sessid, attrname, *args, **kwargs): - """ - attrname - name of attribute to track - sessid - sessid of session to report to - """ - self.oobhandler = oobhandler - self.attrname = attrname - self.sessid = sessid - - def update(self, new_value, *args, **kwargs): - "Called by cache when attribute's db_value field updates" - try: - new_value = new_value.dbobj - except AttributeError: - new_value = to_str(new_value, force_string=True) - kwargs[self.attrname] = new_value - # this is a wrapper call for sending oob data back to session - self.oobhandler.msg(self.sessid, "report", *args, **kwargs) - - - -# Ticker of auto-updating objects - -class OOBTicker(Ticker): - """ - Version of Ticker that executes an executable rather than trying to call - a hook method. - """ - @inlineCallbacks - def _callback(self): - "See original for more info" - for key, (_, args, kwargs) in self.subscriptions.items(): - # args = (sessid, callback_function) - session = SESSIONS.session_from_sessid(args[0]) - try: - # execute the oob callback - yield args[1](OOB_HANDLER, session, *args[2:], **kwargs) - except Exception: - logger.log_trace() - -class OOBTickerPool(TickerPool): - ticker_class = OOBTicker - -class OOBTickerHandler(TickerHandler): - ticker_pool_class = OOBTickerPool - - -# Main OOB Handler - -class OOBHandler(object): - """ - The OOBHandler maintains all dynamic on-object oob hooks. It will store the - creation instructions and and re-apply them at a server reload (but - not after a server shutdown) - """ - def __init__(self): - """ - Initialize handler - """ - self.sessionhandler = SESSIONS - self.oob_tracker_storage = {} - self.tickerhandler = OOBTickerHandler("oob_ticker_storage") - - def save(self): - """ - Save the command_storage as a serialized string into a temporary - ServerConf field - """ - if self.oob_tracker_storage: - #print "saved tracker_storage:", self.oob_tracker_storage - ServerConfig.objects.conf(key="oob_tracker_storage", - value=dbserialize(self.oob_tracker_storage)) - self.tickerhandler.save() - - def restore(self): - """ - Restore the command_storage from database and re-initialize the handler from storage.. This is - only triggered after a server reload, not after a shutdown-restart - """ - # load stored command instructions and use them to re-initialize handler - tracker_storage = ServerConfig.objects.conf(key="oob_tracker_storage") - if tracker_storage: - self.oob_tracker_storage = dbunserialize(tracker_storage) - for (obj, sessid, fieldname, trackerclass, args, kwargs) in self.oob_tracker_storage.values(): - #print "restoring tracking:",obj, sessid, fieldname, trackerclass - self._track(unpack_dbobj(obj), sessid, fieldname, trackerclass, *args, **kwargs) - # make sure to purge the storage - ServerConfig.objects.conf(key="oob_tracker_storage", delete=True) - self.tickerhandler.restore() - - def _track(self, obj, sessid, propname, trackerclass, *args, **kwargs): - """ - Create an OOB obj of class _oob_MAPPING[tracker_key] on obj. args, - kwargs will be used to initialize the OOB hook before adding - it to obj. - If propname is not given, but the OOB has a class property - named as propname, this will be used as the property name when assigning - the OOB to obj, otherwise tracker_key is used as the property name. - """ - try: - obj = obj.dbobj - except AttributeError: - pass - - if not "_trackerhandler" in _GA(obj, "__dict__"): - # assign trackerhandler to object - _SA(obj, "_trackerhandler", TrackerHandler(obj)) - # initialize object - tracker = trackerclass(self, propname, sessid, *args, **kwargs) - _GA(obj, "_trackerhandler").add(propname, tracker) - # store calling arguments as a pickle for retrieval later - obj_packed = pack_dbobj(obj) - storekey = (obj_packed, sessid, propname) - stored = (obj_packed, sessid, propname, trackerclass, args, kwargs) - self.oob_tracker_storage[storekey] = stored - #print "_track:", obj, id(obj), obj.__dict__ - - def _untrack(self, obj, sessid, propname, trackerclass, *args, **kwargs): - """ - Remove the OOB from obj. If oob implements an - at_delete hook, this will be called with args, kwargs - """ - try: - obj = obj.dbobj - except AttributeError: - pass - try: - # call at_remove hook on the trackerclass - _GA(obj, "_trackerhandler").remove(propname, trackerclass, *args, **kwargs) - except AttributeError: - pass - # remove the pickle from storage - store_key = (pack_dbobj(obj), sessid, propname) - self.oob_tracker_storage.pop(store_key, None) - - def get_all_tracked(self, session): - """ - Get the names of all variables this session is tracking. - """ - sessid = session.sessid - return [stored for key, stored in self.oob_tracker_storage.items() if key[1] == sessid] - - def track_field(self, obj, sessid, field_name, trackerclass=ReportFieldTracker): - """ - Shortcut wrapper method for specifically tracking a database field. - Takes the tracker class as argument. - """ - # all database field names starts with db_* - field_name = field_name if field_name.startswith("db_") else "db_%s" % field_name - self._track(obj, sessid, field_name, trackerclass, field_name) - - def untrack_field(self, obj, sessid, field_name, trackerclass=ReportFieldTracker): - """ - Shortcut for untracking a database field. Uses OOBTracker by defualt - """ - field_name = field_name if field_name.startswith("db_") else "db_%s" % field_name - self._untrack(obj, sessid, field_name, trackerclass) - - def track_attribute(self, obj, sessid, attr_name, trackerclass=ReportAttributeTracker): - """ - Shortcut wrapper method for specifically tracking the changes of an - Attribute on an object. Will create a tracker on the Attribute - Object and name in a way the Attribute expects. - """ - # get the attribute object if we can - try: - attrobj = obj.dbobj - except AttributeError: - pass - attrobj = obj.attributes.get(attr_name, return_obj=True) - #print "track_attribute attrobj:", attrobj, id(attrobj) - if attrobj: - self._track(attrobj, sessid, "db_value", trackerclass, attr_name) - - def untrack_attribute(self, obj, sessid, attr_name, trackerclass=ReportAttributeTracker): - """ - Shortcut for deactivating tracking for a given attribute. - """ - try: - obj = obj.dbobj - except AttributeError: - pass - attrobj = obj.attributes.get(attr_name, return_obj=True) - if attrobj: - self._untrack(attrobj, sessid, "db_value", trackerclass, attr_name) - - def repeat(self, obj, sessid, interval=20, callback=None, *args, **kwargs): - """ - Start a repeating action. Every interval seconds, trigger - callback(*args, **kwargs). The callback is called with - args and kwargs; note that *args and **kwargs may not contain - anything un-picklable (use dbrefs if wanting to use objects). - """ - self.tickerhandler.add(obj, interval, sessid, callback, *args, **kwargs) - - def unrepeat(self, obj, sessid, interval=20): - """ - Stop a repeating action - """ - self.tickerhandler.remove(obj, interval) - - - # access method - called from session.msg() - - def execute_cmd(self, session, func_key, *args, **kwargs): - """ - Retrieve oobfunc from OOB_FUNCS and execute it immediately - using *args and **kwargs - """ - oobfunc = _OOB_FUNCS.get(func_key, None) - if not oobfunc: - # function not found - errmsg = "OOB Error: function '%s' not recognized." % func_key - if _OOB_ERROR: - _OOB_ERROR(self, session, errmsg, *args, **kwargs) - logger.log_trace() - else: - logger.log_trace(errmsg) - return - - # execute the found function - try: - #print "OOB execute_cmd:", session, func_key, args, kwargs, _OOB_FUNCS.keys() - oobfunc(self, session, *args, **kwargs) - except Exception, err: - errmsg = "OOB Error: Exception in '%s'(%s, %s):\n%s" % (func_key, args, kwargs, err) - if _OOB_ERROR: - _OOB_ERROR(self, session, errmsg, *args, **kwargs) - logger.log_trace(errmsg) - raise Exception(errmsg) - - def msg(self, sessid, funcname, *args, **kwargs): - "Shortcut to force-send an OOB message through the oobhandler to a session" - session = self.sessionhandler.session_from_sessid(sessid) - #print "oobhandler msg:", sessid, session, funcname, args, kwargs - if session: - session.msg(oob=(funcname, args, kwargs)) - - -# access object -OOB_HANDLER = OOBHandler() diff --git a/src/server/portal/__init__.py b/src/server/portal/__init__.py deleted file mode 100644 index 40a96afc6..000000000 --- a/src/server/portal/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/src/server/portal/imc2lib/__init__.py b/src/server/portal/imc2lib/__init__.py deleted file mode 100644 index 40a96afc6..000000000 --- a/src/server/portal/imc2lib/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/src/server/portal/msdp.py b/src/server/portal/msdp.py deleted file mode 100644 index aa36bd65d..000000000 --- a/src/server/portal/msdp.py +++ /dev/null @@ -1,243 +0,0 @@ -""" - -MSDP (Mud Server Data Protocol) - -This implements the MSDP protocol as per -http://tintin.sourceforge.net/msdp/. MSDP manages out-of-band -communication between the client and server, for updating health bars -etc. - -""" -import re -from src.utils.utils import to_str - -# MSDP-relevant telnet cmd/opt-codes -MSDP = chr(69) -MSDP_VAR = chr(1) -MSDP_VAL = chr(2) -MSDP_TABLE_OPEN = chr(3) -MSDP_TABLE_CLOSE = chr(4) -MSDP_ARRAY_OPEN = chr(5) -MSDP_ARRAY_CLOSE = chr(6) - -IAC = chr(255) -SB = chr(250) -SE = chr(240) - -force_str = lambda inp: to_str(inp, force_string=True) - -# pre-compiled regexes -# returns 2-tuple -regex_array = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL, - MSDP_ARRAY_OPEN, - MSDP_ARRAY_CLOSE)) -# returns 2-tuple (may be nested) -regex_table = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL, - MSDP_TABLE_OPEN, - MSDP_TABLE_CLOSE)) -regex_var = re.compile(MSDP_VAR) -regex_val = re.compile(MSDP_VAL) - - -# Msdp object handler - -class Msdp(object): - """ - Implements the MSDP protocol. - """ - - def __init__(self, protocol): - """ - Initiates by storing the protocol - on itself and trying to determine - if the client supports MSDP. - """ - self.protocol = protocol - self.protocol.protocol_flags['MSDP'] = False - self.protocol.negotiationMap[MSDP] = self.msdp_to_evennia - self.protocol.will(MSDP).addCallbacks(self.do_msdp, self.no_msdp) - self.msdp_reported = {} - - def no_msdp(self, option): - "No msdp supported or wanted" - self.protocol.handshake_done() - - def do_msdp(self, option): - """ - Called when client confirms that it can do MSDP. - """ - self.protocol.protocol_flags['MSDP'] = True - self.protocol.handshake_done() - - def evennia_to_msdp(self, cmdname, *args, **kwargs): - """ - handle return data from cmdname by converting it to - a proper msdp structure. data can either be a single value (will be - converted to a string), a list (will be converted to an MSDP_ARRAY), - or a dictionary (will be converted to MSDP_TABLE). - - OBS - there is no actual use of arrays and tables in the MSDP - specification or default commands -- are returns are implemented - as simple lists or named lists (our name for them here, these - un-bounded structures are not named in the specification). So for - now, this routine will not explicitly create arrays nor tables, - although there are helper methods ready should it be needed in - the future. - """ - - def make_table(name, **kwargs): - "build a table that may be nested with other tables or arrays." - string = MSDP_VAR + force_str(name) + MSDP_VAL + MSDP_TABLE_OPEN - for key, val in kwargs.items(): - if isinstance(val, dict): - string += make_table(string, key, **val) - elif hasattr(val, '__iter__'): - string += make_array(string, key, *val) - else: - string += MSDP_VAR + force_str(key) + MSDP_VAL + force_str(val) - string += MSDP_TABLE_CLOSE - return string - - def make_array(name, *args): - "build a array. Arrays may not nest tables by definition." - string = MSDP_VAR + force_str(name) + MSDP_ARRAY_OPEN - string += MSDP_VAL.join(force_str(arg) for arg in args) - string += MSDP_ARRAY_CLOSE - return string - - def make_list(name, *args): - "build a simple list - an array without start/end markers" - string = MSDP_VAR + force_str(name) - string += MSDP_VAL.join(force_str(arg) for arg in args) - return string - - def make_named_list(name, **kwargs): - "build a named list - a table without start/end markers" - string = MSDP_VAR + force_str(name) - for key, val in kwargs.items(): - string += MSDP_VAR + force_str(key) + MSDP_VAL + force_str(val) - return string - - # Default MSDP commands - - print "MSDP outgoing:", cmdname, args, kwargs - - cupper = cmdname.upper() - if cupper == "LIST": - if args: - args = list(args) - mode = args.pop(0).upper() - self.data_out(make_array(mode, *args)) - elif cupper == "REPORT": - self.data_out(make_list("REPORT", *args)) - elif cupper == "UNREPORT": - self.data_out(make_list("UNREPORT", *args)) - elif cupper == "RESET": - self.data_out(make_list("RESET", *args)) - elif cupper == "SEND": - self.data_out(make_named_list("SEND", **kwargs)) - else: - # return list or named lists. - msdp_string = "" - if args: - msdp_string += make_list(cupper, *args) - if kwargs: - msdp_string += make_named_list(cupper, **kwargs) - self.data_out(msdp_string) - - def msdp_to_evennia(self, data): - """ - Handle a client's requested negotiation, converting - it into a function mapping - either one of the MSDP - default functions (LIST, SEND etc) or a custom one - in OOB_FUNCS dictionary. command names are case-insensitive. - - varname, var --> mapped to function varname(var) - arrayname, array --> mapped to function arrayname(*array) - tablename, table --> mapped to function tablename(**table) - - Note: Combinations of args/kwargs to one function is not supported - in this implementation (it complicates the code for limited - gain - arrayname(*array) is usually as complex as anyone should - ever need to go anyway (I hope!). - - """ - tables = {} - arrays = {} - variables = {} - - if hasattr(data, "__iter__"): - data = "".join(data) - - #logger.log_infomsg("MSDP SUBNEGOTIATION: %s" % data) - - for key, table in regex_table.findall(data): - tables[key] = {} - for varval in regex_var.split(table): - parts = regex_val.split(varval) - tables[key].expand({parts[0]: tuple(parts[1:]) if len(parts) > 1 else ("",)}) - for key, array in regex_array.findall(data): - arrays[key] = [] - for val in regex_val.split(array): - arrays[key].append(val) - arrays[key] = tuple(arrays[key]) - for varval in regex_var.split(regex_array.sub("", regex_table.sub("", data))): - # get remaining varvals after cleaning away tables/arrays - parts = regex_val.split(varval) - variables[parts[0].upper()] = tuple(parts[1:]) if len(parts) > 1 else ("", ) - - #print "MSDP: table, array, variables:", tables, arrays, variables - - # all variables sent through msdp to Evennia are considered commands - # with arguments. There are three forms of commands possible - # through msdp: - # - # VARNAME VAR -> varname(var) - # ARRAYNAME VAR VAL VAR VAL VAR VAL ENDARRAY -> arrayname(val,val,val) - # TABLENAME TABLE VARNAME VAL VARNAME VAL ENDTABLE -> - # tablename(varname=val, varname=val) - # - - # default MSDP functions - if "LIST" in variables: - self.data_in("list", *variables.pop("LIST")) - if "REPORT" in variables: - self.data_in("report", *variables.pop("REPORT")) - if "REPORT" in arrays: - self.data_in("report", *(arrays.pop("REPORT"))) - if "UNREPORT" in variables: - self.data_in("unreport", *(arrays.pop("UNREPORT"))) - if "RESET" in variables: - self.data_in("reset", *variables.pop("RESET")) - if "RESET" in arrays: - self.data_in("reset", *(arrays.pop("RESET"))) - if "SEND" in variables: - self.data_in("send", *variables.pop("SEND")) - if "SEND" in arrays: - self.data_in("send", *(arrays.pop("SEND"))) - - # if there are anything left consider it a call to a custom function - - for varname, var in variables.items(): - # a simple function + argument - self.data_in(varname, (var,)) - for arrayname, array in arrays.items(): - # we assume the array are multiple arguments to the function - self.data_in(arrayname, *array) - for tablename, table in tables.items(): - # we assume tables are keyword arguments to the function - self.data_in(tablename, **table) - - def data_out(self, msdp_string): - """ - Return a msdp-valid subnegotiation across the protocol. - """ - #print "msdp data_out (without IAC SE):", msdp_string - self.protocol ._write(IAC + SB + MSDP + force_str(msdp_string) + IAC + SE) - - def data_in(self, funcname, *args, **kwargs): - """ - Send oob data to Evennia - """ - #print "msdp data_in:", funcname, args, kwargs - self.protocol.data_in(text=None, oob=(funcname, args, kwargs)) diff --git a/src/server/tests.py b/src/server/tests.py deleted file mode 100644 index 6ad24bf01..000000000 --- a/src/server/tests.py +++ /dev/null @@ -1,76 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -Unit testing of the 'objects' Evennia component. - -Runs as part of the Evennia's test suite with 'manage.py test" - -Please add new tests to this module as needed. - -Guidelines: - A 'test case' is testing a specific component and is defined as a class - inheriting from unittest.TestCase. The test case class can have a method - setUp() that creates and sets up the testing environment. - All methods inside the test case class whose names start with 'test' are - used as test methods by the runner. Inside the test methods, special member - methods assert*() are used to test the behaviour. -""" - -import sys -import glob - -try: - from django.utils.unittest import TestCase -except ImportError: - from django.test import TestCase -try: - from django.utils import unittest -except ImportError: - import unittest - -from django.conf import settings -from django.test.simple import DjangoTestSuiteRunner -from src.utils.utils import mod_import - - -class EvenniaTestSuiteRunner(DjangoTestSuiteRunner): - """ - This test runner only runs tests on the apps specified in src/ and game/ to - avoid running the large number of tests defined by Django - """ - def build_suite(self, test_labels, extra_tests=None, **kwargs): - """ - Build a test suite for Evennia. test_labels is a list of apps to test. - If not given, a subset of settings.INSTALLED_APPS will be used. - """ - if not test_labels: - test_labels = [applabel.rsplit('.', 1)[1] for applabel in settings.INSTALLED_APPS - if (applabel.startswith('src.') or applabel.startswith('game.'))] - return super(EvenniaTestSuiteRunner, self).build_suite(test_labels, extra_tests=extra_tests, **kwargs) - - -def suite(): - """ - This function is called automatically by the django test runner. - This also collates tests from packages that are not formally django applications. - """ - from src.locks import tests as locktests - from src.utils import tests as utiltests - from src.commands.default import tests as commandtests - - tsuite = unittest.TestSuite() - tsuite.addTest(unittest.defaultTestLoader.loadTestsFromModule(sys.modules[__name__])) - - # test modules from non-django apps - tsuite.addTest(unittest.defaultTestLoader.loadTestsFromModule(commandtests)) - tsuite.addTest(unittest.defaultTestLoader.loadTestsFromModule(locktests)) - tsuite.addTest(unittest.defaultTestLoader.loadTestsFromModule(utiltests)) - - for path in glob.glob("../src/tests/test_*.py"): - testmod = mod_import(path) - tsuite.addTest(unittest.defaultTestLoader.loadTestsFromModule(testmod)) - - #from src.tests import test_commands_cmdhandler - #tsuite.addTest(unittest.defaultTestLoader.loadTestsFromModule(test_commands_cmdhandler)) - - return tsuite diff --git a/src/tests/test_commands_cmdhandler.py b/src/tests/test_commands_cmdhandler.py deleted file mode 100644 index 4bc11b7e4..000000000 --- a/src/tests/test_commands_cmdhandler.py +++ /dev/null @@ -1,14 +0,0 @@ -import unittest - -class TestGetAndMergeCmdsets(unittest.TestCase): - def test_get_and_merge_cmdsets(self): - # self.assertEqual(expected, get_and_merge_cmdsets(caller, session, player, obj, callertype, sessid)) - assert True # TODO: implement your test here - -class TestCmdhandler(unittest.TestCase): - def test_cmdhandler(self): - # self.assertEqual(expected, cmdhandler(called_by, raw_string, _testing, callertype, sessid)) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_commands_cmdparser.py b/src/tests/test_commands_cmdparser.py deleted file mode 100644 index d6bf2e26d..000000000 --- a/src/tests/test_commands_cmdparser.py +++ /dev/null @@ -1,24 +0,0 @@ -import unittest - -class TestCmdparser(unittest.TestCase): - def test_cmdparser(self): - # self.assertEqual(expected, cmdparser(raw_string, cmdset, caller, match_index)) - assert True # TODO: implement your test here - -class TestAtSearchResult(unittest.TestCase): - def test_at_search_result(self): - # self.assertEqual(expected, at_search_result(msg_obj, ostring, results, global_search, nofound_string, multimatch_string)) - assert True # TODO: implement your test here - -class TestAtMultimatchInput(unittest.TestCase): - def test_at_multimatch_input(self): - # self.assertEqual(expected, at_multimatch_input(ostring)) - assert True # TODO: implement your test here - -class TestAtMultimatchCmd(unittest.TestCase): - def test_at_multimatch_cmd(self): - # self.assertEqual(expected, at_multimatch_cmd(caller, matches)) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_commands_cmdset.py b/src/tests/test_commands_cmdset.py deleted file mode 100644 index e5bf82445..000000000 --- a/src/tests/test_commands_cmdset.py +++ /dev/null @@ -1,74 +0,0 @@ -import unittest - -class test__CmdSetMeta(unittest.TestCase): - def test___init__(self): - # __cmd_set_meta = _CmdSetMeta(*args, **kwargs) - assert True # TODO: implement your test here - -class TestCmdSet(unittest.TestCase): - def test___add__(self): - # cmd_set = CmdSet(cmdsetobj, key) - # self.assertEqual(expected, cmd_set.__add__(cmdset_b)) - assert True # TODO: implement your test here - - def test___contains__(self): - # cmd_set = CmdSet(cmdsetobj, key) - # self.assertEqual(expected, cmd_set.__contains__(othercmd)) - assert True # TODO: implement your test here - - def test___init__(self): - # cmd_set = CmdSet(cmdsetobj, key) - assert True # TODO: implement your test here - - def test___iter__(self): - # cmd_set = CmdSet(cmdsetobj, key) - # self.assertEqual(expected, cmd_set.__iter__()) - assert True # TODO: implement your test here - - def test___str__(self): - # cmd_set = CmdSet(cmdsetobj, key) - # self.assertEqual(expected, cmd_set.__str__()) - assert True # TODO: implement your test here - - def test_add(self): - # cmd_set = CmdSet(cmdsetobj, key) - # self.assertEqual(expected, cmd_set.add(cmd)) - assert True # TODO: implement your test here - - def test_at_cmdset_creation(self): - # cmd_set = CmdSet(cmdsetobj, key) - # self.assertEqual(expected, cmd_set.at_cmdset_creation()) - assert True # TODO: implement your test here - - def test_count(self): - # cmd_set = CmdSet(cmdsetobj, key) - # self.assertEqual(expected, cmd_set.count()) - assert True # TODO: implement your test here - - def test_get(self): - # cmd_set = CmdSet(cmdsetobj, key) - # self.assertEqual(expected, cmd_set.get(cmd)) - assert True # TODO: implement your test here - - def test_get_all_cmd_keys_and_aliases(self): - # cmd_set = CmdSet(cmdsetobj, key) - # self.assertEqual(expected, cmd_set.get_all_cmd_keys_and_aliases(caller)) - assert True # TODO: implement your test here - - def test_get_system_cmds(self): - # cmd_set = CmdSet(cmdsetobj, key) - # self.assertEqual(expected, cmd_set.get_system_cmds()) - assert True # TODO: implement your test here - - def test_make_unique(self): - # cmd_set = CmdSet(cmdsetobj, key) - # self.assertEqual(expected, cmd_set.make_unique(caller)) - assert True # TODO: implement your test here - - def test_remove(self): - # cmd_set = CmdSet(cmdsetobj, key) - # self.assertEqual(expected, cmd_set.remove(cmd)) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_commands_cmdsethandler.py b/src/tests/test_commands_cmdsethandler.py deleted file mode 100644 index 8a84b9501..000000000 --- a/src/tests/test_commands_cmdsethandler.py +++ /dev/null @@ -1,64 +0,0 @@ -import unittest - -class TestImportCmdset(unittest.TestCase): - def test_import_cmdset(self): - # self.assertEqual(expected, import_cmdset(python_path, cmdsetobj, emit_to_obj, no_logging)) - assert True # TODO: implement your test here - -class TestCmdSetHandler(unittest.TestCase): - def test___init__(self): - # cmd_set_handler = CmdSetHandler(obj) - assert True # TODO: implement your test here - - def test___str__(self): - # cmd_set_handler = CmdSetHandler(obj) - # self.assertEqual(expected, cmd_set_handler.__str__()) - assert True # TODO: implement your test here - - def test_add(self): - # cmd_set_handler = CmdSetHandler(obj) - # self.assertEqual(expected, cmd_set_handler.add(cmdset, emit_to_obj, permanent)) - assert True # TODO: implement your test here - - def test_add_default(self): - # cmd_set_handler = CmdSetHandler(obj) - # self.assertEqual(expected, cmd_set_handler.add_default(cmdset, emit_to_obj, permanent)) - assert True # TODO: implement your test here - - def test_all(self): - # cmd_set_handler = CmdSetHandler(obj) - # self.assertEqual(expected, cmd_set_handler.all()) - assert True # TODO: implement your test here - - def test_clear(self): - # cmd_set_handler = CmdSetHandler(obj) - # self.assertEqual(expected, cmd_set_handler.clear()) - assert True # TODO: implement your test here - - def test_delete(self): - # cmd_set_handler = CmdSetHandler(obj) - # self.assertEqual(expected, cmd_set_handler.delete(cmdset)) - assert True # TODO: implement your test here - - def test_delete_default(self): - # cmd_set_handler = CmdSetHandler(obj) - # self.assertEqual(expected, cmd_set_handler.delete_default()) - assert True # TODO: implement your test here - - def test_has_cmdset(self): - # cmd_set_handler = CmdSetHandler(obj) - # self.assertEqual(expected, cmd_set_handler.has_cmdset(cmdset_key, must_be_default)) - assert True # TODO: implement your test here - - def test_reset(self): - # cmd_set_handler = CmdSetHandler(obj) - # self.assertEqual(expected, cmd_set_handler.reset()) - assert True # TODO: implement your test here - - def test_update(self): - # cmd_set_handler = CmdSetHandler(obj) - # self.assertEqual(expected, cmd_set_handler.update(init_mode)) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_commands_command.py b/src/tests/test_commands_command.py deleted file mode 100644 index 540002b37..000000000 --- a/src/tests/test_commands_command.py +++ /dev/null @@ -1,69 +0,0 @@ -import unittest - -class TestCommandMeta(unittest.TestCase): - def test___init__(self): - # command_meta = CommandMeta(*args, **kwargs) - assert True # TODO: implement your test here - -class TestCommand(unittest.TestCase): - def test___contains__(self): - # command = Command(**kwargs) - # self.assertEqual(expected, command.__contains__(query)) - assert True # TODO: implement your test here - - def test___eq__(self): - # command = Command(**kwargs) - # self.assertEqual(expected, command.__eq__(cmd)) - assert True # TODO: implement your test here - - def test___init__(self): - # command = Command(**kwargs) - assert True # TODO: implement your test here - - def test___ne__(self): - # command = Command(**kwargs) - # self.assertEqual(expected, command.__ne__(cmd)) - assert True # TODO: implement your test here - - def test___str__(self): - # command = Command(**kwargs) - # self.assertEqual(expected, command.__str__()) - assert True # TODO: implement your test here - - def test_access(self): - # command = Command(**kwargs) - # self.assertEqual(expected, command.access(srcobj, access_type, default)) - assert True # TODO: implement your test here - - def test_at_post_cmd(self): - # command = Command(**kwargs) - # self.assertEqual(expected, command.at_post_cmd()) - assert True # TODO: implement your test here - - def test_at_pre_cmd(self): - # command = Command(**kwargs) - # self.assertEqual(expected, command.at_pre_cmd()) - assert True # TODO: implement your test here - - def test_func(self): - # command = Command(**kwargs) - # self.assertEqual(expected, command.func()) - assert True # TODO: implement your test here - - def test_match(self): - # command = Command(**kwargs) - # self.assertEqual(expected, command.match(cmdname)) - assert True # TODO: implement your test here - - def test_msg(self): - # command = Command(**kwargs) - # self.assertEqual(expected, command.msg(msg, to_obj, from_obj, sessid, all_sessions, **kwargs)) - assert True # TODO: implement your test here - - def test_parse(self): - # command = Command(**kwargs) - # self.assertEqual(expected, command.parse()) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_comms_channelhandler.py b/src/tests/test_comms_channelhandler.py deleted file mode 100644 index e93edb31b..000000000 --- a/src/tests/test_comms_channelhandler.py +++ /dev/null @@ -1,45 +0,0 @@ -import unittest - -class TestChannelCommand(unittest.TestCase): - def test_func(self): - # channel_command = ChannelCommand() - # self.assertEqual(expected, channel_command.func()) - assert True # TODO: implement your test here - - def test_parse(self): - # channel_command = ChannelCommand() - # self.assertEqual(expected, channel_command.parse()) - assert True # TODO: implement your test here - -class TestChannelHandler(unittest.TestCase): - def test___init__(self): - # channel_handler = ChannelHandler() - assert True # TODO: implement your test here - - def test___str__(self): - # channel_handler = ChannelHandler() - # self.assertEqual(expected, channel_handler.__str__()) - assert True # TODO: implement your test here - - def test_add_channel(self): - # channel_handler = ChannelHandler() - # self.assertEqual(expected, channel_handler.add_channel(channel)) - assert True # TODO: implement your test here - - def test_clear(self): - # channel_handler = ChannelHandler() - # self.assertEqual(expected, channel_handler.clear()) - assert True # TODO: implement your test here - - def test_get_cmdset(self): - # channel_handler = ChannelHandler() - # self.assertEqual(expected, channel_handler.get_cmdset(source_object)) - assert True # TODO: implement your test here - - def test_update(self): - # channel_handler = ChannelHandler() - # self.assertEqual(expected, channel_handler.update()) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_comms_comms.py b/src/tests/test_comms_comms.py deleted file mode 100644 index baace8518..000000000 --- a/src/tests/test_comms_comms.py +++ /dev/null @@ -1,90 +0,0 @@ -import unittest - -class TestChannel(unittest.TestCase): - def test_at_channel_create(self): - # channel = Channel() - # self.assertEqual(expected, channel.at_channel_create()) - assert True # TODO: implement your test here - - def test_at_init(self): - # channel = Channel() - # self.assertEqual(expected, channel.at_init()) - assert True # TODO: implement your test here - - def test_channel_prefix(self): - # channel = Channel() - # self.assertEqual(expected, channel.channel_prefix(msg, emit)) - assert True # TODO: implement your test here - - def test_distribute_message(self): - # channel = Channel() - # self.assertEqual(expected, channel.distribute_message(msg, online)) - assert True # TODO: implement your test here - - def test_format_external(self): - # channel = Channel() - # self.assertEqual(expected, channel.format_external(msg, senders, emit)) - assert True # TODO: implement your test here - - def test_format_message(self): - # channel = Channel() - # self.assertEqual(expected, channel.format_message(msg, emit)) - assert True # TODO: implement your test here - - def test_format_senders(self): - # channel = Channel() - # self.assertEqual(expected, channel.format_senders(senders)) - assert True # TODO: implement your test here - - def test_message_transform(self): - # channel = Channel() - # self.assertEqual(expected, channel.message_transform(msg, emit, prefix, sender_strings, external)) - assert True # TODO: implement your test here - - def test_msg(self): - # channel = Channel() - # self.assertEqual(expected, channel.msg(msgobj, header, senders, sender_strings, persistent, online, emit, external)) - assert True # TODO: implement your test here - - def test_pose_transform(self): - # channel = Channel() - # self.assertEqual(expected, channel.pose_transform(msg, sender_string)) - assert True # TODO: implement your test here - - def test_post_join_channel(self): - # channel = Channel() - # self.assertEqual(expected, channel.post_join_channel(joiner)) - assert True # TODO: implement your test here - - def test_post_leave_channel(self): - # channel = Channel() - # self.assertEqual(expected, channel.post_leave_channel(leaver)) - assert True # TODO: implement your test here - - def test_post_send_message(self): - # channel = Channel() - # self.assertEqual(expected, channel.post_send_message(msg)) - assert True # TODO: implement your test here - - def test_pre_join_channel(self): - # channel = Channel() - # self.assertEqual(expected, channel.pre_join_channel(joiner)) - assert True # TODO: implement your test here - - def test_pre_leave_channel(self): - # channel = Channel() - # self.assertEqual(expected, channel.pre_leave_channel(leaver)) - assert True # TODO: implement your test here - - def test_pre_send_message(self): - # channel = Channel() - # self.assertEqual(expected, channel.pre_send_message(msg)) - assert True # TODO: implement your test here - - def test_tempmsg(self): - # channel = Channel() - # self.assertEqual(expected, channel.tempmsg(message, header, senders)) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_comms_models.py b/src/tests/test_comms_models.py deleted file mode 100644 index 8ff649882..000000000 --- a/src/tests/test_comms_models.py +++ /dev/null @@ -1,84 +0,0 @@ -import unittest - -class TestMsg(unittest.TestCase): - def test___init__(self): - # msg = Msg(*args, **kwargs) - assert True # TODO: implement your test here - - def test___str__(self): - # msg = Msg(*args, **kwargs) - # self.assertEqual(expected, msg.__str__()) - assert True # TODO: implement your test here - - def test_remove_receiver(self): - # msg = Msg(*args, **kwargs) - # self.assertEqual(expected, msg.remove_receiver(obj)) - assert True # TODO: implement your test here - - def test_remove_sender(self): - # msg = Msg(*args, **kwargs) - # self.assertEqual(expected, msg.remove_sender(value)) - assert True # TODO: implement your test here - -class TestTempMsg(unittest.TestCase): - def test___init__(self): - # temp_msg = TempMsg(senders, receivers, channels, message, header, type, lockstring, hide_from) - assert True # TODO: implement your test here - - def test___str__(self): - # temp_msg = TempMsg(senders, receivers, channels, message, header, type, lockstring, hide_from) - # self.assertEqual(expected, temp_msg.__str__()) - assert True # TODO: implement your test here - - def test_access(self): - # temp_msg = TempMsg(senders, receivers, channels, message, header, type, lockstring, hide_from) - # self.assertEqual(expected, temp_msg.access(accessing_obj, access_type, default)) - assert True # TODO: implement your test here - - def test_remove_receiver(self): - # temp_msg = TempMsg(senders, receivers, channels, message, header, type, lockstring, hide_from) - # self.assertEqual(expected, temp_msg.remove_receiver(obj)) - assert True # TODO: implement your test here - - def test_remove_sender(self): - # temp_msg = TempMsg(senders, receivers, channels, message, header, type, lockstring, hide_from) - # self.assertEqual(expected, temp_msg.remove_sender(obj)) - assert True # TODO: implement your test here - -class TestChannelDB(unittest.TestCase): - def test___init__(self): - # channel_d_b = ChannelDB(*args, **kwargs) - assert True # TODO: implement your test here - - def test___str__(self): - # channel_d_b = ChannelDB(*args, **kwargs) - # self.assertEqual(expected, channel_d_b.__str__()) - assert True # TODO: implement your test here - - def test_access(self): - # channel_d_b = ChannelDB(*args, **kwargs) - # self.assertEqual(expected, channel_d_b.access(accessing_obj, access_type, default)) - assert True # TODO: implement your test here - - def test_connect(self): - # channel_d_b = ChannelDB(*args, **kwargs) - # self.assertEqual(expected, channel_d_b.connect(player)) - assert True # TODO: implement your test here - - def test_delete(self): - # channel_d_b = ChannelDB(*args, **kwargs) - # self.assertEqual(expected, channel_d_b.delete()) - assert True # TODO: implement your test here - - def test_disconnect(self): - # channel_d_b = ChannelDB(*args, **kwargs) - # self.assertEqual(expected, channel_d_b.disconnect(player)) - assert True # TODO: implement your test here - - def test_has_connection(self): - # channel_d_b = ChannelDB(*args, **kwargs) - # self.assertEqual(expected, channel_d_b.has_connection(player)) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_locks_lockfuncs.py b/src/tests/test_locks_lockfuncs.py deleted file mode 100644 index 3bb937bcb..000000000 --- a/src/tests/test_locks_lockfuncs.py +++ /dev/null @@ -1,129 +0,0 @@ -import unittest - -class TestTrue(unittest.TestCase): - def test_true(self): - # self.assertEqual(expected, true(*args, **kwargs)) - assert True # TODO: implement your test here - -class TestAll(unittest.TestCase): - def test_all(self): - # self.assertEqual(expected, all(*args, **kwargs)) - assert True # TODO: implement your test here - -class TestTrue(unittest.TestCase): - def test_false(self): - # self.assertEqual(expected, false(*args, **kwargs)) - assert True # TODO: implement your test here - -class TestNone(unittest.TestCase): - def test_none(self): - # self.assertEqual(expected, none(*args, **kwargs)) - assert True # TODO: implement your test here - -class TestSelf(unittest.TestCase): - def test_self(self): - # self.assertEqual(expected, self(accessing_obj, accessed_obj, *args, **kwargs)) - assert True # TODO: implement your test here - -class TestPerm(unittest.TestCase): - def test_perm(self): - # self.assertEqual(expected, perm(accessing_obj, accessed_obj, *args, **kwargs)) - assert True # TODO: implement your test here - -class TestPermAbove(unittest.TestCase): - def test_perm_above(self): - # self.assertEqual(expected, perm_above(accessing_obj, accessed_obj, *args, **kwargs)) - assert True # TODO: implement your test here - -class TestPperm(unittest.TestCase): - def test_pperm(self): - # self.assertEqual(expected, pperm(accessing_obj, accessed_obj, *args, **kwargs)) - assert True # TODO: implement your test here - -class TestPpermAbove(unittest.TestCase): - def test_pperm_above(self): - # self.assertEqual(expected, pperm_above(accessing_obj, accessed_obj, *args, **kwargs)) - assert True # TODO: implement your test here - -class TestDbref(unittest.TestCase): - def test_dbref(self): - # self.assertEqual(expected, dbref(accessing_obj, accessed_obj, *args, **kwargs)) - assert True # TODO: implement your test here - -class TestPdbref(unittest.TestCase): - def test_pdbref(self): - # self.assertEqual(expected, pdbref(accessing_obj, accessed_obj, *args, **kwargs)) - assert True # TODO: implement your test here - -class TestId(unittest.TestCase): - def test_id(self): - # self.assertEqual(expected, id(accessing_obj, accessed_obj, *args, **kwargs)) - assert True # TODO: implement your test here - -class TestPid(unittest.TestCase): - def test_pid(self): - # self.assertEqual(expected, pid(accessing_obj, accessed_obj, *args, **kwargs)) - assert True # TODO: implement your test here - -class TestAttr(unittest.TestCase): - def test_attr(self): - # self.assertEqual(expected, attr(accessing_obj, accessed_obj, *args, **kwargs)) - assert True # TODO: implement your test here - -class TestObjattr(unittest.TestCase): - def test_objattr(self): - # self.assertEqual(expected, objattr(accessing_obj, accessed_obj, *args, **kwargs)) - assert True # TODO: implement your test here - -class TestLocattr(unittest.TestCase): - def test_locattr(self): - # self.assertEqual(expected, locattr(accessing_obj, accessed_obj, *args, **kwargs)) - assert True # TODO: implement your test here - -class TestAttrEq(unittest.TestCase): - def test_attr_eq(self): - # self.assertEqual(expected, attr_eq(accessing_obj, accessed_obj, *args, **kwargs)) - assert True # TODO: implement your test here - -class TestAttrGt(unittest.TestCase): - def test_attr_gt(self): - # self.assertEqual(expected, attr_gt(accessing_obj, accessed_obj, *args, **kwargs)) - assert True # TODO: implement your test here - -class TestAttrGe(unittest.TestCase): - def test_attr_ge(self): - # self.assertEqual(expected, attr_ge(accessing_obj, accessed_obj, *args, **kwargs)) - assert True # TODO: implement your test here - -class TestAttrLt(unittest.TestCase): - def test_attr_lt(self): - # self.assertEqual(expected, attr_lt(accessing_obj, accessed_obj, *args, **kwargs)) - assert True # TODO: implement your test here - -class TestAttrLe(unittest.TestCase): - def test_attr_le(self): - # self.assertEqual(expected, attr_le(accessing_obj, accessed_obj, *args, **kwargs)) - assert True # TODO: implement your test here - -class TestAttrNe(unittest.TestCase): - def test_attr_ne(self): - # self.assertEqual(expected, attr_ne(accessing_obj, accessed_obj, *args, **kwargs)) - assert True # TODO: implement your test here - -class TestHolds(unittest.TestCase): - def test_holds(self): - # self.assertEqual(expected, holds(accessing_obj, accessed_obj, *args, **kwargs)) - assert True # TODO: implement your test here - -class TestSuperuser(unittest.TestCase): - def test_superuser(self): - # self.assertEqual(expected, superuser(*args, **kwargs)) - assert True # TODO: implement your test here - -class TestServersetting(unittest.TestCase): - def test_serversetting(self): - # self.assertEqual(expected, serversetting(accessing_obj, accessed_obj, *args, **kwargs)) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_locks_lockhandler.py b/src/tests/test_locks_lockhandler.py deleted file mode 100644 index 93a87b9d2..000000000 --- a/src/tests/test_locks_lockhandler.py +++ /dev/null @@ -1,59 +0,0 @@ -import unittest - -class TestLockHandler(unittest.TestCase): - def test___init__(self): - # lock_handler = LockHandler(obj) - assert True # TODO: implement your test here - - def test___str__(self): - # lock_handler = LockHandler(obj) - # self.assertEqual(expected, lock_handler.__str__()) - assert True # TODO: implement your test here - - def test_add(self): - # lock_handler = LockHandler(obj) - # self.assertEqual(expected, lock_handler.add(lockstring)) - assert True # TODO: implement your test here - - def test_cache_lock_bypass(self): - # lock_handler = LockHandler(obj) - # self.assertEqual(expected, lock_handler.cache_lock_bypass(obj)) - assert True # TODO: implement your test here - - def test_check(self): - # lock_handler = LockHandler(obj) - # self.assertEqual(expected, lock_handler.check(accessing_obj, access_type, default, no_superuser_bypass)) - assert True # TODO: implement your test here - - def test_check_lockstring(self): - # lock_handler = LockHandler(obj) - # self.assertEqual(expected, lock_handler.check_lockstring(accessing_obj, lockstring, no_superuser_bypass)) - assert True # TODO: implement your test here - - def test_clear(self): - # lock_handler = LockHandler(obj) - # self.assertEqual(expected, lock_handler.clear()) - assert True # TODO: implement your test here - - def test_delete(self): - # lock_handler = LockHandler(obj) - # self.assertEqual(expected, lock_handler.delete(access_type)) - assert True # TODO: implement your test here - - def test_get(self): - # lock_handler = LockHandler(obj) - # self.assertEqual(expected, lock_handler.get(access_type)) - assert True # TODO: implement your test here - - def test_replace(self): - # lock_handler = LockHandler(obj) - # self.assertEqual(expected, lock_handler.replace(lockstring)) - assert True # TODO: implement your test here - - def test_reset(self): - # lock_handler = LockHandler(obj) - # self.assertEqual(expected, lock_handler.reset()) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_objects_models.py b/src/tests/test_objects_models.py deleted file mode 100644 index 83ee5096f..000000000 --- a/src/tests/test_objects_models.py +++ /dev/null @@ -1,64 +0,0 @@ -import unittest - -class TestObjectDB(unittest.TestCase): - def test___init__(self): - # object_d_b = ObjectDB(*args, **kwargs) - assert True # TODO: implement your test here - - def test_clear_contents(self): - # object_d_b = ObjectDB(*args, **kwargs) - # self.assertEqual(expected, object_d_b.clear_contents()) - assert True # TODO: implement your test here - - def test_clear_exits(self): - # object_d_b = ObjectDB(*args, **kwargs) - # self.assertEqual(expected, object_d_b.clear_exits()) - assert True # TODO: implement your test here - - def test_contents_get(self): - # object_d_b = ObjectDB(*args, **kwargs) - # self.assertEqual(expected, object_d_b.contents_get(exclude)) - assert True # TODO: implement your test here - - def test_copy(self): - # object_d_b = ObjectDB(*args, **kwargs) - # self.assertEqual(expected, object_d_b.copy(new_key)) - assert True # TODO: implement your test here - - def test_delete(self): - # object_d_b = ObjectDB(*args, **kwargs) - # self.assertEqual(expected, object_d_b.delete()) - assert True # TODO: implement your test here - - def test_execute_cmd(self): - # object_d_b = ObjectDB(*args, **kwargs) - # self.assertEqual(expected, object_d_b.execute_cmd(raw_string, sessid)) - assert True # TODO: implement your test here - - def test_move_to(self): - # object_d_b = ObjectDB(*args, **kwargs) - # self.assertEqual(expected, object_d_b.move_to(destination, quiet, emit_to_obj, use_destination, to_none)) - assert True # TODO: implement your test here - - def test_msg(self): - # object_d_b = ObjectDB(*args, **kwargs) - # self.assertEqual(expected, object_d_b.msg(text, from_obj, sessid, **kwargs)) - assert True # TODO: implement your test here - - def test_msg_contents(self): - # object_d_b = ObjectDB(*args, **kwargs) - # self.assertEqual(expected, object_d_b.msg_contents(message, exclude, from_obj, **kwargs)) - assert True # TODO: implement your test here - - def test_search(self): - # object_d_b = ObjectDB(*args, **kwargs) - # self.assertEqual(expected, object_d_b.search(searchdata, global_search, use_nicks, typeclass, location, attribute_name, quiet, exact)) - assert True # TODO: implement your test here - - def test_search_player(self): - # object_d_b = ObjectDB(*args, **kwargs) - # self.assertEqual(expected, object_d_b.search_player(searchdata, quiet)) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_objects_objects.py b/src/tests/test_objects_objects.py deleted file mode 100644 index a09aa9eb5..000000000 --- a/src/tests/test_objects_objects.py +++ /dev/null @@ -1,302 +0,0 @@ -import unittest - -class TestObject(unittest.TestCase): - def test___eq__(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.__eq__(other)) - assert True # TODO: implement your test here - - def test___init__(self): - # object = Object(dbobj) - assert True # TODO: implement your test here - - def test_access(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.access(accessing_obj, access_type, default, **kwargs)) - assert True # TODO: implement your test here - - def test_announce_move_from(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.announce_move_from(destination)) - assert True # TODO: implement your test here - - def test_announce_move_to(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.announce_move_to(source_location)) - assert True # TODO: implement your test here - - def test_at_access(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.at_access(result, accessing_obj, access_type, **kwargs)) - assert True # TODO: implement your test here - - def test_at_access_failure(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.at_access_failure(accessing_obj, access_type)) - assert True # TODO: implement your test here - - def test_at_access_success(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.at_access_success(accessing_obj, access_type)) - assert True # TODO: implement your test here - - def test_at_after_move(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.at_after_move(source_location)) - assert True # TODO: implement your test here - - def test_at_after_traverse(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.at_after_traverse(traversing_object, source_location)) - assert True # TODO: implement your test here - - def test_at_before_move(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.at_before_move(destination)) - assert True # TODO: implement your test here - - def test_at_before_traverse(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.at_before_traverse(traversing_object)) - assert True # TODO: implement your test here - - def test_at_cmdset_get(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.at_cmdset_get(**kwargs)) - assert True # TODO: implement your test here - - def test_at_desc(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.at_desc(looker)) - assert True # TODO: implement your test here - - def test_at_drop(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.at_drop(dropper)) - assert True # TODO: implement your test here - - def test_at_failed_traverse(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.at_failed_traverse(traversing_object)) - assert True # TODO: implement your test here - - def test_at_get(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.at_get(getter)) - assert True # TODO: implement your test here - - def test_at_init(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.at_init()) - assert True # TODO: implement your test here - - def test_at_msg_receive(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.at_msg_receive(text, **kwargs)) - assert True # TODO: implement your test here - - def test_at_msg_send(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.at_msg_send(text, to_obj, **kwargs)) - assert True # TODO: implement your test here - - def test_at_object_creation(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.at_object_creation()) - assert True # TODO: implement your test here - - def test_at_object_delete(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.at_object_delete()) - assert True # TODO: implement your test here - - def test_at_object_leave(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.at_object_leave(moved_obj, target_location)) - assert True # TODO: implement your test here - - def test_at_object_receive(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.at_object_receive(moved_obj, source_location)) - assert True # TODO: implement your test here - - def test_at_post_puppet(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.at_post_puppet()) - assert True # TODO: implement your test here - - def test_at_post_unpuppet(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.at_post_unpuppet(player, sessid)) - assert True # TODO: implement your test here - - def test_at_pre_puppet(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.at_pre_puppet(player, sessid)) - assert True # TODO: implement your test here - - def test_at_pre_unpuppet(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.at_pre_unpuppet()) - assert True # TODO: implement your test here - - def test_at_say(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.at_say(speaker, message)) - assert True # TODO: implement your test here - - def test_at_server_reload(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.at_server_reload()) - assert True # TODO: implement your test here - - def test_at_server_shutdown(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.at_server_shutdown()) - assert True # TODO: implement your test here - - def test_at_traverse(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.at_traverse(traversing_object, target_location)) - assert True # TODO: implement your test here - - def test_basetype_posthook_setup(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.basetype_posthook_setup()) - assert True # TODO: implement your test here - - def test_basetype_setup(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.basetype_setup()) - assert True # TODO: implement your test here - - def test_check_permstring(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.check_permstring(permstring)) - assert True # TODO: implement your test here - - def test_copy(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.copy(new_key)) - assert True # TODO: implement your test here - - def test_delete(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.delete()) - assert True # TODO: implement your test here - - def test_execute_cmd(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.execute_cmd(raw_string, sessid)) - assert True # TODO: implement your test here - - def test_is_typeclass(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.is_typeclass(typeclass, exact)) - assert True # TODO: implement your test here - - def test_move_to(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.move_to(destination, quiet, emit_to_obj, use_destination, to_none)) - assert True # TODO: implement your test here - - def test_msg(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.msg(text, from_obj, sessid, **kwargs)) - assert True # TODO: implement your test here - - def test_msg_contents(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.msg_contents(text, exclude, from_obj, **kwargs)) - assert True # TODO: implement your test here - - def test_return_appearance(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.return_appearance(pobject)) - assert True # TODO: implement your test here - - def test_search(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.search(ostring, global_search, use_nicks, typeclass, location, attribute_name, quiet, exact)) - assert True # TODO: implement your test here - - def test_swap_typeclass(self): - # object = Object(dbobj) - # self.assertEqual(expected, object.swap_typeclass(new_typeclass, clean_attributes, no_default)) - assert True # TODO: implement your test here - -class TestCharacter(unittest.TestCase): - def test_at_after_move(self): - # character = Character() - # self.assertEqual(expected, character.at_after_move(source_location)) - assert True # TODO: implement your test here - - def test_at_object_creation(self): - # character = Character() - # self.assertEqual(expected, character.at_object_creation()) - assert True # TODO: implement your test here - - def test_at_post_puppet(self): - # character = Character() - # self.assertEqual(expected, character.at_post_puppet()) - assert True # TODO: implement your test here - - def test_at_post_unpuppet(self): - # character = Character() - # self.assertEqual(expected, character.at_post_unpuppet(player, sessid)) - assert True # TODO: implement your test here - - def test_at_pre_puppet(self): - # character = Character() - # self.assertEqual(expected, character.at_pre_puppet(player, sessid)) - assert True # TODO: implement your test here - - def test_basetype_setup(self): - # character = Character() - # self.assertEqual(expected, character.basetype_setup()) - assert True # TODO: implement your test here - -class TestRoom(unittest.TestCase): - def test_basetype_setup(self): - # room = Room() - # self.assertEqual(expected, room.basetype_setup()) - assert True # TODO: implement your test here - -class TestExit(unittest.TestCase): - def test_at_after_traverse(self): - # exit = Exit() - # self.assertEqual(expected, exit.at_after_traverse(traversing_object, source_location)) - assert True # TODO: implement your test here - - def test_at_cmdset_get(self): - # exit = Exit() - # self.assertEqual(expected, exit.at_cmdset_get(**kwargs)) - assert True # TODO: implement your test here - - def test_at_failed_traverse(self): - # exit = Exit() - # self.assertEqual(expected, exit.at_failed_traverse(traversing_object)) - assert True # TODO: implement your test here - - def test_at_object_creation(self): - # exit = Exit() - # self.assertEqual(expected, exit.at_object_creation()) - assert True # TODO: implement your test here - - def test_at_traverse(self): - # exit = Exit() - # self.assertEqual(expected, exit.at_traverse(traversing_object, target_location)) - assert True # TODO: implement your test here - - def test_basetype_setup(self): - # exit = Exit() - # self.assertEqual(expected, exit.basetype_setup()) - assert True # TODO: implement your test here - - def test_create_exit_cmdset(self): - # exit = Exit() - # self.assertEqual(expected, exit.create_exit_cmdset(exidbobj)) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_players_bots.py b/src/tests/test_players_bots.py deleted file mode 100644 index c8740a4d6..000000000 --- a/src/tests/test_players_bots.py +++ /dev/null @@ -1,106 +0,0 @@ -import unittest - -class TestBotStarter(unittest.TestCase): - def test_at_repeat(self): - # bot_starter = BotStarter() - # self.assertEqual(expected, bot_starter.at_repeat()) - assert True # TODO: implement your test here - - def test_at_script_creation(self): - # bot_starter = BotStarter() - # self.assertEqual(expected, bot_starter.at_script_creation()) - assert True # TODO: implement your test here - - def test_at_server_reload(self): - # bot_starter = BotStarter() - # self.assertEqual(expected, bot_starter.at_server_reload()) - assert True # TODO: implement your test here - - def test_at_server_shutdown(self): - # bot_starter = BotStarter() - # self.assertEqual(expected, bot_starter.at_server_shutdown()) - assert True # TODO: implement your test here - - def test_at_start(self): - # bot_starter = BotStarter() - # self.assertEqual(expected, bot_starter.at_start()) - assert True # TODO: implement your test here - -class TestCmdBotListen(unittest.TestCase): - def test_func(self): - # cmd_bot_listen = CmdBotListen() - # self.assertEqual(expected, cmd_bot_listen.func()) - assert True # TODO: implement your test here - -class TestBotCmdSet(unittest.TestCase): - def test_at_cmdset_creation(self): - # bot_cmd_set = BotCmdSet() - # self.assertEqual(expected, bot_cmd_set.at_cmdset_creation()) - assert True # TODO: implement your test here - -class TestBot(unittest.TestCase): - def test_basetype_setup(self): - # bot = Bot() - # self.assertEqual(expected, bot.basetype_setup()) - assert True # TODO: implement your test here - - def test_execute_cmd(self): - # bot = Bot() - # self.assertEqual(expected, bot.execute_cmd(raw_string, sessid)) - assert True # TODO: implement your test here - - def test_msg(self): - # bot = Bot() - # self.assertEqual(expected, bot.msg(text, from_obj, sessid, **kwargs)) - assert True # TODO: implement your test here - - def test_start(self): - # bot = Bot() - # self.assertEqual(expected, bot.start(**kwargs)) - assert True # TODO: implement your test here - -class TestIRCBot(unittest.TestCase): - def test_execute_cmd(self): - # i_rc_bot = IRCBot() - # self.assertEqual(expected, i_rc_bot.execute_cmd(text, sessid)) - assert True # TODO: implement your test here - - def test_msg(self): - # i_rc_bot = IRCBot() - # self.assertEqual(expected, i_rc_bot.msg(text, **kwargs)) - assert True # TODO: implement your test here - - def test_start(self): - # i_rc_bot = IRCBot() - # self.assertEqual(expected, i_rc_bot.start(ev_channel, irc_botname, irc_channel, irc_network, irc_port)) - assert True # TODO: implement your test here - -class TestRSSBot(unittest.TestCase): - def test_execute_cmd(self): - # r_ss_bot = RSSBot() - # self.assertEqual(expected, r_ss_bot.execute_cmd(text, sessid)) - assert True # TODO: implement your test here - - def test_start(self): - # r_ss_bot = RSSBot() - # self.assertEqual(expected, r_ss_bot.start(ev_channel, rss_url, rss_rate)) - assert True # TODO: implement your test here - -class TestIMC2Bot(unittest.TestCase): - def test_execute_cmd(self): - # i_m_c2_bot = IMC2Bot() - # self.assertEqual(expected, i_m_c2_bot.execute_cmd(text, sessid)) - assert True # TODO: implement your test here - - def test_msg(self): - # i_m_c2_bot = IMC2Bot() - # self.assertEqual(expected, i_m_c2_bot.msg(text, **kwargs)) - assert True # TODO: implement your test here - - def test_start(self): - # i_m_c2_bot = IMC2Bot() - # self.assertEqual(expected, i_m_c2_bot.start(ev_channel, imc2_network, imc2_mudname, imc2_port, imc2_client_pwd, imc2_server_pwd)) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_players_models.py b/src/tests/test_players_models.py deleted file mode 100644 index 8328d323b..000000000 --- a/src/tests/test_players_models.py +++ /dev/null @@ -1,94 +0,0 @@ -import unittest - -class TestPlayerDB(unittest.TestCase): - def test___init__(self): - # player_d_b = PlayerDB(*args, **kwargs) - assert True # TODO: implement your test here - - def test___str__(self): - # player_d_b = PlayerDB(*args, **kwargs) - # self.assertEqual(expected, player_d_b.__str__()) - assert True # TODO: implement your test here - - def test___unicode__(self): - # player_d_b = PlayerDB(*args, **kwargs) - # self.assertEqual(expected, player_d_b.__unicode__()) - assert True # TODO: implement your test here - - def test_cmdset_storage_del(self): - # player_d_b = PlayerDB(*args, **kwargs) - # self.assertEqual(expected, player_d_b.cmdset_storage_del()) - assert True # TODO: implement your test here - - def test_cmdset_storage_get(self): - # player_d_b = PlayerDB(*args, **kwargs) - # self.assertEqual(expected, player_d_b.cmdset_storage_get()) - assert True # TODO: implement your test here - - def test_cmdset_storage_set(self): - # player_d_b = PlayerDB(*args, **kwargs) - # self.assertEqual(expected, player_d_b.cmdset_storage_set(value)) - assert True # TODO: implement your test here - - def test_delete(self): - # player_d_b = PlayerDB(*args, **kwargs) - # self.assertEqual(expected, player_d_b.delete(*args, **kwargs)) - assert True # TODO: implement your test here - - def test_disconnect_session_from_player(self): - # player_d_b = PlayerDB(*args, **kwargs) - # self.assertEqual(expected, player_d_b.disconnect_session_from_player(sessid)) - assert True # TODO: implement your test here - - def test_execute_cmd(self): - # player_d_b = PlayerDB(*args, **kwargs) - # self.assertEqual(expected, player_d_b.execute_cmd(raw_string, sessid)) - assert True # TODO: implement your test here - - def test_get_all_puppets(self): - # player_d_b = PlayerDB(*args, **kwargs) - # self.assertEqual(expected, player_d_b.get_all_puppets(return_dbobj)) - assert True # TODO: implement your test here - - def test_get_all_sessions(self): - # player_d_b = PlayerDB(*args, **kwargs) - # self.assertEqual(expected, player_d_b.get_all_sessions()) - assert True # TODO: implement your test here - - def test_get_puppet(self): - # player_d_b = PlayerDB(*args, **kwargs) - # self.assertEqual(expected, player_d_b.get_puppet(sessid, return_dbobj)) - assert True # TODO: implement your test here - - def test_get_session(self): - # player_d_b = PlayerDB(*args, **kwargs) - # self.assertEqual(expected, player_d_b.get_session(sessid)) - assert True # TODO: implement your test here - - def test_msg(self): - # player_d_b = PlayerDB(*args, **kwargs) - # self.assertEqual(expected, player_d_b.msg(text, from_obj, sessid, **kwargs)) - assert True # TODO: implement your test here - - def test_puppet_object(self): - # player_d_b = PlayerDB(*args, **kwargs) - # self.assertEqual(expected, player_d_b.puppet_object(sessid, obj, normal_mode)) - assert True # TODO: implement your test here - - def test_search(self): - # player_d_b = PlayerDB(*args, **kwargs) - # self.assertEqual(expected, player_d_b.search(ostring, return_puppet, return_character, **kwargs)) - assert True # TODO: implement your test here - - def test_unpuppet_all(self): - # player_d_b = PlayerDB(*args, **kwargs) - # self.assertEqual(expected, player_d_b.unpuppet_all()) - assert True # TODO: implement your test here - - def test_unpuppet_object(self): - # player_d_b = PlayerDB(*args, **kwargs) - # self.assertEqual(expected, player_d_b.unpuppet_object(sessid)) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_scripts_scripthandler.py b/src/tests/test_scripts_scripthandler.py deleted file mode 100644 index e3fc0b3db..000000000 --- a/src/tests/test_scripts_scripthandler.py +++ /dev/null @@ -1,44 +0,0 @@ -import unittest - -class TestScriptHandler(unittest.TestCase): - def test___init__(self): - # script_handler = ScriptHandler(obj) - assert True # TODO: implement your test here - - def test___str__(self): - # script_handler = ScriptHandler(obj) - # self.assertEqual(expected, script_handler.__str__()) - assert True # TODO: implement your test here - - def test_add(self): - # script_handler = ScriptHandler(obj) - # self.assertEqual(expected, script_handler.add(scriptclass, key, autostart)) - assert True # TODO: implement your test here - - def test_all(self): - # script_handler = ScriptHandler(obj) - # self.assertEqual(expected, script_handler.all(scriptid)) - assert True # TODO: implement your test here - - def test_delete(self): - # script_handler = ScriptHandler(obj) - # self.assertEqual(expected, script_handler.delete(scriptid)) - assert True # TODO: implement your test here - - def test_start(self): - # script_handler = ScriptHandler(obj) - # self.assertEqual(expected, script_handler.start(scriptid)) - assert True # TODO: implement your test here - - def test_stop(self): - # script_handler = ScriptHandler(obj) - # self.assertEqual(expected, script_handler.stop(scriptid)) - assert True # TODO: implement your test here - - def test_validate(self): - # script_handler = ScriptHandler(obj) - # self.assertEqual(expected, script_handler.validate(init_mode)) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_scripts_scripts.py b/src/tests/test_scripts_scripts.py deleted file mode 100644 index c3bfa10d5..000000000 --- a/src/tests/test_scripts_scripts.py +++ /dev/null @@ -1,181 +0,0 @@ -import unittest - -class TestExtendedLoopingCall(unittest.TestCase): - def test___call__(self): - # extended_looping_call = ExtendedLoopingCall() - # self.assertEqual(expected, extended_looping_call.__call__()) - assert True # TODO: implement your test here - - def test_force_repeat(self): - # extended_looping_call = ExtendedLoopingCall() - # self.assertEqual(expected, extended_looping_call.force_repeat()) - assert True # TODO: implement your test here - - def test_next_call_time(self): - # extended_looping_call = ExtendedLoopingCall() - # self.assertEqual(expected, extended_looping_call.next_call_time()) - assert True # TODO: implement your test here - - def test_start(self): - # extended_looping_call = ExtendedLoopingCall() - # self.assertEqual(expected, extended_looping_call.start(interval, now, start_delay, count_start)) - assert True # TODO: implement your test here - -class TestScriptBase(unittest.TestCase): - def test___eq__(self): - # script_base = ScriptBase() - # self.assertEqual(expected, script_base.__eq__(other)) - assert True # TODO: implement your test here - - def test_at_init(self): - # script_base = ScriptBase() - # self.assertEqual(expected, script_base.at_init()) - assert True # TODO: implement your test here - - def test_at_repeat(self): - # script_base = ScriptBase() - # self.assertEqual(expected, script_base.at_repeat()) - assert True # TODO: implement your test here - - def test_at_script_creation(self): - # script_base = ScriptBase() - # self.assertEqual(expected, script_base.at_script_creation()) - assert True # TODO: implement your test here - - def test_at_start(self): - # script_base = ScriptBase() - # self.assertEqual(expected, script_base.at_start()) - assert True # TODO: implement your test here - - def test_at_stop(self): - # script_base = ScriptBase() - # self.assertEqual(expected, script_base.at_stop()) - assert True # TODO: implement your test here - - def test_force_repeat(self): - # script_base = ScriptBase() - # self.assertEqual(expected, script_base.force_repeat()) - assert True # TODO: implement your test here - - def test_is_valid(self): - # script_base = ScriptBase() - # self.assertEqual(expected, script_base.is_valid()) - assert True # TODO: implement your test here - - def test_pause(self): - # script_base = ScriptBase() - # self.assertEqual(expected, script_base.pause()) - assert True # TODO: implement your test here - - def test_remaining_repeats(self): - # script_base = ScriptBase() - # self.assertEqual(expected, script_base.remaining_repeats()) - assert True # TODO: implement your test here - - def test_start(self): - # script_base = ScriptBase() - # self.assertEqual(expected, script_base.start(force_restart)) - assert True # TODO: implement your test here - - def test_stop(self): - # script_base = ScriptBase() - # self.assertEqual(expected, script_base.stop(kill)) - assert True # TODO: implement your test here - - def test_time_until_next_repeat(self): - # script_base = ScriptBase() - # self.assertEqual(expected, script_base.time_until_next_repeat()) - assert True # TODO: implement your test here - - def test_unpause(self): - # script_base = ScriptBase() - # self.assertEqual(expected, script_base.unpause()) - assert True # TODO: implement your test here - -class TestScript(unittest.TestCase): - def test___init__(self): - # script = Script(dbobj) - assert True # TODO: implement your test here - - def test_at_repeat(self): - # script = Script(dbobj) - # self.assertEqual(expected, script.at_repeat()) - assert True # TODO: implement your test here - - def test_at_script_creation(self): - # script = Script(dbobj) - # self.assertEqual(expected, script.at_script_creation()) - assert True # TODO: implement your test here - - def test_at_server_reload(self): - # script = Script(dbobj) - # self.assertEqual(expected, script.at_server_reload()) - assert True # TODO: implement your test here - - def test_at_server_shutdown(self): - # script = Script(dbobj) - # self.assertEqual(expected, script.at_server_shutdown()) - assert True # TODO: implement your test here - - def test_at_start(self): - # script = Script(dbobj) - # self.assertEqual(expected, script.at_start()) - assert True # TODO: implement your test here - - def test_at_stop(self): - # script = Script(dbobj) - # self.assertEqual(expected, script.at_stop()) - assert True # TODO: implement your test here - - def test_is_valid(self): - # script = Script(dbobj) - # self.assertEqual(expected, script.is_valid()) - assert True # TODO: implement your test here - -class TestDoNothing(unittest.TestCase): - def test_at_script_creation(self): - # do_nothing = DoNothing() - # self.assertEqual(expected, do_nothing.at_script_creation()) - assert True # TODO: implement your test here - -class TestStore(unittest.TestCase): - def test_at_script_creation(self): - # store = Store() - # self.assertEqual(expected, store.at_script_creation()) - assert True # TODO: implement your test here - -class TestCheckSessions(unittest.TestCase): - def test_at_repeat(self): - # check_sessions = CheckSessions() - # self.assertEqual(expected, check_sessions.at_repeat()) - assert True # TODO: implement your test here - - def test_at_script_creation(self): - # check_sessions = CheckSessions() - # self.assertEqual(expected, check_sessions.at_script_creation()) - assert True # TODO: implement your test here - -class TestValidateScripts(unittest.TestCase): - def test_at_repeat(self): - # validate_scripts = ValidateScripts() - # self.assertEqual(expected, validate_scripts.at_repeat()) - assert True # TODO: implement your test here - - def test_at_script_creation(self): - # validate_scripts = ValidateScripts() - # self.assertEqual(expected, validate_scripts.at_script_creation()) - assert True # TODO: implement your test here - -class TestValidateChannelHandler(unittest.TestCase): - def test_at_repeat(self): - # validate_channel_handler = ValidateChannelHandler() - # self.assertEqual(expected, validate_channel_handler.at_repeat()) - assert True # TODO: implement your test here - - def test_at_script_creation(self): - # validate_channel_handler = ValidateChannelHandler() - # self.assertEqual(expected, validate_channel_handler.at_script_creation()) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_scripts_tickerhandler.py b/src/tests/test_scripts_tickerhandler.py deleted file mode 100644 index f301ab637..000000000 --- a/src/tests/test_scripts_tickerhandler.py +++ /dev/null @@ -1,84 +0,0 @@ -import unittest - -class TestTicker(unittest.TestCase): - def test___init__(self): - # ticker = Ticker(interval) - assert True # TODO: implement your test here - - def test_add(self): - # ticker = Ticker(interval) - # self.assertEqual(expected, ticker.add(store_key, obj, *args, **kwargs)) - assert True # TODO: implement your test here - - def test_remove(self): - # ticker = Ticker(interval) - # self.assertEqual(expected, ticker.remove(store_key)) - assert True # TODO: implement your test here - - def test_stop(self): - # ticker = Ticker(interval) - # self.assertEqual(expected, ticker.stop()) - assert True # TODO: implement your test here - - def test_validate(self): - # ticker = Ticker(interval) - # self.assertEqual(expected, ticker.validate(start_delay)) - assert True # TODO: implement your test here - -class TestTickerPool(unittest.TestCase): - def test___init__(self): - # ticker_pool = TickerPool() - assert True # TODO: implement your test here - - def test_add(self): - # ticker_pool = TickerPool() - # self.assertEqual(expected, ticker_pool.add(store_key, obj, interval, *args, **kwargs)) - assert True # TODO: implement your test here - - def test_remove(self): - # ticker_pool = TickerPool() - # self.assertEqual(expected, ticker_pool.remove(store_key, interval)) - assert True # TODO: implement your test here - - def test_stop(self): - # ticker_pool = TickerPool() - # self.assertEqual(expected, ticker_pool.stop(interval)) - assert True # TODO: implement your test here - -class TestTickerHandler(unittest.TestCase): - def test___init__(self): - # ticker_handler = TickerHandler(save_name) - assert True # TODO: implement your test here - - def test_add(self): - # ticker_handler = TickerHandler(save_name) - # self.assertEqual(expected, ticker_handler.add(obj, interval, *args, **kwargs)) - assert True # TODO: implement your test here - - def test_all(self): - # ticker_handler = TickerHandler(save_name) - # self.assertEqual(expected, ticker_handler.all(interval)) - assert True # TODO: implement your test here - - def test_clear(self): - # ticker_handler = TickerHandler(save_name) - # self.assertEqual(expected, ticker_handler.clear(interval)) - assert True # TODO: implement your test here - - def test_remove(self): - # ticker_handler = TickerHandler(save_name) - # self.assertEqual(expected, ticker_handler.remove(obj, interval)) - assert True # TODO: implement your test here - - def test_restore(self): - # ticker_handler = TickerHandler(save_name) - # self.assertEqual(expected, ticker_handler.restore()) - assert True # TODO: implement your test here - - def test_save(self): - # ticker_handler = TickerHandler(save_name) - # self.assertEqual(expected, ticker_handler.save()) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_server_amp.py b/src/tests/test_server_amp.py deleted file mode 100644 index 21b31fbbf..000000000 --- a/src/tests/test_server_amp.py +++ /dev/null @@ -1,125 +0,0 @@ -import unittest - -class TestGetRestartMode(unittest.TestCase): - def test_get_restart_mode(self): - # self.assertEqual(expected, get_restart_mode(restart_file)) - assert True # TODO: implement your test here - -class TestAmpServerFactory(unittest.TestCase): - def test___init__(self): - # amp_server_factory = AmpServerFactory(server) - assert True # TODO: implement your test here - - def test_buildProtocol(self): - # amp_server_factory = AmpServerFactory(server) - # self.assertEqual(expected, amp_server_factory.buildProtocol(addr)) - assert True # TODO: implement your test here - -class TestAmpClientFactory(unittest.TestCase): - def test___init__(self): - # amp_client_factory = AmpClientFactory(portal) - assert True # TODO: implement your test here - - def test_buildProtocol(self): - # amp_client_factory = AmpClientFactory(portal) - # self.assertEqual(expected, amp_client_factory.buildProtocol(addr)) - assert True # TODO: implement your test here - - def test_clientConnectionFailed(self): - # amp_client_factory = AmpClientFactory(portal) - # self.assertEqual(expected, amp_client_factory.clientConnectionFailed(connector, reason)) - assert True # TODO: implement your test here - - def test_clientConnectionLost(self): - # amp_client_factory = AmpClientFactory(portal) - # self.assertEqual(expected, amp_client_factory.clientConnectionLost(connector, reason)) - assert True # TODO: implement your test here - - def test_startedConnecting(self): - # amp_client_factory = AmpClientFactory(portal) - # self.assertEqual(expected, amp_client_factory.startedConnecting(connector)) - assert True # TODO: implement your test here - -class TestDumps(unittest.TestCase): - def test_dumps(self): - # self.assertEqual(expected, dumps(data)) - assert True # TODO: implement your test here - -class TestLoads(unittest.TestCase): - def test_loads(self): - # self.assertEqual(expected, loads(data)) - assert True # TODO: implement your test here - -class TestAMPProtocol(unittest.TestCase): - def test_amp_function_call(self): - # a_mp_protocol = AMPProtocol() - # self.assertEqual(expected, a_mp_protocol.amp_function_call(module, function, args, **kwargs)) - assert True # TODO: implement your test here - - def test_amp_msg_portal2server(self): - # a_mp_protocol = AMPProtocol() - # self.assertEqual(expected, a_mp_protocol.amp_msg_portal2server(sessid, ipart, nparts, msg, data)) - assert True # TODO: implement your test here - - def test_amp_msg_server2portal(self): - # a_mp_protocol = AMPProtocol() - # self.assertEqual(expected, a_mp_protocol.amp_msg_server2portal(sessid, ipart, nparts, msg, data)) - assert True # TODO: implement your test here - - def test_amp_portal_admin(self): - # a_mp_protocol = AMPProtocol() - # self.assertEqual(expected, a_mp_protocol.amp_portal_admin(sessid, ipart, nparts, operation, data)) - assert True # TODO: implement your test here - - def test_amp_server_admin(self): - # a_mp_protocol = AMPProtocol() - # self.assertEqual(expected, a_mp_protocol.amp_server_admin(sessid, ipart, nparts, operation, data)) - assert True # TODO: implement your test here - - def test_call_remote_FunctionCall(self): - # a_mp_protocol = AMPProtocol() - # self.assertEqual(expected, a_mp_protocol.call_remote_FunctionCall(modulepath, functionname, *args, **kwargs)) - assert True # TODO: implement your test here - - def test_call_remote_MsgPortal2Server(self): - # a_mp_protocol = AMPProtocol() - # self.assertEqual(expected, a_mp_protocol.call_remote_MsgPortal2Server(sessid, msg, data)) - assert True # TODO: implement your test here - - def test_call_remote_MsgServer2Portal(self): - # a_mp_protocol = AMPProtocol() - # self.assertEqual(expected, a_mp_protocol.call_remote_MsgServer2Portal(sessid, msg, data)) - assert True # TODO: implement your test here - - def test_call_remote_PortalAdmin(self): - # a_mp_protocol = AMPProtocol() - # self.assertEqual(expected, a_mp_protocol.call_remote_PortalAdmin(sessid, operation, data)) - assert True # TODO: implement your test here - - def test_call_remote_ServerAdmin(self): - # a_mp_protocol = AMPProtocol() - # self.assertEqual(expected, a_mp_protocol.call_remote_ServerAdmin(sessid, operation, data)) - assert True # TODO: implement your test here - - def test_connectionMade(self): - # a_mp_protocol = AMPProtocol() - # self.assertEqual(expected, a_mp_protocol.connectionMade()) - assert True # TODO: implement your test here - - def test_errback(self): - # a_mp_protocol = AMPProtocol() - # self.assertEqual(expected, a_mp_protocol.errback(e, info)) - assert True # TODO: implement your test here - - def test_safe_recv(self): - # a_mp_protocol = AMPProtocol() - # self.assertEqual(expected, a_mp_protocol.safe_recv(command, sessid, ipart, nparts, **kwargs)) - assert True # TODO: implement your test here - - def test_safe_send(self): - # a_mp_protocol = AMPProtocol() - # self.assertEqual(expected, a_mp_protocol.safe_send(command, sessid, **kwargs)) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_server_caches.py b/src/tests/test_server_caches.py deleted file mode 100644 index 40d366a00..000000000 --- a/src/tests/test_server_caches.py +++ /dev/null @@ -1,54 +0,0 @@ -import unittest - -class TestHashid(unittest.TestCase): - def test_hashid(self): - # self.assertEqual(expected, hashid(obj, suffix)) - assert True # TODO: implement your test here - -class TestFieldPreSave(unittest.TestCase): - def test_field_pre_save(self): - # self.assertEqual(expected, field_pre_save(sender, instance, update_fields, raw, **kwargs)) - assert True # TODO: implement your test here - -class TestFieldPostSave(unittest.TestCase): - def test_field_post_save(self): - # self.assertEqual(expected, field_post_save(sender, instance, update_fields, raw, **kwargs)) - assert True # TODO: implement your test here - -class TestGetAttrCache(unittest.TestCase): - def test_get_attr_cache(self): - # self.assertEqual(expected, get_attr_cache(obj)) - assert True # TODO: implement your test here - -class TestSetAttrCache(unittest.TestCase): - def test_set_attr_cache(self): - # self.assertEqual(expected, set_attr_cache(obj, store)) - assert True # TODO: implement your test here - -class TestGetPropCache(unittest.TestCase): - def test_get_prop_cache(self): - # self.assertEqual(expected, get_prop_cache(obj, propname)) - assert True # TODO: implement your test here - -class TestSetPropCache(unittest.TestCase): - def test_set_prop_cache(self): - # self.assertEqual(expected, set_prop_cache(obj, propname, propvalue)) - assert True # TODO: implement your test here - -class TestDelPropCache(unittest.TestCase): - def test_del_prop_cache(self): - # self.assertEqual(expected, del_prop_cache(obj, propname)) - assert True # TODO: implement your test here - -class TestFlushPropCache(unittest.TestCase): - def test_flush_prop_cache(self): - # self.assertEqual(expected, flush_prop_cache()) - assert True # TODO: implement your test here - -class TestGetCacheSizes(unittest.TestCase): - def test_get_cache_sizes(self): - # self.assertEqual(expected, get_cache_sizes()) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_server_initial_setup.py b/src/tests/test_server_initial_setup.py deleted file mode 100644 index 3dda142df..000000000 --- a/src/tests/test_server_initial_setup.py +++ /dev/null @@ -1,54 +0,0 @@ -import unittest - -class TestCreateConfigValues(unittest.TestCase): - def test_create_config_values(self): - # self.assertEqual(expected, create_config_values()) - assert True # TODO: implement your test here - -class TestGetGodPlayer(unittest.TestCase): - def test_get_god_player(self): - # self.assertEqual(expected, get_god_player()) - assert True # TODO: implement your test here - -class TestCreateObjects(unittest.TestCase): - def test_create_objects(self): - # self.assertEqual(expected, create_objects()) - assert True # TODO: implement your test here - -class TestCreateChannels(unittest.TestCase): - def test_create_channels(self): - # self.assertEqual(expected, create_channels()) - assert True # TODO: implement your test here - -class TestCreateSystemScripts(unittest.TestCase): - def test_create_system_scripts(self): - # self.assertEqual(expected, create_system_scripts()) - assert True # TODO: implement your test here - -class TestStartGameTime(unittest.TestCase): - def test_start_game_time(self): - # self.assertEqual(expected, start_game_time()) - assert True # TODO: implement your test here - -class TestCreateAdminMediaLinks(unittest.TestCase): - def test_create_admin_media_links(self): - # self.assertEqual(expected, create_admin_media_links()) - assert True # TODO: implement your test here - -class TestAtInitialSetup(unittest.TestCase): - def test_at_initial_setup(self): - # self.assertEqual(expected, at_initial_setup()) - assert True # TODO: implement your test here - -class TestResetServer(unittest.TestCase): - def test_reset_server(self): - # self.assertEqual(expected, reset_server()) - assert True # TODO: implement your test here - -class TestHandleSetup(unittest.TestCase): - def test_handle_setup(self): - # self.assertEqual(expected, handle_setup(last_step)) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_server_manager.py b/src/tests/test_server_manager.py deleted file mode 100644 index 8b8b5fadf..000000000 --- a/src/tests/test_server_manager.py +++ /dev/null @@ -1,15 +0,0 @@ -import unittest - -class TestServerConfigManager(unittest.TestCase): - def test_conf(self): - # server_config_manager = ServerConfigManager() - # self.assertEqual(expected, server_config_manager.conf(key, value, delete, default)) - assert True # TODO: implement your test here - - def test_get_mysql_db_version(self): - # server_config_manager = ServerConfigManager() - # self.assertEqual(expected, server_config_manager.get_mysql_db_version()) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_server_models.py b/src/tests/test_server_models.py deleted file mode 100644 index bd053c8f5..000000000 --- a/src/tests/test_server_models.py +++ /dev/null @@ -1,15 +0,0 @@ -import unittest - -class TestServerConfig(unittest.TestCase): - def test___unicode__(self): - # server_config = ServerConfig() - # self.assertEqual(expected, server_config.__unicode__()) - assert True # TODO: implement your test here - - def test_store(self): - # server_config = ServerConfig() - # self.assertEqual(expected, server_config.store(key, value)) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_server_oob_msdp.py b/src/tests/test_server_oob_msdp.py deleted file mode 100644 index 51bb5d22e..000000000 --- a/src/tests/test_server_oob_msdp.py +++ /dev/null @@ -1,49 +0,0 @@ -import unittest - -class TestOOBFieldTracker(unittest.TestCase): - def test___init__(self): - # o_ob_field_tracker = OOBFieldTracker(oobhandler, fieldname, sessid, *args, **kwargs) - assert True # TODO: implement your test here - - def test_update(self): - # o_ob_field_tracker = OOBFieldTracker(oobhandler, fieldname, sessid, *args, **kwargs) - # self.assertEqual(expected, o_ob_field_tracker.update(new_value, *args, **kwargs)) - assert True # TODO: implement your test here - -class TestOOBAttributeTracker(unittest.TestCase): - def test___init__(self): - # o_ob_attribute_tracker = OOBAttributeTracker(oobhandler, fieldname, sessid, attrname, *args, **kwargs) - assert True # TODO: implement your test here - - def test_update(self): - # o_ob_attribute_tracker = OOBAttributeTracker(oobhandler, fieldname, sessid, attrname, *args, **kwargs) - # self.assertEqual(expected, o_ob_attribute_tracker.update(new_value, *args, **kwargs)) - assert True # TODO: implement your test here - -class TestOobError(unittest.TestCase): - def test_oob_error(self): - # self.assertEqual(expected, oob_error(oobhandler, session, errmsg, *args, **kwargs)) - assert True # TODO: implement your test here - -class TestList(unittest.TestCase): - def test_list(self): - # self.assertEqual(expected, list(oobhandler, session, mode, *args, **kwargs)) - assert True # TODO: implement your test here - -class TestSend(unittest.TestCase): - def test_send(self): - # self.assertEqual(expected, send(oobhandler, session, *args, **kwargs)) - assert True # TODO: implement your test here - -class TestReport(unittest.TestCase): - def test_report(self): - # self.assertEqual(expected, report(oobhandler, session, *args, **kwargs)) - assert True # TODO: implement your test here - -class TestUnreport(unittest.TestCase): - def test_unreport(self): - # self.assertEqual(expected, unreport(oobhandler, session, vartype, *args, **kwargs)) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_server_oobhandler.py b/src/tests/test_server_oobhandler.py deleted file mode 100644 index fc386c122..000000000 --- a/src/tests/test_server_oobhandler.py +++ /dev/null @@ -1,114 +0,0 @@ -import unittest - -class TestTrackerHandler(unittest.TestCase): - def test___init__(self): - # tracker_handler = TrackerHandler(obj) - assert True # TODO: implement your test here - - def test_add(self): - # tracker_handler = TrackerHandler(obj) - # self.assertEqual(expected, tracker_handler.add(fieldname, tracker)) - assert True # TODO: implement your test here - - def test_remove(self): - # tracker_handler = TrackerHandler(obj) - # self.assertEqual(expected, tracker_handler.remove(fieldname, trackerclass, *args, **kwargs)) - assert True # TODO: implement your test here - - def test_update(self): - # tracker_handler = TrackerHandler(obj) - # self.assertEqual(expected, tracker_handler.update(fieldname, new_value)) - assert True # TODO: implement your test here - -class TestTrackerBase(unittest.TestCase): - def test___init__(self): - # tracker_base = TrackerBase(*args, **kwargs) - assert True # TODO: implement your test here - - def test_at_remove(self): - # tracker_base = TrackerBase(*args, **kwargs) - # self.assertEqual(expected, tracker_base.at_remove(*args, **kwargs)) - assert True # TODO: implement your test here - - def test_update(self): - # tracker_base = TrackerBase(*args, **kwargs) - # self.assertEqual(expected, tracker_base.update(*args, **kwargs)) - assert True # TODO: implement your test here - -class TestOOBTicker(unittest.TestCase): - def test___init__(self): - # o_ob_ticker = OOBTicker(interval) - assert True # TODO: implement your test here - -class TestOOBHandler(unittest.TestCase): - def test___init__(self): - # o_ob_handler = OOBHandler() - assert True # TODO: implement your test here - - def test_execute_cmd(self): - # o_ob_handler = OOBHandler() - # self.assertEqual(expected, o_ob_handler.execute_cmd(session, func_key, *args, **kwargs)) - assert True # TODO: implement your test here - - def test_get_all_tracked(self): - # o_ob_handler = OOBHandler() - # self.assertEqual(expected, o_ob_handler.get_all_tracked(session)) - assert True # TODO: implement your test here - - def test_msg(self): - # o_ob_handler = OOBHandler() - # self.assertEqual(expected, o_ob_handler.msg(sessid, funcname, *args, **kwargs)) - assert True # TODO: implement your test here - - def test_repeat(self): - # o_ob_handler = OOBHandler() - # self.assertEqual(expected, o_ob_handler.repeat(obj, sessid, func_key, interval, *args, **kwargs)) - assert True # TODO: implement your test here - - def test_restore(self): - # o_ob_handler = OOBHandler() - # self.assertEqual(expected, o_ob_handler.restore()) - assert True # TODO: implement your test here - - def test_save(self): - # o_ob_handler = OOBHandler() - # self.assertEqual(expected, o_ob_handler.save()) - assert True # TODO: implement your test here - - def test_track(self): - # o_ob_handler = OOBHandler() - # self.assertEqual(expected, o_ob_handler.track(obj, sessid, fieldname, trackerclass, *args, **kwargs)) - assert True # TODO: implement your test here - - def test_track_attribute(self): - # o_ob_handler = OOBHandler() - # self.assertEqual(expected, o_ob_handler.track_attribute(obj, sessid, attr_name, trackerclass)) - assert True # TODO: implement your test here - - def test_track_field(self): - # o_ob_handler = OOBHandler() - # self.assertEqual(expected, o_ob_handler.track_field(obj, sessid, field_name, trackerclass)) - assert True # TODO: implement your test here - - def test_unrepeat(self): - # o_ob_handler = OOBHandler() - # self.assertEqual(expected, o_ob_handler.unrepeat(obj, sessid, func_key, interval)) - assert True # TODO: implement your test here - - def test_untrack(self): - # o_ob_handler = OOBHandler() - # self.assertEqual(expected, o_ob_handler.untrack(obj, sessid, fieldname, trackerclass, *args, **kwargs)) - assert True # TODO: implement your test here - - def test_untrack_attribute(self): - # o_ob_handler = OOBHandler() - # self.assertEqual(expected, o_ob_handler.untrack_attribute(obj, sessid, attr_name, trackerclass)) - assert True # TODO: implement your test here - - def test_untrack_field(self): - # o_ob_handler = OOBHandler() - # self.assertEqual(expected, o_ob_handler.untrack_field(obj, sessid, field_name)) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_server_server.py b/src/tests/test_server_server.py deleted file mode 100644 index 66e6080c4..000000000 --- a/src/tests/test_server_server.py +++ /dev/null @@ -1,39 +0,0 @@ -import unittest - -class TestEvennia(unittest.TestCase): - def test___init__(self): - # evennia = Evennia(application) - assert True # TODO: implement your test here - - def test_run_init_hooks(self): - # evennia = Evennia(application) - # self.assertEqual(expected, evennia.run_init_hooks()) - assert True # TODO: implement your test here - - def test_run_initial_setup(self): - # evennia = Evennia(application) - # self.assertEqual(expected, evennia.run_initial_setup()) - assert True # TODO: implement your test here - - def test_set_restart_mode(self): - # evennia = Evennia(application) - # self.assertEqual(expected, evennia.set_restart_mode(mode)) - assert True # TODO: implement your test here - - def test_shutdown(self): - # evennia = Evennia(application) - # self.assertEqual(expected, evennia.shutdown(mode, _reactor_stopping)) - assert True # TODO: implement your test here - - def test_sqlite3_prep(self): - # evennia = Evennia(application) - # self.assertEqual(expected, evennia.sqlite3_prep()) - assert True # TODO: implement your test here - - def test_update_defaults(self): - # evennia = Evennia(application) - # self.assertEqual(expected, evennia.update_defaults()) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_server_serversession.py b/src/tests/test_server_serversession.py deleted file mode 100644 index b5526b3a3..000000000 --- a/src/tests/test_server_serversession.py +++ /dev/null @@ -1,104 +0,0 @@ -import unittest - -class TestServerSession(unittest.TestCase): - def test___eq__(self): - # server_session = ServerSession() - # self.assertEqual(expected, server_session.__eq__(other)) - assert True # TODO: implement your test here - - def test___init__(self): - # server_session = ServerSession() - assert True # TODO: implement your test here - - def test___str__(self): - # server_session = ServerSession() - # self.assertEqual(expected, server_session.__str__()) - assert True # TODO: implement your test here - - def test___unicode__(self): - # server_session = ServerSession() - # self.assertEqual(expected, server_session.__unicode__()) - assert True # TODO: implement your test here - - def test_access(self): - # server_session = ServerSession() - # self.assertEqual(expected, server_session.access(*args, **kwargs)) - assert True # TODO: implement your test here - - def test_at_cmdset_get(self): - # server_session = ServerSession() - # self.assertEqual(expected, server_session.at_cmdset_get(**kwargs)) - assert True # TODO: implement your test here - - def test_at_disconnect(self): - # server_session = ServerSession() - # self.assertEqual(expected, server_session.at_disconnect()) - assert True # TODO: implement your test here - - def test_at_login(self): - # server_session = ServerSession() - # self.assertEqual(expected, server_session.at_login(player)) - assert True # TODO: implement your test here - - def test_at_sync(self): - # server_session = ServerSession() - # self.assertEqual(expected, server_session.at_sync()) - assert True # TODO: implement your test here - - def test_data_in(self): - # server_session = ServerSession() - # self.assertEqual(expected, server_session.data_in(text, **kwargs)) - assert True # TODO: implement your test here - - def test_data_out(self): - # server_session = ServerSession() - # self.assertEqual(expected, server_session.data_out(text, **kwargs)) - assert True # TODO: implement your test here - - def test_get_player(self): - # server_session = ServerSession() - # self.assertEqual(expected, server_session.get_player()) - assert True # TODO: implement your test here - - def test_get_puppet(self): - # server_session = ServerSession() - # self.assertEqual(expected, server_session.get_puppet()) - assert True # TODO: implement your test here - - def test_get_puppet_or_player(self): - # server_session = ServerSession() - # self.assertEqual(expected, server_session.get_puppet_or_player()) - assert True # TODO: implement your test here - - def test_log(self): - # server_session = ServerSession() - # self.assertEqual(expected, server_session.log(message, channel)) - assert True # TODO: implement your test here - - def test_msg(self): - # server_session = ServerSession() - # self.assertEqual(expected, server_session.msg(text, **kwargs)) - assert True # TODO: implement your test here - - def test_ndb_del(self): - # server_session = ServerSession() - # self.assertEqual(expected, server_session.ndb_del()) - assert True # TODO: implement your test here - - def test_ndb_get(self): - # server_session = ServerSession() - # self.assertEqual(expected, server_session.ndb_get()) - assert True # TODO: implement your test here - - def test_ndb_set(self): - # server_session = ServerSession() - # self.assertEqual(expected, server_session.ndb_set(value)) - assert True # TODO: implement your test here - - def test_update_session_counters(self): - # server_session = ServerSession() - # self.assertEqual(expected, server_session.update_session_counters(idle)) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_server_session.py b/src/tests/test_server_session.py deleted file mode 100644 index cf96cbaf8..000000000 --- a/src/tests/test_server_session.py +++ /dev/null @@ -1,40 +0,0 @@ -import unittest - -class TestSession(unittest.TestCase): - def test_at_sync(self): - # session = Session() - # self.assertEqual(expected, session.at_sync()) - assert True # TODO: implement your test here - - def test_data_in(self): - # session = Session() - # self.assertEqual(expected, session.data_in(text, **kwargs)) - assert True # TODO: implement your test here - - def test_data_out(self): - # session = Session() - # self.assertEqual(expected, session.data_out(text, **kwargs)) - assert True # TODO: implement your test here - - def test_disconnect(self): - # session = Session() - # self.assertEqual(expected, session.disconnect(reason)) - assert True # TODO: implement your test here - - def test_get_sync_data(self): - # session = Session() - # self.assertEqual(expected, session.get_sync_data()) - assert True # TODO: implement your test here - - def test_init_session(self): - # session = Session() - # self.assertEqual(expected, session.init_session(protocol_key, address, sessionhandler)) - assert True # TODO: implement your test here - - def test_load_sync_data(self): - # session = Session() - # self.assertEqual(expected, session.load_sync_data(sessdata)) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_server_webserver.py b/src/tests/test_server_webserver.py deleted file mode 100644 index c2a9d3602..000000000 --- a/src/tests/test_server_webserver.py +++ /dev/null @@ -1,46 +0,0 @@ -import unittest - -class TestHTTPChannelWithXForwardedFor(unittest.TestCase): - def test_allHeadersReceived(self): - # h_ttp_channel_with_x_forwarded_for = HTTPChannelWithXForwardedFor() - # self.assertEqual(expected, h_ttp_channel_with_x_forwarded_for.allHeadersReceived()) - assert True # TODO: implement your test here - -class TestEvenniaReverseProxyResource(unittest.TestCase): - def test_getChild(self): - # evennia_reverse_proxy_resource = EvenniaReverseProxyResource() - # self.assertEqual(expected, evennia_reverse_proxy_resource.getChild(path, request)) - assert True # TODO: implement your test here - - def test_render(self): - # evennia_reverse_proxy_resource = EvenniaReverseProxyResource() - # self.assertEqual(expected, evennia_reverse_proxy_resource.render(request)) - assert True # TODO: implement your test here - -class TestDjangoWebRoot(unittest.TestCase): - def test___init__(self): - # django_web_root = DjangoWebRoot(pool) - assert True # TODO: implement your test here - - def test_getChild(self): - # django_web_root = DjangoWebRoot(pool) - # self.assertEqual(expected, django_web_root.getChild(path, request)) - assert True # TODO: implement your test here - -class TestWSGIWebServer(unittest.TestCase): - def test___init__(self): - # w_sgi_web_server = WSGIWebServer(pool, *args, **kwargs) - assert True # TODO: implement your test here - - def test_startService(self): - # w_sgi_web_server = WSGIWebServer(pool, *args, **kwargs) - # self.assertEqual(expected, w_sgi_web_server.startService()) - assert True # TODO: implement your test here - - def test_stopService(self): - # w_sgi_web_server = WSGIWebServer(pool, *args, **kwargs) - # self.assertEqual(expected, w_sgi_web_server.stopService()) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_typeclasses_models.py b/src/tests/test_typeclasses_models.py deleted file mode 100644 index ef4e2b329..000000000 --- a/src/tests/test_typeclasses_models.py +++ /dev/null @@ -1,266 +0,0 @@ -import unittest - -class TestAttribute(unittest.TestCase): - def test___init__(self): - # attribute = Attribute(*args, **kwargs) - assert True # TODO: implement your test here - - def test___str__(self): - # attribute = Attribute(*args, **kwargs) - # self.assertEqual(expected, attribute.__str__()) - assert True # TODO: implement your test here - - def test___unicode__(self): - # attribute = Attribute(*args, **kwargs) - # self.assertEqual(expected, attribute.__unicode__()) - assert True # TODO: implement your test here - - def test_access(self): - # attribute = Attribute(*args, **kwargs) - # self.assertEqual(expected, attribute.access(accessing_obj, access_type, default, **kwargs)) - assert True # TODO: implement your test here - - def test_at_set(self): - # attribute = Attribute(*args, **kwargs) - # self.assertEqual(expected, attribute.at_set(new_value)) - assert True # TODO: implement your test here - -class TestAttributeHandler(unittest.TestCase): - def test___init__(self): - # attribute_handler = AttributeHandler(obj) - assert True # TODO: implement your test here - - def test_add(self): - # attribute_handler = AttributeHandler(obj) - # self.assertEqual(expected, attribute_handler.add(key, value, category, lockstring, strattr, accessing_obj, default_access)) - assert True # TODO: implement your test here - - def test_all(self): - # attribute_handler = AttributeHandler(obj) - # self.assertEqual(expected, attribute_handler.all(accessing_obj, default_access)) - assert True # TODO: implement your test here - - def test_clear(self): - # attribute_handler = AttributeHandler(obj) - # self.assertEqual(expected, attribute_handler.clear(category, accessing_obj, default_access)) - assert True # TODO: implement your test here - - def test_get(self): - # attribute_handler = AttributeHandler(obj) - # self.assertEqual(expected, attribute_handler.get(key, category, default, return_obj, strattr, raise_exception, accessing_obj, default_access, not_found_none)) - assert True # TODO: implement your test here - - def test_has(self): - # attribute_handler = AttributeHandler(obj) - # self.assertEqual(expected, attribute_handler.has(key, category)) - assert True # TODO: implement your test here - - def test_remove(self): - # attribute_handler = AttributeHandler(obj) - # self.assertEqual(expected, attribute_handler.remove(key, raise_exception, category, accessing_obj, default_access)) - assert True # TODO: implement your test here - -class TestNickHandler(unittest.TestCase): - def test_add(self): - # nick_handler = NickHandler() - # self.assertEqual(expected, nick_handler.add(key, replacement, category, **kwargs)) - assert True # TODO: implement your test here - - def test_get(self): - # nick_handler = NickHandler() - # self.assertEqual(expected, nick_handler.get(key, category, **kwargs)) - assert True # TODO: implement your test here - - def test_has(self): - # nick_handler = NickHandler() - # self.assertEqual(expected, nick_handler.has(key, category)) - assert True # TODO: implement your test here - - def test_nickreplace(self): - # nick_handler = NickHandler() - # self.assertEqual(expected, nick_handler.nickreplace(raw_string, categories, include_player)) - assert True # TODO: implement your test here - - def test_remove(self): - # nick_handler = NickHandler() - # self.assertEqual(expected, nick_handler.remove(key, category, **kwargs)) - assert True # TODO: implement your test here - -class TestNAttributeHandler(unittest.TestCase): - def test___init__(self): - # n_attribute_handler = NAttributeHandler(obj) - assert True # TODO: implement your test here - - def test_add(self): - # n_attribute_handler = NAttributeHandler(obj) - # self.assertEqual(expected, n_attribute_handler.add(key, value)) - assert True # TODO: implement your test here - - def test_all(self): - # n_attribute_handler = NAttributeHandler(obj) - # self.assertEqual(expected, n_attribute_handler.all()) - assert True # TODO: implement your test here - - def test_get(self): - # n_attribute_handler = NAttributeHandler(obj) - # self.assertEqual(expected, n_attribute_handler.get(key)) - assert True # TODO: implement your test here - - def test_has(self): - # n_attribute_handler = NAttributeHandler(obj) - # self.assertEqual(expected, n_attribute_handler.has(key)) - assert True # TODO: implement your test here - - def test_remove(self): - # n_attribute_handler = NAttributeHandler(obj) - # self.assertEqual(expected, n_attribute_handler.remove(key)) - assert True # TODO: implement your test here - -class TestTag(unittest.TestCase): - def test___str__(self): - # tag = Tag() - # self.assertEqual(expected, tag.__str__()) - assert True # TODO: implement your test here - - def test___unicode__(self): - # tag = Tag() - # self.assertEqual(expected, tag.__unicode__()) - assert True # TODO: implement your test here - -class TestTagHandler(unittest.TestCase): - def test___init__(self): - # tag_handler = TagHandler(obj) - assert True # TODO: implement your test here - - def test___str__(self): - # tag_handler = TagHandler(obj) - # self.assertEqual(expected, tag_handler.__str__()) - assert True # TODO: implement your test here - - def test_add(self): - # tag_handler = TagHandler(obj) - # self.assertEqual(expected, tag_handler.add(tag, category, data)) - assert True # TODO: implement your test here - - def test_all(self): - # tag_handler = TagHandler(obj) - # self.assertEqual(expected, tag_handler.all(category, return_key_and_category)) - assert True # TODO: implement your test here - - def test_clear(self): - # tag_handler = TagHandler(obj) - # self.assertEqual(expected, tag_handler.clear()) - assert True # TODO: implement your test here - - def test_get(self): - # tag_handler = TagHandler(obj) - # self.assertEqual(expected, tag_handler.get(key, category, return_tagobj)) - assert True # TODO: implement your test here - - def test_remove(self): - # tag_handler = TagHandler(obj) - # self.assertEqual(expected, tag_handler.remove(key, category)) - assert True # TODO: implement your test here - -class TestTypedObject(unittest.TestCase): - def test___eq__(self): - # typed_object = TypedObject(*args, **kwargs) - # self.assertEqual(expected, typed_object.__eq__(other)) - assert True # TODO: implement your test here - - def test___getattribute__(self): - # typed_object = TypedObject(*args, **kwargs) - # self.assertEqual(expected, typed_object.__getattribute__(propname)) - assert True # TODO: implement your test here - - def test___init__(self): - # typed_object = TypedObject(*args, **kwargs) - assert True # TODO: implement your test here - - def test___str__(self): - # typed_object = TypedObject(*args, **kwargs) - # self.assertEqual(expected, typed_object.__str__()) - assert True # TODO: implement your test here - - def test___unicode__(self): - # typed_object = TypedObject(*args, **kwargs) - # self.assertEqual(expected, typed_object.__unicode__()) - assert True # TODO: implement your test here - - def test_access(self): - # typed_object = TypedObject(*args, **kwargs) - # self.assertEqual(expected, typed_object.access(accessing_obj, access_type, default, **kwargs)) - assert True # TODO: implement your test here - - def test_attr(self): - # typed_object = TypedObject(*args, **kwargs) - # self.assertEqual(expected, typed_object.attr(attribute_name, value, delete)) - assert True # TODO: implement your test here - - def test_check_permstring(self): - # typed_object = TypedObject(*args, **kwargs) - # self.assertEqual(expected, typed_object.check_permstring(permstring)) - assert True # TODO: implement your test here - - def test_del_attribute(self): - # typed_object = TypedObject(*args, **kwargs) - # self.assertEqual(expected, typed_object.del_attribute(attribute_name, raise_exception)) - assert True # TODO: implement your test here - - def test_delete(self): - # typed_object = TypedObject(*args, **kwargs) - # self.assertEqual(expected, typed_object.delete(*args, **kwargs)) - assert True # TODO: implement your test here - - def test_flush_from_cache(self): - # typed_object = TypedObject(*args, **kwargs) - # self.assertEqual(expected, typed_object.flush_from_cache()) - assert True # TODO: implement your test here - - def test_get_all_attributes(self): - # typed_object = TypedObject(*args, **kwargs) - # self.assertEqual(expected, typed_object.get_all_attributes()) - assert True # TODO: implement your test here - - def test_get_attribute(self): - # typed_object = TypedObject(*args, **kwargs) - # self.assertEqual(expected, typed_object.get_attribute(attribute_name, default, raise_exception)) - assert True # TODO: implement your test here - - def test_get_attribute_obj(self): - # typed_object = TypedObject(*args, **kwargs) - # self.assertEqual(expected, typed_object.get_attribute_obj(attribute_name, default)) - assert True # TODO: implement your test here - - def test_has_attribute(self): - # typed_object = TypedObject(*args, **kwargs) - # self.assertEqual(expected, typed_object.has_attribute(attribute_name)) - assert True # TODO: implement your test here - - def test_is_typeclass(self): - # typed_object = TypedObject(*args, **kwargs) - # self.assertEqual(expected, typed_object.is_typeclass(typeclass, exact)) - assert True # TODO: implement your test here - - def test_nattr(self): - # typed_object = TypedObject(*args, **kwargs) - # self.assertEqual(expected, typed_object.nattr(attribute_name, value, delete)) - assert True # TODO: implement your test here - - def test_secure_attr(self): - # typed_object = TypedObject(*args, **kwargs) - # self.assertEqual(expected, typed_object.secure_attr(accessing_object, attribute_name, value, delete, default_access_read, default_access_edit, default_access_create)) - assert True # TODO: implement your test here - - def test_set_attribute(self): - # typed_object = TypedObject(*args, **kwargs) - # self.assertEqual(expected, typed_object.set_attribute(attribute_name, new_value, lockstring)) - assert True # TODO: implement your test here - - def test_swap_typeclass(self): - # typed_object = TypedObject(*args, **kwargs) - # self.assertEqual(expected, typed_object.swap_typeclass(new_typeclass, clean_attributes, no_default)) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_typeclasses_typeclass.py b/src/tests/test_typeclasses_typeclass.py deleted file mode 100644 index 7d4ef14df..000000000 --- a/src/tests/test_typeclasses_typeclass.py +++ /dev/null @@ -1,49 +0,0 @@ -import unittest - -class TestMetaTypeClass(unittest.TestCase): - def test___init__(self): - # meta_type_class = MetaTypeClass(*args, **kwargs) - assert True # TODO: implement your test here - - def test___str__(self): - # meta_type_class = MetaTypeClass(*args, **kwargs) - # self.assertEqual(expected, meta_type_class.__str__()) - assert True # TODO: implement your test here - -class TestTypeClass(unittest.TestCase): - def test___delattr__(self): - # type_class = TypeClass(dbobj) - # self.assertEqual(expected, type_class.__delattr__(propname)) - assert True # TODO: implement your test here - - def test___eq__(self): - # type_class = TypeClass(dbobj) - # self.assertEqual(expected, type_class.__eq__(other)) - assert True # TODO: implement your test here - - def test___getattribute__(self): - # type_class = TypeClass(dbobj) - # self.assertEqual(expected, type_class.__getattribute__(propname)) - assert True # TODO: implement your test here - - def test___init__(self): - # type_class = TypeClass(dbobj) - assert True # TODO: implement your test here - - def test___setattr__(self): - # type_class = TypeClass(dbobj) - # self.assertEqual(expected, type_class.__setattr__(propname, value)) - assert True # TODO: implement your test here - - def test___str__(self): - # type_class = TypeClass(dbobj) - # self.assertEqual(expected, type_class.__str__()) - assert True # TODO: implement your test here - - def test___unicode__(self): - # type_class = TypeClass(dbobj) - # self.assertEqual(expected, type_class.__unicode__()) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_utils_ansi.py b/src/tests/test_utils_ansi.py deleted file mode 100644 index 35c7417a2..000000000 --- a/src/tests/test_utils_ansi.py +++ /dev/null @@ -1,139 +0,0 @@ -import unittest - -class TestANSIParser(unittest.TestCase): - def test_parse_ansi(self): - # a_nsi_parser = ANSIParser() - # self.assertEqual(expected, a_nsi_parser.parse_ansi(string, strip_ansi, xterm256)) - assert True # TODO: implement your test here - - def test_strip_raw_codes(self): - # a_nsi_parser = ANSIParser() - # self.assertEqual(expected, a_nsi_parser.strip_raw_codes(string)) - assert True # TODO: implement your test here - - def test_sub_ansi(self): - # a_nsi_parser = ANSIParser() - # self.assertEqual(expected, a_nsi_parser.sub_ansi(ansimatch)) - assert True # TODO: implement your test here - - def test_sub_xterm256(self): - # a_nsi_parser = ANSIParser() - # self.assertEqual(expected, a_nsi_parser.sub_xterm256(rgbmatch)) - assert True # TODO: implement your test here - - def test_parse_rgb(self): - # a_nsi_parser = ANSIParser() - # self.assertEqual(expected, a_nsi_parser.parse_rgb(rgbmatch)) - assert True # TODO: implement your test here - -class TestParseAnsi(unittest.TestCase): - def test_parse_ansi(self): - # self.assertEqual(expected, parse_ansi(string, strip_ansi, parser, xterm256)) - assert True # TODO: implement your test here - -class TestRaw(unittest.TestCase): - def test_raw(self): - # self.assertEqual(expected, raw(string)) - assert True # TODO: implement your test here - -class TestGroup(unittest.TestCase): - def test_group(self): - # self.assertEqual(expected, group(lst, n)) - assert True # TODO: implement your test here - -class TestANSIMeta(unittest.TestCase): - def test___init__(self): - # a_nsi_meta = ANSIMeta(*args, **kwargs) - assert True # TODO: implement your test here - -class TestANSIString(unittest.TestCase): - def test___add__(self): - # a_nsi_string = ANSIString(*args, **kwargs) - # self.assertEqual(expected, a_nsi_string.__add__(other)) - assert True # TODO: implement your test here - - def test___getitem__(self): - # a_nsi_string = ANSIString(*args, **kwargs) - # self.assertEqual(expected, a_nsi_string.__getitem__(item)) - assert True # TODO: implement your test here - - def test___getslice__(self): - # a_nsi_string = ANSIString(*args, **kwargs) - # self.assertEqual(expected, a_nsi_string.__getslice__(i, j)) - assert True # TODO: implement your test here - - def test___init__(self): - # a_nsi_string = ANSIString(*args, **kwargs) - assert True # TODO: implement your test here - - def test___new__(self): - # a_nsi_string = ANSIString(*args, **kwargs) - assert True # TODO: implement your test here - - def test___radd__(self): - # a_nsi_string = ANSIString(*args, **kwargs) - # self.assertEqual(expected, a_nsi_string.__radd__(other)) - assert True # TODO: implement your test here - - def test___repr__(self): - # a_nsi_string = ANSIString(*args, **kwargs) - # self.assertEqual(expected, a_nsi_string.__repr__()) - assert True # TODO: implement your test here - - def test___str__(self): - # a_nsi_string = ANSIString(*args, **kwargs) - # self.assertEqual(expected, a_nsi_string.__str__()) - assert True # TODO: implement your test here - - def test___unicode__(self): - # a_nsi_string = ANSIString(*args, **kwargs) - # self.assertEqual(expected, a_nsi_string.__unicode__()) - assert True # TODO: implement your test here - - def test_center(self): - # a_nsi_string = ANSIString(*args, **kwargs) - # self.assertEqual(expected, a_nsi_string.center(width, fillchar, difference)) - assert True # TODO: implement your test here - - def test_clean(self): - # a_nsi_string = ANSIString(*args, **kwargs) - # self.assertEqual(expected, a_nsi_string.clean()) - assert True # TODO: implement your test here - - def test_join(self): - # a_nsi_string = ANSIString(*args, **kwargs) - # self.assertEqual(expected, a_nsi_string.join(iterable)) - assert True # TODO: implement your test here - - def test_ljust(self): - # a_nsi_string = ANSIString(*args, **kwargs) - # self.assertEqual(expected, a_nsi_string.ljust(width, fillchar, difference)) - assert True # TODO: implement your test here - - def test_partition(self): - # a_nsi_string = ANSIString(*args, **kwargs) - # self.assertEqual(expected, a_nsi_string.partition(sep, reverse)) - assert True # TODO: implement your test here - - def test_raw(self): - # a_nsi_string = ANSIString(*args, **kwargs) - # self.assertEqual(expected, a_nsi_string.raw()) - assert True # TODO: implement your test here - - def test_rjust(self): - # a_nsi_string = ANSIString(*args, **kwargs) - # self.assertEqual(expected, a_nsi_string.rjust(width, fillchar, difference)) - assert True # TODO: implement your test here - - def test_rsplit(self): - # a_nsi_string = ANSIString(*args, **kwargs) - # self.assertEqual(expected, a_nsi_string.rsplit(by, maxsplit)) - assert True # TODO: implement your test here - - def test_split(self): - # a_nsi_string = ANSIString(*args, **kwargs) - # self.assertEqual(expected, a_nsi_string.split(by, maxsplit)) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_utils_ansi_new.py b/src/tests/test_utils_ansi_new.py deleted file mode 100644 index c1a161020..000000000 --- a/src/tests/test_utils_ansi_new.py +++ /dev/null @@ -1,9 +0,0 @@ -import unittest - -class TestSubMeth(unittest.TestCase): - def test_sub_meth(self): - # self.assertEqual(expected, sub_meth(obj, function)) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_utils_batchprocessors.py b/src/tests/test_utils_batchprocessors.py deleted file mode 100644 index 0bcadebe3..000000000 --- a/src/tests/test_utils_batchprocessors.py +++ /dev/null @@ -1,36 +0,0 @@ -import unittest - -class TestReadBatchfile(unittest.TestCase): - def test_read_batchfile(self): - # self.assertEqual(expected, read_batchfile(pythonpath, file_ending)) - assert True # TODO: implement your test here - -class TestBatchCommandProcessor(unittest.TestCase): - def test_parse_file(self): - # batch_command_processor = BatchCommandProcessor() - # self.assertEqual(expected, batch_command_processor.parse_file(pythonpath)) - assert True # TODO: implement your test here - -class TestTbFilename(unittest.TestCase): - def test_tb_filename(self): - # self.assertEqual(expected, tb_filename(tb)) - assert True # TODO: implement your test here - -class TestTbIter(unittest.TestCase): - def test_tb_iter(self): - # self.assertEqual(expected, tb_iter(tb)) - assert True # TODO: implement your test here - -class TestBatchCodeProcessor(unittest.TestCase): - def test_code_exec(self): - # batch_code_processor = BatchCodeProcessor() - # self.assertEqual(expected, batch_code_processor.code_exec(codedict, extra_environ, debug)) - assert True # TODO: implement your test here - - def test_parse_file(self): - # batch_code_processor = BatchCodeProcessor() - # self.assertEqual(expected, batch_code_processor.parse_file(pythonpath)) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_utils_create.py b/src/tests/test_utils_create.py deleted file mode 100644 index 5bdf15765..000000000 --- a/src/tests/test_utils_create.py +++ /dev/null @@ -1,44 +0,0 @@ -import unittest - -class TestHandleDbref(unittest.TestCase): - def test_handle_dbref(self): - # self.assertEqual(expected, handle_dbref(inp, objclass, raise_errors)) - assert True # TODO: implement your test here - -class TestCreateObject(unittest.TestCase): - def test_create_object(self): - # self.assertEqual(expected, create_object(typeclass, key, location, home, permissions, locks, aliases, destination, report_to, nohome)) - assert True # TODO: implement your test here - -class TestCreateScript(unittest.TestCase): - def test_create_script(self): - # self.assertEqual(expected, create_script(typeclass, key, obj, player, locks, interval, start_delay, repeats, persistent, autostart, report_to)) - assert True # TODO: implement your test here - -class TestCreateHelpEntry(unittest.TestCase): - def test_create_help_entry(self): - # self.assertEqual(expected, create_help_entry(key, entrytext, category, locks)) - assert True # TODO: implement your test here - -class TestCreateMessage(unittest.TestCase): - def test_create_message(self): - # self.assertEqual(expected, create_message(senderobj, message, channels, receivers, locks, header)) - assert True # TODO: implement your test here - -class TestCreateChannel(unittest.TestCase): - def test_create_channel(self): - # self.assertEqual(expected, create_channel(key, aliases, desc, locks, keep_log, typeclass)) - assert True # TODO: implement your test here - -class TestCreateTag(unittest.TestCase): - def test_create_tag(self): - # self.assertEqual(expected, create_tag(self, key, category, data)) - assert True # TODO: implement your test here - -class TestCreatePlayer(unittest.TestCase): - def test_create_player(self): - # self.assertEqual(expected, create_player(key, email, password, typeclass, is_superuser, locks, permissions, report_to)) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_utils_dbserialize.py b/src/tests/test_utils_dbserialize.py deleted file mode 100644 index c7af53848..000000000 --- a/src/tests/test_utils_dbserialize.py +++ /dev/null @@ -1,124 +0,0 @@ -import unittest - -class test__SaverMutable(unittest.TestCase): - def test___delitem__(self): - # __saver_mutable = _SaverMutable(*args, **kwargs) - # self.assertEqual(expected, __saver_mutable.__delitem__(key)) - assert True # TODO: implement your test here - - def test___getitem__(self): - # __saver_mutable = _SaverMutable(*args, **kwargs) - # self.assertEqual(expected, __saver_mutable.__getitem__(key)) - assert True # TODO: implement your test here - - def test___init__(self): - # __saver_mutable = _SaverMutable(*args, **kwargs) - assert True # TODO: implement your test here - - def test___iter__(self): - # __saver_mutable = _SaverMutable(*args, **kwargs) - # self.assertEqual(expected, __saver_mutable.__iter__()) - assert True # TODO: implement your test here - - def test___len__(self): - # __saver_mutable = _SaverMutable(*args, **kwargs) - # self.assertEqual(expected, __saver_mutable.__len__()) - assert True # TODO: implement your test here - - def test___repr__(self): - # __saver_mutable = _SaverMutable(*args, **kwargs) - # self.assertEqual(expected, __saver_mutable.__repr__()) - assert True # TODO: implement your test here - - def test___setitem__(self): - # __saver_mutable = _SaverMutable(*args, **kwargs) - # self.assertEqual(expected, __saver_mutable.__setitem__(key, value)) - assert True # TODO: implement your test here - -class test__SaverList(unittest.TestCase): - def test___add__(self): - # __saver_list = _SaverList(*args, **kwargs) - # self.assertEqual(expected, __saver_list.__add__(otherlist)) - assert True # TODO: implement your test here - - def test___init__(self): - # __saver_list = _SaverList(*args, **kwargs) - assert True # TODO: implement your test here - - def test_insert(self): - # __saver_list = _SaverList(*args, **kwargs) - # self.assertEqual(expected, __saver_list.insert(index, value)) - assert True # TODO: implement your test here - -class test__SaverDict(unittest.TestCase): - def test___init__(self): - # __saver_dict = _SaverDict(*args, **kwargs) - assert True # TODO: implement your test here - - def test_has_key(self): - # __saver_dict = _SaverDict(*args, **kwargs) - # self.assertEqual(expected, __saver_dict.has_key(key)) - assert True # TODO: implement your test here - -class test__SaverSet(unittest.TestCase): - def test___contains__(self): - # __saver_set = _SaverSet(*args, **kwargs) - # self.assertEqual(expected, __saver_set.__contains__(value)) - assert True # TODO: implement your test here - - def test___init__(self): - # __saver_set = _SaverSet(*args, **kwargs) - assert True # TODO: implement your test here - - def test_add(self): - # __saver_set = _SaverSet(*args, **kwargs) - # self.assertEqual(expected, __saver_set.add(value)) - assert True # TODO: implement your test here - - def test_discard(self): - # __saver_set = _SaverSet(*args, **kwargs) - # self.assertEqual(expected, __saver_set.discard(value)) - assert True # TODO: implement your test here - -class TestPackDbobj(unittest.TestCase): - def test_pack_dbobj(self): - # self.assertEqual(expected, pack_dbobj(item)) - assert True # TODO: implement your test here - -class TestUnpackDbobj(unittest.TestCase): - def test_unpack_dbobj(self): - # self.assertEqual(expected, unpack_dbobj(item)) - assert True # TODO: implement your test here - -class TestToPickle(unittest.TestCase): - def test_to_pickle(self): - # self.assertEqual(expected, to_pickle(data)) - assert True # TODO: implement your test here - -class TestFromPickle(unittest.TestCase): - def test_from_pickle(self): - # self.assertEqual(expected, from_pickle(data, db_obj)) - assert True # TODO: implement your test here - -class TestDoPickle(unittest.TestCase): - def test_do_pickle(self): - # self.assertEqual(expected, do_pickle(data)) - assert True # TODO: implement your test here - -class TestDoUnpickle(unittest.TestCase): - def test_do_unpickle(self): - # self.assertEqual(expected, do_unpickle(data)) - assert True # TODO: implement your test here - -class TestDbserialize(unittest.TestCase): - def test_dbserialize(self): - # self.assertEqual(expected, dbserialize(data)) - assert True # TODO: implement your test here - -class TestDbunserialize(unittest.TestCase): - def test_dbunserialize(self): - # self.assertEqual(expected, dbunserialize(data, db_obj)) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_utils_evform.py b/src/tests/test_utils_evform.py deleted file mode 100644 index 80d9d89a3..000000000 --- a/src/tests/test_utils_evform.py +++ /dev/null @@ -1,29 +0,0 @@ -import unittest - -class TestEvForm(unittest.TestCase): - def test___init__(self): - # ev_form = EvForm(filename, cells, tables, form, **kwargs) - assert True # TODO: implement your test here - - def test___str__(self): - # ev_form = EvForm(filename, cells, tables, form, **kwargs) - # self.assertEqual(expected, ev_form.__str__()) - assert True # TODO: implement your test here - - def test___unicode__(self): - # ev_form = EvForm(filename, cells, tables, form, **kwargs) - # self.assertEqual(expected, ev_form.__unicode__()) - assert True # TODO: implement your test here - - def test_map(self): - # ev_form = EvForm(filename, cells, tables, form, **kwargs) - # self.assertEqual(expected, ev_form.map(cells, tables, **kwargs)) - assert True # TODO: implement your test here - - def test_reload(self): - # ev_form = EvForm(filename, cells, tables, form, **kwargs) - # self.assertEqual(expected, ev_form.reload(filename, form, **kwargs)) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_utils_evtable.py b/src/tests/test_utils_evtable.py deleted file mode 100644 index 2eb7ab446..000000000 --- a/src/tests/test_utils_evtable.py +++ /dev/null @@ -1,109 +0,0 @@ -import unittest - -class TestMakeIter(unittest.TestCase): - def test_make_iter(self): - # self.assertEqual(expected, make_iter(obj)) - assert True # TODO: implement your test here - -class TestWrap(unittest.TestCase): - def test_wrap(self): - # self.assertEqual(expected, wrap(text, width, **kwargs)) - assert True # TODO: implement your test here - -class TestFill(unittest.TestCase): - def test_fill(self): - # self.assertEqual(expected, fill(text, width, **kwargs)) - assert True # TODO: implement your test here - -class TestCell(unittest.TestCase): - def test___init__(self): - # cell = Cell(data, **kwargs) - assert True # TODO: implement your test here - - def test___str__(self): - # cell = Cell(data, **kwargs) - # self.assertEqual(expected, cell.__str__()) - assert True # TODO: implement your test here - - def test___unicode__(self): - # cell = Cell(data, **kwargs) - # self.assertEqual(expected, cell.__unicode__()) - assert True # TODO: implement your test here - - def test_get(self): - # cell = Cell(data, **kwargs) - # self.assertEqual(expected, cell.get()) - assert True # TODO: implement your test here - - def test_get_height(self): - # cell = Cell(data, **kwargs) - # self.assertEqual(expected, cell.get_height()) - assert True # TODO: implement your test here - - def test_get_min_height(self): - # cell = Cell(data, **kwargs) - # self.assertEqual(expected, cell.get_min_height()) - assert True # TODO: implement your test here - - def test_get_min_width(self): - # cell = Cell(data, **kwargs) - # self.assertEqual(expected, cell.get_min_width()) - assert True # TODO: implement your test here - - def test_get_width(self): - # cell = Cell(data, **kwargs) - # self.assertEqual(expected, cell.get_width()) - assert True # TODO: implement your test here - - def test_reformat(self): - # cell = Cell(data, **kwargs) - # self.assertEqual(expected, cell.reformat(**kwargs)) - assert True # TODO: implement your test here - - def test_replace_data(self): - # cell = Cell(data, **kwargs) - # self.assertEqual(expected, cell.replace_data(data, **kwargs)) - assert True # TODO: implement your test here - -class TestEvTable(unittest.TestCase): - def test___init__(self): - # ev_table = EvTable(*args, **kwargs) - assert True # TODO: implement your test here - - def test___str__(self): - # ev_table = EvTable(*args, **kwargs) - # self.assertEqual(expected, ev_table.__str__()) - assert True # TODO: implement your test here - - def test___unicode__(self): - # ev_table = EvTable(*args, **kwargs) - # self.assertEqual(expected, ev_table.__unicode__()) - assert True # TODO: implement your test here - - def test_add_column(self): - # ev_table = EvTable(*args, **kwargs) - # self.assertEqual(expected, ev_table.add_column(*args, **kwargs)) - assert True # TODO: implement your test here - - def test_add_header(self): - # ev_table = EvTable(*args, **kwargs) - # self.assertEqual(expected, ev_table.add_header(*args, **kwargs)) - assert True # TODO: implement your test here - - def test_add_row(self): - # ev_table = EvTable(*args, **kwargs) - # self.assertEqual(expected, ev_table.add_row(*args, **kwargs)) - assert True # TODO: implement your test here - - def test_get(self): - # ev_table = EvTable(*args, **kwargs) - # self.assertEqual(expected, ev_table.get()) - assert True # TODO: implement your test here - - def test_reformat(self): - # ev_table = EvTable(*args, **kwargs) - # self.assertEqual(expected, ev_table.reformat(**kwargs)) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_utils_gametime.py b/src/tests/test_utils_gametime.py deleted file mode 100644 index bec50c2cb..000000000 --- a/src/tests/test_utils_gametime.py +++ /dev/null @@ -1,55 +0,0 @@ -import unittest - -class TestGameTime(unittest.TestCase): - def test_at_repeat(self): - # game_time = GameTime() - # self.assertEqual(expected, game_time.at_repeat()) - assert True # TODO: implement your test here - - def test_at_script_creation(self): - # game_time = GameTime() - # self.assertEqual(expected, game_time.at_script_creation()) - assert True # TODO: implement your test here - - def test_at_start(self): - # game_time = GameTime() - # self.assertEqual(expected, game_time.at_start()) - assert True # TODO: implement your test here - -class TestSave(unittest.TestCase): - def test_save(self): - # self.assertEqual(expected, save()) - assert True # TODO: implement your test here - -class TestRuntime(unittest.TestCase): - def test_runtime(self): - # self.assertEqual(expected, runtime(format)) - assert True # TODO: implement your test here - -class TestUptime(unittest.TestCase): - def test_uptime(self): - # self.assertEqual(expected, uptime(format)) - assert True # TODO: implement your test here - -class TestGametime(unittest.TestCase): - def test_gametime(self): - # self.assertEqual(expected, gametime(format)) - assert True # TODO: implement your test here - -class TestGametimeToRealtime(unittest.TestCase): - def test_gametime_to_realtime(self): - # self.assertEqual(expected, gametime_to_realtime(secs, mins, hrs, days, weeks, months, yrs, format)) - assert True # TODO: implement your test here - -class TestRealtimeToGametime(unittest.TestCase): - def test_realtime_to_gametime(self): - # self.assertEqual(expected, realtime_to_gametime(secs, mins, hrs, days, weeks, months, yrs, format)) - assert True # TODO: implement your test here - -class TestInitGametime(unittest.TestCase): - def test_init_gametime(self): - # self.assertEqual(expected, init_gametime()) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_utils_logger.py b/src/tests/test_utils_logger.py deleted file mode 100644 index ceda40450..000000000 --- a/src/tests/test_utils_logger.py +++ /dev/null @@ -1,29 +0,0 @@ -import unittest - -class TestLogTrace(unittest.TestCase): - def test_log_trace(self): - # self.assertEqual(expected, log_trace(errmsg)) - assert True # TODO: implement your test here - -class TestLogErrmsg(unittest.TestCase): - def test_log_errmsg(self): - # self.assertEqual(expected, log_errmsg(errmsg)) - assert True # TODO: implement your test here - -class TestLogWarnmsg(unittest.TestCase): - def test_log_warnmsg(self): - # self.assertEqual(expected, log_warnmsg(warnmsg)) - assert True # TODO: implement your test here - -class TestLogInfomsg(unittest.TestCase): - def test_log_infomsg(self): - # self.assertEqual(expected, log_infomsg(infomsg)) - assert True # TODO: implement your test here - -class TestLogDepmsg(unittest.TestCase): - def test_log_depmsg(self): - # self.assertEqual(expected, log_depmsg(depmsg)) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_utils_picklefield.py b/src/tests/test_utils_picklefield.py deleted file mode 100644 index 7087ffc57..000000000 --- a/src/tests/test_utils_picklefield.py +++ /dev/null @@ -1,64 +0,0 @@ -import unittest - -class test__ObjectWrapper(unittest.TestCase): - def test___init__(self): - # __object_wrapper = _ObjectWrapper(obj) - assert True # TODO: implement your test here - -class TestWrapConflictualObject(unittest.TestCase): - def test_wrap_conflictual_object(self): - # self.assertEqual(expected, wrap_conflictual_object(obj)) - assert True # TODO: implement your test here - -class TestDbsafeEncode(unittest.TestCase): - def test_dbsafe_encode(self): - # self.assertEqual(expected, dbsafe_encode(value, compress_object, pickle_protocol)) - assert True # TODO: implement your test here - -class TestDbsafeDecode(unittest.TestCase): - def test_dbsafe_decode(self): - # self.assertEqual(expected, dbsafe_decode(value, compress_object)) - assert True # TODO: implement your test here - -class TestPickledObjectField(unittest.TestCase): - def test___init__(self): - # pickled_object_field = PickledObjectField(*args, **kwargs) - assert True # TODO: implement your test here - - def test_get_db_prep_lookup(self): - # pickled_object_field = PickledObjectField(*args, **kwargs) - # self.assertEqual(expected, pickled_object_field.get_db_prep_lookup(lookup_type, value, connection, prepared)) - assert True # TODO: implement your test here - - def test_get_db_prep_value(self): - # pickled_object_field = PickledObjectField(*args, **kwargs) - # self.assertEqual(expected, pickled_object_field.get_db_prep_value(value, connection, prepared)) - assert True # TODO: implement your test here - - def test_get_default(self): - # pickled_object_field = PickledObjectField(*args, **kwargs) - # self.assertEqual(expected, pickled_object_field.get_default()) - assert True # TODO: implement your test here - - def test_get_internal_type(self): - # pickled_object_field = PickledObjectField(*args, **kwargs) - # self.assertEqual(expected, pickled_object_field.get_internal_type()) - assert True # TODO: implement your test here - - def test_pre_save(self): - # pickled_object_field = PickledObjectField(*args, **kwargs) - # self.assertEqual(expected, pickled_object_field.pre_save(model_instance, add)) - assert True # TODO: implement your test here - - def test_to_python(self): - # pickled_object_field = PickledObjectField(*args, **kwargs) - # self.assertEqual(expected, pickled_object_field.to_python(value)) - assert True # TODO: implement your test here - - def test_value_to_string(self): - # pickled_object_field = PickledObjectField(*args, **kwargs) - # self.assertEqual(expected, pickled_object_field.value_to_string(obj)) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_utils_prettytable.py b/src/tests/test_utils_prettytable.py deleted file mode 100644 index 97ec4de1c..000000000 --- a/src/tests/test_utils_prettytable.py +++ /dev/null @@ -1,134 +0,0 @@ -import unittest - -class TestPrettyTable(unittest.TestCase): - def test___getattr__(self): - # pretty_table = PrettyTable(field_names, **kwargs) - # self.assertEqual(expected, pretty_table.__getattr__(name)) - assert True # TODO: implement your test here - - def test___getitem__(self): - # pretty_table = PrettyTable(field_names, **kwargs) - # self.assertEqual(expected, pretty_table.__getitem__(index)) - assert True # TODO: implement your test here - - def test___init__(self): - # pretty_table = PrettyTable(field_names, **kwargs) - assert True # TODO: implement your test here - - def test___str__(self): - # pretty_table = PrettyTable(field_names, **kwargs) - # self.assertEqual(expected, pretty_table.__str__()) - assert True # TODO: implement your test here - - def test___str___case_2(self): - # pretty_table = PrettyTable(field_names, **kwargs) - # self.assertEqual(expected, pretty_table.__str__()) - assert True # TODO: implement your test here - - def test___unicode__(self): - # pretty_table = PrettyTable(field_names, **kwargs) - # self.assertEqual(expected, pretty_table.__unicode__()) - assert True # TODO: implement your test here - - def test_add_column(self): - # pretty_table = PrettyTable(field_names, **kwargs) - # self.assertEqual(expected, pretty_table.add_column(fieldname, column, align, valign)) - assert True # TODO: implement your test here - - def test_add_row(self): - # pretty_table = PrettyTable(field_names, **kwargs) - # self.assertEqual(expected, pretty_table.add_row(row)) - assert True # TODO: implement your test here - - def test_clear(self): - # pretty_table = PrettyTable(field_names, **kwargs) - # self.assertEqual(expected, pretty_table.clear()) - assert True # TODO: implement your test here - - def test_clear_rows(self): - # pretty_table = PrettyTable(field_names, **kwargs) - # self.assertEqual(expected, pretty_table.clear_rows()) - assert True # TODO: implement your test here - - def test_copy(self): - # pretty_table = PrettyTable(field_names, **kwargs) - # self.assertEqual(expected, pretty_table.copy()) - assert True # TODO: implement your test here - - def test_del_row(self): - # pretty_table = PrettyTable(field_names, **kwargs) - # self.assertEqual(expected, pretty_table.del_row(row_index)) - assert True # TODO: implement your test here - - def test_get_html_string(self): - # pretty_table = PrettyTable(field_names, **kwargs) - # self.assertEqual(expected, pretty_table.get_html_string(**kwargs)) - assert True # TODO: implement your test here - - def test_get_string(self): - # pretty_table = PrettyTable(field_names, **kwargs) - # self.assertEqual(expected, pretty_table.get_string(**kwargs)) - assert True # TODO: implement your test here - - def test_set_style(self): - # pretty_table = PrettyTable(field_names, **kwargs) - # self.assertEqual(expected, pretty_table.set_style(style)) - assert True # TODO: implement your test here - -class TestFromCsv(unittest.TestCase): - def test_from_csv(self): - # self.assertEqual(expected, from_csv(fp, field_names, **kwargs)) - assert True # TODO: implement your test here - -class TestFromDbCursor(unittest.TestCase): - def test_from_db_cursor(self): - # self.assertEqual(expected, from_db_cursor(cursor, **kwargs)) - assert True # TODO: implement your test here - -class TestTableHandler(unittest.TestCase): - def test___init__(self): - # table_handler = TableHandler(**kwargs) - assert True # TODO: implement your test here - - def test_generate_table(self): - # table_handler = TableHandler(**kwargs) - # self.assertEqual(expected, table_handler.generate_table(rows)) - assert True # TODO: implement your test here - - def test_handle_data(self): - # table_handler = TableHandler(**kwargs) - # self.assertEqual(expected, table_handler.handle_data(data)) - assert True # TODO: implement your test here - - def test_handle_endtag(self): - # table_handler = TableHandler(**kwargs) - # self.assertEqual(expected, table_handler.handle_endtag(tag)) - assert True # TODO: implement your test here - - def test_handle_starttag(self): - # table_handler = TableHandler(**kwargs) - # self.assertEqual(expected, table_handler.handle_starttag(tag, attrs)) - assert True # TODO: implement your test here - - def test_make_fields_unique(self): - # table_handler = TableHandler(**kwargs) - # self.assertEqual(expected, table_handler.make_fields_unique(fields)) - assert True # TODO: implement your test here - -class TestFromHtml(unittest.TestCase): - def test_from_html(self): - # self.assertEqual(expected, from_html(html_code, **kwargs)) - assert True # TODO: implement your test here - -class TestFromHtmlOne(unittest.TestCase): - def test_from_html_one(self): - # self.assertEqual(expected, from_html_one(html_code, **kwargs)) - assert True # TODO: implement your test here - -class TestMain(unittest.TestCase): - def test_main(self): - # self.assertEqual(expected, main()) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_utils_search.py b/src/tests/test_utils_search.py deleted file mode 100644 index 40d6e1a28..000000000 --- a/src/tests/test_utils_search.py +++ /dev/null @@ -1,24 +0,0 @@ -import unittest - -class TestSearchObjectTag(unittest.TestCase): - def test_search_object_tag(self): - # self.assertEqual(expected, search_object_tag(key, category)) - assert True # TODO: implement your test here - -class TestSearchPlayerTag(unittest.TestCase): - def test_search_player_tag(self): - # self.assertEqual(expected, search_player_tag(key, category)) - assert True # TODO: implement your test here - -class TestSearchScriptTag(unittest.TestCase): - def test_search_script_tag(self): - # self.assertEqual(expected, search_script_tag(key, category)) - assert True # TODO: implement your test here - -class TestSearchChannelTag(unittest.TestCase): - def test_search_channel_tag(self): - # self.assertEqual(expected, search_channel_tag(key, category)) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_utils_tests.py b/src/tests/test_utils_tests.py deleted file mode 100644 index 9fd6ea5f1..000000000 --- a/src/tests/test_utils_tests.py +++ /dev/null @@ -1,40 +0,0 @@ -import unittest - -class TestANSIStringTestCase(unittest.TestCase): - def test_checker(self): - # a_nsi_string_test_case = ANSIStringTestCase() - # self.assertEqual(expected, a_nsi_string_test_case.checker(ansi, raw, clean)) - assert True # TODO: implement your test here - - def test_table_check(self): - # a_nsi_string_test_case = ANSIStringTestCase() - # self.assertEqual(expected, a_nsi_string_test_case.table_check(ansi, char, code)) - assert True # TODO: implement your test here - - def test_test_instance(self): - # a_nsi_string_test_case = ANSIStringTestCase() - # self.assertEqual(expected, a_nsi_string_test_case.test_instance()) - assert True # TODO: implement your test here - - def test_test_join(self): - # a_nsi_string_test_case = ANSIStringTestCase() - # self.assertEqual(expected, a_nsi_string_test_case.test_join()) - assert True # TODO: implement your test here - - def test_test_len(self): - # a_nsi_string_test_case = ANSIStringTestCase() - # self.assertEqual(expected, a_nsi_string_test_case.test_len()) - assert True # TODO: implement your test here - - def test_test_slice(self): - # a_nsi_string_test_case = ANSIStringTestCase() - # self.assertEqual(expected, a_nsi_string_test_case.test_slice()) - assert True # TODO: implement your test here - - def test_test_split(self): - # a_nsi_string_test_case = ANSIStringTestCase() - # self.assertEqual(expected, a_nsi_string_test_case.test_split()) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_utils_text2html.py b/src/tests/test_utils_text2html.py deleted file mode 100644 index 6cbdc98f8..000000000 --- a/src/tests/test_utils_text2html.py +++ /dev/null @@ -1,55 +0,0 @@ -import unittest - -class TestTextToHTMLparser(unittest.TestCase): - def test_convert_linebreaks(self): - # text_to_htm_lparser = TextToHTMLparser() - # self.assertEqual(expected, text_to_htm_lparser.convert_linebreaks(text)) - assert True # TODO: implement your test here - - def test_convert_urls(self): - # text_to_htm_lparser = TextToHTMLparser() - # self.assertEqual(expected, text_to_htm_lparser.convert_urls(text)) - assert True # TODO: implement your test here - - def test_do_sub(self): - # text_to_htm_lparser = TextToHTMLparser() - # self.assertEqual(expected, text_to_htm_lparser.do_sub(m)) - assert True # TODO: implement your test here - - def test_parse(self): - # text_to_htm_lparser = TextToHTMLparser() - # self.assertEqual(expected, text_to_htm_lparser.parse(text, strip_ansi)) - assert True # TODO: implement your test here - - def test_re_bold(self): - # text_to_htm_lparser = TextToHTMLparser() - # self.assertEqual(expected, text_to_htm_lparser.re_bold(text)) - assert True # TODO: implement your test here - - def test_re_color(self): - # text_to_htm_lparser = TextToHTMLparser() - # self.assertEqual(expected, text_to_htm_lparser.re_color(text)) - assert True # TODO: implement your test here - - def test_re_underline(self): - # text_to_htm_lparser = TextToHTMLparser() - # self.assertEqual(expected, text_to_htm_lparser.re_underline(text)) - assert True # TODO: implement your test here - - def test_remove_backspaces(self): - # text_to_htm_lparser = TextToHTMLparser() - # self.assertEqual(expected, text_to_htm_lparser.remove_backspaces(text)) - assert True # TODO: implement your test here - - def test_remove_bells(self): - # text_to_htm_lparser = TextToHTMLparser() - # self.assertEqual(expected, text_to_htm_lparser.remove_bells(text)) - assert True # TODO: implement your test here - -class TestParseHtml(unittest.TestCase): - def test_parse_html(self): - # self.assertEqual(expected, parse_html(string, strip_ansi, parser)) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/test_utils_utils.py b/src/tests/test_utils_utils.py deleted file mode 100644 index bb2af71bc..000000000 --- a/src/tests/test_utils_utils.py +++ /dev/null @@ -1,196 +0,0 @@ -# test with game/manage.py test -import unittest - -from src.utils import utils - -class TestIsIter(unittest.TestCase): - def test_is_iter(self): - self.assertEqual(True, utils.is_iter([1,2,3,4])) - self.assertEqual(False, utils.is_iter("This is not an iterable")) - -class TestCrop(unittest.TestCase): - def test_crop(self): - # No text, return no text - self.assertEqual("", utils.crop("", width=10, suffix="[...]")) - # Input length equal to max width, no crop - self.assertEqual("0123456789", utils.crop("0123456789", width=10, suffix="[...]")) - # Input length greater than max width, crop (suffix included in width) - self.assertEqual("0123[...]", utils.crop("0123456789", width=9, suffix="[...]")) - # Input length less than desired width, no crop - self.assertEqual("0123", utils.crop("0123", width=9, suffix="[...]")) - # Width too small or equal to width of suffix - self.assertEqual("012", utils.crop("0123", width=3, suffix="[...]")) - self.assertEqual("01234", utils.crop("0123456", width=5, suffix="[...]")) - -class TestDedent(unittest.TestCase): - def test_dedent(self): - #print "Did TestDedent run?" - # Empty string, return empty string - self.assertEqual("", utils.dedent("")) - # No leading whitespace - self.assertEqual("TestDedent", utils.dedent("TestDedent")) - # Leading whitespace, single line - self.assertEqual("TestDedent", utils.dedent(" TestDedent")) - # Leading whitespace, multi line - input_string = " hello\n world" - expected_string = "hello\nworld" - self.assertEqual(expected_string, utils.dedent(input_string)) - -class TestListToString(unittest.TestCase): - """ - Default function header from utils.py: - list_to_string(inlist, endsep="and", addquote=False) - - Examples: - no endsep: - [1,2,3] -> '1, 2, 3' - with endsep=='and': - [1,2,3] -> '1, 2 and 3' - with addquote and endsep - [1,2,3] -> '"1", "2" and "3"' - """ - #print "Did TestListToString run?" - def test_list_to_string(self): - self.assertEqual('1, 2, 3', utils.list_to_string([1,2,3], endsep="")) - self.assertEqual('"1", "2", "3"', utils.list_to_string([1,2,3], endsep="", addquote=True)) - self.assertEqual('1, 2 and 3', utils.list_to_string([1,2,3])) - self.assertEqual('"1", "2" and "3"', utils.list_to_string([1,2,3], endsep="and", addquote=True)) - - -class TestWildcardToRegexp(unittest.TestCase): - def test_wildcard_to_regexp(self): - # self.assertEqual(expected, wildcard_to_regexp(instring)) - assert True # TODO: implement your test here - -class TestTimeFormat(unittest.TestCase): - def test_time_format(self): - # self.assertEqual(expected, time_format(seconds, style)) - assert True # TODO: implement your test here - -class TestDatetimeFormat(unittest.TestCase): - def test_datetime_format(self): - # self.assertEqual(expected, datetime_format(dtobj)) - assert True # TODO: implement your test here - -class TestHostOsIs(unittest.TestCase): - def test_host_os_is(self): - # self.assertEqual(expected, host_os_is(osname)) - assert True # TODO: implement your test here - -class TestGetEvenniaVersion(unittest.TestCase): - def test_get_evennia_version(self): - # self.assertEqual(expected, get_evennia_version()) - assert True # TODO: implement your test here - -class TestPypathToRealpath(unittest.TestCase): - def test_pypath_to_realpath(self): - # self.assertEqual(expected, pypath_to_realpath(python_path, file_ending)) - assert True # TODO: implement your test here - -class TestToUnicode(unittest.TestCase): - def test_to_unicode(self): - # self.assertEqual(expected, to_unicode(obj, encoding, force_string)) - assert True # TODO: implement your test here - -class TestToStr(unittest.TestCase): - def test_to_str(self): - # self.assertEqual(expected, to_str(obj, encoding, force_string)) - assert True # TODO: implement your test here - -class TestValidateEmailAddress(unittest.TestCase): - def test_validate_email_address(self): - # self.assertEqual(expected, validate_email_address(emailaddress)) - assert True # TODO: implement your test here - -class TestInheritsFrom(unittest.TestCase): - def test_inherits_from(self): - # self.assertEqual(expected, inherits_from(obj, parent)) - assert True # TODO: implement your test here - -class TestServerServices(unittest.TestCase): - def test_server_services(self): - # self.assertEqual(expected, server_services()) - assert True # TODO: implement your test here - -class TestUsesDatabase(unittest.TestCase): - def test_uses_database(self): - # self.assertEqual(expected, uses_database(name)) - assert True # TODO: implement your test here - -class TestDelay(unittest.TestCase): - def test_delay(self): - # self.assertEqual(expected, delay(delay, callback, retval)) - assert True # TODO: implement your test here - -class TestCleanObjectCaches(unittest.TestCase): - def test_clean_object_caches(self): - # self.assertEqual(expected, clean_object_caches(obj)) - assert True # TODO: implement your test here - -class TestRunAsync(unittest.TestCase): - def test_run_async(self): - # self.assertEqual(expected, run_async(to_execute, *args, **kwargs)) - assert True # TODO: implement your test here - -class TestCheckEvenniaDependencies(unittest.TestCase): - def test_check_evennia_dependencies(self): - # self.assertEqual(expected, check_evennia_dependencies()) - assert True # TODO: implement your test here - -class TestHasParent(unittest.TestCase): - def test_has_parent(self): - # self.assertEqual(expected, has_parent(basepath, obj)) - assert True # TODO: implement your test here - -class TestModImport(unittest.TestCase): - def test_mod_import(self): - # self.assertEqual(expected, mod_import(module)) - assert True # TODO: implement your test here - -class TestAllFromModule(unittest.TestCase): - def test_all_from_module(self): - # self.assertEqual(expected, all_from_module(module)) - assert True # TODO: implement your test here - -class TestVariableFromModule(unittest.TestCase): - def test_variable_from_module(self): - # self.assertEqual(expected, variable_from_module(module, variable, default)) - assert True # TODO: implement your test here - -class TestStringFromModule(unittest.TestCase): - def test_string_from_module(self): - # self.assertEqual(expected, string_from_module(module, variable, default)) - assert True # TODO: implement your test here - -class TestInitNewPlayer(unittest.TestCase): - def test_init_new_player(self): - # self.assertEqual(expected, init_new_player(player)) - assert True # TODO: implement your test here - -class TestStringSimilarity(unittest.TestCase): - def test_string_similarity(self): - # self.assertEqual(expected, string_similarity(string1, string2)) - assert True # TODO: implement your test here - -class TestStringSuggestions(unittest.TestCase): - def test_string_suggestions(self): - # self.assertEqual(expected, string_suggestions(string, vocabulary, cutoff, maxnum)) - assert True # TODO: implement your test here - -class TestStringPartialMatching(unittest.TestCase): - def test_string_partial_matching(self): - # self.assertEqual(expected, string_partial_matching(alternatives, inp, ret_index)) - assert True # TODO: implement your test here - -class TestFormatTable(unittest.TestCase): - def test_format_table(self): - # self.assertEqual(expected, format_table(table, extra_space)) - assert True # TODO: implement your test here - -class TestGetEvenniaPids(unittest.TestCase): - def test_get_evennia_pids(self): - # self.assertEqual(expected, get_evennia_pids()) - assert True # TODO: implement your test here - -if __name__ == '__main__': - unittest.main() diff --git a/src/typeclasses/__init__.py b/src/typeclasses/__init__.py deleted file mode 100644 index 40a96afc6..000000000 --- a/src/typeclasses/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/src/typeclasses/migrations/__init__.py b/src/typeclasses/migrations/__init__.py deleted file mode 100644 index 40a96afc6..000000000 --- a/src/typeclasses/migrations/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/src/typeclasses/models.py b/src/typeclasses/models.py deleted file mode 100644 index 7757bceda..000000000 --- a/src/typeclasses/models.py +++ /dev/null @@ -1,1595 +0,0 @@ -""" -This is the *abstract* django models for many of the database objects -in Evennia. A django abstract (obs, not the same as a Python metaclass!) is -a model which is not actually created in the database, but which only exists -for other models to inherit from, to avoid code duplication. Any model can -import and inherit from these classes. - -Attributes are database objects stored on other objects. The implementing -class needs to supply a ForeignKey field attr_object pointing to the kind -of object being mapped. Attributes storing iterables actually store special -types of iterables named PackedList/PackedDict respectively. These make -sure to save changes to them to database - this is criticial in order to -allow for obj.db.mylist[2] = data. Also, all dbobjects are saved as -dbrefs but are also aggressively cached. - -TypedObjects are objects 'decorated' with a typeclass - that is, the typeclass -(which is a normal Python class implementing some special tricks with its -get/set attribute methods, allows for the creation of all sorts of different -objects all with the same database object underneath. Usually attributes are -used to permanently store things not hard-coded as field on the database object. -The admin should usually not have to deal directly with the database object -layer. - -This module also contains the Managers for the respective models; inherit from -these to create custom managers. - -""" - -import sys -import re -import traceback -import weakref - -from django.db import models -from django.core.exceptions import ObjectDoesNotExist -from django.conf import settings -from django.utils.encoding import smart_str - -from src.utils.idmapper.models import SharedMemoryModel -from src.server.caches import get_prop_cache, set_prop_cache -#from src.server.caches import set_attr_cache - -#from src.server.caches import call_ndb_hooks -from src.server.models import ServerConfig -from src.typeclasses import managers -from src.locks.lockhandler import LockHandler -from src.utils import logger -from src.utils.utils import ( - make_iter, is_iter, to_str, inherits_from, lazy_property) -from src.utils.dbserialize import to_pickle, from_pickle -from src.utils.picklefield import PickledObjectField - -__all__ = ("Attribute", "TypeNick", "TypedObject") - -TICKER_HANDLER = None - -_PERMISSION_HIERARCHY = [p.lower() for p in settings.PERMISSION_HIERARCHY] -_TYPECLASS_AGGRESSIVE_CACHE = settings.TYPECLASS_AGGRESSIVE_CACHE - -_GA = object.__getattribute__ -_SA = object.__setattr__ -_DA = object.__delattr__ - - -#------------------------------------------------------------ -# -# Attributes -# -#------------------------------------------------------------ - -class Attribute(SharedMemoryModel): - """ - Abstract django model. - - Attributes are things that are specific to different types of objects. For - example, a drink container needs to store its fill level, whereas an exit - needs to store its open/closed/locked/unlocked state. These are done via - attributes, rather than making different classes for each object type and - storing them directly. The added benefit is that we can add/remove - attributes on the fly as we like. - The Attribute class defines the following properties: - key - primary identifier - lock_storage - perm strings - obj - which object the attribute is defined on - date_created - when the attribute was created. - value - the data stored in the attribute, in pickled form - using wrappers to be able to store/retrieve models. - strvalue - string-only data. This data is not pickled and is - thus faster to search for in the database. - category - optional character string for grouping the Attribute - - """ - - # - # Attribute Database Model setup - # - # These database fields are all set using their corresponding properties, - # named same as the field, but withtout the db_* prefix. - db_key = models.CharField('key', max_length=255, db_index=True) - db_value = PickledObjectField( - 'value', null=True, - help_text="The data returned when the attribute is accessed. Must be " - "written as a Python literal if editing through the admin " - "interface. Attribute values which are not Python literals " - "cannot be edited through the admin interface.") - db_strvalue = models.TextField( - 'strvalue', null=True, blank=True, - help_text="String-specific storage for quick look-up") - db_category = models.CharField( - 'category', max_length=128, db_index=True, blank=True, null=True, - help_text="Optional categorization of attribute.") - # Lock storage - db_lock_storage = models.TextField( - 'locks', blank=True, - help_text="Lockstrings for this object are stored here.") - db_model = models.CharField( - 'model', max_length=32, db_index=True, blank=True, null=True, - help_text="Which model of object this attribute is attached to (A " - "natural key like objects.dbobject). You should not change " - "this value unless you know what you are doing.") - # subclass of Attribute (None or nick) - db_attrtype = models.CharField( - 'attrtype', max_length=16, db_index=True, blank=True, null=True, - help_text="Subclass of Attribute (None or nick)") - # time stamp - db_date_created = models.DateTimeField( - 'date_created', editable=False, auto_now_add=True) - - # Database manager - #objects = managers.AttributeManager() - - @lazy_property - def locks(self): - return LockHandler(self) - - class Meta: - "Define Django meta options" - verbose_name = "Evennia Attribute" - - # read-only wrappers - key = property(lambda self: self.db_key) - strvalue = property(lambda self: self.db_strvalue) - category = property(lambda self: self.db_category) - model = property(lambda self: self.db_model) - attrtype = property(lambda self: self.db_attrtype) - date_created = property(lambda self: self.db_date_created) - - def __lock_storage_get(self): - return self.db_lock_storage - def __lock_storage_set(self, value): - self.db_lock_storage = value - self.save(update_fields=["db_lock_storage"]) - def __lock_storage_del(self): - self.db_lock_storage = "" - self.save(update_fields=["db_lock_storage"]) - lock_storage = property(__lock_storage_get, __lock_storage_set, __lock_storage_del) - - # Wrapper properties to easily set database fields. These are - # @property decorators that allows to access these fields using - # normal python operations (without having to remember to save() - # etc). So e.g. a property 'attr' has a get/set/del decorator - # defined that allows the user to do self.attr = value, - # value = self.attr and del self.attr respectively (where self - # is the object in question). - - # value property (wraps db_value) - #@property - def __value_get(self): - """ - Getter. Allows for value = self.value. - We cannot cache here since it makes certain cases (such - as storing a dbobj which is then deleted elsewhere) out-of-sync. - The overhead of unpickling seems hard to avoid. - """ - return from_pickle(self.db_value, db_obj=self) - - #@value.setter - def __value_set(self, new_value): - """ - Setter. Allows for self.value = value. We cannot cache here, - see self.__value_get. - """ - self.db_value = to_pickle(new_value) - self.save(update_fields=["db_value"]) - - #@value.deleter - def __value_del(self): - "Deleter. Allows for del attr.value. This removes the entire attribute." - self.delete() - value = property(__value_get, __value_set, __value_del) - - # - # - # Attribute methods - # - # - - def __str__(self): - return smart_str("%s(%s)" % (_GA(self, "db_key"), _GA(self, "id"))) - - def __unicode__(self): - return u"%s(%s)" % (_GA(self, "db_key"), _GA(self, "id")) - - def access(self, accessing_obj, access_type='read', default=False, **kwargs): - """ - 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 - **kwargs - passed to at_access hook along with result. - """ - result = self.locks.check(accessing_obj, access_type=access_type, default=default) - #self.at_access(result, **kwargs) - return result - - -# -# Handlers making use of the Attribute model -# - -class AttributeHandler(object): - """ - Handler for adding Attributes to the object. - """ - _m2m_fieldname = "db_attributes" - _attrcreate = "attrcreate" - _attredit = "attredit" - _attrread = "attrread" - _attrtype = None - - def __init__(self, obj): - "Initialize handler" - self.obj = obj - self._objid = obj.id - self._model = to_str(obj.__class__.__name__.lower()) - self._cache = None - - def _recache(self): - "Cache all attributes of this object" - query = {"%s__id" % self._model : self._objid, - "attribute__db_attrtype" : self._attrtype} - attrs = [conn.attribute for conn in getattr(self.obj, self._m2m_fieldname).through.objects.filter(**query)] - self._cache = dict(("%s-%s" % (to_str(attr.db_key).lower(), - attr.db_category.lower() if conn.attribute.db_category else None), - attr) for attr in attrs) - - def has(self, key, category=None): - """ - Checks if the given Attribute (or list of Attributes) exists on - the object. - - If an iterable is given, returns list of booleans. - """ - if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE: - self._recache() - key = [k.strip().lower() for k in make_iter(key) if k] - category = category.strip().lower() if category is not None else None - searchkeys = ["%s-%s" % (k, category) for k in make_iter(key)] - ret = [self._cache.get(skey) for skey in searchkeys if skey in self._cache] - return ret[0] if len(ret) == 1 else ret - - def get(self, key=None, category=None, default=None, return_obj=False, - strattr=False, raise_exception=False, accessing_obj=None, - default_access=True, not_found_none=False): - """ - Returns the value of the given Attribute or list of Attributes. - strattr will cause the string-only value field instead of the normal - pickled field data. Use to get back values from Attributes added with - the strattr keyword. - If return_obj=True, return the matching Attribute object - instead. Returns default if no matches (or [ ] if key was a list - with no matches). If raise_exception=True, failure to find a - match will raise AttributeError instead. - - If accessing_obj is given, its "attrread" permission lock will be - checked before displaying each looked-after Attribute. If no - accessing_obj is given, no check will be done. - """ - - class RetDefault(object): - "Holds default values" - def __init__(self): - self.value = default - self.strvalue = str(default) if default is not None else None - - if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE: - self._recache() - ret = [] - key = [k.strip().lower() for k in make_iter(key) if k] - category = category.strip().lower() if category is not None else None - #print "cache:", self._cache.keys(), key - if not key: - # return all with matching category (or no category) - catkey = "-%s" % category if category is not None else None - ret = [attr for key, attr in self._cache.items() if key and key.endswith(catkey)] - else: - for searchkey in ("%s-%s" % (k, category) for k in key): - attr_obj = self._cache.get(searchkey) - if attr_obj: - ret.append(attr_obj) - else: - if raise_exception: - raise AttributeError - else: - ret.append(RetDefault()) - if accessing_obj: - # check 'attrread' locks - ret = [attr for attr in ret if attr.access(accessing_obj, self._attrread, default=default_access)] - if strattr: - ret = ret if return_obj else [attr.strvalue for attr in ret if attr] - else: - ret = ret if return_obj else [attr.value for attr in ret if attr] - if not ret: - return ret if len(key) > 1 else default - return ret[0] if len(ret)==1 else ret - - - def add(self, key, value, category=None, lockstring="", - strattr=False, accessing_obj=None, default_access=True): - """ - Add attribute to object, with optional lockstring. - - If strattr is set, the db_strvalue field will be used (no pickling). - Use the get() method with the strattr keyword to get it back. - - If accessing_obj is given, self.obj's 'attrcreate' lock access - will be checked against it. If no accessing_obj is given, no check - will be done. - """ - if accessing_obj and not self.obj.access(accessing_obj, - self._attrcreate, default=default_access): - # check create access - return - if self._cache is None: - self._recache() - if not key: - return - - category = category.strip().lower() if category is not None else None - keystr = key.strip().lower() - cachekey = "%s-%s" % (keystr, category) - attr_obj = self._cache.get(cachekey) - - if attr_obj: - # update an existing attribute object - if strattr: - # store as a simple string (will not notify OOB handlers) - attr_obj.db_strvalue = value - attr_obj.save(update_fields=["db_strvalue"]) - else: - # store normally (this will also notify OOB handlers) - attr_obj.value = value - else: - # create a new Attribute (no OOB handlers can be notified) - kwargs = {"db_key" : keystr, "db_category" : category, - "db_model" : self._model, "db_attrtype" : self._attrtype, - "db_value" : None if strattr else to_pickle(value), - "db_strvalue" : value if strattr else None} - new_attr = Attribute(**kwargs) - new_attr.save() - getattr(self.obj, self._m2m_fieldname).add(new_attr) - self._cache[cachekey] = new_attr - - - def batch_add(self, key, value, category=None, lockstring="", - strattr=False, accessing_obj=None, default_access=True): - """ - Batch-version of add(). This is more efficient than - repeat-calling add. - - key and value must be sequences of the same length, each - representing a key-value pair. - - """ - if accessing_obj and not self.obj.access(accessing_obj, - self._attrcreate, default=default_access): - # check create access - return - if self._cache is None: - self._recache() - if not key: - return - - keys, values= make_iter(key), make_iter(value) - - if len(keys) != len(values): - raise RuntimeError("AttributeHandler.add(): key and value of different length: %s vs %s" % key, value) - category = category.strip().lower() if category is not None else None - new_attrobjs = [] - for ikey, keystr in enumerate(keys): - keystr = keystr.strip().lower() - new_value = values[ikey] - cachekey = "%s-%s" % (keystr, category) - attr_obj = self._cache.get(cachekey) - - if attr_obj: - # update an existing attribute object - if strattr: - # store as a simple string (will not notify OOB handlers) - attr_obj.db_strvalue = new_value - attr_obj.save(update_fields=["db_strvalue"]) - else: - # store normally (this will also notify OOB handlers) - attr_obj.value = new_value - else: - # create a new Attribute (no OOB handlers can be notified) - kwargs = {"db_key" : keystr, "db_category" : category, - "db_attrtype" : self._attrtype, - "db_value" : None if strattr else to_pickle(new_value), - "db_strvalue" : value if strattr else None} - new_attr = Attribute(**kwargs) - new_attr.save() - new_attrobjs.append(new_attr) - if new_attrobjs: - # Add new objects to m2m field all at once - getattr(self.obj, self._m2m_fieldname).add(*new_attrobjs) - self._recache() - - - def remove(self, key, raise_exception=False, category=None, - accessing_obj=None, default_access=True): - """Remove attribute or a list of attributes from object. - - If accessing_obj is given, will check against the 'attredit' lock. - If not given, this check is skipped. - """ - if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE: - self._recache() - key = [k.strip().lower() for k in make_iter(key) if k] - category = category.strip().lower() if category is not None else None - for searchstr in ("%s-%s" % (k, category) for k in key): - attr_obj = self._cache.get(searchstr) - if attr_obj: - if not (accessing_obj and not attr_obj.access(accessing_obj, - self._attredit, default=default_access)): - attr_obj.delete() - elif not attr_obj and raise_exception: - raise AttributeError - self._recache() - - def clear(self, category=None, accessing_obj=None, default_access=True): - """ - Remove all Attributes on this object. If accessing_obj is - given, check the 'attredit' lock on each Attribute before - continuing. If not given, skip check. - """ - if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE: - self._recache() - if accessing_obj: - [attr.delete() for attr in self._cache.values() - if attr.access(accessing_obj, self._attredit, default=default_access)] - else: - [attr.delete() for attr in self._cache.values()] - self._recache() - - def all(self, accessing_obj=None, default_access=True): - """ - Return all Attribute objects on this object. - - If accessing_obj is given, check the "attrread" lock on - each attribute before returning them. If not given, this - check is skipped. - """ - if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE: - self._recache() - attrs = sorted(self._cache.values(), key=lambda o: o.id) - if accessing_obj: - return [attr for attr in attrs - if attr.access(accessing_obj, self._attredit, default=default_access)] - else: - return attrs - - -class NickHandler(AttributeHandler): - """ - Handles the addition and removal of Nicks - (uses Attributes' strvalue and category fields) - - Nicks are stored as Attributes - with categories nick_ - """ - _attrtype = "nick" - - def has(self, key, category="inputline"): - return super(NickHandler, self).has(key, category=category) - - def get(self, key=None, category="inputline", **kwargs): - "Get the replacement value matching the given key and category" - return super(NickHandler, self).get(key=key, category=category, strattr=True, **kwargs) - - def add(self, key, replacement, category="inputline", **kwargs): - "Add a new nick" - super(NickHandler, self).add(key, replacement, category=category, strattr=True, **kwargs) - - def remove(self, key, category="inputline", **kwargs): - "Remove Nick with matching category" - super(NickHandler, self).remove(key, category=category, **kwargs) - - def nickreplace(self, raw_string, categories=("inputline", "channel"), include_player=True): - "Replace entries in raw_string with nick replacement" - raw_string - obj_nicks, player_nicks = [], [] - for category in make_iter(categories): - obj_nicks.extend([n for n in make_iter(self.get(category=category, return_obj=True)) if n]) - if include_player and self.obj.has_player: - for category in make_iter(categories): - player_nicks.extend([n for n in make_iter(self.obj.player.nicks.get(category=category, return_obj=True)) if n]) - for nick in obj_nicks + player_nicks: - # make a case-insensitive match here - match = re.match(re.escape(nick.db_key), raw_string, re.IGNORECASE) - if match: - raw_string = raw_string.replace(match.group(), nick.db_strvalue, 1) - break - return raw_string - - -class NAttributeHandler(object): - """ - This stand-alone handler manages non-database saving. - It is similar to AttributeHandler and is used - by the .ndb handler in the same way as .db does - for the AttributeHandler. - """ - def __init__(self, obj): - "initialized on the object" - self._store = {} - self.obj = weakref.proxy(obj) - - def has(self, key): - "Check if object has this attribute or not" - return key in self._store - - def get(self, key): - "Returns named key value" - return self._store.get(key, None) - - def add(self, key, value): - "Add new key and value" - self._store[key] = value - self.obj.set_recache_protection() - - def remove(self, key): - "Remove key from storage" - if key in self._store: - del self._store[key] - self.obj.set_recache_protection(self._store) - - def clear(self): - "Remove all nattributes from handler" - self._store = {} - - def all(self, return_tuples=False): - "List all keys or (keys, values) stored, except _keys" - if return_tuples: - return [(key, value) for (key, value) in self._store.items() if not key.startswith("_")] - return [key for key in self._store if not key.startswith("_")] - - -#------------------------------------------------------------ -# -# Tags -# -#------------------------------------------------------------ - -class Tag(models.Model): - """ - Tags are quick markers for objects in-game. An typeobject - can have any number of tags, stored via its db_tags property. - Tagging similar objects will make it easier to quickly locate the - group later (such as when implementing zones). The main advantage - of tagging as opposed to using Attributes is speed; a tag is very - limited in what data it can hold, and the tag key+category is - indexed for efficient lookup in the database. Tags are shared between - objects - a new tag is only created if the key+category combination - did not previously exist, making them unsuitable for storing - object-related data (for this a full Attribute - should be used). - The 'db_data' field is intended as a documentation - field for the tag itself, such as to document what this tag+category - stands for and display that in a web interface or similar. - - The main default use for Tags is to implement Aliases for objects. - this uses the 'aliases' tag category, which is also checked by the - default search functions of Evennia to allow quick searches by alias. - """ - db_key = models.CharField('key', max_length=255, null=True, - help_text="tag identifier", db_index=True) - db_category = models.CharField('category', max_length=64, null=True, - help_text="tag category", db_index=True) - db_data = models.TextField('data', null=True, blank=True, - help_text="optional data field with extra information. This is not searched for.") - # this is "objectdb" etc. Required behind the scenes - db_model = models.CharField('model', max_length=32, null=True, help_text="database model to Tag", db_index=True) - # this is None, alias or permission - db_tagtype = models.CharField('tagtype', max_length=16, null=True, help_text="overall type of Tag", db_index=True) - - class Meta: - "Define Django meta options" - verbose_name = "Tag" - unique_together = (('db_key', 'db_category', 'db_tagtype'),) - index_together = (('db_key', 'db_category', 'db_tagtype'),) - - def __unicode__(self): - return u"%s" % self.db_key - - def __str__(self): - return str(self.db_key) - - -# -# Handlers making use of the Tags model -# - -class TagHandler(object): - """ - Generic tag-handler. Accessed via TypedObject.tags. - """ - _m2m_fieldname = "db_tags" - _tagtype = None - - def __init__(self, obj): - """ - Tags are stored internally in the TypedObject.db_tags m2m field - with an tag.db_model based on the obj the taghandler is stored on - and with a tagtype given by self.handlertype - """ - self.obj = obj - self._objid = obj.id - self._model = obj.__class__.__name__.lower() - self._cache = None - - def _recache(self): - "Cache all tags of this object" - query = {"%s__id" % self._model : self._objid, - "tag__db_tagtype" : self._tagtype} - tagobjs = [conn.tag for conn in getattr(self.obj, self._m2m_fieldname).through.objects.filter(**query)] - self._cache = dict(("%s-%s" % (to_str(tagobj.db_key).lower(), - tagobj.db_category.lower() if tagobj.db_category else None), - tagobj) for tagobj in tagobjs) - - def add(self, tag=None, category=None, data=None): - "Add a new tag to the handler. Tag is a string or a list of strings." - if not tag: - return - for tagstr in make_iter(tag): - if not tagstr: - continue - tagstr = tagstr.strip().lower() - category = category.strip().lower() if category is not None else None - data = str(data) if data is not None else None - # this will only create tag if no matches existed beforehand (it - # will overload data on an existing tag since that is not - # considered part of making the tag unique) - tagobj = self.obj.__class__.objects.create_tag(key=tagstr, category=category, data=data, - tagtype=self._tagtype) - getattr(self.obj, self._m2m_fieldname).add(tagobj) - if self._cache is None: - self._recache() - cachestring = "%s-%s" % (tagstr, category) - self._cache[cachestring] = tagobj - - def get(self, key, category="", return_tagobj=False): - """ - Get the tag for the given key or list of tags. If - return_data=True, return the matching Tag objects instead. - Returns a single tag if a unique match, otherwise a list - """ - if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE: - self._recache() - ret = [] - category = category.strip().lower() if category is not None else None - searchkey = ["%s-%s" % (key.strip().lower(), category) if key is not None else None for key in make_iter(key)] - ret = [val for val in (self._cache.get(keystr) for keystr in searchkey) if val] - ret = [to_str(tag.db_data) for tag in ret] if return_tagobj else ret - return ret[0] if len(ret) == 1 else ret - - def remove(self, key, category=None): - "Remove a tag from the handler based ond key and category." - for key in make_iter(key): - if not (key or key.strip()): # we don't allow empty tags - continue - tagstr = key.strip().lower() - category = category.strip().lower() if category is not None else None - - # This does not delete the tag object itself. Maybe it should do - # that when no objects reference the tag anymore (how to check)? - tagobj = self.obj.db_tags.filter(db_key=tagstr, db_category=category) - if tagobj: - getattr(self.obj, self._m2m_fieldname).remove(tagobj[0]) - self._recache() - - def clear(self): - "Remove all tags from the handler" - getattr(self.obj, self._m2m_fieldname).clear() - self._recache() - - def all(self, category=None, return_key_and_category=False): - """ - Get all tags in this handler. - If category is given, return only Tags with this category. If - return_keys_and_categories is set, return a list of tuples [(key, category), ...] - """ - if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE: - self._recache() - if category: - category = category.strip().lower() if category is not None else None - matches = [tag for tag in self._cache.values() if tag.db_category == category] - else: - matches = self._cache.values() - - if matches: - matches = sorted(matches, key=lambda o: o.id) - if return_key_and_category: - # return tuple (key, category) - return [(to_str(p.db_key), to_str(p.db_category)) for p in matches] - else: - return [to_str(p.db_key) for p in matches] - return [] - - def __str__(self): - return ",".join(self.all()) - - def __unicode(self): - return u",".join(self.all()) - - -class AliasHandler(TagHandler): - _tagtype = "alias" - - -class PermissionHandler(TagHandler): - _tagtype = "permission" - - -#------------------------------------------------------------ -# -# Typed Objects -# -#------------------------------------------------------------ - - -class TypedObject(SharedMemoryModel): - """ - Abstract Django model. - - This is the basis for a typed object. It also contains all the - mechanics for managing connected attributes. - - The TypedObject has the following properties: - key - main name - name - alias for key - typeclass_path - the path to the decorating typeclass - typeclass - auto-linked typeclass - date_created - time stamp of object creation - permissions - perm strings - dbref - #id of object - db - persistent attribute storage - ndb - non-persistent attribute storage - - """ - - # - # TypedObject Database Model setup - # - # - # These databse fields are all accessed and set using their corresponding - # properties, named same as the field, but without the db_* prefix - # (no separate save() call is needed) - - # Main identifier of the object, for searching. Is accessed with self.key - # or self.name - db_key = models.CharField('key', max_length=255, db_index=True) - # This is the python path to the type class this object is tied to the - # typeclass is what defines what kind of Object this is) - db_typeclass_path = models.CharField('typeclass', max_length=255, null=True, - help_text="this defines what 'type' of entity this is. This variable holds a Python path to a module with a valid Evennia Typeclass.") - # Creation date. This is not changed once the object is created. - db_date_created = models.DateTimeField('creation date', editable=False, auto_now_add=True) - # Permissions (access these through the 'permissions' property) - #db_permissions = models.CharField('permissions', max_length=255, blank=True, - # help_text="a comma-separated list of text strings checked by - # in-game locks. They are often used for hierarchies, such as letting a Player have permission 'Wizards', 'Builders' etc. Character objects use 'Players' by default. Most other objects don't have any permissions.") - # Lock storage - db_lock_storage = models.TextField('locks', blank=True, - help_text="locks limit access to an entity. A lock is defined as a 'lock string' on the form 'type:lockfunctions', defining what functionality is locked and how to determine access. Not defining a lock means no access is granted.") - # many2many relationships - db_attributes = models.ManyToManyField(Attribute, null=True, - help_text='attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).') - db_tags = models.ManyToManyField(Tag, null=True, - help_text='tags on this object. Tags are simple string markers to identify, group and alias objects.') - - # Database manager - objects = managers.TypedObjectManager() - - # quick on-object typeclass cache for speed - _cached_typeclass = None - - # lock handler self.locks - def __init__(self, *args, **kwargs): - "We must initialize the parent first - important!" - super(TypedObject, self).__init__(*args, **kwargs) - _SA(self, "dbobj", self) # this allows for self-reference - - # initialize all handlers in a lazy fashion - @lazy_property - def attributes(self): - return AttributeHandler(self) - - @lazy_property - def locks(self): - return LockHandler(self) - - @lazy_property - def tags(self): - return TagHandler(self) - - @lazy_property - def aliases(self): - return AliasHandler(self) - - @lazy_property - def permissions(self): - return PermissionHandler(self) - - @lazy_property - def nattributes(self): - return NAttributeHandler(self) - - - class Meta: - """ - Django setup info. - """ - abstract = True - verbose_name = "Evennia Database Object" - ordering = ['-db_date_created', 'id', 'db_typeclass_path', 'db_key'] - - # wrapper - # Wrapper properties to easily set database fields. These are - # @property decorators that allows to access these fields using - # normal python operations (without having to remember to save() - # etc). So e.g. a property 'attr' has a get/set/del decorator - # defined that allows the user to do self.attr = value, - # value = self.attr and del self.attr respectively (where self - # is the object in question). - - # name property (alias to self.key) - def __name_get(self): - return self.key - - def __name_set(self, value): - self.key = value - - def __name_del(self): - raise Exception("Cannot delete name") - name = property(__name_get, __name_set, __name_del) - - # - # - # TypedObject main class methods and properties - # - # - - _typeclass_paths = settings.OBJECT_TYPECLASS_PATHS - - def __eq__(self, other): - return other and hasattr(other, 'dbid') and self.dbid == other.dbid - - def __str__(self): - return smart_str("%s" % _GA(self, "db_key")) - - def __unicode__(self): - return u"%s" % _GA(self, "db_key") - - def __getattribute__(self, propname): - """ - Will predominantly look for an attribute - on this object, but if not found we will - check if it might exist on the typeclass instead. Since - the typeclass refers back to the databaseobject as well, we - have to be very careful to avoid loops. - """ - try: - return _GA(self, propname) - except AttributeError: - if propname.startswith('_'): - # don't relay private/special varname lookups to the typeclass - raise AttributeError("private property %s not found on db model (typeclass not searched)." % propname) - # check if the attribute exists on the typeclass instead - # (we make sure to not incur a loop by not triggering the - # typeclass' __getattribute__, since that one would - # try to look back to this very database object.) - return _GA(_GA(self, 'typeclass'), propname) - - def _hasattr(self, obj, attrname): - """ - Loop-safe version of hasattr, to avoid running a lookup that - will be rerouted up the typeclass. Returns True/False. - """ - try: - _GA(obj, attrname) - return True - except AttributeError: - return False - - #@property - def __dbid_get(self): - """ - Caches and returns the unique id of the object. - Use this instead of self.id, which is not cached. - """ - dbid = get_prop_cache(self, "_dbid") - if not dbid: - dbid = _GA(self, "id") - set_prop_cache(self, "_dbid", dbid) - return dbid - - def __dbid_set(self, value): - raise Exception("dbid cannot be set!") - - def __dbid_del(self): - raise Exception("dbid cannot be deleted!") - dbid = property(__dbid_get, __dbid_set, __dbid_del) - - #@property - def __dbref_get(self): - """ - Returns the object's dbref on the form #NN. - """ - return "#%s" % _GA(self, "_TypedObject__dbid_get")() - - def __dbref_set(self): - raise Exception("dbref cannot be set!") - - def __dbref_del(self): - raise Exception("dbref cannot be deleted!") - dbref = property(__dbref_get, __dbref_set, __dbref_del) - - # the latest error string will be stored here for accessing methods to access. - # It is set by _display_errmsg, which will print to log if error happens - # during server startup. - typeclass_last_errmsg = "" - - # typeclass property - #@property - def __typeclass_get(self): - """ - Getter. Allows for value = self.typeclass. - The typeclass is a class object found at self.typeclass_path; - it allows for extending the Typed object for all different - types of objects that the game needs. This property - handles loading and initialization of the typeclass on the fly. - - Note: The liberal use of _GA and __setattr__ (instead - of normal dot notation) is due to optimization: it avoids calling - the custom self.__getattribute__ more than necessary. - """ - path = _GA(self, "typeclass_path") - typeclass = _GA(self, "_cached_typeclass") - try: - if typeclass and _GA(typeclass, "path") == path: - # don't call at_init() when returning from cache - return typeclass - except AttributeError: - pass - errstring = "" - if not path: - # this means we should get the default obj without giving errors. - return _GA(self, "_get_default_typeclass")(cache=True, silent=True, save=True) - else: - # handle loading/importing of typeclasses, searching all paths. - # (self._typeclass_paths is a shortcut to settings.TYPECLASS_*_PATHS - # where '*' is either OBJECT, SCRIPT or PLAYER depending on the - # typed entities). - typeclass_paths = [path] + ["%s.%s" % (prefix, path) - for prefix in _GA(self, '_typeclass_paths')] - - for tpath in typeclass_paths: - - # try to import and analyze the result - typeclass = _GA(self, "_path_import")(tpath) - if callable(typeclass): - # we succeeded to import. Cache and return. - _SA(self, "typeclass_path", tpath) - typeclass = typeclass(self) - _SA(self, "_cached_typeclass", typeclass) - try: - typeclass.at_init() - except AttributeError: - logger.log_trace("\n%s: Error initializing typeclass %s. Using default." % (self, tpath)) - break - except Exception: - logger.log_trace() - return typeclass - elif hasattr(typeclass, '__file__'): - errstring += "\n%s seems to be just the path to a module. You need" % tpath - errstring += " to specify the actual typeclass name inside the module too." - elif typeclass: - errstring += "\n%s" % typeclass.strip() # this will hold a growing error message. - if not errstring: - errstring = "\nMake sure the path is set correctly. Paths tested:\n" - errstring += ", ".join(typeclass_paths) - errstring += "\nTypeclass code was not found or failed to load." - # If we reach this point we couldn't import any typeclasses. Return - # default. It's up to the calling method to use e.g. self.is_typeclass() - # to detect that the result is not the one asked for. - _GA(self, "_display_errmsg")(errstring.strip()) - return _GA(self, "_get_default_typeclass")(cache=False, silent=False, save=False) - - #@typeclass.deleter - def __typeclass_del(self): - "Deleter. Disallow 'del self.typeclass'" - raise Exception("The typeclass property should never be deleted, only changed in-place!") - - # typeclass property - typeclass = property(__typeclass_get, fdel=__typeclass_del) - - - def _path_import(self, path): - """ - Import a class from a python path of the - form src.objects.object.Object - """ - errstring = "" - if not path: - # this needs not be bad, it just means - # we should use defaults. - return None - try: - modpath, class_name = path.rsplit('.', 1) - module = __import__(modpath, fromlist=["none"]) - return module.__dict__[class_name] - except ImportError: - trc = sys.exc_traceback - if not trc.tb_next: - # we separate between not finding the module, and finding - # a buggy one. - pass - #errstring = "Typeclass not found trying path '%s'." % path - else: - # a bug in the module is reported normally. - trc = traceback.format_exc().strip() - errstring = "\n%sError importing '%s'." % (trc, path) - except (ValueError, TypeError): - errstring = "Malformed typeclass path '%s'." % path - except KeyError: - errstring = "No class '%s' was found in module '%s'." - errstring = errstring % (class_name, modpath) - except Exception: - trc = traceback.format_exc().strip() - errstring = "\n%sException importing '%s'." % (trc, path) - # return the error. - return errstring - - def _display_errmsg(self, message): - """ - Helper function to display error. - """ - _SA(self, "typeclass_last_errmsg", message) - if ServerConfig.objects.conf("server_starting_mode"): - print message - else: - logger.log_errmsg(message) - return - - def _get_default_typeclass(self, cache=False, silent=False, save=False): - """ - This is called when a typeclass fails to - load for whatever reason. - Overload this in different entities. - - Default operation is to load a default typeclass. - """ - defpath = _GA(self, "_default_typeclass_path") - typeclass = _GA(self, "_path_import")(defpath) - # if not silent: - # #errstring = "\n\nUsing Default class '%s'." % defpath - # _GA(self, "_display_errmsg")(errstring) - - if not callable(typeclass): - # if typeclass still doesn't exist at this point, we're in trouble. - # fall back to hardcoded core class which is wrong for e.g. - # scripts/players etc. - failpath = defpath - defpath = "src.objects.objects.Object" - typeclass = _GA(self, "_path_import")(defpath) - if not silent: - #errstring = " %s\n%s" % (typeclass, errstring) - errstring = " Default class '%s' failed to load." % failpath - errstring += "\n Using Evennia's default root '%s'." % defpath - _GA(self, "_display_errmsg")(errstring.strip()) - if not callable(typeclass): - # if this is still giving an error, Evennia is wrongly - # configured or buggy - raise Exception("CRITICAL ERROR: The final fallback typeclass %s cannot load!!" % defpath) - typeclass = typeclass(self) - if save: - _SA(self, 'db_typeclass_path', defpath) - _GA(self, 'save')() - if cache: - _SA(self, "_cached_db_typeclass_path", defpath) - - _SA(self, "_cached_typeclass", typeclass) - try: - typeclass.at_init() - except Exception: - logger.log_trace() - return typeclass - - def is_typeclass(self, typeclass, exact=True): - """ - Returns true if this object has this type - OR has a typeclass which is an subclass of - the given typeclass. This operates on the actually - loaded typeclass (this is important since a failing - typeclass may instead have its default currently loaded) - - typeclass - can be a class object or the - python path to such an object to match against. - - exact - returns true only if the object's - type is exactly this typeclass, ignoring - parents. - """ - try: - typeclass = _GA(typeclass, "path") - except AttributeError: - pass - typeclasses = [typeclass] + ["%s.%s" % (path, typeclass) - for path in _GA(self, "_typeclass_paths")] - if exact: - current_path = _GA(self.typeclass, "path") #"_GA(self, "_cached_db_typeclass_path") - return typeclass and any((current_path == typec for typec in typeclasses)) - else: - # check parent chain - return any((cls for cls in self.typeclass.__class__.mro() - if any(("%s.%s" % (_GA(cls, "__module__"), - _GA(cls, "__name__")) == typec - for typec in typeclasses)))) - - # - # Object manipulation methods - # - - def swap_typeclass(self, new_typeclass, clean_attributes=False, - run_start_hooks=True, no_default=True): - """ - This performs an in-situ swap of the typeclass. This means - that in-game, this object will suddenly be something else. - Player will not be affected. To 'move' a player to a different - object entirely (while retaining this object's type), use - self.player.swap_object(). - - Note that this might be an error prone operation if the - old/new typeclass was heavily customized - your code - might expect one and not the other, so be careful to - bug test your code if using this feature! Often its easiest - to create a new object and just swap the player over to - that one instead. - - Arguments: - new_typeclass (path/classobj) - type to switch to - clean_attributes (bool/list) - will delete all attributes - stored on this object (but not any - of the database fields such as name or - location). You can't get attributes back, - but this is often the safest bet to make - sure nothing in the new typeclass clashes - with the old one. If you supply a list, - only those named attributes will be cleared. - no_default - if this is active, the swapper will not allow for - swapping to a default typeclass in case the given - one fails for some reason. Instead the old one - will be preserved. - Returns: - boolean True/False depending on if the swap worked or not. - - """ - if callable(new_typeclass): - # this is an actual class object - build the path - cls = new_typeclass - new_typeclass = "%s.%s" % (cls.__module__, cls.__name__) - else: - new_typeclass = "%s" % to_str(new_typeclass) - - # Try to set the new path - # this will automatically save to database - old_typeclass_path = self.typeclass_path - - if inherits_from(self, "src.scripts.models.ScriptDB"): - if self.interval > 0: - raise RuntimeError("Cannot use swap_typeclass on time-dependent " \ - "Script '%s'.\nStop and start a new Script of the " \ - "right type instead." % self.key) - - _SA(self, "typeclass_path", new_typeclass.strip()) - # this will automatically use a default class if - # there is an error with the given typeclass. - new_typeclass = self.typeclass - if self.typeclass_path != new_typeclass.path and no_default: - # something went wrong; the default was loaded instead, - # and we don't allow that; instead we return to previous. - _SA(self, "typeclass_path", old_typeclass_path) - return False - - if clean_attributes: - # Clean out old attributes - if is_iter(clean_attributes): - for attr in clean_attributes: - self.attributes.remove(attr) - for nattr in clean_attributes: - if hasattr(self.ndb, nattr): - self.nattributes.remove(nattr) - else: - #print "deleting attrs ..." - self.attributes.clear() - self.nattributes.clear() - - if run_start_hooks: - # run hooks for this new typeclass - if inherits_from(self, "src.objects.models.ObjectDB"): - new_typeclass.basetype_setup() - new_typeclass.at_object_creation() - elif inherits_from(self, "src.players.models.PlayerDB"): - new_typeclass.basetype_setup() - new_typeclass.at_player_creation() - elif inherits_from(self, "src.scripts.models.ScriptDB"): - new_typeclass.at_script_creation() - new_typeclass.start() - elif inherits_from(self, "src.channels.models.Channel"): - # channels do no initial setup - pass - - return True - - # - # Lock / permission methods - # - - def access(self, accessing_obj, access_type='read', default=False, **kwargs): - """ - 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 - **kwargs - this is ignored, but is there to make the api consistent with the - object-typeclass method access, which use it to feed to its hook methods. - """ - return self.locks.check(accessing_obj, access_type=access_type, default=default) - - def check_permstring(self, permstring): - """ - This explicitly checks if we hold particular permission without - involving any locks. It does -not- trigger the at_access hook. - """ - if hasattr(self, "player"): - if self.player and self.player.is_superuser: - return True - else: - if self.is_superuser: - return True - - if not permstring: - return False - perm = permstring.lower() - perms = [p.lower() for p in self.permissions.all()] - if perm in perms: - # simplest case - we have a direct match - return True - if perm in _PERMISSION_HIERARCHY: - # check if we have a higher hierarchy position - ppos = _PERMISSION_HIERARCHY.index(perm) - return any(True for hpos, hperm in enumerate(_PERMISSION_HIERARCHY) - if hperm in perms and hpos > ppos) - return False - - # - # Deletion methods - # - - def _deleted(self, *args, **kwargs): - "Scrambling method for already deleted objects" - raise ObjectDoesNotExist("This object was already deleted!") - - _is_deleted = False # this is checked by db_* wrappers - - def delete(self): - "Cleaning up handlers on the typeclass level" - global TICKER_HANDLER - if not TICKER_HANDLER: - from src.scripts.tickerhandler import TICKER_HANDLER - TICKER_HANDLER.remove(self) # removes objects' all ticker subscriptions - _GA(self, "permissions").clear() - _GA(self, "attributes").clear() - _GA(self, "aliases").clear() - if hasattr(self, "nicks"): - _GA(self, "nicks").clear() - _SA(self, "_cached_typeclass", None) - _GA(self, "flush_from_cache")() - - # scrambling properties - self.delete = self._deleted - self._is_deleted = True - super(TypedObject, self).delete() - - # - # Memory management - # - - def flush_from_cache(self): - """ - Flush this object instance from cache, forcing an object reload. - Note that this will kill all temporary attributes on this object - since it will be recreated as a new Typeclass instance. - """ - self.__class__.flush_cached_instance(self) - - # - # Attribute storage - # - - #@property db - def __db_get(self): - """ - Attribute handler wrapper. Allows for the syntax - obj.db.attrname = value - and - value = obj.db.attrname - and - del obj.db.attrname - and - all_attr = obj.db.all() (unless there is an attribute - named 'all', in which case that will be returned instead). - """ - try: - return self._db_holder - except AttributeError: - class DbHolder(object): - "Holder for allowing property access of attributes" - def __init__(self, obj): - _SA(self, "attrhandler", _GA(obj, "attributes")) - - def __getattribute__(self, attrname): - if attrname == 'all': - # we allow to overload our default .all - attr = _GA(self, "attrhandler").get("all") - if attr: - return attr - return _GA(self, 'all') - return _GA(self, "attrhandler").get(attrname) - - def __setattr__(self, attrname, value): - _GA(self, "attrhandler").add(attrname, value) - - def __delattr__(self, attrname): - _GA(self, "attrhandler").remove(attrname) - - def get_all(self): - return _GA(self, "attrhandler").all() - all = property(get_all) - self._db_holder = DbHolder(self) - return self._db_holder - - #@db.setter - def __db_set(self, value): - "Stop accidentally replacing the db object" - string = "Cannot assign directly to db object! " - string += "Use db.attr=value instead." - raise Exception(string) - - #@db.deleter - def __db_del(self): - "Stop accidental deletion." - raise Exception("Cannot delete the db object!") - db = property(__db_get, __db_set, __db_del) - - # - # Non-persistent (ndb) storage - # - - #@property ndb - def __ndb_get(self): - """ - A non-attr_obj store (ndb: NonDataBase). Everything stored - to this is guaranteed to be cleared when a server is shutdown. - Syntax is same as for the _get_db_holder() method and - property, e.g. obj.ndb.attr = value etc. - """ - try: - return self._ndb_holder - except AttributeError: - class NDbHolder(object): - "Holder for allowing property access of attributes" - def __init__(self, obj): - _SA(self, "nattrhandler", _GA(obj, "nattributes")) - - def __getattribute__(self, attrname): - if attrname == 'all': - # we allow to overload our default .all - attr = _GA(self, "nattrhandler").get("all") - if attr: - return attr - return _GA(self, 'all') - return _GA(self, "nattrhandler").get(attrname) - - def __setattr__(self, attrname, value): - _GA(self, "nattrhandler").add(attrname, value) - - def __delattr__(self, attrname): - _GA(self, "nattrhandler").remove(attrname) - - def get_all(self): - return _GA(self, "nattrhandler").all() - all = property(get_all) - self._ndb_holder = NDbHolder(self) - return self._ndb_holder - - #@db.setter - def __ndb_set(self, value): - "Stop accidentally replacing the ndb object" - string = "Cannot assign directly to ndb object! " - string += "Use ndb.attr=value instead." - raise Exception(string) - - #@db.deleter - def __ndb_del(self): - "Stop accidental deletion." - raise Exception("Cannot delete the ndb object!") - ndb = property(__ndb_get, __ndb_set, __ndb_del) - -# # -# # ***** DEPRECATED METHODS BELOW ******* -# # -# -# # -# # Full attr_obj attributes. You usually access these -# # through the obj.db.attrname method. -# -# # Helper methods for attr_obj attributes -# -# def has_attribute(self, attribute_name): -# """ -# See if we have an attribute set on the object. -# -# attribute_name: (str) The attribute's name. -# """ -# logger.log_depmsg("obj.has_attribute() is deprecated. Use obj.attributes.has().") -# return _GA(self, "attributes").has(attribute_name) -# -# def set_attribute(self, attribute_name, new_value=None, lockstring=""): -# """ -# Sets an attribute on an object. Creates the attribute if need -# be. -# -# attribute_name: (str) The attribute's name. -# new_value: (python obj) The value to set the attribute to. If this is not -# a str, the object will be stored as a pickle. -# lockstring - this sets an access restriction on the attribute object. Note that -# this is normally NOT checked - use the secureattr() access method -# below to perform access-checked modification of attributes. Lock -# types checked by secureattr are 'attrread','attredit','attrcreate'. -# """ -# logger.log_depmsg("obj.set_attribute() is deprecated. Use obj.db.attr=value or obj.attributes.add().") -# _GA(self, "attributes").add(attribute_name, new_value, lockstring=lockstring) -# -# def get_attribute_obj(self, attribute_name, default=None): -# """ -# Get the actual attribute object named attribute_name -# """ -# logger.log_depmsg("obj.get_attribute_obj() is deprecated. Use obj.attributes.get(..., return_obj=True)") -# return _GA(self, "attributes").get(attribute_name, default=default, return_obj=True) -# -# def get_attribute(self, attribute_name, default=None, raise_exception=False): -# """ -# Returns the value of an attribute on an object. You may need to -# type cast the returned value from this function since the attribute -# can be of any type. Returns default if no match is found. -# -# attribute_name: (str) The attribute's name. -# default: What to return if no attribute is found -# raise_exception (bool) - raise an exception if no object exists instead of returning default. -# """ -# logger.log_depmsg("obj.get_attribute() is deprecated. Use obj.db.attr or obj.attributes.get().") -# return _GA(self, "attributes").get(attribute_name, default=default, raise_exception=raise_exception) -# -# def del_attribute(self, attribute_name, raise_exception=False): -# """ -# Removes an attribute entirely. -# -# attribute_name: (str) The attribute's name. -# raise_exception (bool) - raise exception if attribute to delete -# could not be found -# """ -# logger.log_depmsg("obj.del_attribute() is deprecated. Use del obj.db.attr or obj.attributes.remove().") -# _GA(self, "attributes").remove(attribute_name, raise_exception=raise_exception) -# -# def get_all_attributes(self): -# """ -# Returns all attributes defined on the object. -# """ -# logger.log_depmsg("obj.get_all_attributes() is deprecated. Use obj.db.all() or obj.attributes.all().") -# return _GA(self, "attributes").all() -# -# def attr(self, attribute_name=None, value=None, delete=False): -# """ -# This is a convenient wrapper for -# get_attribute, set_attribute, del_attribute -# and get_all_attributes. -# If value is None, attr will act like -# a getter, otherwise as a setter. -# set delete=True to delete the named attribute. -# -# Note that you cannot set the attribute -# value to None using this method. Use set_attribute. -# """ -# logger.log_depmsg("obj.attr() is deprecated. Use handlers obj.db or obj.attributes.") -# if attribute_name is None: -# # act as a list method -# return _GA(self, "attributes").all() -# elif delete is True: -# _GA(self, "attributes").remove(attribute_name) -# elif value is None: -# # act as a getter. -# return _GA(self, "attributes").get(attribute_name) -# else: -# # act as a setter -# self._GA(self, "attributes").add(attribute_name, value) -# -# def secure_attr(self, accessing_object, attribute_name=None, value=None, delete=False, -# default_access_read=True, default_access_edit=True, default_access_create=True): -# """ -# This is a version of attr that requires the accessing object -# as input and will use that to check eventual access locks on -# the Attribute before allowing any changes or reads. -# -# In the cases when this method wouldn't return, it will return -# True for a successful operation, None otherwise. -# -# locktypes checked on the Attribute itself: -# attrread - control access to reading the attribute value -# attredit - control edit/delete access -# locktype checked on the object on which the Attribute is/will be stored: -# attrcreate - control attribute create access (this is checked *on the object* not on the Attribute!) -# -# default_access_* defines which access is assumed if no -# suitable lock is defined on the Atttribute. -# -# """ -# logger.log_depmsg("obj.secure_attr() is deprecated. Use obj.attributes methods, giving accessing_obj keyword.") -# if attribute_name is None: -# return _GA(self, "attributes").all(accessing_obj=accessing_object, default_access=default_access_read) -# elif delete is True: -# # act as deleter -# _GA(self, "attributes").remove(attribute_name, accessing_obj=accessing_object, default_access=default_access_edit) -# elif value is None: -# # act as getter -# return _GA(self, "attributes").get(attribute_name, accessing_obj=accessing_object, default_access=default_access_read) -# else: -# # act as setter -# attr = _GA(self, "attributes").get(attribute_name, return_obj=True) -# if attr: -# # attribute already exists -# _GA(self, "attributes").add(attribute_name, value, accessing_obj=accessing_object, default_access=default_access_edit) -# else: -# # creating a new attribute - check access on storing object! -# _GA(self, "attributes").add(attribute_name, value, accessing_obj=accessing_object, default_access=default_access_create) -# -# def nattr(self, attribute_name=None, value=None, delete=False): -# """ -# This allows for assigning non-persistent data on the object using -# a method call. Will return None if trying to access a non-existing property. -# """ -# logger.log_depmsg("obj.nattr() is deprecated. Use obj.nattributes instead.") -# if attribute_name is None: -# # act as a list method -# if callable(self.ndb.all): -# return self.ndb.all() -# else: -# return [val for val in self.ndb.__dict__.keys() -# if not val.startswith['_']] -# elif delete is True: -# if hasattr(self.ndb, attribute_name): -# _DA(_GA(self, "ndb"), attribute_name) -# elif value is None: -# # act as a getter. -# if hasattr(self.ndb, attribute_name): -# _GA(_GA(self, "ndb"), attribute_name) -# else: -# return None -# else: -# # act as a setter -# _SA(self.ndb, attribute_name, value) -# -# - diff --git a/src/typeclasses/typeclass.py b/src/typeclasses/typeclass.py deleted file mode 100644 index a8fe2a02d..000000000 --- a/src/typeclasses/typeclass.py +++ /dev/null @@ -1,190 +0,0 @@ -""" -A typeclass is the companion of a TypedObject django model. -It 'decorates' the model without actually having to add new -fields to the model - transparently storing data onto its -associated model without the admin/user just having to deal -with a 'normal' Python class. The only restrictions is that -the typeclass must inherit from TypeClass and not reimplement -the get/setters defined below. There are also a few properties -that are protected, so as to not overwrite property names -used by the typesystem or django itself. -""" - -from src.utils.logger import log_trace, log_errmsg - -__all__ = ("TypeClass",) - -# these are called so many times it's worth to avoid lookup calls -_GA = object.__getattribute__ -_SA = object.__setattr__ -_DA = object.__delattr__ - -# To ensure the sanity of the model, there are a -# few property names we won't allow the admin to -# set on the typeclass just like that. Note that these are *not* related -# to *in-game* safety (if you can edit typeclasses you have -# full access anyway), so no protection against changing -# e.g. 'locks' or 'permissions' should go here. -PROTECTED = ('id', 'dbobj', 'db', 'ndb', 'objects', 'typeclass', 'db_player', - 'attr', 'save', 'delete', 'db_model_name','attribute_class', - 'typeclass_paths') - - -# If this is true, all non-protected property assignments -# are directly stored to a database attribute - -class MetaTypeClass(type): - """ - This metaclass just makes sure the class object gets - printed in a nicer way (it might end up having no name at all - otherwise due to the magics being done with get/setattribute). - """ - def __init__(mcs, *args, **kwargs): - """ - Adds some features to typeclassed objects - """ - super(MetaTypeClass, mcs).__init__(*args, **kwargs) - mcs.typename = mcs.__name__ - mcs.path = "%s.%s" % (mcs.__module__, mcs.__name__) - - def __str__(cls): - return "%s" % cls.__name__ - - -class TypeClass(object): - """ - This class implements a 'typeclass' object. This is connected - to a database object inheriting from TypedObject. - the TypeClass allows for all customization. - Most of the time this means that the admin never has to - worry about database access but only deal with extending - TypeClasses to create diverse objects in the game. - - The ObjectType class has all functionality for wrapping a - database object transparently. - - It's up to its child classes to implement eventual custom hooks - and other functions called by the engine. - - """ - __metaclass__ = MetaTypeClass - - def __init__(self, dbobj): - """ - Initialize the object class. There are two ways to call this class. - o = object_class(dbobj) : this is used to initialize dbobj with the - class name - o = dbobj.object_class(dbobj) : this is used when dbobj.object_class - is already set. - - """ - # typecheck of dbobj - we can't allow it to be added here - # unless it's really a TypedObject. - dbobj_cls = _GA(dbobj, '__class__') - dbobj_mro = _GA(dbobj_cls, '__mro__') - if not any('src.typeclasses.models.TypedObject' in str(mro) for mro in dbobj_mro): - raise Exception("dbobj is not a TypedObject: %s: %s" % (dbobj_cls, dbobj_mro)) - - # we should always be able to use dbobj/typeclass to get back an object of the desired type - _SA(self, 'dbobj', dbobj) - _SA(self, 'typeclass', self) - - def __getattribute__(self, propname): - """ - Change the normal property access to - transparently include the properties on - self.dbobj. Note that dbobj properties have - priority, so if you define a same-named - property on the class, it will NOT be - accessible through getattr. - """ - if propname.startswith('__') and propname.endswith('__'): - # python specials are parsed as-is (otherwise things like - # isinstance() fail to identify the typeclass) - return _GA(self, propname) - #print "get %s (dbobj:%s)" % (propname, type(dbobj)) - try: - return _GA(self, propname) - except AttributeError: - try: - dbobj = _GA(self, 'dbobj') - except AttributeError: - log_trace("Typeclass CRITICAL ERROR! dbobj not found for Typeclass %s!" % self) - raise - try: - return _GA(dbobj, propname) - except AttributeError: - string = "Object: '%s' not found on %s(#%s), nor on its typeclass %s." - raise AttributeError(string % (propname, dbobj, _GA(dbobj, "dbid"), _GA(dbobj, "typeclass_path"))) - - def __setattr__(self, propname, value): - """ - Transparently save data. Use property on Typeclass only if - that property is already defined, otherwise relegate to the - dbobj object in all situations. Note that this does not - necessarily mean storing it to the database. - """ - #print "set %s -> %s" % (propname, value) - if propname in PROTECTED: - string = "%s: '%s' is a protected attribute name." - string += " (protected: [%s])" % (", ".join(PROTECTED)) - log_errmsg(string % (self.name, propname)) - return - try: - _GA(self, propname) - _SA(self, propname, value) - except AttributeError: - try: - dbobj = _GA(self, 'dbobj') - except AttributeError: - dbobj = None - if dbobj: - _SA(dbobj, propname, value) - else: - # only as a last resort do we save on the typeclass object - _SA(self, propname, value) - - def __eq__(self, other): - """ - dbobj-recognized comparison - """ - try: - return _GA(_GA(self, "dbobj"), "dbid") == _GA(_GA(other, "dbobj"), "dbid") - except AttributeError: - return id(self) == id(other) - - def __delattr__(self, propname): - """ - Transparently deletes data from the typeclass or dbobj by first - searching on the typeclass, secondly on the dbobj.db. - Will not allow deletion of properties stored directly on dbobj. - """ - if propname in PROTECTED: - string = "%s: '%s' is a protected attribute name." - string += " (protected: [%s])" % (", ".join(PROTECTED)) - log_errmsg(string % (self.name, propname)) - return - - try: - _DA(self, propname) - except AttributeError: - # not on typeclass, try to delete on db/ndb - try: - dbobj = _GA(self, 'dbobj') - except AttributeError: - log_trace("This is probably due to an unsafe reload.") - return # ignore delete - try: - dbobj.del_attribute(propname, raise_exception=True) - except AttributeError: - string = "Object: '%s' not found on %s(#%s), nor on its typeclass %s." - raise AttributeError(string % (propname, dbobj, - dbobj.dbid, - dbobj.typeclass_path,)) - - def __str__(self): - "represent the object" - return self.key - - def __unicode__(self): - return u"%s" % self.key diff --git a/src/utils/__init__.py b/src/utils/__init__.py deleted file mode 100644 index 95c6c4fc2..000000000 --- a/src/utils/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# simple check to determine if we are currently running under pypy. -try: - import __pypy__ as is_pypy -except ImportError: - is_pypy = False diff --git a/src/utils/create.py b/src/utils/create.py deleted file mode 100644 index 5aa5502f8..000000000 --- a/src/utils/create.py +++ /dev/null @@ -1,562 +0,0 @@ -""" -This module gathers all the essential database-creation -functions for the game engine's various object types. - -Only objects created 'stand-alone' are in here, e.g. object Attributes -are always created directly through their respective objects. - -Each creation_* function also has an alias named for the entity being -created, such as create_object() and object(). This is for -consistency with the utils.search module and allows you to do the -shorter "create.object()". - -The respective object managers hold more methods for manipulating and -searching objects already existing in the database. - -Models covered: - Objects - Scripts - Help - Message - Channel - Players -""" -from django.conf import settings -from django.db import IntegrityError -from src.utils.idmapper.models import SharedMemoryModel -from src.utils import utils, logger -from src.utils.utils import make_iter - -# delayed imports -_User = None -_Object = None -_ObjectDB = None -_Script = None -_ScriptDB = None -_HelpEntry = None -_Msg = None -_Player = None -_PlayerDB = None -_to_object = None -_ChannelDB = None -_channelhandler = None - - -# limit symbol import from API -__all__ = ("create_object", "create_script", "create_help_entry", - "create_message", "create_channel", "create_player") - -_GA = object.__getattribute__ - -# Helper function - -def handle_dbref(inp, objclass, raise_errors=True): - """ - Convert a #dbid to a valid object of objclass. objclass - should be a valid object class to filter against (objclass.filter ...) - If not raise_errors is set, this will swallow errors of non-existing - objects. - """ - if not (isinstance(inp, basestring) and inp.startswith("#")): - try: - return inp.dbobj - except AttributeError: - return inp - - # a string, analyze it - inp = inp.lstrip('#') - try: - if int(inp) < 0: - return None - except ValueError: - return None - - # if we get to this point, inp is an integer dbref; get the matching object - try: - return objclass.objects.get(id=inp) - except Exception: - if raise_errors: - raise - return inp - -# -# Game Object creation -# - -def create_object(typeclass=None, key=None, location=None, - home=None, permissions=None, locks=None, - aliases=None, destination=None, report_to=None, nohome=False): - """ - Create a new in-game object. Any game object is a combination - of a database object that stores data persistently to - the database, and a typeclass, which on-the-fly 'decorates' - the database object into whataver different type of object - it is supposed to be in the game. - - See src.objects.managers for methods to manipulate existing objects - in the database. src.objects.objects holds the base typeclasses - and src.objects.models hold the database model. - - report_to is an optional object for reporting errors to in string form. - If report_to is not set, errors will be raised as en Exception - containing the error message. If set, this method will return - None upon errors. - nohome - this allows the creation of objects without a default home location; - this only used when creating the default location itself or during unittests - """ - global _Object, _ObjectDB - if not _Object: - from src.objects.objects import Object as _Object - if not _ObjectDB: - from src.objects.models import ObjectDB as _ObjectDB - - # input validation - - if not typeclass: - typeclass = settings.BASE_OBJECT_TYPECLASS - elif isinstance(typeclass, _ObjectDB): - # this is already an objectdb instance, extract its typeclass - typeclass = typeclass.typeclass.path - elif isinstance(typeclass, _Object) or utils.inherits_from(typeclass, _Object): - # this is already an object typeclass, extract its path - typeclass = typeclass.path - typeclass = utils.to_unicode(typeclass) - - # Setup input for the create command - - location = handle_dbref(location, _ObjectDB) - destination = handle_dbref(destination, _ObjectDB) - home = handle_dbref(home, _ObjectDB) - if not home: - try: - home = handle_dbref(settings.DEFAULT_HOME, _ObjectDB) if not nohome else None - except _ObjectDB.DoesNotExist: - raise _ObjectDB.DoesNotExist("settings.DEFAULT_HOME (= '%s') does not exist, or the setting is malformed." % - settings.DEFAULT_HOME) - - # create new database object all in one go - new_db_object = _ObjectDB(db_key=key, db_location=location, - db_destination=destination, db_home=home, - db_typeclass_path=typeclass) - - if not key: - # the object should always have a key, so if not set we give a default - new_db_object.key = "#%i" % new_db_object.dbid - - # this will either load the typeclass or the default one (will also save object) - new_object = new_db_object.typeclass - - if not _GA(new_object, "is_typeclass")(typeclass, exact=True): - # this will fail if we gave a typeclass as input and it still - # gave us a default - try: - SharedMemoryModel.delete(new_db_object) - except AssertionError: - # this happens if object was never created - pass - if report_to: - report_to = handle_dbref(report_to, _ObjectDB) - _GA(report_to, "msg")("Error creating %s (%s).\n%s" % (new_db_object.key, typeclass, - _GA(new_db_object, "typeclass_last_errmsg"))) - return None - else: - raise Exception(_GA(new_db_object, "typeclass_last_errmsg")) - - # from now on we can use the typeclass object - # as if it was the database object. - - # call the hook methods. This is where all at_creation - # customization happens as the typeclass stores custom - # things on its database object. - - # note - this may override input keys, locations etc! - new_object.basetype_setup() # setup the basics of Exits, Characters etc. - new_object.at_object_creation() - - # we want the input to override that set in the hooks, so - # we re-apply those if needed - if new_object.key != key: - new_object.key = key - if new_object.location != location: - new_object.location = location - if new_object.home != home: - new_object.home = home - if new_object.destination != destination: - new_object.destination = destination - - # custom-given perms/locks do overwrite hooks - if permissions: - new_object.permissions.add(permissions) - if locks: - new_object.locks.add(locks) - if aliases: - new_object.aliases.add(aliases) - - # trigger relevant move_to hooks in order to display messages. - if location: - location.at_object_receive(new_object, None) - new_object.at_after_move(None) - - # post-hook setup (mainly used by Exits) - new_object.basetype_posthook_setup() - - return new_object - -#alias for create_object -object = create_object - - -# -# Script creation -# - -def create_script(typeclass, key=None, obj=None, player=None, locks=None, - interval=None, start_delay=None, repeats=None, - persistent=None, autostart=True, report_to=None): - """ - Create a new script. All scripts are a combination - of a database object that communicates with the - database, and an typeclass that 'decorates' the - database object into being different types of scripts. - It's behaviour is similar to the game objects except - scripts has a time component and are more limited in - scope. - - Argument 'typeclass' can be either an actual - typeclass object or a python path to such an object. - Only set key here if you want a unique name for this - particular script (set it in config to give - same key to all scripts of the same type). Set obj - to tie this script to a particular object. - - See src.scripts.manager for methods to manipulate existing - scripts in the database. - - report_to is an obtional object to receive error messages. - If report_to is not set, an Exception with the - error will be raised. If set, this method will - return None upon errors. - """ - global _Script, _ScriptDB - if not _Script: - from src.scripts.scripts import Script as _Script - if not _ScriptDB: - from src.scripts.models import ScriptDB as _ScriptDB - - if not typeclass: - typeclass = settings.BASE_SCRIPT_TYPECLASS - elif isinstance(typeclass, _ScriptDB): - # this is already an scriptdb instance, extract its typeclass - typeclass = typeclass.typeclass.path - elif isinstance(typeclass, _Script) or utils.inherits_from(typeclass, _Script): - # this is already an object typeclass, extract its path - typeclass = typeclass.path - - # create new database script - new_db_script = _ScriptDB() - - # assign the typeclass - typeclass = utils.to_unicode(typeclass) - new_db_script.typeclass_path = typeclass - - # the name/key is often set later in the typeclass. This - # is set here as a failsafe. - if key: - new_db_script.key = key - else: - new_db_script.key = "#%i" % new_db_script.id - - # this will either load the typeclass or the default one - new_script = new_db_script.typeclass - - if not _GA(new_db_script, "is_typeclass")(typeclass, exact=True): - # this will fail if we gave a typeclass as input and it still - # gave us a default - SharedMemoryModel.delete(new_db_script) - if report_to: - _GA(report_to, "msg")("Error creating %s (%s): %s" % (new_db_script.key, typeclass, - _GA(new_db_script, "typeclass_last_errmsg"))) - return None - else: - raise Exception(_GA(new_db_script, "typeclass_last_errmsg")) - - if obj: - new_script.obj = obj - if player: - new_script.player = player - - # call the hook method. This is where all at_creation - # customization happens as the typeclass stores custom - # things on its database object. - new_script.at_script_creation() - - # custom-given variables override the hook - if key: - new_script.key = key - if locks: - new_script.locks.add(locks) - if interval is not None: - new_script.interval = interval - if start_delay is not None: - new_script.start_delay = start_delay - if repeats is not None: - new_script.repeats = repeats - if persistent is not None: - new_script.persistent = persistent - - # must do this before starting the script since some - # scripts may otherwise run for a very short time and - # try to delete itself before we have a time to save it. - new_db_script.save() - - # a new created script should usually be started. - if autostart: - new_script.start() - - return new_script -#alias -script = create_script - - -# -# Help entry creation -# - -def create_help_entry(key, entrytext, category="General", locks=None): - """ - Create a static help entry in the help database. Note that Command - help entries are dynamic and directly taken from the __doc__ entries - of the command. The database-stored help entries are intended for more - general help on the game, more extensive info, in-game setting information - and so on. - """ - global _HelpEntry - if not _HelpEntry: - from src.help.models import HelpEntry as _HelpEntry - - try: - new_help = _HelpEntry() - new_help.key = key - new_help.entrytext = entrytext - new_help.help_category = category - if locks: - new_help.locks.add(locks) - new_help.save() - return new_help - except IntegrityError: - string = "Could not add help entry: key '%s' already exists." % key - logger.log_errmsg(string) - return None - except Exception: - logger.log_trace() - return None -# alias -help_entry = create_help_entry - - -# -# Comm system methods -# - -def create_message(senderobj, message, channels=None, - receivers=None, locks=None, header=None): - """ - Create a new communication message. Msgs are used for all - player-to-player communication, both between individual players - and over channels. - senderobj - the player sending the message. This must be the actual object. - message - text with the message. Eventual headers, titles etc - should all be included in this text string. Formatting - will be retained. - channels - a channel or a list of channels to send to. The channels - may be actual channel objects or their unique key strings. - receivers - a player to send to, or a list of them. May be Player objects - or playernames. - locks - lock definition string - header - mime-type or other optional information for the message - - The Comm system is created very open-ended, so it's fully possible - to let a message both go to several channels and to several receivers - at the same time, it's up to the command definitions to limit this as - desired. - """ - global _Msg - if not _Msg: - from src.comms.models import Msg as _Msg - if not message: - # we don't allow empty messages. - return - new_message = _Msg(db_message=message) - new_message.save() - for sender in make_iter(senderobj): - new_message.senders = sender - new_message.header = header - for channel in make_iter(channels): - new_message.channels = channel - for receiver in make_iter(receivers): - new_message.receivers = receiver - if locks: - new_message.locks.add(locks) - new_message.save() - return new_message -message = create_message - - -def create_channel(key, aliases=None, desc=None, - locks=None, keep_log=True, - typeclass=None): - """ - Create A communication Channel. A Channel serves as a central - hub for distributing Msgs to groups of people without - specifying the receivers explicitly. Instead players may - 'connect' to the channel and follow the flow of messages. By - default the channel allows access to all old messages, but - this can be turned off with the keep_log switch. - - key - this must be unique. - aliases - list of alternative (likely shorter) keynames. - locks - lock string definitions - """ - global _ChannelDB, _channelhandler - if not _ChannelDB: - from src.comms.models import ChannelDB as _ChannelDB - if not _channelhandler: - from src.comms import channelhandler as _channelhandler - if not typeclass: - typeclass = settings.BASE_CHANNEL_TYPECLASS - try: - new_channel = _ChannelDB(typeclass=typeclass, db_key=key) - new_channel.save() - new_channel = new_channel.typeclass - if aliases: - if not utils.is_iter(aliases): - aliases = [aliases] - new_channel.aliases.add(aliases) - new_channel.save() - new_channel.db.desc = desc - new_channel.db.keep_log = keep_log - except IntegrityError: - string = "Could not add channel: key '%s' already exists." % key - logger.log_errmsg(string) - return None - if locks: - new_channel.locks.add(locks) - new_channel.save() - _channelhandler.CHANNELHANDLER.add_channel(new_channel) - new_channel.at_channel_create() - return new_channel - -channel = create_channel - - - -# -# Player creation methods -# - -def create_player(key, email, password, - typeclass=None, - is_superuser=False, - locks=None, permissions=None, - report_to=None): - - """ - This creates a new player. - - key - the player's name. This should be unique. - email - email on valid addr@addr.domain form. - password - password in cleartext - is_superuser - wether or not this player is to be a superuser - locks - lockstring - permission - list of permissions - report_to - an object with a msg() method to report errors to. If - not given, errors will be logged. - - Will return the Player-typeclass or None/raise Exception if the - Typeclass given failed to load. - - Concerning is_superuser: - Usually only the server admin should need to be superuser, all - other access levels can be handled with more fine-grained - permissions or groups. A superuser bypasses all lock checking - operations and is thus not suitable for play-testing the game. - - """ - global _PlayerDB, _Player - if not _PlayerDB: - from src.players.models import PlayerDB as _PlayerDB - if not _Player: - from src.players.player import Player as _Player - - if not email: - email = "dummy@dummy.com" - if _PlayerDB.objects.filter(username__iexact=key): - raise ValueError("A Player with the name '%s' already exists." % key) - - # this handles a given dbref-relocate to a player. - report_to = handle_dbref(report_to, _PlayerDB) - - try: - - # create the correct Player object - if is_superuser: - new_db_player = _PlayerDB.objects.create_superuser(key, email, password) - else: - new_db_player = _PlayerDB.objects.create_user(key, email, password) - - if not typeclass: - typeclass = settings.BASE_PLAYER_TYPECLASS - elif isinstance(typeclass, _PlayerDB): - # this is an PlayerDB instance, extract its typeclass path - typeclass = typeclass.typeclass.path - elif isinstance(typeclass, _Player) or utils.inherits_from(typeclass, _Player): - # this is Player object typeclass, extract its path - typeclass = typeclass.path - - # assign the typeclass - typeclass = utils.to_unicode(typeclass) - new_db_player.typeclass_path = typeclass - - # this will either load the typeclass or the default one - new_player = new_db_player.typeclass - - if not _GA(new_db_player, "is_typeclass")(typeclass, exact=True): - # this will fail if we gave a typeclass as input - # and it still gave us a default - SharedMemoryModel.delete(new_db_player) - if report_to: - _GA(report_to, "msg")("Error creating %s (%s):\n%s" % (new_db_player.key, typeclass, - _GA(new_db_player, "typeclass_last_errmsg"))) - return None - else: - raise Exception(_GA(new_db_player, "typeclass_last_errmsg")) - - new_player.basetype_setup() # setup the basic locks and cmdset - # call hook method (may override default permissions) - new_player.at_player_creation() - - # custom given arguments potentially overrides the hook - if permissions: - new_player.permissions.add(permissions) - elif not new_player.permissions.all(): - new_player.permissions.add(settings.PERMISSION_PLAYER_DEFAULT) - if locks: - new_player.locks.add(locks) - return new_player - - except Exception: - # a failure in creating the player; we try to clean - # up as much as we can - logger.log_trace() - try: - new_player.delete() - except Exception: - pass - try: - del new_player - except Exception: - pass - raise - -# alias -player = create_player diff --git a/src/utils/dummyrunner/dummyrunner.py b/src/utils/dummyrunner/dummyrunner.py deleted file mode 100644 index fdc907141..000000000 --- a/src/utils/dummyrunner/dummyrunner.py +++ /dev/null @@ -1,310 +0,0 @@ -""" -Dummy client runner - -This module implements a stand-alone launcher for stress-testing -an Evennia game. It will launch any number of fake clients. These -clients will log into the server and start doing random operations. -Customizing and weighing these operations differently depends on -which type of game is tested. The module contains a testing module -for plain Evennia. - -Please note that you shouldn't run this on a production server! -Launch the program without any arguments or options to see a -full step-by-step setup help. - -Basically (for testing default Evennia): - - - Use an empty/testing database. - - set PERMISSION_PLAYER_DEFAULT = "Builders" - - start server, eventually with profiling active - - launch this client runner - -If you want to customize the runner's client actions -(because you changed the cmdset or needs to better -match your use cases or add more actions), you can -change which actions by adding a path to - - DUMMYRUNNER_ACTIONS_MODULE = - -in your settings. See utils.dummyrunner_actions.py -for instructions on how to define this module. - -""" - -import os, sys, time, random -from optparse import OptionParser -from twisted.conch import telnet -from twisted.internet import reactor, protocol -# from twisted.application import internet, service -# from twisted.web import client -from twisted.internet.task import LoopingCall - -# Tack on the root evennia directory to the python path and initialize django settings -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) -os.environ["DJANGO_SETTINGS_MODULE"] = "game.settings" -#from game import settings -#try: -# from django.conf import settings as settings2 -# settings2.configure() -#except RuntimeError: -# pass -#finally: -# del settings2 - -from django.conf import settings -from src.utils import utils - -HELPTEXT = """ - -Usage: dummyrunner.py [-h][-v][-V] [nclients] - -DO NOT RUN THIS ON A PRODUCTION SERVER! USE A CLEAN/TESTING DATABASE! - -This stand-alone program launches dummy telnet clients against a -running Evennia server. The idea is to mimic real players logging in -and repeatedly doing resource-heavy commands so as to stress test the -game. It uses the default command set to log in and issue commands, so -if that was customized, some of the functionality will not be tested -(it will not fail, the commands will just not be recognized). The -running clients will create new objects and rooms all over the place -as part of their running, so using a clean/testing database is -strongly recommended. - -Setup: - 1) setup a fresh/clean database (if using sqlite, just safe-copy - away your real evennia.db3 file and create a new one with - manage.py) - 2) in game/settings.py, add - - PERMISSION_PLAYER_DEFAULT="Builders" - - 3a) Start Evennia like normal. - 3b) If you want profiling, start Evennia like this instead: - - python runner.py -S start - - this will start Evennia under cProfiler with output server.prof. - 4) run this dummy runner: - - python dummyclients.py [timestep] [port] - - Default is to connect one client to port 4000, using a 5 second - timestep. Increase the number of clients and shorten the - timestep (minimum is 1s) to further stress the game. - - You can stop the dummy runner with Ctrl-C. - - 5) Log on and determine if game remains responsive despite the - heavier load. Note that if you do profiling, there is an - additional overhead from the profiler too! - 6) If you use profiling, let the game run long enough to gather - data, then stop the server. You can inspect the server.prof file - from a python prompt (see Python's manual on cProfiler). - -""" -# number of clients to launch if no input is given on command line -DEFAULT_NCLIENTS = 1 -# time between each 'tick', in seconds, if not set on command -# line. All launched clients will be called upon to possibly do an -# action with this frequency. -DEFAULT_TIMESTEP = 2 -# chance of a client performing an action, per timestep. This helps to -# spread out usage randomly, like it would be in reality. -CHANCE_OF_ACTION = 0.05 -# spread out the login action separately, having many players create accounts -# and connect simultaneously is generally unlikely. -CHANCE_OF_LOGIN = 0.5 -# Port to use, if not specified on command line -DEFAULT_PORT = settings.TELNET_PORTS[0] -# -NLOGGED_IN = 0 -NCLIENTS = 0 - -#------------------------------------------------------------ -# Helper functions -#------------------------------------------------------------ - -def idcounter(): - "generates subsequent id numbers" - idcount = 0 - while True: - idcount += 1 - yield idcount -OID = idcounter() -CID = idcounter() - -def makeiter(obj): - "makes everything iterable" - if not hasattr(obj, '__iter__'): - return [obj] - return obj - -#------------------------------------------------------------ -# Client classes -#------------------------------------------------------------ - -class DummyClient(telnet.StatefulTelnetProtocol): - """ - Handles connection to a running Evennia server, - mimicking a real player by sending commands on - a timer. - """ - - def connectionMade(self): - - # public properties - self.cid = CID.next() - self.istep = 0 - self.exits = [] # exit names created - self.objs = [] # obj names created - - self._report = "" - self._cmdlist = [] # already stepping in a cmd definition - self._ncmds = 0 - self._actions = self.factory.actions - self._echo_brief = self.factory.verbose == 1 - self._echo_all = self.factory.verbose == 2 - #print " ** client %i connected." % self.cid - - reactor.addSystemEventTrigger('before', 'shutdown', self.logout) - - # start client tick - d = LoopingCall(self.step) - # dissipate exact step by up to +/- 0.5 second - timestep = self.factory.timestep + (-0.5 + (random.random()*1.0)) - d.start(timestep, now=True).addErrback(self.error) - - def dataReceived(self, data): - "Echo incoming data to stdout" - if self._echo_all: - print data - - def connectionLost(self, reason): - "loosing the connection" - #print " ** client %i lost connection." % self.cid - - def error(self, err): - "error callback" - print err - - def counter(self): - "produces a unique id, also between clients" - return OID.next() - - def logout(self): - "Causes the client to log out of the server. Triggered by ctrl-c signal." - cmd, report = self._actions[1](self) - print "client %i %s (%s actions)" % (self.cid, report, self.istep) - self.sendLine(cmd) - - def step(self): - """ - Perform a step. This is called repeatedly by the runner - and causes the client to issue commands to the server. - This holds all "intelligence" of the dummy client. - """ - if self.istep == 0 and random.random() > CHANCE_OF_LOGIN: - return - elif random.random() > CHANCE_OF_ACTION: - return - - global NLOGGED_IN - if not self._cmdlist: - # no cmdlist in store, get a new one - if self.istep == 0: - NLOGGED_IN += 1 - cfunc = self._actions[0] - else: # random selection using cumulative probabilities - rand = random.random() - cfunc = [func for cprob, func in self._actions[2] if cprob >= rand][0] - # assign to internal cmdlist - cmd, self._report = cfunc(self) - self._cmdlist = list(makeiter(cmd)) - self._ncmds = len(self._cmdlist) - # output - if self.istep == 0 and not (self._echo_brief or self._echo_all): - # only print login - print "client %i %s (%i/%i)" % (self.cid, self._report, NLOGGED_IN, NCLIENTS) - elif self.istep == 0 or self._echo_brief or self._echo_all: - print "client %i %s (%i/%i)" % (self.cid, self._report, self._ncmds-(len(self._cmdlist)-1), self._ncmds) - # launch the action by popping the first element from cmdlist (don't hide tracebacks) - self.sendLine(str(self._cmdlist.pop(0))) - self.istep += 1 # only steps up if an action is taken - -class DummyFactory(protocol.ClientFactory): - protocol = DummyClient - def __init__(self, actions, timestep, verbose): - "Setup the factory base (shared by all clients)" - self.actions = actions - self.timestep = timestep - self.verbose = verbose - -#------------------------------------------------------------ -# Access method: -# Starts clients and connects them to a running server. -#------------------------------------------------------------ - -def start_all_dummy_clients(actions, nclients=1, timestep=5, telnet_port=4000, verbose=0): - - # validating and preparing the action tuple - global NCLIENTS - NCLIENTS = nclients - - # make sure the probabilities add up to 1 - pratio = 1.0 / sum(tup[0] for tup in actions[2:]) - flogin, flogout, probs, cfuncs = actions[0], actions[1], [tup[0] * pratio for tup in actions[2:]], [tup[1] for tup in actions[2:]] - # create cumulative probabilies for the random actions - cprobs = [sum(v for i,v in enumerate(probs) if i<=k) for k in range(len(probs))] - # rebuild a new, optimized action structure - actions = (flogin, flogout, zip(cprobs, cfuncs)) - - # setting up all clients (they are automatically started) - factory = DummyFactory(actions, timestep, verbose) - for i in range(nclients): - reactor.connectTCP("localhost", telnet_port, factory) - # start reactor - reactor.run() - -#------------------------------------------------------------ -# Command line interface -#------------------------------------------------------------ - -if __name__ == '__main__': - - # parsing command line with default vals - parser = OptionParser(usage="%prog [options] [timestep, [port]]", - description="This program requires some preparations to run properly. Start it without any arguments or options for full help.") - parser.add_option('-v', '--verbose', action='store_const', const=1, dest='verbose', - default=0,help="echo brief description of what clients do every timestep.") - parser.add_option('-V', '--very-verbose', action='store_const',const=2, dest='verbose', - default=0,help="echo all client returns to stdout (hint: use only with nclients=1!)") - - options, args = parser.parse_args() - - nargs = len(args) - nclients = DEFAULT_NCLIENTS - timestep = DEFAULT_TIMESTEP - port = DEFAULT_PORT - try: - if not args : raise Exception - if nargs > 0: nclients = max(1, int(args[0])) - if nargs > 1: timestep = max(1, int(args[1])) - if nargs > 2: port = int(args[2]) - except Exception: - print HELPTEXT - sys.exit() - - # import the ACTION tuple from a given module - try: - action_modpath = settings.DUMMYRUNNER_ACTIONS_MODULE - except AttributeError: - # use default - action_modpath = "src.utils.dummyrunner.dummyrunner_actions" - actions = utils.variable_from_module(action_modpath, "ACTIONS") - - print "Connecting %i dummy client(s) to port %i using a %i second timestep ... " % (nclients, port, timestep) - t0 = time.time() - start_all_dummy_clients(actions, nclients, timestep, port, - verbose=options.verbose) - ttot = time.time() - t0 - print "... dummy client runner finished after %i seconds." % ttot diff --git a/src/utils/dummyrunner/dummyrunner_actions.py b/src/utils/dummyrunner/dummyrunner_actions.py deleted file mode 100644 index 3aba1f9b7..000000000 --- a/src/utils/dummyrunner/dummyrunner_actions.py +++ /dev/null @@ -1,231 +0,0 @@ -""" -These are actions for the dummy client runner, using -the default command set and intended for unmodified Evennia. - -Each client action is defined as a function. The clients -will perform these actions randomly (except the login action). - -Each action-definition function should take one argument- "client", -which is a reference to the client currently performing the action -Use the client object for saving data between actions. - -The client object has the following relevant properties and methods: - cid - unique client id - istep - the current step - exits - an empty list. Can be used to store exit names - objs - an empty list. Can be used to store object names - counter() - get an integer value. This counts up for every call and - is always unique between clients. - -The action-definition function should return the command that the -client should send to the server (as if it was input in a mud client). -It should also return a string detailing the action taken. This string is -used by the "brief verbose" mode of the runner and is prepended by -"Client N " to produce output like "Client 3 is creating objects ..." - -This module *must* also define a variable named ACTIONS. This is a tuple -where the first element is the function object for the action function -to call when the client logs onto the server. The following elements -are 2-tuples (probability, action_func), where probability defines how -common it is for that particular action to happen. The runner will -randomly pick between those functions based on the probability. - -ACTIONS = (login_func, (0.3, func1), (0.1, func2) ... ) - -To change the runner to use your custom ACTION and/or action -definitions, edit settings.py and add - - DUMMYRUNNER_ACTIONS_MODULE = "path.to.your.module" - -""" - -# it's very useful to have a unique id for this run to avoid any risk -# of clashes - -import time -RUNID = time.time() - -# some convenient templates - -START_ROOM = "testing_room_start-%s-%s" % (RUNID, "%i") -ROOM_TEMPLATE = "testing_room_%s-%s" % (RUNID, "%i") -EXIT_TEMPLATE = "exit_%s-%s" % (RUNID, "%i") -OBJ_TEMPLATE = "testing_obj_%s-%s" % (RUNID, "%i") -TOBJ_TEMPLATE = "testing_button_%s-%s" % (RUNID, "%i") -TOBJ_TYPECLASS = "examples.red_button.RedButton" - -# action function definitions - -def c_login(client): - "logins to the game" - cname = "Dummy-%s-%i" % (RUNID, client.cid) - #cemail = "%s@dummy.com" % (cname.lower()) - cpwd = "%s-%s" % (RUNID, client.cid) - # set up for digging a first room (to move to) - roomname = ROOM_TEMPLATE % client.counter() - exitname1 = EXIT_TEMPLATE % client.counter() - exitname2 = EXIT_TEMPLATE % client.counter() - client.exits.extend([exitname1, exitname2]) - #cmd = '@dig %s = %s, %s' % (roomname, exitname1, exitname2) - cmd = ('create %s %s' % (cname, cpwd), - 'connect %s %s' % (cname, cpwd), - '@dig %s' % START_ROOM % client.cid, - '@teleport %s' % START_ROOM % client.cid, - '@dig %s = %s, %s' % (roomname, exitname1, exitname2) - ) - - return cmd, "logs in as %s ..." % cname - -def c_login_nodig(client): - "logins, don't dig its own room" - cname = "Dummy-%s-%i" % (RUNID, client.cid) - cpwd = "%s-%s" % (RUNID, client.cid) - cmd = ('create %s %s' % (cname, cpwd), - 'connect %s %s' % (cname, cpwd)) - return cmd, "logs in as %s ..." % cname - -def c_logout(client): - "logouts of the game" - return "@quit", "logs out" - -def c_looks(client): - "looks at various objects" - cmd = ["look %s" % obj for obj in client.objs] - if not cmd: - cmd = ["look %s" % exi for exi in client.exits] - if not cmd: - cmd = "look" - return cmd, "looks ..." - -def c_examines(client): - "examines various objects" - cmd = ["examine %s" % obj for obj in client.objs] - if not cmd: - cmd = ["examine %s" % exi for exi in client.exits] - if not cmd: - cmd = "examine me" - return cmd, "examines objs ..." - -def c_help(client): - "reads help files" - cmd = ('help', - 'help @teleport', - 'help look', - 'help @tunnel', - 'help @dig') - return cmd, "reads help ..." - -def c_digs(client): - "digs a new room, storing exit names on client" - roomname = ROOM_TEMPLATE % client.counter() - exitname1 = EXIT_TEMPLATE % client.counter() - exitname2 = EXIT_TEMPLATE % client.counter() - client.exits.extend([exitname1, exitname2]) - cmd = '@dig/tel %s = %s, %s' % (roomname, exitname1, exitname2) - return cmd, "digs ..." - -def c_creates_obj(client): - "creates normal objects, storing their name on client" - objname = OBJ_TEMPLATE % client.counter() - client.objs.append(objname) - cmd = ('@create %s' % objname, - '@desc %s = "this is a test object' % objname, - '@set %s/testattr = this is a test attribute value.' % objname, - '@set %s/testattr2 = this is a second test attribute.' % objname) - return cmd, "creates obj ..." - -def c_creates_button(client): - "creates example button, storing name on client" - objname = TOBJ_TEMPLATE % client.counter() - client.objs.append(objname) - cmd = ('@create %s:%s' % (objname, TOBJ_TYPECLASS), - '@desc %s = test red button!' % objname) - return cmd, "creates button ..." - -def c_socialize(client): - "socializechats on channel" - cmd = ('ooc Hello!', - 'ooc Testing ...', - 'ooc Testing ... times 2', - 'say Yo!', - 'emote stands looking around.') - return cmd, "socializes ..." - -def c_moves(client): - "moves to a previously created room, using the stored exits" - cmd = client.exits # try all exits - finally one will work - if not cmd: cmd = "look" - return cmd, "moves ..." - -def c_moves_n(client): - "move through north exit if available" - cmd = ("north",) - return cmd, "moves n..." - -def c_moves_s(client): - "move through north exit if available" - cmd = ("north",) - return cmd, "moves s..." - -# Action tuple (required) -# -# This is a tuple of client action functions. The first element is the -# function the client should use to log into the game and move to -# STARTROOM . The second element is the logout command, for cleanly -# exiting the mud. The following elements are 2-tuples of (probability, -# action_function). The probablities should normally sum up to 1, -# otherwise the system will normalize them. -# - -## "normal builder" definitionj -#ACTIONS = ( c_login, -# c_logout, -# (0.5, c_looks), -# (0.08, c_examines), -# (0.1, c_help), -# (0.01, c_digs), -# (0.01, c_creates_obj), -# (0.3, c_moves)) -## "heavy" builder definition -#ACTIONS = ( c_login, -# c_logout, -# (0.2, c_looks), -# (0.1, c_examines), -# (0.2, c_help), -# (0.1, c_digs), -# (0.1, c_creates_obj), -# #(0.01, c_creates_button), -# (0.2, c_moves)) -## "passive player" definition -#ACTIONS = ( c_login, -# c_logout, -# (0.7, c_looks), -# #(0.1, c_examines), -# (0.3, c_help)) -# #(0.1, c_digs), -# #(0.1, c_creates_obj), -# #(0.1, c_creates_button), -# #(0.4, c_moves)) -## "normal player" definition -ACTIONS = ( c_login, - c_logout, - (0.01, c_digs), - (0.39, c_looks), - (0.2, c_help), - (0.4, c_moves)) -#ACTIONS = (c_login_nodig, -# c_logout, -# (1.0, c_moves_n)) -## "socializing heavy builder" definition -#ACTIONS = (c_login, -# c_logout, -# (0.1, c_socialize), -# (0.1, c_looks), -# (0.2, c_help), -# (0.1, c_creates_obj), -# (0.2, c_digs), -# (0.3, c_moves)) -## "heavy digger memory tester" definition -#ACTIONS = (c_login, -# c_logout, -# (1.0, c_digs)) diff --git a/src/utils/idmapper/LICENSE b/src/utils/idmapper/LICENSE deleted file mode 100644 index 17616fe08..000000000 --- a/src/utils/idmapper/LICENSE +++ /dev/null @@ -1,9 +0,0 @@ -Copyright (c) 2009, David Cramer -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/src/utils/idmapper/README.rst b/src/utils/idmapper/README.rst deleted file mode 100755 index 5fa4d0a4c..000000000 --- a/src/utils/idmapper/README.rst +++ /dev/null @@ -1,45 +0,0 @@ -This fork of django-idmapper fixes some bugs that prevented the idmapper from -being used in many instances. In particular, the caching manager is now inherited -by SharedMemoryManager subclasses, and it is used when Django uses an automatic -manager (see http://docs.djangoproject.com/en/dev/topics/db/managers/#controlling-automatic-manager-types). This means access through foreign keys now uses -identity mapping. - -Tested with Django version 1.2 alpha 1 SVN-12375. - -My modifications are usually accompanied by comments marked with "CL:". - -Django Identity Mapper -====================== - -A pluggable Django application which allows you to explicitally mark your models to use an identity mapping pattern. This will share instances of the same model in memory throughout your interpreter. - -Please note, that deserialization (such as from the cache) will *not* use the identity mapper. - -Usage ------ -To use the shared memory model you simply need to inherit from it (instead of models.Model). This enable all queries (and relational queries) to this model to use the shared memory instance cache, effectively creating a single instance for each unique row (based on primary key) in the queryset. - -For example, if you want to simply mark all of your models as a SharedMemoryModel, you might as well just import it as models. -:: - - from idmapper import models - - class MyModel(models.SharedMemoryModel): - name = models.CharField(...) - -Because the system is isolated, you may mix and match SharedMemoryModels with regular Models. The module idmapper.models imports everything from django.db.models and only adds SharedMemoryModel, so you can simply replace your import of models from django.db. -:: - - from idmapper import models - - class MyModel(models.SharedMemoryModel): - name = models.CharField(...) - fkey = models.ForeignKey('Other') - - class Other(models.Model): - name = models.CharField(...) - -References ----------- - -Original code and concept: http://code.djangoproject.com/ticket/17 \ No newline at end of file diff --git a/src/utils/idmapper/__init__.py b/src/utils/idmapper/__init__.py deleted file mode 100755 index 4ac7b4573..000000000 --- a/src/utils/idmapper/__init__.py +++ /dev/null @@ -1,41 +0,0 @@ -import os.path -import warnings - -__version__ = (0, 2) - -def _get_git_revision(path): - revision_file = os.path.join(path, 'refs', 'heads', 'master') - if not os.path.exists(revision_file): - return None - fh = open(revision_file, 'r') - try: - return fh.read() - finally: - fh.close() - -def get_revision(): - """ - :returns: Revision number of this branch/checkout, if available. None if - no revision number can be determined. - """ - package_dir = os.path.dirname(__file__) - checkout_dir = os.path.normpath(os.path.join(package_dir, '..')) - path = os.path.join(checkout_dir, '.git') - if os.path.exists(path): - return _get_git_revision(path) - return None - -__build__ = get_revision() - -def lazy_object(location): - def inner(*args, **kwargs): - parts = location.rsplit('.', 1) - warnings.warn('`idmapper.%s` is deprecated. Please use `%s` instead.' % (parts[1], location), DeprecationWarning) - imp = __import__(parts[0], globals(), locals(), [parts[1]], -1) - func = getattr(imp, parts[1]) - if callable(func): - return func(*args, **kwargs) - return func - return inner - -SharedMemoryModel = lazy_object('idmapper.models.SharedMemoryModel') \ No newline at end of file diff --git a/src/utils/idmapper/models.py b/src/utils/idmapper/models.py deleted file mode 100755 index c70fd1909..000000000 --- a/src/utils/idmapper/models.py +++ /dev/null @@ -1,2 +0,0 @@ -from django.db.models import * -from base import SharedMemoryModel, WeakSharedMemoryModel