diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 353140b4d7..a4d41c6bbf 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -19,6 +19,10 @@ happens to be named "command"! A PR allows you to request that your custom fixes/additions/changes be pulled into the main Evennia repository. To make a PR you must first [fork Evennia on GitHub][8]. Read the [Contribution][3] page for more help. +If you are working to solve an Issue in the issue tracker, note which branch you should make the PR +against (`master` or `develop`). If you are making a PR for a new feature or contrib, do so against +the `develop' branch. + - All contributions should abide by Evennia's [style guide](https://github.com/evennia/evennia/blob/master/CODING_STYLE.md). - For your own sanity and ours, separate unrelated contributions into their own branches and make a new PR for each. You can still update the branch after the PR is up - the PR will update automatically. diff --git a/CHANGELOG.md b/CHANGELOG.md index ab9b5ae728..a05d65fdc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,11 @@ # Evennia Changelog + +# Sept 2017: +Release of Evennia 0.7; upgrade to Django 1.11, change 'Player' to +'Account', rework the website template and a slew of other updates. +Info on what changed and how to migrate is found here: +https://groups.google.com/forum/#!msg/evennia/0JYYNGY-NfE/cDFaIwmPBAAJ + ## Feb 2017: New devel branch created, to lead up to Evennia 0.7. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..299ad5d241 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting griatch AT gmail DOT com or Griatch in the #evennia channel on irc.freenode.net. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/Dockerfile b/Dockerfile index 4d095d6db9..4ca00d254b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,40 +1,54 @@ ##### # Base docker image for running Evennia-based games in a container. # -# This Dockerfile creates the evennia/evennia docker image -# on DockerHub, which can be used as the basis for creating -# an Evennia game within a container. This base image can be -# found in DockerHub at https://hub.docker.com/r/evennia/evennia/ -# -# For more information on using it to build a container to run your game, see +# Install: +# install `docker` (http://docker.com) # -# https://github.com/evennia/evennia/wiki/Running%20Evennia%20in%20Docker +# Usage: +# cd to a folder where you want your game data to be (or where it already is). # -FROM python:2.7-alpine -MAINTAINER Dan Feeney "feend78@gmail.com" +# docker run -it -p 4000:4000 -p 4001:4001 -p 4005:4005 -v $PWD:/usr/src/game evennia/evennia +# +# (If your OS does not support $PWD, replace it with the full path to your current +# folder). +# +# You will end up in a shell where the `evennia` command is available. From here you +# can install and run the game normally. Use Ctrl-D to exit the evennia docker container. +# +# The evennia/evennia base image is found on DockerHub and can also be used +# as a base for creating your own custom containerized Evennia game. For more +# info, see https://github.com/evennia/evennia/wiki/Running%20Evennia%20in%20Docker . +# +FROM alpine + +MAINTAINER www.evennia.com # install compilation environment -RUN apk update && apk add gcc musl-dev +RUN apk update && apk add python py-pip python-dev py-setuptools gcc musl-dev jpeg-dev zlib-dev bash # add the project source ADD . /usr/src/evennia # install dependencies -RUN pip install -e /usr/src/evennia +RUN pip install --upgrade pip && pip install /usr/src/evennia --trusted-host pypi.python.org -# add the game source during game builds +# add the game source when rebuilding a new docker image from inside +# a game dir ONBUILD ADD . /usr/src/game # make the game source hierarchy persistent with a named volume. -# during development this is typically superceded by directives in -# docker-compose.yml or the CLI to mount a local directory. +# mount on-disk game location here when using the container +# to just get an evennia environment. VOLUME /usr/src/game # set the working directory WORKDIR /usr/src/game -# startup command -CMD ["evennia", "-i", "start"] +# set bash prompt +ENV PS1 "evennia|docker \w $ " -# expose the default ports -EXPOSE 8000 8001 4000 +# startup a shell when we start the container +ENTRYPOINT bash -c "source /usr/src/evennia/bin/unix/evennia-docker-start.sh" + +# expose the telnet, webserver and websocket client ports +EXPOSE 4000 4001 4005 diff --git a/INSTALL.md b/INSTALL.md index d76c3a3a11..8c45cb357e 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -2,7 +2,7 @@ # Evennia installation The latest and more detailed installation instructions can be found -[here](https://github.com/evennia/evennia/wiki/Getting-Started). +[here](https://github.com/evennia/evennia/wiki/Getting-Started). ## Installing Python @@ -37,7 +37,7 @@ 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: +else if you prefer). Activate the virtual environment like this: ``` # for Linux/Unix/Mac: @@ -64,13 +64,13 @@ 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: +to use this instead: ``` git clone git@github.com:evennia/evennia.git ``` -In the future you just enter the new `evennia` folder and do +In the future you just enter the new `evennia` folder and do ``` git pull @@ -78,7 +78,7 @@ git pull to get the latest Evennia updates. -## Evennia package install +## Evennia package install Stand at the root of your new `evennia` directory and run @@ -90,12 +90,12 @@ pip install -e . 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`. +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. +This should be done somewhere *outside* of your `evennia` folder. ``` @@ -118,10 +118,10 @@ evennia 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 +initializes. After this Evennia will be running. Use ``` -evennia -h +evennia -h ``` for help with starting, stopping and other operations. @@ -131,7 +131,7 @@ 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*. +pointing your web browser to *http://localhost:4001*. Finally, login with the superuser account and password you provided earlier. Welcome to Evennia! diff --git a/README.md b/README.md index 6439c8861e..0383c98034 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ 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 +[screenshot]: https://user-images.githubusercontent.com/294267/30773728-ea45afb6-a076-11e7-8820-49be2168a6b8.png [logo]: https://github.com/evennia/evennia/blob/master/evennia/web/website/static/website/images/evennia_logo.png [travisimg]: https://travis-ci.org/evennia/evennia.svg?branch=master [travislink]: https://travis-ci.org/evennia/evennia diff --git a/bin/player-account-step1.patch b/bin/player-account-step1.patch new file mode 100644 index 0000000000..c13e8e9d25 --- /dev/null +++ b/bin/player-account-step1.patch @@ -0,0 +1,223 @@ +diff --git a/evennia/comms/migrations/0015_auto_20170706_2041.py b/evennia/comms/migrations/0015_auto_20170706_2041.py +index ec5fc29..62b7936 100644 +--- a/evennia/comms/migrations/0015_auto_20170706_2041.py ++++ b/evennia/comms/migrations/0015_auto_20170706_2041.py +@@ -2,7 +2,12 @@ + # Generated by Django 1.11.2 on 2017-07-06 20:41 + from __future__ import unicode_literals + +-from django.db import migrations ++from django.db import migrations, connection ++ ++ ++def _table_exists(db_cursor, tablename): ++ "Returns bool if table exists or not" ++ return tablename in connection.introspection.table_names() + + + class Migration(migrations.Migration): +@@ -11,17 +16,23 @@ class Migration(migrations.Migration): + ('comms', '0014_auto_20170705_1736'), + ] + +- operations = [ +- migrations.RemoveField( +- model_name='channeldb', +- name='db_subscriptions', +- ), +- migrations.RemoveField( +- model_name='msg', +- name='db_receivers_players', +- ), +- migrations.RemoveField( +- model_name='msg', +- name='db_sender_players', +- ), +- ] ++ db_cursor = connection.cursor() ++ ++ if not _table_exists(db_cursor, "channels.channeldb_db_receivers_players"): ++ # OBS - this is run BEFORE migrations are run! ++ operations = [] ++ else: ++ operations = [ ++ migrations.RemoveField( ++ model_name='channeldb', ++ name='db_subscriptions', # this is now db_account_subscriptions ++ ), ++ migrations.RemoveField( ++ model_name='msg', ++ name='db_receivers_players', ++ ), ++ migrations.RemoveField( ++ model_name='msg', ++ name='db_sender_players', ++ ), ++ ] +diff --git a/evennia/objects/migrations/0007_objectdb_db_account.py b/evennia/objects/migrations/0007_objectdb_db_account.py +index b27c75c..6e40252 100644 +--- a/evennia/objects/migrations/0007_objectdb_db_account.py ++++ b/evennia/objects/migrations/0007_objectdb_db_account.py +@@ -2,21 +2,31 @@ + # Generated by Django 1.11.2 on 2017-07-05 17:27 + from __future__ import unicode_literals + +-from django.db import migrations, models ++from django.db import migrations, models, connection + import django.db.models.deletion + + ++def _table_exists(db_cursor, tablename): ++ "Returns bool if table exists or not" ++ return tablename in connection.introspection.table_names() ++ ++ + class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0002_copy_player_to_account'), + ('objects', '0006_auto_20170606_1731'), + ] + +- operations = [ +- migrations.AddField( +- model_name='objectdb', +- name='db_account', +- field=models.ForeignKey(help_text=b'an Account connected to this object, if any.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.AccountDB', verbose_name=b'account'), +- ), +- ] ++ db_cursor = connection.cursor() ++ operations = [] ++ if _table_exists(db_cursor, "players_playerdb"): ++ # OBS - this is run BEFORE migrations even start, so if we have a player table ++ # here we are not starting from scratch. ++ operations = [ ++ migrations.AddField( ++ model_name='objectdb', ++ name='db_account', ++ field=models.ForeignKey(help_text=b'an Account connected to this object, if any.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.AccountDB', verbose_name=b'account'), ++ ), ++ ] +diff --git a/evennia/objects/migrations/0009_remove_objectdb_db_player.py b/evennia/objects/migrations/0009_remove_objectdb_db_player.py +index 80161a1..10fb225 100644 +--- a/evennia/objects/migrations/0009_remove_objectdb_db_player.py ++++ b/evennia/objects/migrations/0009_remove_objectdb_db_player.py +@@ -2,7 +2,12 @@ + # Generated by Django 1.11.2 on 2017-07-06 20:41 + from __future__ import unicode_literals + +-from django.db import migrations ++from django.db import migrations, connection ++ ++ ++def _table_exists(db_cursor, tablename): ++ "Returns bool if table exists or not" ++ return tablename in connection.introspection.table_names() + + + class Migration(migrations.Migration): +@@ -11,9 +16,15 @@ class Migration(migrations.Migration): + ('objects', '0008_auto_20170705_1736'), + ] + +- operations = [ +- migrations.RemoveField( +- model_name='objectdb', +- name='db_player', +- ), +- ] ++ db_cursor = connection.cursor() ++ ++ if not _table_exists(db_cursor, "objectdb_db_player"): ++ # OBS - this is run BEFORE migrations are run! ++ operations = [] ++ else: ++ operations = [ ++ migrations.RemoveField( ++ model_name='objectdb', ++ name='db_player', ++ ), ++ ] +diff --git a/evennia/scripts/migrations/0009_scriptdb_db_account.py b/evennia/scripts/migrations/0009_scriptdb_db_account.py +index 99baf70..23f6df9 100644 +--- a/evennia/scripts/migrations/0009_scriptdb_db_account.py ++++ b/evennia/scripts/migrations/0009_scriptdb_db_account.py +@@ -2,21 +2,31 @@ + # Generated by Django 1.11.2 on 2017-07-05 17:27 + from __future__ import unicode_literals + +-from django.db import migrations, models ++from django.db import migrations, models, connection + import django.db.models.deletion + + ++def _table_exists(db_cursor, tablename): ++ "Returns bool if table exists or not" ++ return tablename in connection.introspection.table_names() ++ ++ + class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0002_copy_player_to_account'), + ('scripts', '0008_auto_20170606_1731'), + ] + +- operations = [ +- migrations.AddField( +- model_name='scriptdb', +- name='db_account', +- field=models.ForeignKey(blank=True, help_text=b'the account to store this script on (should not be set if db_obj is set)', null=True, on_delete=django.db.models.deletion.CASCADE, to='accounts.AccountDB', verbose_name=b'scripted account'), +- ), +- ] ++ db_cursor = connection.cursor() ++ operations = [] ++ if _table_exists(db_cursor, "players_playerdb"): ++ # OBS - this is run BEFORE migrations even start, so if we have a player table ++ # here we are not starting from scratch. ++ operations = [ ++ migrations.AddField( ++ model_name='scriptdb', ++ name='db_account', ++ field=models.ForeignKey(blank=True, help_text=b'the account to store this script on (should not be set if db_obj is set)', null=True, on_delete=django.db.models.deletion.CASCADE, to='accounts.AccountDB', verbose_name=b'scripted account'), ++ ), ++ ] +diff --git a/evennia/scripts/migrations/0011_remove_scriptdb_db_player.py b/evennia/scripts/migrations/0011_remove_scriptdb_db_player.py +index d3746a5..20fa63f 100644 +--- a/evennia/scripts/migrations/0011_remove_scriptdb_db_player.py ++++ b/evennia/scripts/migrations/0011_remove_scriptdb_db_player.py +@@ -2,7 +2,12 @@ + # Generated by Django 1.11.2 on 2017-07-06 20:41 + from __future__ import unicode_literals + +-from django.db import migrations ++from django.db import migrations, connection ++ ++ ++def _table_exists(db_cursor, tablename): ++ "Returns bool if table exists or not" ++ return tablename in connection.introspection.table_names() + + + class Migration(migrations.Migration): +@@ -11,9 +16,14 @@ class Migration(migrations.Migration): + ('scripts', '0010_auto_20170705_1736'), + ] + +- operations = [ +- migrations.RemoveField( +- model_name='scriptdb', +- name='db_player', +- ), +- ] ++ db_cursor = connection.cursor() ++ ++ if not _table_exists(db_cursor, 'scripts_scriptdb_db_player'): ++ operations = [] ++ else: ++ operations = [ ++ migrations.RemoveField( ++ model_name='scriptdb', ++ name='db_player', ++ ), ++ ] diff --git a/bin/project_rename.py b/bin/project_rename.py new file mode 100644 index 0000000000..8f22d75f1c --- /dev/null +++ b/bin/project_rename.py @@ -0,0 +1,337 @@ +""" +Project rename utility + +Created for the Player->Account renaming + +Griatch 2017, released under the BSD license. + +""" +from __future__ import print_function + +import re +import sys +import os +import fnmatch + +ANSI_HILITE = "\033[1m" +ANSI_RED = "\033[31m" +ANSI_GREEN = "\033[32m" +ANSI_YELLOW = "\033[33m" +ANSI_NORMAL = "\033[0m" + +USE_COLOR = True +FAKE_MODE = False + +# if these words are longer than output word, retain given case +CASE_WORD_EXCEPTIONS = ('an', ) + +_HELP_TEXT = """This program interactively renames words in all files of your project. It's +currently renaming {sources} to {targets}. + +If it wants to replace text in a file, it will show all lines (and line numbers) it wants to +replace, each directly followed by the suggested replacement. + +If a rename is not okay, you can de-select it by entering 'i' followed by one or more +comma-separated line numbers. You cannot ignore partial lines, those you need to remember to change +manually later. + +[q]uit - exits the program immediately. +[h]elp - this help. +[s]kip file - make no changes at all in this file, continue on to the next. +[i]ignore lines - specify line numbers to not change. +[c]lear ignores - this reverts all your ignores if you make a mistake. +[a]accept/save file - apply all accepted renames and continue on to the next file. + +(return to continue) +""" + +# Helper functions + + +def _green(string): + if USE_COLOR: + return "%s%s%s" % (ANSI_GREEN, string, ANSI_NORMAL) + return string + + +def _yellow(string): + if USE_COLOR: + return "%s%s%s" % (ANSI_YELLOW, string, ANSI_NORMAL) + return string + + +def _red(string): + if USE_COLOR: + return "%s%s%s" % (ANSI_HILITE + ANSI_RED, string, ANSI_NORMAL) + return string + + +def _case_sensitive_replace(string, old, new): + """ + Replace text, retaining exact case. + + Args: + string (str): String in which to perform replacement. + old (str): Word or substring to replace. + new (str): What to replace `old` with. + + Returns: + repl_string (str): Version of string where instances of + `old` has been replaced with `new`, retaining case. + + """ + def repl(match): + current = match.group() + # treat multi-word sentences word-by-word + old_words = current.split(" ") + new_words = new.split(" ") + out = [] + for old_word, new_word in zip(old_words, new_words): + result = [] + all_upper = True + for ind, chr in enumerate(old_word): + if ind >= len(new_word): + break + if chr.isupper(): + result.append(new_word[ind].upper()) + else: + result.append(new_word[ind].lower()) + all_upper = False + # special cases - keep remaing case) + if new_word.lower() in CASE_WORD_EXCEPTIONS: + result.append(new_word[ind + 1:]) + # append any remaining characters from new + elif all_upper: + result.append(new_word[ind + 1:].upper()) + else: + result.append(new_word[ind + 1:].lower()) + out.append("".join(result)) + # if we have more new words than old ones, just add them verbatim + out.extend([new_word for ind, new_word in enumerate(new_words) if ind >= len(old_words)]) + return " ".join(out) + + regex = re.compile(re.escape(old), re.I) + return regex.sub(repl, string) + + +def rename_in_tree(path, in_list, out_list, excl_list, fileend_list, is_interactive): + """ + Rename across a recursive directory structure. + + Args: + path (str): Root directory to traverse. All subdirectories + will be visited. + in_list (list): List of src words to replace. + out_list (list): Matching list of words to replace with. + excl_list (list): List of paths to exclude. + fileend_list (list): List of file endings to accept. If + not given, accept all file endings. + is_interactive (bool): If we should stop to ask about the + replacements in each file. + + """ + repl_mapping = zip(in_list, out_list) + + for root, dirs, files in os.walk(path): + + print("\ndir: %s\n" % root) + + if any(fnmatch.fnmatch(root, excl) for excl in excl_list): + print("%s skipped (excluded)." % root) + continue + + for file in files: + + full_path = os.path.join(root, file) + if any(fnmatch.fnmatch(full_path, excl) for excl in excl_list): + print("%s skipped (excluded)." % full_path) + continue + + if not fileend_list or any(file.endswith(ending) for ending in fileend_list): + rename_in_file(full_path, in_list, out_list, is_interactive) + + # rename file - always ask + new_file = file + for src, dst in repl_mapping: + new_file = _case_sensitive_replace(new_file, src, dst) + if new_file != file: + inp = raw_input(_green("Rename %s\n -> %s\n Y/[N]? > " % (file, new_file))) + if inp.upper() == 'Y': + new_full_path = os.path.join(root, new_file) + try: + os.rename(full_path, new_full_path) + except OSError as err: + raw_input(_red("Could not rename - %s (return to skip)" % err)) + else: + print("... Renamed.") + else: + print("... Skipped.") + # rename the dir + new_root = root + for src, dst in repl_mapping: + new_root = _case_sensitive_replace(new_root, src, dst) + if new_root != root: + inp = raw_input(_green("Dir Rename %s\n -> %s\n Y/[N]? > " % (root, new_root))) + if inp.upper() == 'Y': + try: + os.rename(root, new_root) + except OSError as err: + raw_input(_red("Could not rename - %s (return to skip)" % err)) + else: + print("... Renamed.") + else: + print("... Skipped.") + + +def rename_in_file(path, in_list, out_list, is_interactive): + """ + Args: + path (str): Path to file in which to perform renaming. + in_list (list): List of src words to replace. + out_list (list): Matching list of words to replace with. + is_interactive (bool): If we should stop to ask about the + replacements in each file. + + """ + print("-- %s" % path) + + org_text = "" + new_text = None + if os.path.isdir(path): + print("%s is a directory. You should use the --recursive option." % path) + sys.exit() + + with open(path, 'r') as fil: + org_text = fil.read() + + repl_mapping = zip(in_list, out_list) + + if not is_interactive: + # just replace everything immediately + new_text = org_text + for src, dst in repl_mapping: + new_text = _case_sensitive_replace(new_text, src, dst) + if new_text != org_text: + if FAKE_MODE: + print(" ... Saved changes to %s. (faked)" % path) + else: + with open(path, 'w') as fil: + fil.write(new_text) + print(" ... Saved changes to %s." % path) + else: + # interactive mode + while True: + renamed = {} + + org_lines = org_text.split("\n") + + for iline, old_line in enumerate(org_lines): + new_line = old_line + for src, dst in repl_mapping: + new_line = _case_sensitive_replace(new_line, src, dst) + if new_line != old_line: + renamed[iline] = new_line + + if not renamed: + # no changes + print(" ... no changes to %s." % path) + return + + while True: + + for iline, renamed_line in sorted(renamed.items(), key=lambda tup: tup[0]): + print("%3i orig: %s" % (iline + 1, org_lines[iline])) + print(" new : %s" % (_yellow(renamed_line))) + print(_green("%s (%i lines changed)" % (path, len(renamed)))) + + ret = raw_input(_green("Choose: " + "[q]uit, " + "[h]elp, " + "[s]kip file, " + "[i]gnore lines, " + "[c]lear ignores, " + "[a]ccept/save file: ".lower())) + + if ret == "s": + # skip file entirely + print(" ... Skipping file %s." % path) + return + elif ret == "c": + # clear ignores - rerun rename + break + elif ret == "a": + # save result + for iline, renamed_line in renamed.items(): + org_lines[iline] = renamed_line + + if FAKE_MODE: + print(" ... Saved file %s (faked)" % path) + return + with open(path, 'w') as fil: + fil.writelines("\n".join(org_lines)) + print(" ... Saved file %s" % path) + return + elif ret == "q": + print("Quit renaming program.") + sys.exit() + elif ret == "h": + raw_input(_HELP_TEXT.format(sources=in_list, targets=out_list)) + elif ret.startswith("i"): + # ignore one or more lines + ignores = [int(ind) - 1 for ind in ret[1:].split(',') if ind.strip().isdigit()] + if not ignores: + raw_input("Ignore example: i 2,7,34,133\n (return to continue)") + continue + for ign in ignores: + renamed.pop(ign, None) + continue + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Rename text in a source tree, or a single file") + + parser.add_argument('-i', '--input', action='append', + help="Source word to rename (quote around multiple words)") + parser.add_argument('-o', '--output', action='append', + help="Word to rename a matching src-word to") + parser.add_argument('-x', '--exc', action='append', + help="File path patterns to exclude") + parser.add_argument('-a', '--auto', action='store_true', + help="Automatic mode, don't ask to rename") + parser.add_argument('-r', '--recursive', action='store_true', + help="Recurse subdirs") + parser.add_argument('-f', '--fileending', action='append', + help="Change which file endings to allow (default .py and .html)") + parser.add_argument('--nocolor', action='store_true', + help="Turn off in-program color") + parser.add_argument('--fake', action='store_true', + help="Simulate run but don't actually save") + parser.add_argument('path', + help="File or directory in which to rename text") + + args = parser.parse_args() + + in_list, out_list, exc_list, fileend_list = args.input, args.output, args.exc, args.fileending + + if not (in_list and out_list): + print('At least one source- and destination word must be given.') + sys.exit() + if len(in_list) != len(out_list): + print('Number of sources must be identical to the number of destination arguments.') + sys.exit() + + exc_list = exc_list or [] + fileend_list = fileend_list or [".py", ".html"] + is_interactive = not args.auto + is_recursive = args.recursive + + USE_COLOR = not args.nocolor + FAKE_MODE = args.fake + + if is_recursive: + rename_in_tree(args.path, in_list, out_list, exc_list, fileend_list, is_interactive) + else: + rename_in_file(args.path, in_list, out_list, is_interactive) diff --git a/bin/unix/evennia-docker-start.sh b/bin/unix/evennia-docker-start.sh new file mode 100644 index 0000000000..270f6ec627 --- /dev/null +++ b/bin/unix/evennia-docker-start.sh @@ -0,0 +1,13 @@ +#! /bin/bash + +# called by the Dockerfile to start the server in docker mode + +# remove leftover .pid files (such as from when dropping the container) +rm /usr/src/game/server/*.pid >& /dev/null || true + +# start evennia server; log to server.log but also output to stdout so it can +# be viewed with docker-compose logs +exec 3>&1; evennia start 2>&1 1>&3 | tee /usr/src/game/server/logs/server.log; exec 3>&- + +# start a shell to keep the container running +bash diff --git a/bin/windows/evennia_launcher.py b/bin/windows/evennia_launcher.py index 1784b05fe4..ba11eb8274 100755 --- a/bin/windows/evennia_launcher.py +++ b/bin/windows/evennia_launcher.py @@ -5,12 +5,13 @@ the python bin directory and makes the 'evennia' program available on the command %path%. """ -import os, sys +import os +import 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 +from evennia.server.evennia_launcher import main main() diff --git a/evennia/VERSION.txt b/evennia/VERSION.txt index a918a2aa18..f8d71478f5 100644 --- a/evennia/VERSION.txt +++ b/evennia/VERSION.txt @@ -1 +1 @@ -0.6.0 +0.8.0-dev diff --git a/evennia/__init__.py b/evennia/__init__.py index b65e1edf9b..6fdc4aaece 100644 --- a/evennia/__init__.py +++ b/evennia/__init__.py @@ -25,7 +25,7 @@ from builtins import object # Typeclasses -DefaultPlayer = None +DefaultAccount = None DefaultGuest = None DefaultObject = None DefaultCharacter = None @@ -36,7 +36,7 @@ DefaultScript = None # Database models ObjectDB = None -PlayerDB = None +AccountDB = None ScriptDB = None ChannelDB = None Msg = None @@ -51,7 +51,7 @@ InterruptCommand = None # search functions search_object = None search_script = None -search_player = None +search_account = None search_channel = None search_message = None search_help = None @@ -60,7 +60,7 @@ search_tag = None # create functions create_object = None create_script = None -create_player = None +create_account = None create_channel = None create_message = None create_help_entry = None @@ -79,9 +79,11 @@ EvMenu = None EvTable = None EvForm = None EvEditor = None +EvMore = None # Handlers SESSION_HANDLER = None +TASK_HANDLER = None TICKER_HANDLER = None MONITOR_HANDLER = None CHANNEL_HANDLER = None @@ -108,26 +110,29 @@ def _create_version(): 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. """ - global DefaultPlayer, DefaultObject, DefaultGuest, DefaultCharacter + global DefaultAccount, DefaultObject, DefaultGuest, DefaultCharacter global DefaultRoom, DefaultExit, DefaultChannel, DefaultScript - global ObjectDB, PlayerDB, ScriptDB, ChannelDB, Msg + global ObjectDB, AccountDB, ScriptDB, ChannelDB, Msg global Command, CmdSet, default_cmds, syscmdkeys, InterruptCommand - global search_object, search_script, search_player, search_channel, search_help, search_tag - global create_object, create_script, create_player, create_channel, create_message, create_help_entry - global settings,lockfuncs, logger, utils, gametime, ansi, spawn, managers - global contrib, TICKER_HANDLER, MONITOR_HANDLER, SESSION_HANDLER, CHANNEL_HANDLER + global search_object, search_script, search_account, search_channel, search_help, search_tag, search_message + global create_object, create_script, create_account, create_channel, create_message, create_help_entry + global settings, lockfuncs, logger, utils, gametime, ansi, spawn, managers + global contrib, TICKER_HANDLER, MONITOR_HANDLER, SESSION_HANDLER, CHANNEL_HANDLER, TASK_HANDLER + global EvMenu, EvTable, EvForm, EvMore, EvEditor - from .players.players import DefaultPlayer - from .players.players import DefaultGuest + from .accounts.accounts import DefaultAccount + from .accounts.accounts import DefaultGuest from .objects.objects import DefaultObject from .objects.objects import DefaultCharacter from .objects.objects import DefaultRoom @@ -137,7 +142,7 @@ def _init(): # Database models from .objects.models import ObjectDB - from .players.models import PlayerDB + from .accounts.models import AccountDB from .scripts.models import ScriptDB from .comms.models import ChannelDB from .comms.models import Msg @@ -149,7 +154,7 @@ def _init(): # 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_account from .utils.search import search_message from .utils.search import search_channel from .utils.search import search_help @@ -158,7 +163,7 @@ def _init(): # 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_account from .utils.create import create_channel from .utils.create import create_message from .utils.create import create_help_entry @@ -178,6 +183,7 @@ def _init(): # handlers from .scripts.tickerhandler import TICKER_HANDLER + from .scripts.taskhandler import TASK_HANDLER from .server.sessionhandler import SESSION_HANDLER from .comms.channelhandler import CHANNEL_HANDLER from .scripts.monitorhandler import MONITOR_HANDLER @@ -189,6 +195,7 @@ def _init(): Parent for other containers """ + def _help(self): "Returns list of contents" names = [name for name in self.__class__.__dict__ if not name.startswith('_')] @@ -196,13 +203,12 @@ def _init(): print(self.__doc__ + "-" * 60 + "\n" + ", ".join(names)) help = property(_help) - class DBmanagers(_EvContainer): """ Links to instantiated database managers. helpentry - HelpEntry.objects - players - PlayerDB.objects + accounts - AccountDB.objects scripts - ScriptDB.objects msgs - Msg.objects channels - Channel.objects @@ -213,7 +219,7 @@ def _init(): """ from .help.models import HelpEntry - from .players.models import PlayerDB + from .accounts.models import AccountDB from .scripts.models import ScriptDB from .comms.models import Msg, ChannelDB from .objects.models import ObjectDB @@ -223,7 +229,7 @@ def _init(): # create container's properties helpentries = HelpEntry.objects - players = PlayerDB.objects + accounts = AccountDB.objects scripts = ScriptDB.objects msgs = Msg.objects channels = ChannelDB.objects @@ -232,14 +238,13 @@ def _init(): attributes = Attribute.objects tags = Tag.objects # remove these so they are not visible as properties - del HelpEntry, PlayerDB, ScriptDB, Msg, ChannelDB + del HelpEntry, AccountDB, 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. @@ -250,10 +255,10 @@ def _init(): """ from .commands.default.cmdset_character import CharacterCmdSet - from .commands.default.cmdset_player import PlayerCmdSet + from .commands.default.cmdset_account import AccountCmdSet from .commands.default.cmdset_unloggedin import UnloggedinCmdSet from .commands.default.cmdset_session import SessionCmdSet - from .commands.default.muxcommand import MuxCommand, MuxPlayerCommand + from .commands.default.muxcommand import MuxCommand, MuxAccountCommand def __init__(self): "populate the object with commands" @@ -264,15 +269,15 @@ def _init(): self.__dict__.update(dict([(c.__name__, c) for c in cmdlist])) from .commands.default import (admin, batchprocess, - building, comms, general, - player, help, system, unloggedin) + building, comms, general, + account, help, system, unloggedin) add_cmds(admin) add_cmds(building) add_cmds(batchprocess) add_cmds(building) add_cmds(comms) add_cmds(general) - add_cmds(player) + add_cmds(account) add_cmds(help) add_cmds(system) add_cmds(unloggedin) @@ -280,7 +285,6 @@ def _init(): default_cmds = DefaultCmds() del DefaultCmds - class SystemCmds(_EvContainer): """ Creating commands with keys set to these constants will make @@ -293,7 +297,7 @@ def _init(): 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 + first command when an account connects to the server. To access in code, do 'from evennia import syscmdkeys' then @@ -311,6 +315,7 @@ def _init(): del SystemCmds del _EvContainer + del object del absolute_import del print_function diff --git a/evennia/accounts/__init__.py b/evennia/accounts/__init__.py new file mode 100644 index 0000000000..4ba99b4568 --- /dev/null +++ b/evennia/accounts/__init__.py @@ -0,0 +1,6 @@ +""" +This sub-package defines the out-of-character entities known as +Accounts. These are equivalent to 'accounts' and can puppet one or +more Objects depending on settings. An Account has no in-game existence. + +""" diff --git a/evennia/players/players.py b/evennia/accounts/accounts.py similarity index 75% rename from evennia/players/players.py rename to evennia/accounts/accounts.py index ce726a8c71..3a9736d738 100644 --- a/evennia/players/players.py +++ b/evennia/accounts/accounts.py @@ -1,5 +1,5 @@ """ -Typeclass for Player objects +Typeclass for Account objects Note that this object is primarily intended to store OOC information, not game info! This @@ -15,8 +15,8 @@ import time from django.conf import settings from django.utils import timezone from evennia.typeclasses.models import TypeclassBase -from evennia.players.manager import PlayerManager -from evennia.players.models import PlayerDB +from evennia.accounts.manager import AccountManager +from evennia.accounts.models import AccountDB from evennia.objects.models import ObjectDB from evennia.comms.models import ChannelDB from evennia.commands import cmdhandler @@ -31,32 +31,32 @@ from evennia.commands.cmdsethandler import CmdSetHandler from django.utils.translation import ugettext as _ from future.utils import with_metaclass -__all__ = ("DefaultPlayer",) +__all__ = ("DefaultAccount",) _SESSIONS = None _AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1)) _MULTISESSION_MODE = settings.MULTISESSION_MODE _MAX_NR_CHARACTERS = settings.MAX_NR_CHARACTERS -_CMDSET_PLAYER = settings.CMDSET_PLAYER +_CMDSET_ACCOUNT = settings.CMDSET_ACCOUNT _CONNECT_CHANNEL = None -class PlayerSessionHandler(object): +class AccountSessionHandler(object): """ - Manages the session(s) attached to a player. + Manages the session(s) attached to an account. """ - def __init__(self, player): + def __init__(self, account): """ Initializes the handler. Args: - player (Player): The Player on which this handler is defined. + account (Account): The Account on which this handler is defined. """ - self.player = player + self.account = account def get(self, sessid=None): """ @@ -75,9 +75,9 @@ class PlayerSessionHandler(object): if not _SESSIONS: from evennia.server.sessionhandler import SESSIONS as _SESSIONS if sessid: - return make_iter(_SESSIONS.session_from_player(self.player, sessid)) + return make_iter(_SESSIONS.session_from_account(self.account, sessid)) else: - return _SESSIONS.sessions_from_player(self.player) + return _SESSIONS.sessions_from_account(self.account) def all(self): """ @@ -100,19 +100,19 @@ class PlayerSessionHandler(object): return len(self.get()) -class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): +class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)): """ - This is the base Typeclass for all Players. Players represent + This is the base Typeclass for all Accounts. Accounts represent the person playing the game and tracks account info, password - etc. They are OOC entities without presence in-game. A Player + etc. They are OOC entities without presence in-game. An Account can connect to a Character Object in order to "enter" the game. - Player Typeclass API: + Account Typeclass API: * Available properties (only available on initiated typeclass objects) - - key (string) - name of player + - key (string) - name of account - name (string)- wrapper for user.username - aliases (list of strings) - aliases to the object. Will be saved to database as AliasDB entries but returned as strings. @@ -120,9 +120,9 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): - 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 + - obj (Object) - game object controlled by account. 'character' can also be used. - - sessions (list of Sessions) - sessions connected to this player + - sessions (list of Sessions) - sessions connected to this account - is_superuser (bool, read-only) - if the connected user is a superuser * Handlers @@ -142,7 +142,7 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): - execute_cmd(raw_string) - search(ostring, global_search=False, attribute_name=None, use_nicks=False, location=None, - ignore_errors=False, player=False) + ignore_errors=False, account=False) - is_typeclass(typeclass, exact=False) - swap_typeclass(new_typeclass, clean_attributes=False, no_default=True) - access(accessing_obj, access_type='read', default=False, no_superuser_bypass=False) @@ -151,7 +151,7 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): * Hook methods basetype_setup() - at_player_creation() + at_account_creation() > note that the following hooks are also found on Objects and are usually handled on the character level: @@ -169,7 +169,7 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): """ - objects = PlayerManager() + objects = AccountManager() # properties @lazy_property @@ -186,24 +186,25 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): @lazy_property def sessions(self): - return PlayerSessionHandler(self) + return AccountSessionHandler(self) # session-related methods - def disconnect_session_from_player(self, session): + def disconnect_session_from_account(self, session, reason=None): """ Access method for disconnecting a given session from the - player (connection happens automatically in the + account (connection happens automatically in the sessionhandler) Args: session (Session): Session to disconnect. + reason (str, optional): Eventual reason for the disconnect. """ global _SESSIONS if not _SESSIONS: from evennia.server.sessionhandler import SESSIONS as _SESSIONS - _SESSIONS.disconnect(session) + _SESSIONS.disconnect(session, reason) # puppeting operations @@ -235,9 +236,9 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): # no access self.msg("You don't have permission to puppet '%s'." % obj.key) return - if obj.player: + if obj.account: # object already puppeted - if obj.player == self: + if obj.account == self: if obj.sessions.count(): # we may take over another of our sessions # output messages to the affected sessions @@ -252,9 +253,9 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): self.msg(txt1 % obj.name, session=session) self.msg(txt2 % obj.name, session=obj.sessions.all()) self.unpuppet_object(obj.sessions.get()) - elif obj.player.is_connected: - # controlled by another player - self.msg("|c%s|R is already puppeted by another Player." % obj.key) + elif obj.account.is_connected: + # controlled by another account + self.msg("|c%s|R is already puppeted by another Account." % obj.key) return # do the puppeting @@ -262,14 +263,14 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): # cleanly unpuppet eventual previous object puppeted by this session self.unpuppet_object(session) # if we get to this point the character is ready to puppet or it - # was left with a lingering player/session reference from an unclean + # was left with a lingering account/session reference from an unclean # server kill or similar obj.at_pre_puppet(self, session=session) # do the connection obj.sessions.add(session) - obj.player = self + obj.account = self session.puid = obj.id session.puppet = obj # validate/start persistent scripts on object @@ -299,7 +300,7 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): obj.at_pre_unpuppet() obj.sessions.remove(session) if not obj.sessions.count(): - del obj.player + del obj.account obj.at_post_unpuppet(self, session=session) # Just to be sure we're always clear. session.puppet = None @@ -314,9 +315,9 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): def get_puppet(self, session): """ - Get an object puppeted by this session through this player. This is + Get an object puppeted by this session through this account. This is the main method for retrieving the puppeted object from the - player's end. + account's end. Args: session (Session): Find puppeted object based on this session @@ -333,7 +334,7 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): Returns: puppets (list): All puppeted objects currently controlled - by this Player. + by this Account. """ return list(set(session.puppet for session in self.sessions.all() if session.puppet)) @@ -359,7 +360,7 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): def delete(self, *args, **kwargs): """ - Deletes the player permanently. + Deletes the account permanently. Notes: `*args` and `**kwargs` are passed on to the base delete @@ -375,12 +376,12 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): except RuntimeError: # no puppet to disconnect from pass - session.sessionhandler.disconnect(session, reason=_("Player being deleted.")) + session.sessionhandler.disconnect(session, reason=_("Account being deleted.")) self.scripts.stop() self.attributes.clear() self.nicks.clear() self.aliases.clear() - super(DefaultPlayer, self).delete(*args, **kwargs) + super(DefaultAccount, self).delete(*args, **kwargs) # methods inherited from database model def msg(self, text=None, from_obj=None, session=None, options=None, **kwargs): @@ -391,8 +392,8 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): 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. + from_obj (Object or Account or list, optional): Object sending. If given, its + at_msg_send() hook will be called. If iterable, call on all entities. session (Session or list, optional): Session object or a list of Sessions to receive this send. If given, overrules the default send behavior for the current @@ -404,14 +405,15 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): """ if from_obj: # call hook - try: - from_obj.at_msg_send(text=text, to_obj=self, **kwargs) - except Exception: - # this may not be assigned. - pass + for obj in make_iter(from_obj): + try: + obj.at_msg_send(text=text, to_obj=self, **kwargs) + except Exception: + # this may not be assigned. + logger.log_trace() try: if not self.at_msg_receive(text=text, **kwargs): - # abort message to this player + # abort message to this account return except Exception: # this may not be assigned. @@ -426,9 +428,9 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): def execute_cmd(self, raw_string, session=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 + Do something as this account. This method is never called normally, + but only when the account object itself is supposed to execute the + command. It takes account nicks into account, but not nicks of eventual puppets. Args: @@ -445,33 +447,33 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): """ raw_string = to_unicode(raw_string) - raw_string = self.nicks.nickreplace(raw_string, categories=("inputline", "channel"), include_player=False) + raw_string = self.nicks.nickreplace(raw_string, categories=("inputline", "channel"), include_account=False) if not session and _MULTISESSION_MODE in (0, 1): # for these modes we use the first/only session sessions = self.sessions.get() session = sessions[0] if sessions else None return cmdhandler.cmdhandler(self, raw_string, - callertype="player", session=session, **kwargs) + callertype="account", session=session, **kwargs) def search(self, searchdata, return_puppet=False, search_object=False, - typeclass=None, nofound_string=None, multimatch_string=None, **kwargs): + typeclass=None, nofound_string=None, multimatch_string=None, use_nicks=True, **kwargs): """ This is similar to `DefaultObject.search` but defaults to searching - for Players only. + for Accounts only. Args: - searchdata (str or int): Search criterion, the Player's + searchdata (str or int): Search criterion, the Account's key or dbref to search for. return_puppet (bool, optional): Instructs the method to - return matches as the object the Player controls rather - than the Player itself (or None) if nothing is puppeted). + return matches as the object the Account controls rather + than the Account itself (or None) if nothing is puppeted). search_object (bool, optional): Search for Objects instead of - Players. This is used by e.g. the @examine command when + Accounts. This is used by e.g. the @examine command when wanting to examine Objects while OOC. - typeclass (Player typeclass, optional): Limit the search + typeclass (Account typeclass, optional): Limit the search only to this particular typeclass. This can be used to - limit to specific player typeclasses or to limit the search + limit to specific account typeclasses or to limit the search to a particular Object typeclass if `search_object` is True. nofound_string (str, optional): A one-time error message to echo if `searchdata` leads to no matches. If not given, @@ -479,9 +481,10 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): multimatch_string (str, optional): A one-time error message to echo if `searchdata` leads to multiple matches. If not given, will fall back to the default handler. + use_nicks (bool, optional): Use account-level nick replacement. Return: - match (Player, Object or None): A single Player or Object match. + match (Account, Object or None): A single Account or Object match. Notes: Extra keywords are ignored, but are allowed in call in order to make API more consistent with @@ -494,9 +497,11 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): if searchdata.lower() in ("me", "*me", "self", "*self",): return self if search_object: - matches = ObjectDB.objects.object_search(searchdata, typeclass=typeclass) + matches = ObjectDB.objects.object_search(searchdata, typeclass=typeclass, use_nicks=use_nicks) else: - matches = PlayerDB.objects.player_search(searchdata, typeclass=typeclass) + searchdata = self.nicks.nickreplace(searchdata, categories=("account", ), include_account=False) + + matches = AccountDB.objects.account_search(searchdata, typeclass=typeclass) matches = _AT_SEARCH_RESULT(matches, self, query=searchdata, nofound_string=nofound_string, multimatch_string=multimatch_string) @@ -527,8 +532,8 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): result (bool): Result of access check. """ - result = super(DefaultPlayer, self).access(accessing_obj, access_type=access_type, - default=default, no_superuser_bypass=no_superuser_bypass) + result = super(DefaultAccount, self).access(accessing_obj, access_type=access_type, + default=default, no_superuser_bypass=no_superuser_bypass) self.at_access(result, accessing_obj, access_type, **kwargs) return result @@ -554,33 +559,34 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): return time.time() - float(min(conn)) return None - # player hooks + # account 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. + This sets up the basic properties for an account. Overload this + with at_account_creation rather than changing this method. """ # A basic security setup - lockstring = "examine:perm(Wizards);edit:perm(Wizards);" \ - "delete:perm(Wizards);boot:perm(Wizards);msg:all()" + lockstring = "examine:perm(Admin);edit:perm(Admin);" \ + "delete:perm(Admin);boot:perm(Admin);msg:all();" \ + "noidletimeout:perm(Builder) or perm(noidletimeout)" self.locks.add(lockstring) - # The ooc player cmdset - self.cmdset.add_default(_CMDSET_PLAYER, permanent=True) + # The ooc account cmdset + self.cmdset.add_default(_CMDSET_ACCOUNT, permanent=True) - def at_player_creation(self): + def at_account_creation(self): """ - This is called once, the very first time the player is created + This is called once, the very first time the account is created (i.e. first time they register with the game). It's a good - place to store attributes all players should have, like + place to store attributes all accounts should have, like configuration values etc. """ - # set an (empty) attribute holding the characters this player has + # set an (empty) attribute holding the characters this account has lockstring = "attrread:perm(Admins);attredit:perm(Admins);" \ - "attrcreate:perm(Admins)" + "attrcreate:perm(Admins);" self.attributes.add("_playable_characters", [], lockstring=lockstring) self.attributes.add("_saved_protocol_flags", {}, lockstring=lockstring) @@ -590,8 +596,8 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): 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 + restart or reload. In the case of account objects, this usually + happens the moment the account logs in or reconnects after a reload. """ @@ -601,7 +607,7 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): # 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. + # character is currently connected to this account. def at_first_save(self): """ @@ -611,11 +617,11 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): """ self.basetype_setup() - self.at_player_creation() + self.at_account_creation() - permissions = settings.PERMISSION_PLAYER_DEFAULT + permissions = settings.PERMISSION_ACCOUNT_DEFAULT if hasattr(self, "_createdict"): - # this will only be set if the utils.create_player + # this will only be set if the utils.create_account # function was used to create the object. cdict = self._createdict if cdict.get("locks"): @@ -624,11 +630,11 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): permissions = cdict["permissions"] del self._createdict - self.permissions.add(permissions) + self.permissions.batch_add(*permissions) def at_access(self, result, accessing_obj, access_type, **kwargs): """ - This is triggered after an access-call on this Player has + This is triggered after an access-call on this Account has completed. Args: @@ -653,33 +659,41 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): def at_cmdset_get(self, **kwargs): """ - Called just *before* cmdsets on this player are requested by + Called just *before* cmdsets on this account are requested by the command handler. The cmdsets are available as `self.cmdset`. If changes need to be done on the fly to the cmdset before passing them on to the cmdhandler, this is the - place to do it. This is called also if the player currently + place to do it. This is called also if the account currently have no cmdsets. kwargs are usually not used unless the cmdset is generated dynamically. """ pass - def at_first_login(self): + def at_first_login(self, **kwargs): """ - Called the very first time this player logs into the game. + Called the very first time this account logs into the game. Note that this is called *before* at_pre_login, so no session is established and usually no character is yet assigned at - this point. This hook is intended for player-specific setup + this point. This hook is intended for account-specific setup like configurations. + Args: + **kwargs (dict): Arbitrary, optional arguments for users + overriding the call (unused by default). + """ pass - def at_pre_login(self): + def at_pre_login(self, **kwargs): """ Called every time the user logs in, just before the actual login-state is set. + Args: + **kwargs (dict): Arbitrary, optional arguments for users + overriding the call (unused by default). + """ pass @@ -706,13 +720,15 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): else: logger.log_info("[%s]: %s" % (now, message)) - def at_post_login(self, session=None): + def at_post_login(self, session=None, **kwargs): """ Called at the end of the login process, just before letting - the player loose. + the account loose. Args: session (Session, optional): Session logging in, if any. + **kwargs (dict): Arbitrary, optional arguments for users + overriding the call (unused by default). Notes: This is called *before* an eventual Character's @@ -747,14 +763,14 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): return 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. - # we make sure to clean up the _playable_characers list in case + # screen. We execute look on the account. + # we make sure to clean up the _playable_characters list in case # any was deleted in the interim. self.db._playable_characters = [char for char in self.db._playable_characters if char] self.msg(self.at_look(target=self.db._playable_characters, session=session)) - def at_failed_login(self, session): + def at_failed_login(self, session, **kwargs): """ Called by the login process if a user account is targeted correctly but provided with an invalid password. By default it does nothing, @@ -762,41 +778,87 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): Args: session (session): Session logging in. + **kwargs (dict): Arbitrary, optional arguments for users + overriding the call (unused by default). """ pass - def at_disconnect(self, reason=None): + def at_disconnect(self, reason=None, **kwargs): """ Called just before user is disconnected. Args: reason (str, optional): The reason given for the disconnect, (echoed to the connection channel by default). + **kwargs (dict): Arbitrary, optional arguments for users + overriding the call (unused by default). + """ - reason = reason and "(%s)" % reason or "" - self._send_to_connect_channel("|R%s disconnected %s|n" % (self.key, reason)) + reason = " (%s)" % reason if reason else "" + self._send_to_connect_channel("|R%s disconnected%s|n" % (self.key, reason)) - def at_post_disconnect(self): + def at_post_disconnect(self, **kwargs): """ 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, + can be relayed to the account from here. After this call, the + account should not be accessed any more, making this a good + spot for deleting it (in the case of a guest account account, for example). + + Args: + **kwargs (dict): Arbitrary, optional arguments for users + overriding the call (unused by default). + """ pass - def at_message_receive(self, message, from_obj=None): + def at_msg_receive(self, text=None, from_obj=None, **kwargs): """ - This is currently unused. + This hook is called whenever someone sends a message to this + object using the `msg` method. + + Note that from_obj may be None if the sender did not include + itself as an argument to the obj.msg() call - so you have to + check for this. . + + Consider this a pre-processing method before msg is passed on + to the user session. If this method returns False, the msg + will not be passed on. + + Args: + text (str, optional): The message received. + from_obj (any, optional): The object sending the message. + + Kwargs: + This includes any keywords sent to the `msg` method. + + Returns: + receive (bool): If this message should be received. + + Notes: + If this method returns False, the `msg` operation + will abort without sending the message. """ return True - def at_message_send(self, message, to_object): + def at_msg_send(self, text=None, to_obj=None, **kwargs): """ - This is currently unused. + This is a hook that is called when *this* object sends a + message to another object with `obj.msg(text, to_obj=obj)`. + + Args: + text (str, optional): Text to send. + to_obj (any, optional): The object to send to. + + Kwargs: + Keywords passed from msg() + + Notes: + Since this method is executed by `from_obj`, if no `from_obj` + was passed to `DefaultCharacter.msg` this hook will never + get called. """ pass @@ -817,7 +879,7 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): """ pass - def at_look(self, target=None, session=None): + def at_look(self, target=None, session=None, **kwargs): """ Called when this object executes a look. It allows to customize just what this means. @@ -826,6 +888,8 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): target (Object or list, optional): An object or a list objects to inspect. session (Session, optional): The session doing this look. + **kwargs (dict): Arbitrary, optional arguments for users + overriding the call (unused by default). Returns: look_string (str): A prepared look string, ready to send @@ -849,10 +913,10 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): result.append(nsess == 1 and "\n\n|wConnected session:|n" or "\n\n|wConnected sessions (%i):|n" % nsess) for isess, sess in enumerate(sessions): csessid = sess.sessid - addr = "%s (%s)" % (sess.protocol_key, isinstance(sess.address, tuple) - and str(sess.address[0]) or str(sess.address)) - result.append("\n %s %s" % (session.sessid == csessid and "|w* %s|n" % (isess + 1) - or " %s" % (isess + 1), addr)) + addr = "%s (%s)" % (sess.protocol_key, isinstance(sess.address, tuple) and + str(sess.address[0]) or str(sess.address)) + result.append("\n %s %s" % (session.sessid == csessid and "|w* %s|n" % (isess + 1) or + " %s" % (isess + 1), addr)) result.append("\n\n |whelp|n - more commands") result.append("\n |wooc |n - talk on public channel") @@ -893,18 +957,21 @@ class DefaultPlayer(with_metaclass(TypeclassBase, PlayerDB)): return look_string -class DefaultGuest(DefaultPlayer): +class DefaultGuest(DefaultAccount): """ - This class is used for guest logins. Unlike Players, Guests and + This class is used for guest logins. Unlike Accounts, Guests and their characters are deleted after disconnection. """ - def at_post_login(self, session=None): + + def at_post_login(self, session=None, **kwargs): """ In theory, guests only have one character regardless of which MULTISESSION_MODE we're in. They don't get a choice. Args: session (Session, optional): Session connecting. + **kwargs (dict): Arbitrary, optional arguments for users + overriding the call (unused by default). """ self._send_to_connect_channel("|G%s connected|n" % self.key) @@ -922,9 +989,14 @@ class DefaultGuest(DefaultPlayer): print "deleting Character:", character character.delete() - def at_post_disconnect(self): + def at_post_disconnect(self, **kwargs): """ Once having disconnected, destroy the guest's characters and + + Args: + **kwargs (dict): Arbitrary, optional arguments for users + overriding the call (unused by default). + """ super(DefaultGuest, self).at_post_disconnect() characters = self.db._playable_characters diff --git a/evennia/players/admin.py b/evennia/accounts/admin.py similarity index 70% rename from evennia/players/admin.py rename to evennia/accounts/admin.py index 227a3828f7..5678db1124 100644 --- a/evennia/players/admin.py +++ b/evennia/accounts/admin.py @@ -9,19 +9,19 @@ 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 evennia.players.models import PlayerDB +from evennia.accounts.models import AccountDB from evennia.typeclasses.admin import AttributeInline, TagInline from evennia.utils import create # handle the custom User editor -class PlayerDBChangeForm(UserChangeForm): +class AccountDBChangeForm(UserChangeForm): """ - Modify the playerdb class. + Modify the accountdb class. """ class Meta(object): - model = PlayerDB + model = AccountDB fields = '__all__' username = forms.RegexField( @@ -44,19 +44,19 @@ class PlayerDBChangeForm(UserChangeForm): username = self.cleaned_data['username'] if username.upper() == self.instance.username.upper(): return username - elif PlayerDB.objects.filter(username__iexact=username): - raise forms.ValidationError('A player with that name ' + elif AccountDB.objects.filter(username__iexact=username): + raise forms.ValidationError('An account with that name ' 'already exists.') return self.cleaned_data['username'] -class PlayerDBCreationForm(UserCreationForm): +class AccountDBCreationForm(UserCreationForm): """ - Create a new PlayerDB instance. + Create a new AccountDB instance. """ class Meta(object): - model = PlayerDB + model = AccountDB fields = '__all__' username = forms.RegexField( @@ -76,24 +76,24 @@ class PlayerDBCreationForm(UserCreationForm): Cleanup username. """ username = self.cleaned_data['username'] - if PlayerDB.objects.filter(username__iexact=username): - raise forms.ValidationError('A player with that name already ' + if AccountDB.objects.filter(username__iexact=username): + raise forms.ValidationError('An account with that name already ' 'exists.') return username -class PlayerForm(forms.ModelForm): +class AccountForm(forms.ModelForm): """ - Defines how to display Players + Defines how to display Accounts """ class Meta(object): - model = PlayerDB + model = AccountDB fields = '__all__' db_key = forms.RegexField( label="Username", - initial="PlayerDummy", + initial="AccountDummy", max_length=30, regex=r'^[\w. @+-]+$', required=False, @@ -101,32 +101,32 @@ class PlayerForm(forms.ModelForm): error_messages={ 'invalid': "This value may contain only letters, spaces, numbers" " and @/./+/-/_ characters."}, - help_text="This should be the same as the connected Player's key " + help_text="This should be the same as the connected Account's key " "name. 30 characters or fewer. Letters, spaces, digits and " "@/./+/-/_ only.") db_typeclass_path = forms.CharField( label="Typeclass", - initial=settings.BASE_PLAYER_TYPECLASS, + initial=settings.BASE_ACCOUNT_TYPECLASS, widget=forms.TextInput( attrs={'size': '78'}), help_text="Required. Defines what 'type' of entity this is. This " "variable holds a Python path to a module with a valid " "Evennia Typeclass. Defaults to " - "settings.BASE_PLAYER_TYPECLASS.") + "settings.BASE_ACCOUNT_TYPECLASS.") db_permissions = forms.CharField( label="Permissions", - initial=settings.PERMISSION_PLAYER_DEFAULT, + initial=settings.PERMISSION_ACCOUNT_DEFAULT, required=False, widget=forms.TextInput( attrs={'size': '78'}), help_text="In-game permissions. A comma-separated list of text " "strings checked by certain locks. They are often used for " - "hierarchies, such as letting a Player have permission " - "'Wizards', 'Builders' etc. A Player permission can be " + "hierarchies, such as letting an Account have permission " + "'Admin', 'Builder' etc. An Account permission can be " "overloaded by the permissions of a controlled Character. " - "Normal players use 'Players' by default.") + "Normal accounts use 'Accounts' by default.") db_lock_storage = forms.CharField( label="Locks", @@ -137,64 +137,64 @@ class PlayerForm(forms.ModelForm): "type:lockfunction(args);type2:lockfunction2(args);...") db_cmdset_storage = forms.CharField( label="cmdset", - initial=settings.CMDSET_PLAYER, + initial=settings.CMDSET_ACCOUNT, widget=forms.TextInput(attrs={'size': '78'}), required=False, - help_text="python path to player cmdset class (set in " - "settings.CMDSET_PLAYER by default)") + help_text="python path to account cmdset class (set in " + "settings.CMDSET_ACCOUNT by default)") -class PlayerInline(admin.StackedInline): +class AccountInline(admin.StackedInline): """ - Inline creation of Player + Inline creation of Account """ - model = PlayerDB - template = "admin/players/stacked.html" - form = PlayerForm + model = AccountDB + template = "admin/accounts/stacked.html" + form = AccountForm fieldsets = ( ("In-game Permissions and Locks", {'fields': ('db_lock_storage',), - #{'fields': ('db_permissions', 'db_lock_storage'), + #{'fields': ('db_permissions', 'db_lock_storage'), 'description': "These are permissions/locks for in-game use. " "They are unrelated to website access rights."}), - ("In-game Player data", + ("In-game Account data", {'fields': ('db_typeclass_path', 'db_cmdset_storage'), 'description': "These fields define in-game-specific properties " - "for the Player object in-game."})) + "for the Account object in-game."})) extra = 1 max_num = 1 -class PlayerTagInline(TagInline): +class AccountTagInline(TagInline): """ - Inline Player Tags. + Inline Account Tags. """ - model = PlayerDB.db_tags.through - related_field = "playerdb" + model = AccountDB.db_tags.through + related_field = "accountdb" -class PlayerAttributeInline(AttributeInline): +class AccountAttributeInline(AttributeInline): """ - Inline Player Attributes. + Inline Account Attributes. """ - model = PlayerDB.db_attributes.through - related_field = "playerdb" + model = AccountDB.db_attributes.through + related_field = "accountdb" -class PlayerDBAdmin(BaseUserAdmin): +class AccountDBAdmin(BaseUserAdmin): """ - This is the main creation screen for Users/players + This is the main creation screen for Users/accounts """ list_display = ('username', 'email', 'is_staff', 'is_superuser') - form = PlayerDBChangeForm - add_form = PlayerDBCreationForm - inlines = [PlayerTagInline, PlayerAttributeInline] + form = AccountDBChangeForm + add_form = AccountDBCreationForm + inlines = [AccountTagInline, AccountAttributeInline] fieldsets = ( (None, {'fields': ('username', 'password', 'email')}), ('Website profile', { @@ -215,11 +215,11 @@ class PlayerDBAdmin(BaseUserAdmin): 'db_lock_storage'), 'description': 'These are attributes that are more relevant ' 'to gameplay.'})) - # ('Game Options', {'fields': ( - # 'db_typeclass_path', 'db_cmdset_storage', - # 'db_permissions', 'db_lock_storage'), - # 'description': 'These are attributes that are ' - # 'more relevant to gameplay.'})) + # ('Game Options', {'fields': ( + # 'db_typeclass_path', 'db_cmdset_storage', + # 'db_permissions', 'db_lock_storage'), + # 'description': 'These are attributes that are ' + # 'more relevant to gameplay.'})) add_fieldsets = ( (None, @@ -240,16 +240,17 @@ class PlayerDBAdmin(BaseUserAdmin): """ obj.save() if not change: - #calling hooks for new player - obj.set_class_from_typeclass(typeclass_path=settings.BASE_PLAYER_TYPECLASS) + # calling hooks for new account + obj.set_class_from_typeclass(typeclass_path=settings.BASE_ACCOUNT_TYPECLASS) obj.basetype_setup() - obj.at_player_creation() + obj.at_account_creation() def response_add(self, request, obj, post_url_continue=None): from django.http import HttpResponseRedirect from django.core.urlresolvers import reverse if '_continue' in request.POST: - return HttpResponseRedirect(reverse("admin:players_playerdb_change", args=[obj.id])) - return HttpResponseRedirect(reverse("admin:players_playerdb_change", args=[obj.id])) + return HttpResponseRedirect(reverse("admin:accounts_accountdb_change", args=[obj.id])) + return HttpResponseRedirect(reverse("admin:accounts_accountdb_change", args=[obj.id])) -admin.site.register(PlayerDB, PlayerDBAdmin) + +admin.site.register(AccountDB, AccountDBAdmin) diff --git a/evennia/players/bots.py b/evennia/accounts/bots.py similarity index 93% rename from evennia/players/bots.py rename to evennia/accounts/bots.py index c415b78c69..b0a765fbba 100644 --- a/evennia/players/bots.py +++ b/evennia/accounts/bots.py @@ -1,12 +1,12 @@ """ Bots are a special child typeclasses of -Player that are controlled by the server. +Account that are controlled by the server. """ from __future__ import print_function import time from django.conf import settings -from evennia.players.players import DefaultPlayer +from evennia.accounts.accounts import DefaultAccount from evennia.scripts.scripts import DefaultScript from evennia.utils import search from evennia.utils import utils @@ -28,6 +28,7 @@ class BotStarter(DefaultScript): into gear when it is initialized. """ + def at_script_creation(self): """ Called once, when script is created. @@ -37,10 +38,6 @@ class BotStarter(DefaultScript): self.desc = "bot start/keepalive" self.persistent = True self.db.started = False - if _IDLE_TIMEOUT > 0: - # call before idle_timeout triggers - self.interval = int(max(60, _IDLE_TIMEOUT * 0.90)) - self.start_delay = True def at_start(self): """ @@ -48,7 +45,7 @@ class BotStarter(DefaultScript): """ if not self.db.started: - self.player.start() + self.account.start() self.db.started = True def at_repeat(self): @@ -63,7 +60,7 @@ class BotStarter(DefaultScript): global _SESSIONS if not _SESSIONS: from evennia.server.sessionhandler import SESSIONS as _SESSIONS - for session in _SESSIONS.sessions_from_player(self.player): + for session in _SESSIONS.sessions_from_account(self.account): session.update_session_counters(idle=True) def at_server_reload(self): @@ -85,7 +82,7 @@ class BotStarter(DefaultScript): # Bot base class -class Bot(DefaultPlayer): +class Bot(DefaultAccount): """ A Bot will start itself when the server starts (it will generally not do so on a reload - that will be handled by the normal Portal @@ -100,8 +97,9 @@ class Bot(DefaultPlayer): """ # the text encoding to use. self.db.encoding = "utf-8" - # A basic security setup - lockstring = "examine:perm(Wizards);edit:perm(Wizards);delete:perm(Wizards);boot:perm(Wizards);msg:false()" + # A basic security setup (also avoid idle disconnects) + lockstring = "examine:perm(Admin);edit:perm(Admin);delete:perm(Admin);" \ + "boot:perm(Admin);msg:false();noidletimeout:true()" self.locks.add(lockstring) # set the basics of being a bot script_key = "%s" % self.key @@ -148,6 +146,7 @@ class IRCBot(Bot): Bot for handling IRC connections. """ + def start(self, ev_channel=None, irc_botname=None, irc_channel=None, irc_network=None, irc_port=None, irc_ssl=None): """ Start by telling the portal to start a new session. @@ -206,12 +205,16 @@ class IRCBot(Bot): "ssl": self.db.irc_ssl} _SESSIONS.start_bot_session("evennia.server.portal.irc.IRCBotFactory", configdict) + def at_msg_send(self, **kwargs): + "Shortcut here or we can end up in infinite loop" + pass + def get_nicklist(self, caller): """ Retrive the nick list from the connected channel. Args: - caller (Object or Player): The requester of the list. This will + caller (Object or Account): The requester of the list. This will be stored and echoed to when the irc network replies with the requested info. @@ -231,7 +234,7 @@ class IRCBot(Bot): Fire a ping to the IRC server. Args: - caller (Object or Player): The requester of the ping. + caller (Object or Account): The requester of the ping. """ if not hasattr(self, "_ping_callers"): @@ -242,7 +245,7 @@ class IRCBot(Bot): def reconnect(self): """ Force a protocol-side reconnect of the client without - having to destroy/recreate the bot "player". + having to destroy/recreate the bot "account". """ super(IRCBot, self).msg(reconnect="") @@ -257,7 +260,7 @@ class IRCBot(Bot): Kwargs: options (dict): Options dict with the following allowed keys: - from_channel (str): dbid of a channel this text originated from. - - from_obj (list): list of objects this text. + - from_obj (list): list of objects sending this text. """ from_obj = kwargs.get("from_obj", None) @@ -266,7 +269,7 @@ class IRCBot(Bot): # cache channel lookup self.ndb.ev_channel = self.db.ev_channel if "from_channel" in options and text and self.ndb.ev_channel.dbid == options["from_channel"]: - if not from_obj or from_obj != [self.id]: + if not from_obj or from_obj != [self]: super(IRCBot, self).msg(channel=text) def execute_cmd(self, session=None, txt=None, **kwargs): @@ -323,8 +326,8 @@ class IRCBot(Bot): for sess in _SESSIONS.get_sessions(): delta_cmd = t0 - sess.cmd_last_visible delta_conn = t0 - session.conn_time - player = sess.get_player() - whos.append("%s (%s/%s)" % (utils.crop("|w%s|n" % player.name, width=25), + account = sess.get_account() + whos.append("%s (%s/%s)" % (utils.crop("|w%s|n" % account.name, width=25), utils.time_format(delta_conn, 0), utils.time_format(delta_cmd, 1))) text = "Who list (online/idle): %s" % ", ".join(sorted(whos, key=lambda w: w.lower())) @@ -347,7 +350,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.id) + self.ndb.ev_channel.msg(text, senders=self) # # RSS @@ -359,6 +362,7 @@ class RSSBot(Bot): its feed at regular intervals. """ + def start(self, ev_channel=None, rss_url=None, rss_rate=None): """ Start by telling the portal to start a new RSS session diff --git a/evennia/players/manager.py b/evennia/accounts/manager.py similarity index 56% rename from evennia/players/manager.py rename to evennia/accounts/manager.py index 15bb79f49d..c612cf930d 100644 --- a/evennia/players/manager.py +++ b/evennia/accounts/manager.py @@ -1,25 +1,22 @@ """ -The managers for the custom Player object and permissions. +The managers for the custom Account object and permissions. """ import datetime from django.utils import timezone from django.contrib.auth.models import UserManager -#from functools import update_wrapper -from evennia.typeclasses.managers import (returns_typeclass_list, returns_typeclass, - TypedObjectManager, TypeclassManager) -from evennia.utils.utils import make_iter -__all__ = ("PlayerManager",) +from evennia.typeclasses.managers import (TypedObjectManager, TypeclassManager) +__all__ = ("AccountManager",) # -# Player Manager +# Account Manager # -class PlayerDBManager(TypedObjectManager, UserManager): +class AccountDBManager(TypedObjectManager, UserManager): """ - This PlayerManager implements methods for searching - and manipulating Players directly from the database. + This AccountManager implements methods for searching + and manipulating Accounts directly from the database. Evennia-specific search methods (will return Characters if possible or a Typeclass/list of Typeclassed objects, whereas @@ -30,49 +27,48 @@ class PlayerDBManager(TypedObjectManager, UserManager): get_dbref_range object_totals typeclass_search - num_total_players - get_connected_players - get_recently_created_players - get_recently_connected_players - get_player_from_email - get_player_from_uid - get_player_from_name - player_search (equivalent to evennia.search_player) + num_total_accounts + get_connected_accounts + get_recently_created_accounts + get_recently_connected_accounts + get_account_from_email + get_account_from_uid + get_account_from_name + account_search (equivalent to evennia.search_account) #swap_character """ - def num_total_players(self): + + def num_total_accounts(self): """ - Get total number of players. + Get total number of accounts. Returns: - count (int): The total number of registered players. + count (int): The total number of registered accounts. """ return self.count() - @returns_typeclass_list - def get_connected_players(self): + def get_connected_accounts(self): """ - Get all currently connected players. + Get all currently connected accounts. Returns: - count (list): Player objects with currently + count (list): Account objects with currently connected sessions. """ return self.filter(db_is_connected=True) - @returns_typeclass_list - def get_recently_created_players(self, days=7): + def get_recently_created_accounts(self, days=7): """ - Get players recently created. + Get accounts recently created. Args: days (int, optional): How many days in the past "recently" means. Returns: - players (list): The Players created the last `days` interval. + accounts (list): The Accounts created the last `days` interval. """ end_date = timezone.now() @@ -80,16 +76,15 @@ class PlayerDBManager(TypedObjectManager, UserManager): start_date = end_date - tdelta return self.filter(date_joined__range=(start_date, end_date)) - @returns_typeclass_list - def get_recently_connected_players(self, days=7): + def get_recently_connected_accounts(self, days=7): """ - Get players recently connected to the game. + Get accounts recently connected to the game. Args: days (int, optional): Number of days backwards to check Returns: - players (list): The Players connected to the game in the + accounts (list): The Accounts connected to the game in the last `days` interval. """ @@ -97,33 +92,31 @@ class PlayerDBManager(TypedObjectManager, UserManager): tdelta = datetime.timedelta(days) start_date = end_date - tdelta return self.filter(last_login__range=( - start_date, end_date)).order_by('-last_login') + start_date, end_date)).order_by('-last_login') - @returns_typeclass - def get_player_from_email(self, uemail): + def get_account_from_email(self, uemail): """ - Search player by - Returns a player object based on email address. + Search account by + Returns an account object based on email address. Args: uemail (str): An email address to search for. Returns: - player (Player): A found player, if found. + account (Account): A found account, if found. """ return self.filter(email__iexact=uemail) - @returns_typeclass - def get_player_from_uid(self, uid): + def get_account_from_uid(self, uid): """ - Get a player by id. + Get an account by id. Args: - uid (int): Player database id. + uid (int): Account database id. Returns: - player (Player): The result. + account (Account): The result. """ try: @@ -131,16 +124,15 @@ class PlayerDBManager(TypedObjectManager, UserManager): except self.model.DoesNotExist: return None - @returns_typeclass - def get_player_from_name(self, uname): + def get_account_from_name(self, uname): """ - Get player object based on name. + Get account object based on name. Args: - uname (str): The Player name to search for. + uname (str): The Account name to search for. Returns: - player (Player): The found player. + account (Account): The found account. """ try: @@ -148,10 +140,9 @@ class PlayerDBManager(TypedObjectManager, UserManager): except self.model.DoesNotExist: return None - @returns_typeclass_list - def search_player(self, ostring, exact=True, typeclass=None): + def search_account(self, ostring, exact=True, typeclass=None): """ - Searches for a particular player by name or + Searches for a particular account by name or database id. Args: @@ -161,7 +152,7 @@ class PlayerDBManager(TypedObjectManager, UserManager): otherwise also match also keys containing the `ostring` (non-case-sensitive fuzzy match). typeclass (str or Typeclass, optional): Limit the search only to - players of this typeclass. + accounts of this typeclass. """ dbref = self.dbref(ostring) @@ -183,7 +174,8 @@ class PlayerDBManager(TypedObjectManager, UserManager): else: return self.filter(**query) # back-compatibility alias - player_search = search_player + account_search = search_account -class PlayerManager(PlayerDBManager, TypeclassManager): + +class AccountManager(AccountDBManager, TypeclassManager): pass diff --git a/evennia/players/migrations/0001_initial.py b/evennia/accounts/migrations/0001_initial.py similarity index 95% rename from evennia/players/migrations/0001_initial.py rename to evennia/accounts/migrations/0001_initial.py index de2b93f5bd..d8d267f20c 100644 --- a/evennia/players/migrations/0001_initial.py +++ b/evennia/accounts/migrations/0001_initial.py @@ -15,7 +15,7 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='PlayerDB', + name='AccountDB', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('password', models.CharField(max_length=128, verbose_name='password')), @@ -32,7 +32,7 @@ class Migration(migrations.Migration): ('db_typeclass_path', models.CharField(help_text=b"this defines what 'type' of entity this is. This variable holds a Python path to a module with a valid Evennia Typeclass.", max_length=255, null=True, verbose_name=b'typeclass')), ('db_date_created', models.DateTimeField(auto_now_add=True, verbose_name=b'creation date')), ('db_lock_storage', models.TextField(help_text=b"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.", verbose_name=b'locks', blank=True)), - ('db_is_connected', models.BooleanField(default=False, help_text=b'If player is connected to game or not', verbose_name=b'is_connected')), + ('db_is_connected', models.BooleanField(default=False, help_text=b'If account is connected to game or not', verbose_name=b'is_connected')), ('db_cmdset_storage', models.CharField(help_text=b'optional python path to a cmdset class. If creating a Character, this will default to settings.CMDSET_CHARACTER.', max_length=255, null=True, verbose_name=b'cmdset')), ('db_is_bot', models.BooleanField(default=False, help_text=b'Used to identify irc/rss bots', verbose_name=b'is_bot')), ('db_attributes', models.ManyToManyField(help_text=b'attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).', to='typeclasses.Attribute', null=True)), @@ -41,8 +41,8 @@ class Migration(migrations.Migration): ('user_permissions', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Permission', blank=True, help_text='Specific permissions for this user.', verbose_name='user permissions')), ], options={ - 'verbose_name': 'Player', - 'verbose_name_plural': 'Players', + 'verbose_name': 'Account', + 'verbose_name_plural': 'Accounts', }, bases=(models.Model,), ), diff --git a/evennia/accounts/migrations/0002_move_defaults.py b/evennia/accounts/migrations/0002_move_defaults.py new file mode 100644 index 0000000000..461525a4cc --- /dev/null +++ b/evennia/accounts/migrations/0002_move_defaults.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +def convert_defaults(apps, schema_editor): + AccountDB = apps.get_model("accounts", "AccountDB") + for account in AccountDB.objects.filter(db_typeclass_path="src.accounts.account.Account"): + account.db_typeclass_path = "typeclasses.accounts.Account" + account.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0001_initial'), + ] + + operations = [ + migrations.RunPython(convert_defaults), + ] diff --git a/evennia/players/migrations/0003_auto_20150209_2234.py b/evennia/accounts/migrations/0003_auto_20150209_2234.py similarity index 69% rename from evennia/players/migrations/0003_auto_20150209_2234.py rename to evennia/accounts/migrations/0003_auto_20150209_2234.py index 71d588bbcc..ccdb2fb897 100644 --- a/evennia/players/migrations/0003_auto_20150209_2234.py +++ b/evennia/accounts/migrations/0003_auto_20150209_2234.py @@ -7,18 +7,18 @@ from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ - ('players', '0002_move_defaults'), + ('accounts', '0002_move_defaults'), ] operations = [ migrations.CreateModel( - name='DefaultPlayer', + name='DefaultAccount', fields=[ ], options={ 'proxy': True, }, - bases=('players.playerdb',), + bases=('accounts.accountdb',), ), migrations.CreateModel( name='DefaultGuest', @@ -27,10 +27,10 @@ class Migration(migrations.Migration): options={ 'proxy': True, }, - bases=('players.defaultplayer',), + bases=('accounts.defaultaccount',), ), migrations.AlterModelOptions( - name='playerdb', - options={'verbose_name': 'Player'}, + name='accountdb', + options={'verbose_name': 'Account'}, ), ] diff --git a/evennia/players/migrations/0004_auto_20150403_2339.py b/evennia/accounts/migrations/0004_auto_20150403_2339.py similarity index 81% rename from evennia/players/migrations/0004_auto_20150403_2339.py rename to evennia/accounts/migrations/0004_auto_20150403_2339.py index f09faba2b7..36d8110122 100644 --- a/evennia/players/migrations/0004_auto_20150403_2339.py +++ b/evennia/accounts/migrations/0004_auto_20150403_2339.py @@ -2,14 +2,14 @@ from __future__ import unicode_literals from django.db import models, migrations -import evennia.players.manager +import evennia.accounts.manager import django.core.validators class Migration(migrations.Migration): dependencies = [ - ('players', '0003_auto_20150209_2234'), + ('accounts', '0003_auto_20150209_2234'), ] operations = [ @@ -17,31 +17,31 @@ class Migration(migrations.Migration): name='DefaultGuest', ), migrations.DeleteModel( - name='DefaultPlayer', + name='DefaultAccount', ), migrations.AlterModelManagers( - name='playerdb', + name='accountdb', managers=[ - (b'objects', evennia.players.manager.PlayerDBManager()), + (b'objects', evennia.accounts.manager.AccountDBManager()), ], ), migrations.AlterField( - model_name='playerdb', + model_name='accountdb', name='email', field=models.EmailField(max_length=254, verbose_name='email address', blank=True), ), migrations.AlterField( - model_name='playerdb', + model_name='accountdb', name='groups', field=models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Group', blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', verbose_name='groups'), ), migrations.AlterField( - model_name='playerdb', + model_name='accountdb', name='last_login', field=models.DateTimeField(null=True, verbose_name='last login', blank=True), ), migrations.AlterField( - model_name='playerdb', + model_name='accountdb', name='username', field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, max_length=30, validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.', 'invalid')], help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', unique=True, verbose_name='username'), ), diff --git a/evennia/players/migrations/0005_auto_20160905_0902.py b/evennia/accounts/migrations/0005_auto_20160905_0902.py similarity index 89% rename from evennia/players/migrations/0005_auto_20160905_0902.py rename to evennia/accounts/migrations/0005_auto_20160905_0902.py index e62ab4cbb5..7ceaad2ac0 100644 --- a/evennia/players/migrations/0005_auto_20160905_0902.py +++ b/evennia/accounts/migrations/0005_auto_20160905_0902.py @@ -9,12 +9,12 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('players', '0004_auto_20150403_2339'), + ('accounts', '0004_auto_20150403_2339'), ] operations = [ migrations.AlterField( - model_name='playerdb', + model_name='accountdb', name='username', field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=30, unique=True, validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.')], verbose_name='username'), ), diff --git a/evennia/accounts/migrations/0006_auto_20170606_1731.py b/evennia/accounts/migrations/0006_auto_20170606_1731.py new file mode 100644 index 0000000000..e48114b23c --- /dev/null +++ b/evennia/accounts/migrations/0006_auto_20170606_1731.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.2 on 2017-06-06 17:31 +from __future__ import unicode_literals + +import django.contrib.auth.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0005_auto_20160905_0902'), + ] + + operations = [ + migrations.AlterField( + model_name='accountdb', + name='db_attributes', + field=models.ManyToManyField(help_text=b'attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).', to='typeclasses.Attribute'), + ), + migrations.AlterField( + model_name='accountdb', + name='db_tags', + field=models.ManyToManyField(help_text=b'tags on this object. Tags are simple string markers to identify, group and alias objects.', to='typeclasses.Tag'), + ), + migrations.AlterField( + model_name='accountdb', + name='username', + field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.ASCIIUsernameValidator()], verbose_name='username'), + ), + ] diff --git a/evennia/accounts/migrations/0007_copy_player_to_account.py b/evennia/accounts/migrations/0007_copy_player_to_account.py new file mode 100644 index 0000000000..ea1e00448c --- /dev/null +++ b/evennia/accounts/migrations/0007_copy_player_to_account.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.2 on 2017-07-03 19:17 +from __future__ import unicode_literals + +from django.apps import apps as global_apps +from django.db import migrations + + +def forwards(apps, schema_editor): + try: + PlayerDB = apps.get_model('players', 'PlayerDB') + except LookupError: + # playerdb not available. Skip. + return + + AccountDB = apps.get_model('accounts', 'AccountDB') + for player in PlayerDB.objects.all(): + account = AccountDB(id=player.id, + password=player.password, + is_superuser=player.is_superuser, + last_login=player.last_login, + username=player.username, + first_name=player.first_name, + last_name=player.last_name, + email=player.email, + is_staff=player.is_staff, + is_active=player.is_active, + date_joined=player.date_joined, + db_key=player.db_key, + db_typeclass_path=player.db_typeclass_path, + db_date_created=player.db_date_created, + db_lock_storage=player.db_lock_storage, + db_is_connected=player.db_is_connected, + db_cmdset_storage=player.db_cmdset_storage, + db_is_bot=player.db_is_bot) + account.save() + for group in player.groups.all(): + account.groups.add(group) + for user_permission in player.user_permissions.all(): + account.user_permissions.add(user_permission) + for attr in player.db_attributes.all(): + account.db_attributes.add(attr) + for tag in player.db_tags.all(): + account.db_tags.add(tag) + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0006_auto_20170606_1731'), + ] + + operations = [ + migrations.RunPython(forwards, migrations.RunPython.noop) + ] + + if global_apps.is_installed('players'): + dependencies.append(('players', '0006_auto_20170606_1731')) diff --git a/evennia/players/migrations/__init__.py b/evennia/accounts/migrations/__init__.py similarity index 100% rename from evennia/players/migrations/__init__.py rename to evennia/accounts/migrations/__init__.py diff --git a/evennia/players/models.py b/evennia/accounts/models.py similarity index 77% rename from evennia/players/models.py rename to evennia/accounts/models.py index d33985de35..bf391b2d82 100644 --- a/evennia/players/models.py +++ b/evennia/accounts/models.py @@ -1,16 +1,16 @@ """ -Player +Account -The player class is an extension of the default Django user class, +The account 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 +We use the Account to store a more mud-friendly style of permission system as well as to allow the admin more flexibility by storing -attributes on the Player. Within the game we should normally use the -Player manager's methods to create users so that permissions are set +attributes on the Account. Within the game we should normally use the +Account manager's methods to create users so that permissions are set correctly. -To make the Player model more flexible for your own game, it can also +To make the Account model more flexible for your own game, it can also persistently store attributes of its own. This is ideal for extra account info and OOC account configuration variables etc. @@ -22,11 +22,11 @@ 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.accounts.manager import AccountDBManager from evennia.typeclasses.models import TypedObject from evennia.utils.utils import make_iter -__all__ = ("PlayerDB",) +__all__ = ("AccountDB",) #_ME = _("me") #_SELF = _("self") @@ -42,11 +42,11 @@ _TYPECLASS = None #------------------------------------------------------------ # -# PlayerDB +# AccountDB # #------------------------------------------------------------ -class PlayerDB(TypedObject, AbstractUser): +class AccountDB(TypedObject, AbstractUser): """ This is a special model using Django's 'profile' functionality and extends the default Django User model. It is defined as such @@ -66,18 +66,18 @@ class PlayerDB(TypedObject, AbstractUser): - db - persistent attribute storage - ndb - non-persistent attribute storage - The PlayerDB adds the following properties: + The AccountDB adds the following properties: - - is_connected - If any Session is currently connected to this Player + - is_connected - If any Session is currently connected to this Account - 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 + - sessions - sessions connected to this account + - is_superuser - bool if this account is a superuser + - is_bot - bool if this account is a bot and not a real account """ # - # PlayerDB Database model setup + # AccountDB Database model setup # # inherited fields (from TypedObject): # db_key, db_typeclass_path, db_date_created, db_permissions @@ -89,20 +89,20 @@ class PlayerDB(TypedObject, AbstractUser): 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 + help_text="optional python path to a cmdset class. If creating a Character, this will default to settings.CMDSET_CHARACTER.") + # marks if this is a "virtual" bot account object db_is_bot = models.BooleanField(default=False, verbose_name="is_bot", help_text="Used to identify irc/rss bots") # Database manager - objects = PlayerDBManager() + objects = AccountDBManager() # defaults __settingsclasspath__ = settings.BASE_SCRIPT_TYPECLASS - __defaultclasspath__ = "evennia.players.players.DefaultPlayer" - __applabel__ = "players" + __defaultclasspath__ = "evennia.accounts.accounts.DefaultAccount" + __applabel__ = "accounts" class Meta(object): - verbose_name = 'Player' + verbose_name = 'Account' # cmdset_storage property # This seems very sensitive to caching, so leaving it be for now /Griatch @@ -136,10 +136,10 @@ class PlayerDB(TypedObject, AbstractUser): # def __str__(self): - return smart_str("%s(player %s)" % (self.name, self.dbid)) + return smart_str("%s(account %s)" % (self.name, self.dbid)) def __unicode__(self): - return u"%s(player#%s)" % (self.name, self.dbid) + return u"%s(account#%s)" % (self.name, self.dbid) #@property def __username_get(self): diff --git a/evennia/accounts/tests.py b/evennia/accounts/tests.py new file mode 100644 index 0000000000..039a25601f --- /dev/null +++ b/evennia/accounts/tests.py @@ -0,0 +1,160 @@ +from mock import Mock +from random import randint +from unittest import TestCase + +from evennia.accounts.accounts import AccountSessionHandler +from evennia.accounts.accounts import DefaultAccount +from evennia.server.session import Session +from evennia.utils import create + +from django.conf import settings + + +class TestAccountSessionHandler(TestCase): + "Check AccountSessionHandler class" + + def setUp(self): + self.account = create.create_account("TestAccount%s" % randint(0, 999999), email="test@test.com", password="testpassword", typeclass=DefaultAccount) + self.handler = AccountSessionHandler(self.account) + + def test_get(self): + "Check get method" + self.assertEqual(self.handler.get(), []) + self.assertEqual(self.handler.get(100), []) + + import evennia.server.sessionhandler + + s1 = Session() + s1.logged_in = True + s1.uid = self.account.uid + evennia.server.sessionhandler.SESSIONS[s1.uid] = s1 + + s2 = Session() + s2.logged_in = True + s2.uid = self.account.uid + 1 + evennia.server.sessionhandler.SESSIONS[s2.uid] = s2 + + s3 = Session() + s3.logged_in = False + s3.uid = self.account.uid + 2 + evennia.server.sessionhandler.SESSIONS[s3.uid] = s3 + + self.assertEqual(self.handler.get(), [s1]) + self.assertEqual(self.handler.get(self.account.uid), [s1]) + self.assertEqual(self.handler.get(self.account.uid + 1), []) + + def test_all(self): + "Check all method" + self.assertEqual(self.handler.get(), self.handler.all()) + + def test_count(self): + "Check count method" + self.assertEqual(self.handler.count(), len(self.handler.get())) + +class TestDefaultAccount(TestCase): + "Check DefaultAccount class" + + def setUp(self): + self.s1 = Session() + self.s1.sessid = 0 + + def test_puppet_object_no_object(self): + "Check puppet_object method called with no object param" + + try: + DefaultAccount().puppet_object(self.s1, None) + self.fail("Expected error: 'Object not found'") + except RuntimeError as re: + self.assertEqual("Object not found", re.message) + + def test_puppet_object_no_session(self): + "Check puppet_object method called with no session param" + + try: + DefaultAccount().puppet_object(None, Mock()) + self.fail("Expected error: 'Session not found'") + except RuntimeError as re: + self.assertEqual("Session not found", re.message) + + def test_puppet_object_already_puppeting(self): + "Check puppet_object method called, already puppeting this" + + import evennia.server.sessionhandler + + account = create.create_account("TestAccount%s" % randint(0, 999999), email="test@test.com", password="testpassword", typeclass=DefaultAccount) + self.s1.uid = account.uid + evennia.server.sessionhandler.SESSIONS[self.s1.uid] = self.s1 + + self.s1.logged_in = True + self.s1.data_out = Mock(return_value=None) + + obj = Mock() + self.s1.puppet = obj + account.puppet_object(self.s1, obj) + self.s1.data_out.assert_called_with(options=None, text="You are already puppeting this object.") + self.assertIsNone(obj.at_post_puppet.call_args) + + def test_puppet_object_no_permission(self): + "Check puppet_object method called, no permission" + + import evennia.server.sessionhandler + + account = create.create_account("TestAccount%s" % randint(0, 999999), email="test@test.com", password="testpassword", typeclass=DefaultAccount) + self.s1.uid = account.uid + evennia.server.sessionhandler.SESSIONS[self.s1.uid] = self.s1 + + self.s1.puppet = None + self.s1.logged_in = True + self.s1.data_out = Mock(return_value=None) + + obj = Mock() + obj.access = Mock(return_value=False) + + account.puppet_object(self.s1, obj) + + self.assertTrue(self.s1.data_out.call_args[1]['text'].startswith("You don't have permission to puppet")) + self.assertIsNone(obj.at_post_puppet.call_args) + + def test_puppet_object_joining_other_session(self): + "Check puppet_object method called, joining other session" + + import evennia.server.sessionhandler + + account = create.create_account("TestAccount%s" % randint(0, 999999), email="test@test.com", password="testpassword", typeclass=DefaultAccount) + self.s1.uid = account.uid + evennia.server.sessionhandler.SESSIONS[self.s1.uid] = self.s1 + + self.s1.puppet = None + self.s1.logged_in = True + self.s1.data_out = Mock(return_value=None) + + obj = Mock() + obj.access = Mock(return_value=True) + obj.account = account + + account.puppet_object(self.s1, obj) + # works because django.conf.settings.MULTISESSION_MODE is not in (1, 3) + self.assertTrue(self.s1.data_out.call_args[1]['text'].endswith("from another of your sessions.")) + self.assertTrue(obj.at_post_puppet.call_args[1] == {}) + + def test_puppet_object_already_puppeted(self): + "Check puppet_object method called, already puppeted" + + import evennia.server.sessionhandler + + account = create.create_account("TestAccount%s" % randint(0, 999999), email="test@test.com", password="testpassword", typeclass=DefaultAccount) + self.s1.uid = account.uid + evennia.server.sessionhandler.SESSIONS[self.s1.uid] = self.s1 + + self.s1.puppet = None + self.s1.logged_in = True + self.s1.data_out = Mock(return_value=None) + + obj = Mock() + obj.access = Mock(return_value=True) + obj.account = Mock() + obj.at_post_puppet = Mock() + + account.puppet_object(self.s1, obj) + self.assertTrue(self.s1.data_out.call_args[1]['text'].endswith("is already puppeted by another Account.")) + self.assertIsNone(obj.at_post_puppet.call_args) diff --git a/evennia/commands/__init__.py b/evennia/commands/__init__.py index 5f77fa3a30..93497beffd 100644 --- a/evennia/commands/__init__.py +++ b/evennia/commands/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ This sub-package contains Evennia's command system. It handles -everything related to parsing input from the player, building cmdsets +everything related to parsing input from the account, building cmdsets and executing the code associated with a found command class. commands.default contains all the default "mux-like" commands of diff --git a/evennia/commands/cmdhandler.py b/evennia/commands/cmdhandler.py index 63c08962b8..44304b4ea1 100644 --- a/evennia/commands/cmdhandler.py +++ b/evennia/commands/cmdhandler.py @@ -13,7 +13,7 @@ command line. The processing of a command works as follows: - object cmdsets: all objects at caller's location are scanned for non-empty cmdsets. This includes cmdsets on exits. - caller: the caller is searched for its own currently active cmdset. - - player: lastly the cmdsets defined on caller.player are added. + - account: lastly the cmdsets defined on caller.account are added. 3. The collected cmdsets are merged together to a combined, current cmdset. 4. If the input string is empty -> check for CMD_NOINPUT command in current cmdset or fallback to error message. Exit. @@ -85,50 +85,50 @@ CMD_LOGINSTART = "__unloggedin_look_command" _SEARCH_AT_RESULT = utils.variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1)) # Output strings. The first is the IN_GAME_ERRORS return, the second -# is the normal "production message to echo to the player. +# is the normal "production message to echo to the account. _ERROR_UNTRAPPED = ( -""" + """ An untrapped error occurred. """, -""" + """ An untrapped error occurred. Please file a bug report detailing the steps to reproduce. """) _ERROR_CMDSETS = ( -""" + """ A cmdset merger-error occurred. This is often due to a syntax error in one of the cmdsets to merge. """, -""" + """ A cmdset merger-error occurred. Please file a bug report detailing the steps to reproduce. """) _ERROR_NOCMDSETS = ( -""" + """ No command sets found! This is a critical bug that can have multiple causes. """, -""" + """ No command sets found! This is a sign of a critical bug. If disconnecting/reconnecting doesn't" solve the problem, try to contact the server admin through" some other means for assistance. """) _ERROR_CMDHANDLER = ( -""" + """ A command handler bug occurred. If this is not due to a local change, please file a bug report with the Evennia project, including the traceback and steps to reproduce. """, -""" + """ A command handler bug occurred. Please notify staff - they should likely file a bug report with the Evennia project. """) _ERROR_RECURSION_LIMIT = "Command recursion limit ({recursion_limit}) " \ - "reached for '{raw_string}' ({cmdclass})." + "reached for '{raw_cmdname}' ({cmdclass})." # delayed imports @@ -210,7 +210,7 @@ def _process_input(caller, prompt, result, cmd, generator): part of yielding from a Command's `func`. Args: - caller (Character, Player or Session): the caller. + caller (Character, Account or Session): the caller. prompt (basestring): The sent prompt. result (basestring): The unprocessed answer. cmd (Command): The command itself. @@ -234,34 +234,38 @@ class NoCmdSets(Exception): class ExecSystemCommand(Exception): "Run a system command" + def __init__(self, syscmd, sysarg): self.args = (syscmd, sysarg) # needed by exception error handling self.syscmd = syscmd self.sysarg = sysarg + class ErrorReported(Exception): "Re-raised when a subsructure already reported the error" + def __init__(self, raw_string): self.args = (raw_string,) self.raw_string = raw_string # Helper function + @inlineCallbacks -def get_and_merge_cmdsets(caller, session, player, obj, callertype, raw_string): +def get_and_merge_cmdsets(caller, session, account, obj, callertype, raw_string): """ Gather all relevant cmdsets and merge them. Args: - caller (Session, Player or Object): The entity executing the command. Which + caller (Session, Account or Object): The entity executing the command. Which type of object this is depends on the current game state; for example when the user is not logged in, this will be a Session, when being OOC - it will be a Player and when puppeting an object this will (often) be + it will be an Account and when puppeting an object this will (often) be a Character Object. In the end it depends on where the cmdset is stored. session (Session or None): The Session associated with caller, if any. - player (Player or None): The calling Player associated with caller, if any. + account (Account or None): The calling Account associated with caller, if any. obj (Object or None): The Object associated with caller, if any. - callertype (str): This identifies caller as either "player", "object" or "session" + callertype (str): This identifies caller as either "account", "object" or "session" to avoid having to do this check internally. raw_string (str): The input string. This is only used for error reporting. @@ -272,18 +276,18 @@ def get_and_merge_cmdsets(caller, session, player, obj, callertype, raw_string): Notes: The cdmsets are merged in order or generality, so that the Object's cmdset is merged last (and will thus take precedence - over same-named and same-prio commands on Player and Session). + over same-named and same-prio commands on Account and Session). """ try: @inlineCallbacks - def _get_channel_cmdset(player_or_obj): + def _get_channel_cmdset(account_or_obj): """ Helper-method; Get channel-cmdsets """ - # Create cmdset for all player's available channels + # Create cmdset for all account's available channels try: - channel_cmdset = yield CHANNELHANDLER.get_cmdset(player_or_obj) + channel_cmdset = yield CHANNELHANDLER.get_cmdset(account_or_obj) returnValue([channel_cmdset]) except Exception: _msg_err(caller, _ERROR_CMDSETS) @@ -313,16 +317,16 @@ def get_and_merge_cmdsets(caller, session, player, obj, callertype, raw_string): _GA(lobj, "at_cmdset_get")(caller=caller) 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 call-type lock is checked here, it makes sure an account + # is not seeing e.g. the commands on a fellow account (which is why # the no_superuser_bypass must be True) local_obj_cmdsets = \ yield list(chain.from_iterable( - lobj.cmdset.cmdset_stack for lobj in local_objlist - if (lobj.cmdset.current and + lobj.cmdset.cmdset_stack for lobj in local_objlist + if (lobj.cmdset.current and lobj.access(caller, access_type='call', no_superuser_bypass=True)))) for cset in local_obj_cmdsets: - #This is necessary for object sets, or we won't be able to + # This is necessary for object sets, or we won't be able to # separate the command sets from each other in a busy room. We # only keep the setting if duplicates were set to False/True # explicitly. @@ -333,7 +337,6 @@ def get_and_merge_cmdsets(caller, session, player, obj, callertype, raw_string): _msg_err(caller, _ERROR_CMDSETS) raise ErrorReported(raw_string) - @inlineCallbacks def _get_cmdsets(obj): """ @@ -346,7 +349,7 @@ def get_and_merge_cmdsets(caller, session, player, obj, callertype, raw_string): _msg_err(caller, _ERROR_CMDSETS) raise ErrorReported(raw_string) try: - returnValue((obj.cmdset.current, list(obj.cmdset.cmdset_stack))) + returnValue((obj.cmdset.current, list(obj.cmdset.cmdset_stack))) except AttributeError: returnValue(((None, None, None), [])) @@ -355,9 +358,9 @@ def get_and_merge_cmdsets(caller, session, player, obj, callertype, raw_string): # we are calling the command from the session level report_to = session current, cmdsets = yield _get_cmdsets(session) - if player: # this automatically implies logged-in - pcurrent, player_cmdsets = yield _get_cmdsets(player) - cmdsets += player_cmdsets + if account: # this automatically implies logged-in + pcurrent, account_cmdsets = yield _get_cmdsets(account) + cmdsets += account_cmdsets current = current + pcurrent if obj: ocurrent, obj_cmdsets = yield _get_cmdsets(obj) @@ -374,13 +377,13 @@ def get_and_merge_cmdsets(caller, session, player, obj, callertype, raw_string): channel_cmdsets = yield _get_channel_cmdset(obj) cmdsets += channel_cmdsets if not current.no_channels: - channel_cmdsets = yield _get_channel_cmdset(player) + channel_cmdsets = yield _get_channel_cmdset(account) cmdsets += channel_cmdsets - elif callertype == "player": - # we are calling the command from the player level - report_to = player - current, cmdsets = yield _get_cmdsets(player) + elif callertype == "account": + # we are calling the command from the account level + report_to = account + current, cmdsets = yield _get_cmdsets(account) if obj: ocurrent, obj_cmdsets = yield _get_cmdsets(obj) current = current + ocurrent @@ -395,7 +398,7 @@ def get_and_merge_cmdsets(caller, session, player, obj, callertype, raw_string): # also objs may have channels cmdsets += yield _get_channel_cmdset(obj) if not current.no_channels: - cmdsets += yield _get_channel_cmdset(player) + cmdsets += yield _get_channel_cmdset(account) elif callertype == "object": # we are calling the command from the object level @@ -472,22 +475,22 @@ def cmdhandler(called_by, raw_string, _testing=False, callertype="session", sess This is the main mechanism that handles any string sent to the engine. Args: - called_by (Session, Player or Object): Object from which this + called_by (Session, Account or Object): Object from which this command was called. which this was called from. What this is depends on the game state. raw_string (str): The command string as given on the command line. _testing (bool, optional): Used for debug purposes and decides if we should actually execute the command or not. If True, the command instance will be returned. - callertype (str, optional): One of "session", "player" or + callertype (str, optional): One of "session", "account" or "object". These are treated in decending order, so when the Session is the caller, it will merge its own cmdset into - cmdsets from both Player and eventual puppeted Object (and - cmdsets in its room etc). A Player will only include its own + cmdsets from both Account and eventual puppeted Object (and + cmdsets in its room etc). An Account will only include its own cmdset and the Objects and so on. Merge order is the same order, so that Object cmdsets are merged in last, giving them precendence for same-name and same-prio commands. - session (Session, optional): Relevant if callertype is "player" - the session will help + session (Session, optional): Relevant if callertype is "account" - the session will help retrieve the correct cmdsets from puppeted objects. cmdobj (Command, optional): If given a command instance, this will be executed using `called_by` as the caller, `raw_string` representing its arguments and (optionally) @@ -513,20 +516,22 @@ def cmdhandler(called_by, raw_string, _testing=False, callertype="session", sess """ @inlineCallbacks - def _run_command(cmd, cmdname, args, raw_string, cmdset, session, player): + def _run_command(cmd, cmdname, args, raw_cmdname, cmdset, session, account): """ Helper function: This initializes and runs the Command instance once the parser has identified it as either a normal command or one of the system commands. Args: - cmd (Command): Command object. - cmdname (str): Name of command. - args (str): Extra text entered after the identified command. - raw_string (str): Full input string. + cmd (Command): Command object + cmdname (str): Name of command + args (str): extra text entered after the identified command + raw_cmdname (str): Name of Command, unaffected by eventual + prefix-stripping (if no prefix-stripping, this is the same + as cmdname). cmdset (CmdSet): Command sert the command belongs to (if any).. session (Session): Session of caller (if any). - player (Player): Player of caller (if any). + account (Account): Account of caller (if any). Returns: deferred (Deferred): this will fire with the return of the @@ -540,15 +545,17 @@ def cmdhandler(called_by, raw_string, _testing=False, callertype="session", sess try: # Assign useful variables to the instance cmd.caller = caller - cmd.cmdstring = cmdname + cmd.cmdname = cmdname + cmd.raw_cmdname = raw_cmdname + cmd.cmdstring = cmdname # deprecated cmd.args = args cmd.cmdset = cmdset cmd.session = session - cmd.player = player - cmd.raw_string = 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 + cmd.account = account + cmd.raw_string = unformatted_raw_string + # cmd.obj # set via on-object cmdset handler for each command, + # since this may be different for every command when + # merging multuple cmdsets if hasattr(cmd, 'obj') and hasattr(cmd.obj, 'scripts'): # cmd.obj is automatically made available by the cmdhandler. @@ -566,7 +573,7 @@ def cmdhandler(called_by, raw_string, _testing=False, callertype="session", sess _COMMAND_NESTING[called_by] += 1 if _COMMAND_NESTING[called_by] > _COMMAND_RECURSION_LIMIT: err = _ERROR_RECURSION_LIMIT.format(recursion_limit=_COMMAND_RECURSION_LIMIT, - raw_string=raw_string, + raw_cmdname=raw_cmdname, cmdclass=cmd.__class__) raise RuntimeError(err) @@ -611,16 +618,15 @@ def cmdhandler(called_by, raw_string, _testing=False, callertype="session", sess finally: _COMMAND_NESTING[called_by] -= 1 - raw_string = to_unicode(raw_string, force_string=True) - session, player, obj = session, None, None + session, account, obj = session, None, None if callertype == "session": session = called_by - player = session.player + account = session.account obj = session.puppet - elif callertype == "player": - player = called_by + elif callertype == "account": + account = called_by if session: obj = yield session.puppet elif callertype == "object": @@ -629,32 +635,32 @@ def cmdhandler(called_by, raw_string, _testing=False, callertype="session", sess 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 + caller = obj or account or session + # The error_to is the default recipient for errors. Tries to make sure an account # does not get spammed for errors while preserving character mirroring. - error_to = obj or session or player + error_to = obj or session or account try: # catch bugs in cmdhandler itself try: # catch special-type commands if cmdobj: # the command object is already given - cmd = cmdobj() if callable(cmdobj) else cmdobj cmdname = cmdobj_key if cmdobj_key else cmd.key args = raw_string unformatted_raw_string = "%s%s" % (cmdname, args) cmdset = None - session = session - player = player + # session = session + # account = account else: # no explicit cmdobject given, figure it out - - cmdset = yield get_and_merge_cmdsets(caller, session, player, obj, - callertype, raw_string) + cmdset = yield get_and_merge_cmdsets(caller, session, account, obj, + callertype, raw_string) if not cmdset: # this is bad and shouldn't happen. raise NoCmdSets + # store the completely unmodified raw string - including + # whitespace and eventual prefixes-to-be-stripped. unformatted_raw_string = raw_string raw_string = raw_string.strip() if not raw_string: @@ -681,11 +687,11 @@ def cmdhandler(called_by, raw_string, _testing=False, callertype="session", sess sysarg = yield _SEARCH_AT_RESULT([match[2] for match in matches], caller, query=matches[0][0]) raise ExecSystemCommand(syscmd, sysarg) - cmdname, args, cmd = "", "", None + cmdname, args, cmd, raw_cmdname = "", "", None, "" if len(matches) == 1: # We have a unique command match. But it may still be invalid. match = matches[0] - cmdname, args, cmd = match[0], match[1], match[2] + cmdname, args, cmd, raw_cmdname = match[0], match[1], match[2], match[5] if not matches: # No commands match our entered command @@ -697,8 +703,8 @@ def cmdhandler(called_by, raw_string, _testing=False, callertype="session", sess # fallback to default error text sysarg = _("Command '%s' is not available.") % raw_string suggestions = string_suggestions(raw_string, - cmdset.get_all_cmd_keys_and_aliases(caller), - cutoff=0.7, maxnum=3) + cmdset.get_all_cmd_keys_and_aliases(caller), + cutoff=0.7, maxnum=3) if suggestions: sysarg += _(" Maybe you meant %s?") % utils.list_to_string(suggestions, _('or'), addquote=True) else: @@ -718,7 +724,7 @@ def cmdhandler(called_by, raw_string, _testing=False, callertype="session", sess raise ExecSystemCommand(cmd, sysarg) # A normal command. - ret = yield _run_command(cmd, cmdname, args, unformatted_raw_string, cmdset, session, player) + ret = yield _run_command(cmd, cmdname, args, raw_cmdname, cmdset, session, account) returnValue(ret) except ErrorReported as exc: @@ -734,7 +740,7 @@ def cmdhandler(called_by, raw_string, _testing=False, callertype="session", sess if syscmd: ret = yield _run_command(syscmd, syscmd.key, sysarg, - unformatted_raw_string, cmdset, session, player) + unformatted_raw_string, cmdset, session, account) returnValue(ret) elif sysarg: # return system arg diff --git a/evennia/commands/cmdparser.py b/evennia/commands/cmdparser.py index db210b6013..e104233e49 100644 --- a/evennia/commands/cmdparser.py +++ b/evennia/commands/cmdparser.py @@ -12,6 +12,8 @@ from django.conf import settings from evennia.utils.logger import log_trace _MULTIMATCH_REGEX = re.compile(settings.SEARCH_MULTIMATCH_REGEX, re.I + re.U) +_CMD_IGNORE_PREFIXES = settings.CMD_IGNORE_PREFIXES + def cmdparser(raw_string, cmdset, caller, match_index=None): """ @@ -21,7 +23,7 @@ def cmdparser(raw_string, cmdset, caller, match_index=None): Args: raw_string (str): The unparsed text entered by the caller. cmdset (CmdSet): The merged, currently valid cmdset - caller (Session, Player or Object): The caller triggering this parsing. + caller (Session, Account or Object): The caller triggering this parsing. match_index (int, optional): Index to pick a given match in a list of same-named command matches. If this is given, it suggests this is not the first time this function was called: normally @@ -46,7 +48,7 @@ def cmdparser(raw_string, cmdset, caller, match_index=None): """ - def create_match(cmdname, string, cmdobj): + def create_match(cmdname, string, cmdobj, raw_cmdname): """ Builds a command match by splitting the incoming string and evaluating the quality of the match. @@ -55,56 +57,84 @@ def cmdparser(raw_string, cmdset, caller, match_index=None): cmdname (str): Name of command to check for. string (str): The string to match against. cmdobj (str): The full Command instance. + raw_cmdname (str, optional): If CMD_IGNORE_PREFIX is set and the cmdname starts with + one of the prefixes to ignore, this contains the raw, unstripped cmdname, + otherwise it is None. Returns: - match (tuple): This is on the form (cmdname, args, cmdobj, cmdlen, mratio), where + match (tuple): This is on the form (cmdname, args, cmdobj, cmdlen, mratio, raw_cmdname), where `cmdname` is the command's name and `args` the rest of the incoming string, without said command name. `cmdobj` is the Command instance, the cmdlen is the same as len(cmdname) and mratio is a measure of how big a part of the - full input string the cmdname takes up - an exact match would be 1.0. + full input string the cmdname takes up - an exact match would be 1.0. Finally, + the `raw_cmdname` is the cmdname unmodified by eventual prefix-stripping. """ cmdlen, strlen = len(unicode(cmdname)), len(unicode(string)) mratio = 1 - (strlen - cmdlen) / (1.0 * strlen) args = string[cmdlen:] - return (cmdname, args, cmdobj, cmdlen, mratio) + return (cmdname, args, cmdobj, cmdlen, mratio, raw_cmdname) + + def build_matches(raw_string, include_prefixes=False): + l_raw_string = raw_string.lower() + matches = [] + try: + if include_prefixes: + # use the cmdname as-is + for cmd in cmdset: + matches.extend([create_match(cmdname, raw_string, cmd, cmdname) + for cmdname in [cmd.key] + cmd.aliases + if cmdname and l_raw_string.startswith(cmdname.lower()) and + (not cmd.arg_regex or + cmd.arg_regex.match(l_raw_string[len(cmdname):]))]) + else: + # strip prefixes set in settings + for cmd in cmdset: + for raw_cmdname in [cmd.key] + cmd.aliases: + cmdname = raw_cmdname.lstrip(_CMD_IGNORE_PREFIXES) if len(raw_cmdname) > 1 else raw_cmdname + if cmdname and l_raw_string.startswith(cmdname.lower()) and \ + (not cmd.arg_regex or cmd.arg_regex.match(l_raw_string[len(cmdname):])): + matches.append(create_match(cmdname, raw_string, cmd, raw_cmdname)) + except Exception: + log_trace("cmdhandler error. raw_input:%s" % raw_string) + return matches + + def try_num_prefixes(raw_string): + if not matches: + # no matches found + num_ref_match = _MULTIMATCH_REGEX.match(raw_string) + if num_ref_match: + # the user might be trying to identify the command + # with a #num-command style syntax. We expect the regex to + # contain the groups "number" and "name". + mindex, new_raw_string = num_ref_match.group("number"), num_ref_match.group("name") + return mindex, new_raw_string + return None, None if not raw_string: return [] - matches = [] - - # match everything that begins with a matching cmdname. - l_raw_string = raw_string.lower() - for cmd in cmdset: - try: - matches.extend([create_match(cmdname, raw_string, cmd) - for cmdname in [cmd.key] + cmd.aliases - if cmdname and l_raw_string.startswith(cmdname.lower()) - and (not cmd.arg_regex or - cmd.arg_regex.match(l_raw_string[len(cmdname):]))]) - except Exception: - log_trace("cmdhandler error. raw_input:%s" % raw_string) - + # find mathces, first using the full name + matches = build_matches(raw_string, include_prefixes=True) if not matches: - # no matches found - num_ref_match = _MULTIMATCH_REGEX.match(raw_string) - if num_ref_match: - # the user might be trying to identify the command - # with a #num-command style syntax. We expect the regex to - # contain the groups "number" and "name". - mindex, new_raw_string = num_ref_match.group("number"), num_ref_match.group("name") - return cmdparser(new_raw_string, cmdset, - caller, match_index=int(mindex)) + # try to match a number 1-cmdname, 2-cmdname etc + mindex, new_raw_string = try_num_prefixes(raw_string) + if mindex is not None: + return cmdparser(new_raw_string, cmdset, caller, match_index=int(mindex)) + if _CMD_IGNORE_PREFIXES: + # still no match. Try to strip prefixes + raw_string = raw_string.lstrip(_CMD_IGNORE_PREFIXES) if len(raw_string) > 1 else raw_string + matches = build_matches(raw_string, include_prefixes=False) # only select command matches we are actually allowed to call. matches = [match for match in matches if match[2].access(caller, 'cmd')] + # try to bring the number of matches down to 1 if len(matches) > 1: # See if it helps to analyze the match with preserved case but only if # it leaves at least one match. trimmed = [match for match in matches - if raw_string.startswith(match[0])] + if raw_string.startswith(match[0])] if trimmed: matches = trimmed @@ -122,11 +152,10 @@ def cmdparser(raw_string, cmdset, caller, match_index=None): quality = [mat[4] for mat in matches] matches = matches[-quality.count(quality[-1]):] - if len(matches) > 1 and match_index != None and 0 < match_index <= len(matches): + if len(matches) > 1 and match_index is not None and 0 < match_index <= len(matches): # We couldn't separate match by quality, but we have an # index argument to tell us which match to use. - matches = [matches[match_index-1]] + matches = [matches[match_index - 1]] # no matter what we have at this point, we have to return it. return matches - diff --git a/evennia/commands/cmdset.py b/evennia/commands/cmdset.py index fa86734242..c363f87f80 100644 --- a/evennia/commands/cmdset.py +++ b/evennia/commands/cmdset.py @@ -4,7 +4,7 @@ A Command Set (CmdSet) holds a set of commands. The Cmdsets can be merged and combined to create new sets of commands in a non-destructive way. This makes them very powerful for implementing custom game states where different commands (or different variations -of commands) are available to the players depending on circumstance. +of commands) are available to the accounts depending on circumstance. The available merge operations are partly borrowed from mathematical Set theory. @@ -51,7 +51,7 @@ class _CmdSetMeta(type): cls.key = cls.__name__ cls.path = "%s.%s" % (cls.__module__, cls.__name__) - if not type(cls.key_mergetypes) == dict: + if not isinstance(cls.key_mergetypes, dict): cls.key_mergetypes = {} super(_CmdSetMeta, cls).__init__(*args, **kwargs) @@ -110,9 +110,9 @@ class CmdSet(with_metaclass(_CmdSetMeta, object)): merger (i.e. A above) automatically taking precedence. But if allow_duplicates is true, the result will be a merger with more than one of each - name match. This will usually lead to the player + name match. This will usually lead to the account receiving a multiple-match error higher up the road, - but can be good for things like cmdsets on non-player + but can be good for things like cmdsets on non-account objects in a room, to allow the system to warn that more than one 'ball' in the room has the same 'kick' command defined on it, so it may offer a chance to @@ -134,7 +134,7 @@ class CmdSet(with_metaclass(_CmdSetMeta, object)): commands no_channels - ignore the name of channels when matching against commands (WARNING- this is dangerous since the - player can then not even ask staff for help if + account can then not even ask staff for help if something goes wrong) @@ -167,9 +167,9 @@ class CmdSet(with_metaclass(_CmdSetMeta, object)): Creates a new CmdSet instance. Args: - cmdsetobj (Session, Player, Object, optional): This is the database object + cmdsetobj (Session, Account, Object, optional): This is the database object to which this particular instance of cmdset is related. It - is often a character but may also be a regular object, Player + is often a character but may also be a regular object, Account or Session. key (str, optional): The idenfier for this cmdset. This helps if wanting to selectively remov cmdsets. @@ -188,7 +188,7 @@ class CmdSet(with_metaclass(_CmdSetMeta, object)): # initialize system self.at_cmdset_creation() - self._contains_cache = WeakKeyDictionary()#{} + self._contains_cache = WeakKeyDictionary() # {} # Priority-sensitive merge operations for cmdsets @@ -214,7 +214,7 @@ class CmdSet(with_metaclass(_CmdSetMeta, object)): cmdset_c.commands.extend(cmdset_b.commands) else: cmdset_c.commands.extend([cmd for cmd in cmdset_b - if not cmd in cmdset_a]) + if cmd not in cmdset_a]) return cmdset_c def _intersect(self, cmdset_a, cmdset_b): @@ -280,7 +280,7 @@ class CmdSet(with_metaclass(_CmdSetMeta, object)): """ cmdset_c = cmdset_a._duplicate() - cmdset_c.commands = [cmd for cmd in cmdset_b if not cmd in cmdset_a] + cmdset_c.commands = [cmd for cmd in cmdset_b if cmd not in cmdset_a] return cmdset_c def _instantiate(self, cmd): @@ -411,7 +411,7 @@ class CmdSet(with_metaclass(_CmdSetMeta, object)): cmdset_c = self._replace(self, cmdset_a) elif mergetype == "Remove": cmdset_c = self._remove(self, cmdset_a) - else: # Union + else: # Union cmdset_c = self._union(self, cmdset_a) # pass through options whenever they are set, unless the higher-prio @@ -426,7 +426,7 @@ class CmdSet(with_metaclass(_CmdSetMeta, object)): # This is used for diagnosis. cmdset_c.actual_mergetype = mergetype - #print "__add__ for %s (prio %i) called with %s (prio %i)." % (self.key, self.priority, cmdset_a.key, cmdset_a.priority) + # print "__add__ for %s (prio %i) called with %s (prio %i)." % (self.key, self.priority, cmdset_a.key, cmdset_a.priority) # return the system commands to the cmdset cmdset_c.add(sys_commands) @@ -604,7 +604,7 @@ class CmdSet(with_metaclass(_CmdSetMeta, object)): names = [] if caller: [names.extend(cmd._keyaliases) for cmd in self.commands - if cmd.access(caller)] + if cmd.access(caller)] else: [names.extend(cmd._keyaliases) for cmd in self.commands] return names diff --git a/evennia/commands/cmdsethandler.py b/evennia/commands/cmdsethandler.py index 3f01dbcac2..84eea1fdc5 100644 --- a/evennia/commands/cmdsethandler.py +++ b/evennia/commands/cmdsethandler.py @@ -9,8 +9,8 @@ intelligent container that, when added to other CmdSet make sure that same-name commands are treated correctly (usually so there are no doublets). This temporary but up-to-date merger of CmdSet is jointly called the Current Cmset. It is this Current CmdSet that the -commandhandler looks through whenever a player enters a command (it -also adds CmdSets from objects in the room in real-time). All player +commandhandler looks through whenever an account enters a command (it +also adds CmdSets from objects in the room in real-time). All account objects have a 'default cmdset' containing all the normal in-game mud commands (look etc). @@ -19,12 +19,12 @@ So what is all this cmdset complexity good for? In its simplest form, a CmdSet has no commands, only a key name. In this case the cmdset's use is up to each individual game - it can be used by an AI module for example (mobs in cmdset 'roam' move from room -to room, in cmdset 'attack' they enter combat with players). +to room, in cmdset 'attack' they enter combat with accounts). Defining commands in cmdsets offer some further powerful game-design consequences however. Here are some examples: -As mentioned above, all players always have at least the Default +As mentioned above, all accounts always have at least the Default CmdSet. This contains the set of all normal-use commands in-game, stuff like look and @desc etc. Now assume our players end up in a dark room. You don't want the player to be able to do much in that dark @@ -37,7 +37,7 @@ and have this completely replace the default cmdset. Another example: Say you want your players to be able to go fishing. You could implement this as a 'fish' command that fails -whenever the player has no fishing rod. Easy enough. But what if you +whenever the account has no fishing rod. Easy enough. But what if you want to make fishing more complex - maybe you want four-five different commands for throwing your line, reeling in, etc? Most players won't (we assume) have fishing gear, and having all those detailed commands @@ -48,7 +48,7 @@ for a minor thing like fishing? So instead you put all those detailed fishing commands into their own CommandSet called 'Fishing'. Whenever the player gives the command 'fish' (presumably the code checks there is also water nearby), only -THEN this CommandSet is added to the Cmdhandler of the player. The +THEN this CommandSet is added to the Cmdhandler of the account. The 'throw' command (which normally throws rocks) is replaced by the custom 'fishing variant' of throw. What has happened is that the Fishing CommandSet was merged on top of the Default ones, and due to @@ -80,28 +80,40 @@ __all__ = ("import_cmdset", "CmdSetHandler") _CACHED_CMDSETS = {} _CMDSET_PATHS = utils.make_iter(settings.CMDSET_PATHS) _IN_GAME_ERRORS = settings.IN_GAME_ERRORS +_CMDSET_FALLBACKS = settings.CMDSET_FALLBACKS + # Output strings _ERROR_CMDSET_IMPORT = _( -"""{traceback} + """{traceback} Error loading cmdset '{path}' (Traceback was logged {timestamp})""") _ERROR_CMDSET_KEYERROR = _( -"""Error loading cmdset: No cmdset class '{classname}' in '{path}'. + """Error loading cmdset: No cmdset class '{classname}' in '{path}'. (Traceback was logged {timestamp})""") _ERROR_CMDSET_SYNTAXERROR = _( -"""{traceback} + """{traceback} SyntaxError encountered when loading cmdset '{path}'. (Traceback was logged {timestamp})""") _ERROR_CMDSET_EXCEPTION = _( -"""{traceback} + """{traceback} Compile/Run error when loading cmdset '{path}'.", (Traceback was logged {timestamp})""") +_ERROR_CMDSET_FALLBACK = _( + """ +Error encountered for cmdset at path '{path}'. +Replacing with fallback '{fallback_path}'. +""") + +_ERROR_CMDSET_NO_FALLBACK = _( + """Fallback path '{fallback_path}' failed to generate a cmdset.""" +) + class _ErrorCmdSet(CmdSet): """ @@ -110,6 +122,7 @@ class _ErrorCmdSet(CmdSet): key = "_CMDSET_ERROR" errmessage = "Error when loading cmdset." + class _EmptyCmdSet(CmdSet): """ This cmdset represents an empty cmdset @@ -118,6 +131,7 @@ class _EmptyCmdSet(CmdSet): priority = -101 mergetype = "Union" + def import_cmdset(path, cmdsetobj, emit_to_obj=None, no_logging=False): """ This helper function is used by the cmdsethandler to load a cmdset @@ -128,7 +142,7 @@ def import_cmdset(path, cmdsetobj, emit_to_obj=None, no_logging=False): Args: path (str): The path to the command set to load. cmdsetobj (CmdSet): The database object/typeclass on which this cmdset is to be - assigned (this can be also channels and exits, as well as players + assigned (this can be also channels and exits, as well as accounts but there will always be such an object) emit_to_obj (Object, optional): If given, error is emitted to this object (in addition to logging) @@ -142,11 +156,11 @@ def import_cmdset(path, cmdsetobj, emit_to_obj=None, no_logging=False): """ python_paths = [path] + ["%s.%s" % (prefix, path) - for prefix in _CMDSET_PATHS if not path.startswith(prefix)] + for prefix in _CMDSET_PATHS if not path.startswith(prefix)] errstring = "" for python_path in python_paths: - if "." in path: + if "." in path: modpath, classname = python_path.rsplit(".", 1) else: raise ImportError("The path '%s' is not on the form modulepath.ClassName" % path) @@ -179,7 +193,7 @@ def import_cmdset(path, cmdsetobj, emit_to_obj=None, no_logging=False): continue _CACHED_CMDSETS[python_path] = cmdsetclass - #instantiate the cmdset (and catch its errors) + # instantiate the cmdset (and catch its errors) if callable(cmdsetclass): cmdsetclass = cmdsetclass(cmdsetobj) return cmdsetclass @@ -223,7 +237,7 @@ def import_cmdset(path, cmdsetobj, emit_to_obj=None, no_logging=False): err_cmdset = _ErrorCmdSet() err_cmdset.errmessage = errstring return err_cmdset - return None # undefined error + return None # undefined error # classes @@ -266,7 +280,7 @@ class CmdSetHandler(object): self.permanent_paths = [""] if init_true: - self.update(init_mode=True) #is then called from the object __init__. + self.update(init_mode=True) # is then called from the object __init__. def __str__(self): """ @@ -299,8 +313,8 @@ class CmdSetHandler(object): if mergelist: tmpstring = _(" : {current}") string += tmpstring.format(mergelist="+".join(mergelist), - mergetype=mergetype, prio=self.current.priority, - current=self.current) + mergetype=mergetype, prio=self.current.priority, + current=self.current) else: permstring = "non-perm" if self.current.permanent: @@ -310,7 +324,7 @@ class CmdSetHandler(object): prio=self.current.priority, permstring=permstring, keylist=", ".join(cmd.key for - cmd in sorted(self.current, key=lambda o: o.key))) + cmd in sorted(self.current, key=lambda o: o.key))) return string.strip() def _import_cmdset(self, cmdset_path, emit_to_obj=None): @@ -351,6 +365,22 @@ class CmdSetHandler(object): elif path: cmdset = self._import_cmdset(path) if cmdset: + if cmdset.key == '_CMDSET_ERROR': + # If a cmdset fails to load, check if we have a fallback path to use + fallback_path = _CMDSET_FALLBACKS.get(path, None) + if fallback_path: + err = _ERROR_CMDSET_FALLBACK.format(path=path, fallback_path=fallback_path) + logger.log_err(err) + if _IN_GAME_ERRORS: + self.obj.msg(err) + cmdset = self._import_cmdset(fallback_path) + # If no cmdset is returned from the fallback, we can't go further + if not cmdset: + err = _ERROR_CMDSET_NO_FALLBACK.format(fallback_path=fallback_path) + logger.log_err(err) + if _IN_GAME_ERRORS: + self.obj.msg(err) + continue cmdset.permanent = cmdset.key != '_CMDSET_ERROR' self.cmdset_stack.append(cmdset) @@ -514,10 +544,9 @@ class CmdSetHandler(object): # legacy alias delete_default = remove_default - - def all(self): + def get(self): """ - Show all cmdsets. + Get all cmdsets. Returns: cmdsets (list): All the command sets currently in the handler. @@ -525,6 +554,9 @@ class CmdSetHandler(object): """ return self.cmdset_stack + # backwards-compatible alias + all = get + def clear(self): """ Removes all Command Sets from the handler except the default one @@ -560,16 +592,16 @@ class CmdSetHandler(object): else: print [cset.path for cset in self.cmdset_stack], cmdset.path return any([cset for cset in self.cmdset_stack - if cset.path == cmdset.path]) + if cset.path == cmdset.path]) else: # try it as a path or key if must_be_default: return self.cmdset_stack and ( - self.cmdset_stack[0].key == cmdset or - self.cmdset_stack[0].path == cmdset) + self.cmdset_stack[0].key == cmdset or + self.cmdset_stack[0].path == cmdset) else: return any([cset for cset in self.cmdset_stack - if cset.path == cmdset or cset.key == cmdset]) + if cset.path == cmdset or cset.key == cmdset]) # backwards-compatability alias has_cmdset = has diff --git a/evennia/commands/command.py b/evennia/commands/command.py index c5d1515d83..48a4b132da 100644 --- a/evennia/commands/command.py +++ b/evennia/commands/command.py @@ -60,7 +60,7 @@ def _init_command(cls, **kwargs): if "cmd:" not in cls.locks: cls.locks = "cmd:all();" + cls.locks for lockstring in cls.locks.split(';'): - if lockstring and not ':' in lockstring: + if lockstring and ':' not in lockstring: lockstring = "cmd:%s" % lockstring temp.append(lockstring) cls.lock_storage = ";".join(temp) @@ -152,14 +152,14 @@ class Command(with_metaclass(CommandMeta, object)): is_exit = False # define the command not only by key but by the regex form of its arguments arg_regex = settings.COMMAND_DEFAULT_ARG_REGEX - # whether self.msg sends to all sessions of a related player/object (default + # whether self.msg sends to all sessions of a related account/object (default # is to only send to the session sending the command). msg_all_sessions = settings.COMMAND_DEFAULT_MSG_ALL_SESSIONS # auto-set (by Evennia on command instantiation) are: # obj - which object this command is defined on # session - which session is responsible for triggering this command. Only set - # if triggered by a player. + # if triggered by an account. def __init__(self, **kwargs): """ @@ -307,7 +307,7 @@ class Command(with_metaclass(CommandMeta, object)): session=None, **kwargs): """ This is a shortcut instead of calling msg() directly on an - object - it will detect if caller is an Object or a Player and + object - it will detect if caller is an Object or an Account and also appends self.session automatically if self.msg_all_sessions is False. Args: @@ -340,7 +340,7 @@ class Command(with_metaclass(CommandMeta, object)): Args: raw_string (str): Execute this string as a command input. session (Session, optional): If not given, the current command's Session will be used. - obj (Object or Player, optional): Object or Player on which to call the execute_cmd. + obj (Object or Account, optional): Object or Account on which to call the execute_cmd. If not given, self.caller will be used. Kwargs: @@ -443,7 +443,7 @@ class Command(with_metaclass(CommandMeta, object)): commands the caller can use. Args: - caller (Object or Player): the caller asking for help on the command. + caller (Object or Account): the caller asking for help on the command. cmdset (CmdSet): the command set (if you need additional commands). Returns: diff --git a/evennia/commands/default/player.py b/evennia/commands/default/account.py similarity index 73% rename from evennia/commands/default/player.py rename to evennia/commands/default/account.py index 296618334b..20cc374542 100644 --- a/evennia/commands/default/player.py +++ b/evennia/commands/default/account.py @@ -1,19 +1,19 @@ """ -Player (OOC) commands. These are stored on the Player object -and self.caller is thus always a Player, not an Object/Character. +Account (OOC) commands. These are stored on the Account object +and self.caller is thus always an Account, not an Object/Character. -These commands go in the PlayerCmdset and are accessible also +These commands go in the AccountCmdset and are accessible also when puppeting a Character (although with lower priority) -These commands use the player_caller property which tells the command +These commands use the account_caller property which tells the command parent (MuxCommand, usually) to setup caller correctly. They use -self.player to make sure to always use the player object rather than +self.account to make sure to always use the account object rather than self.caller (which change depending on the level you are calling from) The property self.character can be used to access the character when these commands are triggered with a connected character (such as the case of the @ooc command), it is None if we are OOC. -Note that under MULTISESSION_MODE > 2, Player commands should use +Note that under MULTISESSION_MODE > 2, Account commands should use self.msg() and similar methods to reroute returns to the correct method. Otherwise all text will be returned to all connected sessions. @@ -36,7 +36,7 @@ __all__ = ("CmdOOCLook", "CmdIC", "CmdOOC", "CmdPassword", "CmdQuit", "CmdColorTest", "CmdQuell") -class MuxPlayerLookCommand(COMMAND_DEFAULT_CLASS): +class MuxAccountLookCommand(COMMAND_DEFAULT_CLASS): """ Custom parent (only) parsing for OOC looking, sets a "playable" property on the command based on the parsing. @@ -46,19 +46,19 @@ class MuxPlayerLookCommand(COMMAND_DEFAULT_CLASS): def parse(self): """Custom parsing""" - super(MuxPlayerLookCommand, self).parse() + super(MuxAccountLookCommand, self).parse() if _MULTISESSION_MODE < 2: # only one character allowed - not used in this mode self.playable = None return - playable = self.player.db._playable_characters + playable = self.account.db._playable_characters if playable is not None: # clean up list if character object was deleted in between if None in playable: playable = [character for character in playable if character] - self.player.db._playable_characters = playable + self.account.db._playable_characters = playable # store playable property if self.args: self.playable = dict((utils.to_str(char.key.lower()), char) @@ -67,13 +67,13 @@ class MuxPlayerLookCommand(COMMAND_DEFAULT_CLASS): self.playable = playable -# Obs - these are all intended to be stored on the Player, and as such, -# use self.player instead of self.caller, just to be sure. Also self.msg() +# Obs - these are all intended to be stored on the Account, and as such, +# use self.account instead of self.caller, just to be sure. Also self.msg() # is used to make sure returns go to the right session -# note that this is inheriting from MuxPlayerLookCommand, +# note that this is inheriting from MuxAccountLookCommand, # and has the .playable property. -class CmdOOCLook(MuxPlayerLookCommand): +class CmdOOCLook(MuxAccountLookCommand): """ look while out-of-character @@ -84,7 +84,7 @@ class CmdOOCLook(MuxPlayerLookCommand): """ # This is an OOC version of the look command. Since a - # Player doesn't have an in-game existence, there is no + # Account doesn't have an in-game existence, there is no # concept of location or "self". If we are controlling # a character, pass control over to normal look. @@ -94,7 +94,7 @@ class CmdOOCLook(MuxPlayerLookCommand): help_category = "General" # this is used by the parent - player_caller = True + account_caller = True def func(self): """implement the ooc look command""" @@ -104,8 +104,8 @@ class CmdOOCLook(MuxPlayerLookCommand): self.msg("You are out-of-character (OOC).\nUse |w@ic|n to get back into the game.") return - # call on-player look helper method - self.msg(self.player.at_look(target=self.playable, session=self.session)) + # call on-account look helper method + self.msg(self.account.at_look(target=self.playable, session=self.session)) class CmdCharCreate(COMMAND_DEFAULT_CLASS): @@ -121,15 +121,15 @@ class CmdCharCreate(COMMAND_DEFAULT_CLASS): if you want. """ key = "@charcreate" - locks = "cmd:pperm(Players)" + locks = "cmd:pperm(Player)" help_category = "General" # this is used by the parent - player_caller = True + account_caller = True def func(self): """create the new character""" - player = self.player + account = self.account if not self.args: self.msg("Usage: @charcreate [= description]") return @@ -138,9 +138,9 @@ class CmdCharCreate(COMMAND_DEFAULT_CLASS): charmax = _MAX_NR_CHARACTERS if _MULTISESSION_MODE > 1 else 1 - if not player.is_superuser and \ - (player.db._playable_characters and - len(player.db._playable_characters) >= charmax): + if not account.is_superuser and \ + (account.db._playable_characters and + len(account.db._playable_characters) >= charmax): self.msg("You may only create a maximum of %i characters." % charmax) return from evennia.objects.models import ObjectDB @@ -156,19 +156,19 @@ class CmdCharCreate(COMMAND_DEFAULT_CLASS): # create the character start_location = ObjectDB.objects.get_id(settings.START_LOCATION) default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME) - permissions = settings.PERMISSION_PLAYER_DEFAULT + permissions = settings.PERMISSION_ACCOUNT_DEFAULT new_character = create.create_object(typeclass, key=key, location=start_location, home=default_home, permissions=permissions) - # only allow creator (and immortals) to puppet this char - new_character.locks.add("puppet:id(%i) or pid(%i) or perm(Immortals) or pperm(Immortals)" % - (new_character.id, player.id)) - player.db._playable_characters.append(new_character) + # only allow creator (and developers) to puppet this char + new_character.locks.add("puppet:id(%i) or pid(%i) or perm(Developer) or pperm(Developer)" % + (new_character.id, account.id)) + account.db._playable_characters.append(new_character) if desc: new_character.db.desc = desc elif not new_character.db.desc: - new_character.db.desc = "This is a Player." + new_character.db.desc = "This is a character." self.msg("Created new character %s. Use |w@ic %s|n to enter the game as this character." % (new_character.key, new_character.key)) @@ -183,19 +183,19 @@ class CmdCharDelete(COMMAND_DEFAULT_CLASS): Permanently deletes one of your characters. """ key = "@chardelete" - locks = "cmd:pperm(Players)" + locks = "cmd:pperm(Player)" help_category = "General" def func(self): """delete the character""" - player = self.player + account = self.account if not self.args: self.msg("Usage: @chardelete ") return # use the playable_characters list to search - match = [char for char in utils.make_iter(player.db._playable_characters) + match = [char for char in utils.make_iter(account.db._playable_characters) if char.key.lower() == self.args.lower()] if not match: self.msg("You have no such character to delete.") @@ -219,9 +219,9 @@ class CmdCharDelete(COMMAND_DEFAULT_CLASS): del caller.ndb._char_to_delete match = match[0] - player.ndb._char_to_delete = match + account.ndb._char_to_delete = match prompt = "|rThis will permanently destroy '%s'. This cannot be undone.|n Continue yes/[no]?" - get_input(player, prompt % match.key, _callback) + get_input(account, prompt % match.key, _callback) class CmdIC(COMMAND_DEFAULT_CLASS): @@ -234,12 +234,12 @@ class CmdIC(COMMAND_DEFAULT_CLASS): Go in-character (IC) as a given Character. This will attempt to "become" a different object assuming you have - the right to do so. Note that it's the PLAYER character that puppets + the right to do so. Note that it's the ACCOUNT character that puppets characters/objects and which needs to have the correct permission! You cannot become an object that is already controlled by another - player. In principle can be any in-game object as long - as you the player have access right to puppet it. + account. In principle can be any in-game object as long + as you the account have access right to puppet it. """ key = "@ic" @@ -249,24 +249,24 @@ class CmdIC(COMMAND_DEFAULT_CLASS): help_category = "General" # this is used by the parent - player_caller = True + account_caller = True def func(self): """ Main puppet method """ - player = self.player + account = self.account session = self.session new_character = None if not self.args: - new_character = player.db._last_puppet + new_character = account.db._last_puppet if not new_character: self.msg("Usage: @ic ") return if not new_character: # search for a matching character - new_character = [char for char in search.object_search(self.args) if char.access(player, "puppet")] + new_character = [char for char in search.object_search(self.args) if char.access(account, "puppet")] if not new_character: self.msg("That is not a valid character choice.") return @@ -277,15 +277,15 @@ class CmdIC(COMMAND_DEFAULT_CLASS): else: new_character = new_character[0] try: - player.puppet_object(session, new_character) - player.db._last_puppet = new_character + account.puppet_object(session, new_character) + account.db._last_puppet = new_character except RuntimeError as exc: self.msg("|rYou cannot become |C%s|n: %s" % (new_character.name, exc)) -# note that this is inheriting from MuxPlayerLookCommand, +# note that this is inheriting from MuxAccountLookCommand, # and as such has the .playable property. -class CmdOOC(MuxPlayerLookCommand): +class CmdOOC(MuxAccountLookCommand): """ stop puppeting and go ooc @@ -298,30 +298,30 @@ class CmdOOC(MuxPlayerLookCommand): """ key = "@ooc" - locks = "cmd:pperm(Players)" + locks = "cmd:pperm(Player)" aliases = "@unpuppet" help_category = "General" # this is used by the parent - player_caller = True + account_caller = True def func(self): """Implement function""" - player = self.player + account = self.account session = self.session - old_char = player.get_puppet(session) + old_char = account.get_puppet(session) if not old_char: string = "You are already OOC." self.msg(string) return - player.db._last_puppet = old_char + account.db._last_puppet = old_char # disconnect try: - player.unpuppet_object(session) + account.unpuppet_object(session) self.msg("\n|GYou go OOC.|n\n") if _MULTISESSION_MODE < 2: @@ -329,7 +329,7 @@ class CmdOOC(MuxPlayerLookCommand): self.msg("You are out-of-character (OOC).\nUse |w@ic|n to get back into the game.") return - self.msg(player.at_look(target=self.playable, session=session)) + self.msg(account.at_look(target=self.playable, session=session)) except RuntimeError as exc: self.msg("|rCould not unpuppet from |c%s|n: %s" % (old_char, exc)) @@ -350,21 +350,21 @@ class CmdSessions(COMMAND_DEFAULT_CLASS): help_category = "General" # this is used by the parent - player_caller = True + account_caller = True def func(self): """Implement function""" - player = self.player - sessions = player.sessions.all() + account = self.account + sessions = account.sessions.all() table = evtable.EvTable("|wsessid", "|wprotocol", "|whost", "|wpuppet/character", "|wlocation") for sess in sorted(sessions, key=lambda x: x.sessid): - char = player.get_puppet(sess) + char = account.get_puppet(sess) table.add_row(str(sess.sessid), str(sess.protocol_key), - type(sess.address) == tuple and sess.address[0] or sess.address, + isinstance(sess.address, tuple) and sess.address[0] or sess.address, char and str(char) or "None", char and str(char.location) or "N/A") self.msg("|wYour current session(s):|n\n%s" % table) @@ -387,27 +387,27 @@ class CmdWho(COMMAND_DEFAULT_CLASS): locks = "cmd:all()" # this is used by the parent - player_caller = True + account_caller = True def func(self): """ - Get all connected players by polling session. + Get all connected accounts by polling session. """ - player = self.player + account = self.account session_list = SESSIONS.get_sessions() - session_list = sorted(session_list, key=lambda o: o.player.key) + session_list = sorted(session_list, key=lambda o: o.account.key) if self.cmdstring == "doing": show_session_data = False else: - show_session_data = player.check_permstring("Immortals") or player.check_permstring("Wizards") + show_session_data = account.check_permstring("Developer") or account.check_permstring("Admins") - nplayers = (SESSIONS.player_count()) + naccounts = (SESSIONS.account_count()) if show_session_data: # privileged info - table = evtable.EvTable("|wPlayer Name", + table = evtable.EvTable("|wAccount Name", "|wOn for", "|wIdle", "|wPuppeting", @@ -420,10 +420,10 @@ class CmdWho(COMMAND_DEFAULT_CLASS): continue delta_cmd = time.time() - session.cmd_last_visible delta_conn = time.time() - session.conn_time - player = session.get_player() + account = session.get_account() puppet = session.get_puppet() location = puppet.location.key if puppet and puppet.location else "None" - table.add_row(utils.crop(player.name, width=25), + table.add_row(utils.crop(account.name, width=25), utils.time_format(delta_conn, 0), utils.time_format(delta_cmd, 1), utils.crop(puppet.key if puppet else "None", width=25), @@ -433,19 +433,19 @@ class CmdWho(COMMAND_DEFAULT_CLASS): isinstance(session.address, tuple) and session.address[0] or session.address) else: # unprivileged - table = evtable.EvTable("|wPlayer name", "|wOn for", "|wIdle") + table = evtable.EvTable("|wAccount name", "|wOn for", "|wIdle") for session in session_list: if not session.logged_in: continue delta_cmd = time.time() - session.cmd_last_visible delta_conn = time.time() - session.conn_time - player = session.get_player() - table.add_row(utils.crop(player.key, width=25), + account = session.get_account() + table.add_row(utils.crop(account.key, width=25), utils.time_format(delta_conn, 0), utils.time_format(delta_cmd, 1)) - is_one = nplayers == 1 - self.msg("|wPlayers:|n\n%s\n%s unique account%s logged in." - % (table, "One" if is_one else nplayers, "" if is_one else "s")) + is_one = naccounts == 1 + self.msg("|wAccounts:|n\n%s\n%s unique account%s logged in." + % (table, "One" if is_one else naccounts, "" if is_one else "s")) class CmdOption(COMMAND_DEFAULT_CLASS): @@ -470,7 +470,7 @@ class CmdOption(COMMAND_DEFAULT_CLASS): locks = "cmd:all()" # this is used by the parent - player_caller = True + account_caller = True def func(self): """ @@ -508,7 +508,7 @@ class CmdOption(COMMAND_DEFAULT_CLASS): options["SCREENHEIGHT"] = options["SCREENHEIGHT"][0] else: options["SCREENHEIGHT"] = " \n".join("%s : %s" % (screenid, size) - for screenid, size in options["SCREENHEIGHT"].iteritems()) + for screenid, size in options["SCREENHEIGHT"].iteritems()) options.pop("TTYPE", None) header = ("Name", "Value", "Saved") if saved_options else ("Name", "Value") @@ -549,10 +549,13 @@ class CmdOption(COMMAND_DEFAULT_CLASS): try: old_val = flags.get(new_name, False) new_val = validator(new_val) - flags[new_name] = new_val - self.msg("Option |w%s|n was changed from '|w%s|n' to '|w%s|n'." % (new_name, old_val, new_val)) + if old_val == new_val: + self.msg("Option |w%s|n was kept as '|w%s|n'." % (new_name, old_val)) + else: + flags[new_name] = new_val + self.msg("Option |w%s|n was changed from '|w%s|n' to '|w%s|n'." % (new_name, old_val, new_val)) return {new_name: new_val} - except Exception, err: + except Exception as err: self.msg("|rCould not set option |w%s|r:|n %s" % (new_name, err)) return False @@ -572,28 +575,29 @@ class CmdOption(COMMAND_DEFAULT_CLASS): "TERM": utils.to_str, "UTF-8": validate_bool, "XTERM256": validate_bool, - "INPUTDEBUG": validate_bool} + "INPUTDEBUG": validate_bool, + "FORCEDENDLINE": validate_bool} name = self.lhs.upper() val = self.rhs.strip() optiondict = False if val and name in validators: - optiondict = update(name, val, validators[name]) + optiondict = update(name, val, validators[name]) else: self.msg("|rNo option named '|w%s|r'." % name) if optiondict: # a valid setting if "save" in self.switches: # save this option only - saved_options = self.player.attributes.get("_saved_protocol_flags", default={}) + saved_options = self.account.attributes.get("_saved_protocol_flags", default={}) saved_options.update(optiondict) - self.player.attributes.add("_saved_protocol_flags", saved_options) + self.account.attributes.add("_saved_protocol_flags", saved_options) for key in optiondict: self.msg("|gSaved option %s.|n" % key) if "clear" in self.switches: # clear this save for key in optiondict: - self.player.attributes.get("_saved_protocol_flags", {}).pop(key, None) + self.account.attributes.get("_saved_protocol_flags", {}).pop(key, None) self.msg("|gCleared saved %s." % key) self.session.update_flags(**optiondict) @@ -608,27 +612,27 @@ class CmdPassword(COMMAND_DEFAULT_CLASS): Changes your password. Make sure to pick a safe one. """ key = "@password" - locks = "cmd:pperm(Players)" + locks = "cmd:pperm(Player)" # this is used by the parent - player_caller = True + account_caller = True def func(self): """hook function.""" - player = self.player + account = self.account if not self.rhs: self.msg("Usage: @password = ") return oldpass = self.lhslist[0] # Both of these are newpass = self.rhslist[0] # already stripped by parse() - if not player.check_password(oldpass): + if not account.check_password(oldpass): self.msg("The specified old password isn't correct.") elif len(newpass) < 3: self.msg("Passwords must be at least three characters long.") else: - player.set_password(newpass) - player.save() + account.set_password(newpass) + account.save() self.msg("Password changed.") @@ -646,30 +650,31 @@ class CmdQuit(COMMAND_DEFAULT_CLASS): game. Use the /all switch to disconnect from all sessions. """ key = "@quit" - aliases = "quit" locks = "cmd:all()" # this is used by the parent - player_caller = True + account_caller = True def func(self): """hook function""" - player = self.player + account = self.account if 'all' in self.switches: - player.msg("|RQuitting|n all sessions. Hope to see you soon again.", session=self.session) - for session in player.sessions.all(): - player.disconnect_session_from_player(session) + account.msg("|RQuitting|n all sessions. Hope to see you soon again.", session=self.session) + reason = "quit/all" + for session in account.sessions.all(): + account.disconnect_session_from_account(session, reason) else: - nsess = len(player.sessions.all()) + nsess = len(account.sessions.all()) + reason = "quit" if nsess == 2: - player.msg("|RQuitting|n. One session is still connected.", session=self.session) + account.msg("|RQuitting|n. One session is still connected.", session=self.session) elif nsess > 2: - player.msg("|RQuitting|n. %i sessions are still connected." % (nsess-1), session=self.session) + account.msg("|RQuitting|n. %i sessions are still connected." % (nsess - 1), session=self.session) else: # we are quitting the last available session - player.msg("|RQuitting|n. Hope to see you again, soon.", session=self.session) - player.disconnect_session_from_player(self.session) + account.msg("|RQuitting|n. Hope to see you again, soon.", session=self.session) + account.disconnect_session_from_account(self.session, reason) class CmdColorTest(COMMAND_DEFAULT_CLASS): @@ -686,12 +691,19 @@ class CmdColorTest(COMMAND_DEFAULT_CLASS): color - if not you will see rubbish appear. """ key = "@color" - aliases = "color" locks = "cmd:all()" help_category = "General" # this is used by the parent - player_caller = True + account_caller = True + + # the slices of the ANSI_PARSER lists to use for retrieving the + # relevant color tags to display. Replace if using another schema. + # This command can only show one set of markup. + slice_bright_fg = slice(7, 15) # from ANSI_PARSER.ansi_map + slice_dark_fg = slice(15, 23) # from ANSI_PARSER.ansi_map + slice_dark_bg = slice(-8, None) # from ANSI_PARSER.ansi_map + slice_bright_bg = slice(None, None) # from ANSI_PARSER.ansi_xterm256_bright_bg_map def table_format(self, table): """ @@ -718,14 +730,16 @@ class CmdColorTest(COMMAND_DEFAULT_CLASS): ap = ansi.ANSI_PARSER # ansi colors # show all ansi color-related codes - col1 = ["%s%s|n" % (code, code.replace("|", "||")) for code, _ in ap.ext_ansi_map[48:56]] - col2 = ["%s%s|n" % (code, code.replace("|", "||")) for code, _ in ap.ext_ansi_map[56:64]] - col3 = ["%s%s|n" % (code.replace("\\", ""), code.replace("|", "||").replace("\\", "")) - for code, _ in ap.ext_ansi_map[-8:]] - col4 = ["%s%s|n" % (code.replace("\\", ""), code.replace("|", "||").replace("\\", "")) - for code, _ in ap.ansi_bright_bgs[-8:]] - col2.extend(["" for _ in range(len(col1)-len(col2))]) - table = utils.format_table([col1, col2, col4, col3]) + bright_fg = ["%s%s|n" % (code, code.replace("|", "||")) + for code, _ in ap.ansi_map[self.slice_bright_fg]] + dark_fg = ["%s%s|n" % (code, code.replace("|", "||")) + for code, _ in ap.ansi_map[self.slice_dark_fg]] + dark_bg = ["%s%s|n" % (code.replace("\\", ""), code.replace("|", "||").replace("\\", "")) + for code, _ in ap.ansi_map[self.slice_dark_bg]] + bright_bg = ["%s%s|n" % (code.replace("\\", ""), code.replace("|", "||").replace("\\", "")) + for code, _ in ap.ansi_xterm256_bright_bg_map[self.slice_bright_bg]] + dark_fg.extend(["" for _ in range(len(bright_fg) - len(dark_fg))]) + table = utils.format_table([bright_fg, dark_fg, bright_bg, dark_bg]) string = "ANSI colors:" for row in table: string += "\n " + " ".join(row) @@ -743,16 +757,16 @@ class CmdColorTest(COMMAND_DEFAULT_CLASS): # foreground table table[ir].append("|%i%i%i%s|n" % (ir, ig, ib, "||%i%i%i" % (ir, ig, ib))) # background table - table[6+ir].append("|%i%i%i|[%i%i%i%s|n" - % (5 - ir, 5 - ig, 5 - ib, ir, ig, ib, "||[%i%i%i" % (ir, ig, ib))) + table[6 + ir].append("|%i%i%i|[%i%i%i%s|n" + % (5 - ir, 5 - ig, 5 - ib, ir, ig, ib, "||[%i%i%i" % (ir, ig, ib))) table = self.table_format(table) string = "Xterm256 colors (if not all hues show, your client might not report that it can handle xterm256):" string += "\n" + "\n".join("".join(row) for row in table) table = [[], [], [], [], [], [], [], [], [], [], [], []] for ibatch in range(4): for igray in range(6): - letter = chr(97 + (ibatch*6 + igray)) - inverse = chr(122 - (ibatch*6 + igray)) + letter = chr(97 + (ibatch * 6 + igray)) + inverse = chr(122 - (ibatch * 6 + igray)) table[0 + igray].append("|=%s%s |n" % (letter, "||=%s" % letter)) table[6 + igray].append("|=%s|[=%s%s |n" % (inverse, letter, "||[=%s" % letter)) for igray in range(6): @@ -776,30 +790,30 @@ class CmdColorTest(COMMAND_DEFAULT_CLASS): class CmdQuell(COMMAND_DEFAULT_CLASS): """ - use character's permissions instead of player's + use character's permissions instead of account's Usage: quell unquell - Normally the permission level of the Player is used when puppeting a + Normally the permission level of the Account is used when puppeting a Character/Object to determine access. This command will switch the lock system to make use of the puppeted Object's permissions instead. This is useful mainly for testing. - Hierarchical permission quelling only work downwards, thus a Player cannot + Hierarchical permission quelling only work downwards, thus an Account cannot use a higher-permission Character to escalate their permission level. Use the unquell command to revert back to normal operation. """ key = "@quell" aliases = ["@unquell"] - locks = "cmd:pperm(Players)" + locks = "cmd:pperm(Player)" help_category = "General" # this is used by the parent - player_caller = True + account_caller = True - def _recache_locks(self, player): + def _recache_locks(self, account): """Helper method to reset the lockhandler on an already puppeted object""" if self.session: char = self.session.puppet @@ -808,31 +822,31 @@ class CmdQuell(COMMAND_DEFAULT_CLASS): # the lock caches (otherwise the superuser status change # won't be visible until repuppet) char.locks.reset() - player.locks.reset() + account.locks.reset() def func(self): """Perform the command""" - player = self.player - permstr = player.is_superuser and " (superuser)" or "(%s)" % (", ".join(player.permissions.all())) - if self.cmdstring == '@unquell': - if not player.attributes.get('_quell'): - self.msg("Already using normal Player permissions %s." % permstr) + account = self.account + permstr = account.is_superuser and " (superuser)" or "(%s)" % (", ".join(account.permissions.all())) + if self.cmdstring in ('unquell', '@unquell'): + if not account.attributes.get('_quell'): + self.msg("Already using normal Account permissions %s." % permstr) else: - player.attributes.remove('_quell') - self.msg("Player permissions %s restored." % permstr) + account.attributes.remove('_quell') + self.msg("Account permissions %s restored." % permstr) else: - if player.attributes.get('_quell'): - self.msg("Already quelling Player %s permissions." % permstr) + if account.attributes.get('_quell'): + self.msg("Already quelling Account %s permissions." % permstr) return - player.attributes.add('_quell', True) + account.attributes.add('_quell', True) puppet = self.session.puppet if puppet: cpermstr = "(%s)" % ", ".join(puppet.permissions.all()) cpermstr = "Quelling to current puppet's permissions %s." % cpermstr - cpermstr += "\n(Note: If this is higher than Player permissions %s," \ + cpermstr += "\n(Note: If this is higher than Account permissions %s," \ " the lowest of the two will be used.)" % permstr cpermstr += "\nUse @unquell to return to normal permission usage." self.msg(cpermstr) else: - self.msg("Quelling Player permissions%s. Use @unquell to get them back." % permstr) - self._recache_locks(player) + self.msg("Quelling Account permissions%s. Use @unquell to get them back." % permstr) + self._recache_locks(account) diff --git a/evennia/commands/default/admin.py b/evennia/commands/default/admin.py index 28e7f6b011..8b694ffd8f 100644 --- a/evennia/commands/default/admin.py +++ b/evennia/commands/default/admin.py @@ -16,27 +16,27 @@ COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) PERMISSION_HIERARCHY = [p.lower() for p in settings.PERMISSION_HIERARCHY] # limit members for API inclusion -__all__ = ("CmdBoot", "CmdBan", "CmdUnban", "CmdDelPlayer", +__all__ = ("CmdBoot", "CmdBan", "CmdUnban", "CmdDelAccount", "CmdEmit", "CmdNewPassword", "CmdPerm", "CmdWall") class CmdBoot(COMMAND_DEFAULT_CLASS): """ - kick a player from the server. + kick an account from the server. Usage - @boot[/switches] [: reason] + @boot[/switches] [: reason] Switches: - quiet - Silently boot without informing player + quiet - Silently boot without informing account sid - boot by session id instead of name or dbref - Boot a player object from the server. If a reason is + Boot an account object from the server. If a reason is supplied it will be echoed to the user unless /quiet is set. """ key = "@boot" - locks = "cmd:perm(boot) or perm(Wizards)" + locks = "cmd:perm(boot) or perm(Admin)" help_category = "Admin" def func(self): @@ -45,7 +45,7 @@ class CmdBoot(COMMAND_DEFAULT_CLASS): args = self.args if not args: - caller.msg("Usage: @boot[/switches] [:reason]") + caller.msg("Usage: @boot[/switches] [:reason]") return if ':' in args: @@ -64,10 +64,10 @@ class CmdBoot(COMMAND_DEFAULT_CLASS): boot_list.append(sess) break else: - # Boot by player object - pobj = search.player_search(args) + # Boot by account object + pobj = search.account_search(args) if not pobj: - caller.msg("Player %s was not found." % args) + caller.msg("Account %s was not found." % args) return pobj = pobj[0] if not pobj.access(caller, 'boot'): @@ -75,12 +75,12 @@ class CmdBoot(COMMAND_DEFAULT_CLASS): caller.msg(string) return # we have a bootable object with a connected user - matches = SESSIONS.sessions_from_player(pobj) + matches = SESSIONS.sessions_from_account(pobj) for match in matches: boot_list.append(match) if not boot_list: - caller.msg("No matching sessions found. The Player does not seem to be online.") + caller.msg("No matching sessions found. The Account does not seem to be online.") return # Carry out the booting of the sessions in the boot list. @@ -93,7 +93,7 @@ class CmdBoot(COMMAND_DEFAULT_CLASS): for session in boot_list: session.msg(feedback) - session.player.disconnect_session_from_player(session) + session.account.disconnect_session_from_account(session) # regex matching IP addresses with wildcards, eg. 233.122.4.* @@ -118,7 +118,7 @@ def list_bans(banlist): class CmdBan(COMMAND_DEFAULT_CLASS): """ - ban a player from the server + ban an account from the server Usage: @ban [ [: reason]] @@ -128,8 +128,8 @@ class CmdBan(COMMAND_DEFAULT_CLASS): This command bans a user from accessing the game. Supply an optional reason to be able to later remember why the ban was put in place. - It is often preferable to ban a player from the server than to - delete a player with @delplayer. If banned by name, that player + It is often preferable to ban an account from the server than to + delete an account with @delaccount. If banned by name, that account account can no longer be logged into. IP (Internet Protocol) address banning allows blocking all access @@ -151,7 +151,7 @@ class CmdBan(COMMAND_DEFAULT_CLASS): """ key = "@ban" aliases = ["@bans"] - locks = "cmd:perm(ban) or perm(Immortals)" + locks = "cmd:perm(ban) or perm(Developer)" help_category = "Admin" def func(self): @@ -171,9 +171,9 @@ class CmdBan(COMMAND_DEFAULT_CLASS): if not banlist: banlist = [] - if not self.args or (self.switches - and not any(switch in ('ip', 'name') - for switch in self.switches)): + if not self.args or (self.switches and + not any(switch in ('ip', 'name') + for switch in self.switches)): self.caller.msg(list_bans(banlist)) return @@ -206,19 +206,19 @@ class CmdBan(COMMAND_DEFAULT_CLASS): class CmdUnban(COMMAND_DEFAULT_CLASS): """ - remove a ban from a player + remove a ban from an account Usage: @unban - This will clear a player name/ip ban previously set with the @ban + This will clear an account name/ip ban previously set with the @ban command. Use this command without an argument to view a numbered list of bans. Use the numbers in this list to select which one to unban. """ key = "@unban" - locks = "cmd:perm(unban) or perm(Immortals)" + locks = "cmd:perm(unban) or perm(Developer)" help_category = "Admin" def func(self): @@ -249,23 +249,23 @@ class CmdUnban(COMMAND_DEFAULT_CLASS): (num, " ".join([s for s in ban[:2]]))) -class CmdDelPlayer(COMMAND_DEFAULT_CLASS): +class CmdDelAccount(COMMAND_DEFAULT_CLASS): """ - delete a player from the server + delete an account from the server Usage: - @delplayer[/switch] [: reason] + @delaccount[/switch] [: reason] Switch: - delobj - also delete the player's currently + delobj - also delete the account's currently assigned in-game object. Completely deletes a user from the server database, making their nick and e-mail again available. """ - key = "@delplayer" - locks = "cmd:perm(delplayer) or perm(Immortals)" + key = "@delaccount" + locks = "cmd:perm(delaccount) or perm(Developer)" help_category = "Admin" def func(self): @@ -274,49 +274,49 @@ class CmdDelPlayer(COMMAND_DEFAULT_CLASS): caller = self.caller args = self.args - if hasattr(caller, 'player'): - caller = caller.player + if hasattr(caller, 'account'): + caller = caller.account if not args: - self.msg("Usage: @delplayer [: reason]") + self.msg("Usage: @delaccount [: reason]") return reason = "" if ':' in args: args, reason = [arg.strip() for arg in args.split(':', 1)] - # We use player_search since we want to be sure to find also players + # We use account_search since we want to be sure to find also accounts # that lack characters. - players = search.player_search(args) + accounts = search.account_search(args) - if not players: - self.msg('Could not find a player by that name.') + if not accounts: + self.msg('Could not find an account by that name.') return - if len(players) > 1: + if len(accounts) > 1: string = "There were multiple matches:\n" - string += "\n".join(" %s %s" % (player.id, player.key) for player in players) + string += "\n".join(" %s %s" % (account.id, account.key) for account in accounts) self.msg(string) return # one single match - player = players.pop() + account = accounts.first() - if not player.access(caller, 'delete'): - string = "You don't have the permissions to delete that player." + if not account.access(caller, 'delete'): + string = "You don't have the permissions to delete that account." self.msg(string) return - uname = player.username - # boot the player then delete - self.msg("Informing and disconnecting player ...") + uname = account.username + # boot the account then delete + self.msg("Informing and disconnecting account ...") string = "\nYour account '%s' is being *permanently* deleted.\n" % uname if reason: string += " Reason given:\n '%s'" % reason - player.msg(string) - player.delete() - self.msg("Player %s was successfully deleted." % uname) + account.msg(string) + account.delete() + self.msg("Account %s was successfully deleted." % uname) class CmdEmit(COMMAND_DEFAULT_CLASS): @@ -330,18 +330,18 @@ class CmdEmit(COMMAND_DEFAULT_CLASS): Switches: room : limit emits to rooms only (default) - players : limit emits to players only + accounts : limit emits to accounts only contents : send to the contents of matched objects too Emits a message to the selected objects or to your immediate surroundings. If the object is a room, send to its contents. @remit and @pemit are just limited forms of @emit, for sending to rooms and - to players respectively. + to accounts respectively. """ key = "@emit" aliases = ["@pemit", "@remit"] - locks = "cmd:perm(emit) or perm(Builders)" + locks = "cmd:perm(emit) or perm(Builder)" help_category = "Admin" def func(self): @@ -359,7 +359,7 @@ class CmdEmit(COMMAND_DEFAULT_CLASS): return rooms_only = 'rooms' in self.switches - players_only = 'players' in self.switches + accounts_only = 'accounts' in self.switches send_to_contents = 'contents' in self.switches # we check which command was used to force the switches @@ -367,7 +367,7 @@ class CmdEmit(COMMAND_DEFAULT_CLASS): rooms_only = True send_to_contents = True elif self.cmdstring == '@pemit': - players_only = True + accounts_only = True if not self.rhs: message = self.args @@ -384,8 +384,8 @@ class CmdEmit(COMMAND_DEFAULT_CLASS): if rooms_only and obj.location is not None: caller.msg("%s is not a room. Ignored." % objname) continue - if players_only and not obj.has_player: - caller.msg("%s has no active player. Ignored." % objname) + if accounts_only and not obj.has_account: + caller.msg("%s has no active account. Ignored." % objname) continue if obj.access(caller, 'tell'): obj.msg(message) @@ -400,16 +400,16 @@ class CmdEmit(COMMAND_DEFAULT_CLASS): class CmdNewPassword(COMMAND_DEFAULT_CLASS): """ - change the password of a player + change the password of an account Usage: @userpassword = - Set a player's password. + Set an account's password. """ key = "@userpassword" - locks = "cmd:perm(newpassword) or perm(Wizards)" + locks = "cmd:perm(newpassword) or perm(Admin)" help_category = "Admin" def func(self): @@ -421,36 +421,36 @@ class CmdNewPassword(COMMAND_DEFAULT_CLASS): self.msg("Usage: @userpassword = ") return - # the player search also matches 'me' etc. - player = caller.search_player(self.lhs) - if not player: + # the account search also matches 'me' etc. + account = caller.search_account(self.lhs) + if not account: return - player.set_password(self.rhs) - player.save() - self.msg("%s - new password set to '%s'." % (player.name, self.rhs)) - if player.character != caller: - player.msg("%s has changed your password to '%s'." % (caller.name, - self.rhs)) + account.set_password(self.rhs) + account.save() + self.msg("%s - new password set to '%s'." % (account.name, self.rhs)) + if account.character != caller: + account.msg("%s has changed your password to '%s'." % (caller.name, + self.rhs)) class CmdPerm(COMMAND_DEFAULT_CLASS): """ - set the permissions of a player/object + set the permissions of an account/object Usage: @perm[/switch] [= [,,...]] - @perm[/switch] * [= [,,...]] + @perm[/switch] * [= [,,...]] Switches: - del : delete the given permission from or . - player : set permission on a player (same as adding * to name) + del : delete the given permission from or . + account : set permission on an account (same as adding * to name) This command sets/clears individual permission strings on an object - or player. If no permission is given, list all permissions on . + or account. If no permission is given, list all permissions on . """ key = "@perm" aliases = "@setperm" - locks = "cmd:perm(perm) or perm(Immortals)" + locks = "cmd:perm(perm) or perm(Developer)" help_category = "Admin" def func(self): @@ -465,11 +465,11 @@ class CmdPerm(COMMAND_DEFAULT_CLASS): caller.msg(string) return - playermode = 'player' in self.switches or lhs.startswith('*') + accountmode = 'account' in self.switches or lhs.startswith('*') lhs = lhs.lstrip("*") - if playermode: - obj = caller.search_player(lhs) + if accountmode: + obj = caller.search_account(lhs) else: obj = caller.search(lhs, global_search=True) if not obj: @@ -485,19 +485,19 @@ class CmdPerm(COMMAND_DEFAULT_CLASS): string += "" else: string += ", ".join(obj.permissions.all()) - if (hasattr(obj, 'player') and - hasattr(obj.player, 'is_superuser') and - obj.player.is_superuser): + if (hasattr(obj, 'account') and + hasattr(obj.account, 'is_superuser') and + obj.account.is_superuser): string += "\n(... but this object is currently controlled by a SUPERUSER! " string += "All access checks are passed automatically.)" caller.msg(string) return # we supplied an argument on the form obj = perm - locktype = "edit" if playermode else "control" + locktype = "edit" if accountmode else "control" if not obj.access(caller, locktype): caller.msg("You are not allowed to edit this %s's permissions." - % ("player" if playermode else "object")) + % ("account" if accountmode else "object")) return caller_result = [] @@ -525,13 +525,13 @@ class CmdPerm(COMMAND_DEFAULT_CLASS): return if perm in permissions: - caller_result.append("\nPermission '%s' is already defined on %s." % (rhs, obj.name)) + caller_result.append("\nPermission '%s' is already defined on %s." % (perm, obj.name)) else: obj.permissions.add(perm) - plystring = "the Player" if playermode else "the Object/Character" - caller_result.append("\nPermission '%s' given to %s (%s)." % (rhs, obj.name, plystring)) + plystring = "the Account" if accountmode else "the Object/Character" + caller_result.append("\nPermission '%s' given to %s (%s)." % (perm, obj.name, plystring)) target_result.append("\n%s gives you (%s, %s) the permission '%s'." - % (caller.name, obj.name, plystring, rhs)) + % (caller.name, obj.name, plystring, perm)) caller.msg("".join(caller_result).strip()) if target_result: obj.msg("".join(target_result).strip()) @@ -544,10 +544,11 @@ class CmdWall(COMMAND_DEFAULT_CLASS): Usage: @wall - Announces a message to all connected players. + Announces a message to all connected sessions + including all currently unlogged in. """ key = "@wall" - locks = "cmd:perm(wall) or perm(Wizards)" + locks = "cmd:perm(wall) or perm(Admin)" help_category = "Admin" def func(self): @@ -556,5 +557,5 @@ class CmdWall(COMMAND_DEFAULT_CLASS): self.caller.msg("Usage: @wall ") return message = "%s shouts \"%s\"" % (self.caller.name, self.args) - self.msg("Announcing to all connected players ...") + self.msg("Announcing to all connected sessions ...") SESSIONS.announce_all(message) diff --git a/evennia/commands/default/batchprocess.py b/evennia/commands/default/batchprocess.py index 6fbcfc2ec8..f0b117a816 100644 --- a/evennia/commands/default/batchprocess.py +++ b/evennia/commands/default/batchprocess.py @@ -237,7 +237,7 @@ class CmdBatchCommands(_COMMAND_DEFAULT_CLASS): """ key = "@batchcommands" aliases = ["@batchcommand", "@batchcmd"] - locks = "cmd:perm(batchcommands) or superuser()" + locks = "cmd:perm(batchcommands) or perm(Developer)" help_category = "Building" def func(self): diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 80b7e46358..445ec082a9 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -41,6 +41,7 @@ _DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH _PROTOTYPE_PARENTS = None + class ObjManipCommand(COMMAND_DEFAULT_CLASS): """ This is a parent class for some of the defining objmanip commands @@ -73,7 +74,7 @@ class ObjManipCommand(COMMAND_DEFAULT_CLASS): super(ObjManipCommand, self).parse() obj_defs = ([], []) # stores left- and right-hand side of '=' - obj_attrs = ([], []) # " + obj_attrs = ([], []) # " for iside, arglist in enumerate((self.lhslist, self.rhslist)): # lhslist/rhslist is already split by ',' at this point @@ -88,8 +89,8 @@ class ObjManipCommand(COMMAND_DEFAULT_CLASS): objdef, attrs = [part.strip() for part in objdef.split('/', 1)] attrs = [part.strip().lower() for part in attrs.split('/') if part.strip()] # store data - obj_defs[iside].append({"name":objdef, 'option':option, 'aliases':aliases}) - obj_attrs[iside].append({"name":objdef, 'attrs':attrs}) + obj_defs[iside].append({"name": objdef, 'option': option, 'aliases': aliases}) + obj_attrs[iside].append({"name": objdef, 'attrs': attrs}) # store for future access self.lhs_objs = obj_defs[0] @@ -105,9 +106,15 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS): Usage: @alias [= [alias[,alias,alias,...]]] @alias = + @alias/category = [alias[,alias,...]: + + Switches: + category - requires ending input with :category, to store the + given aliases with the given category. Assigns aliases to an object so it can be referenced by more - than one name. Assign empty to remove all aliases from object. + than one name. Assign empty to remove all aliases from object. If + assigning a category, all aliases given will be using this category. Observe that this is not the same thing as personal aliases created with the 'nick' command! Aliases set with @alias are @@ -117,11 +124,11 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS): key = "@alias" aliases = "@setobjalias" - locks = "cmd:perm(setobjalias) or perm(Builders)" + locks = "cmd:perm(setobjalias) or perm(Builder)" help_category = "Building" def func(self): - "Set the aliases." + """Set the aliases.""" caller = self.caller @@ -137,9 +144,12 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS): return if self.rhs is None: # no =, so we just list aliases on object. - aliases = obj.aliases.all() + aliases = obj.aliases.all(return_key_and_category=True) if aliases: - caller.msg("Aliases for '%s': %s" % (obj.get_display_name(caller), ", ".join(aliases))) + caller.msg("Aliases for %s: %s" % ( + obj.get_display_name(caller), + ", ".join("'%s'%s" % (alias, "" if category is None else "[category:'%s']" % category) + for (alias, category) in aliases))) else: caller.msg("No aliases exist for '%s'." % obj.get_display_name(caller)) return @@ -158,17 +168,27 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS): caller.msg("No aliases to clear.") return + category = None + if "category" in self.switches: + if ":" in self.rhs: + rhs, category = self.rhs.rsplit(':', 1) + category = category.strip() + else: + caller.msg("If specifying the /category switch, the category must be given " + "as :category at the end.") + else: + rhs = self.rhs + # merge the old and new aliases (if any) - old_aliases = obj.aliases.all() - new_aliases = [alias.strip().lower() for alias in self.rhs.split(',') - if alias.strip()] + old_aliases = obj.aliases.get(category=category, return_list=True) + new_aliases = [alias.strip().lower() for alias in rhs.split(',') if alias.strip()] # make the aliases only appear once old_aliases.extend(new_aliases) aliases = list(set(old_aliases)) # save back to object. - obj.aliases.add(aliases) + obj.aliases.add(aliases, category=category) # we need to trigger this here, since this will force # (default) Exits to rebuild their Exit commands with the new @@ -176,7 +196,8 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS): obj.at_cmdset_get(force_init=True) # report all aliases on the object - caller.msg("Alias(es) for '%s' set to %s." % (obj.get_display_name(caller), str(obj.aliases))) + caller.msg("Alias(es) for '%s' set to '%s'%s." % (obj.get_display_name(caller), + str(obj.aliases), " (category: '%s')" % category if category else "")) class CmdCopy(ObjManipCommand): @@ -197,11 +218,11 @@ class CmdCopy(ObjManipCommand): """ key = "@copy" - locks = "cmd:perm(copy) or perm(Builders)" + locks = "cmd:perm(copy) or perm(Builder)" help_category = "Building" def func(self): - "Uses ObjManipCommand.parse()" + """Uses ObjManipCommand.parse()""" caller = self.caller args = self.args @@ -242,9 +263,9 @@ class CmdCopy(ObjManipCommand): return copiedobj = ObjectDB.objects.copy_object(from_obj, - new_key=to_obj_name, - new_location=to_obj_location, - new_aliases=to_obj_aliases) + new_key=to_obj_name, + new_location=to_obj_location, + new_aliases=to_obj_aliases) if copiedobj: string = "Copied %s to '%s' (aliases: %s)." % (from_obj_name, to_obj_name, to_obj_aliases) @@ -278,7 +299,7 @@ class CmdCpAttr(ObjManipCommand): If you don't supply a source object, yourself is used. """ key = "@cpattr" - locks = "cmd:perm(cpattr) or perm(Builders)" + locks = "cmd:perm(cpattr) or perm(Builder)" help_category = "Building" def check_from_attr(self, obj, attr, clear=False): @@ -308,8 +329,7 @@ class CmdCpAttr(ObjManipCommand): """ if not obj.attributes.has(attr): self.caller.msg( - "%s doesn't have an attribute %s." - % (obj.name, attr)) + "%s doesn't have an attribute %s." % (obj.name, attr)) return False return True @@ -350,7 +370,7 @@ class CmdCpAttr(ObjManipCommand): if not from_obj or not to_objs: caller.msg("You have to supply both source object and target(s).") return - #copy to all to_obj:ects + # copy to all to_obj:ects if "move" in self.switches: clear = True else: @@ -387,7 +407,7 @@ class CmdCpAttr(ObjManipCommand): 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_attr == to_attr)): from_obj.attributes.remove(from_attr) result.append("\nMoved %s.%s -> %s.%s. (value: %s)" % (from_obj.name, from_attr, @@ -420,7 +440,7 @@ class CmdMvAttr(ObjManipCommand): object. If you don't supply a source object, yourself is used. """ key = "@mvattr" - locks = "cmd:perm(mvattr) or perm(Builders)" + locks = "cmd:perm(mvattr) or perm(Builder)" help_category = "Building" def func(self): @@ -468,12 +488,12 @@ class CmdCreate(ObjManipCommand): """ key = "@create" - locks = "cmd:perm(create) or perm(Builders)" + locks = "cmd:perm(create) or perm(Builder)" help_category = "Building" # lockstring of newly created objects, for easy overloading. # Will be formatted with the {id} of the creating object. - new_obj_lockstring = "control:id({id}) or perm(Wizards);delete:id({id}) or perm(Wizards)" + new_obj_lockstring = "control:id({id}) or perm(Admin);delete:id({id}) or perm(Admin)" def func(self): """ @@ -522,6 +542,7 @@ class CmdCreate(ObjManipCommand): def _desc_load(caller): return caller.db.evmenu_target.db.desc or "" + def _desc_save(caller, buf): """ Save line buffer to the desc prop. This should @@ -531,13 +552,15 @@ def _desc_save(caller, buf): caller.msg("Saved.") return True + def _desc_quit(caller): caller.attributes.remove("evmenu_target") caller.msg("Exited editor.") + class CmdDesc(COMMAND_DEFAULT_CLASS): """ - describe an object + describe an object or the current room. Usage: @desc [ =] @@ -550,7 +573,7 @@ class CmdDesc(COMMAND_DEFAULT_CLASS): """ key = "@desc" aliases = "@describe" - locks = "cmd:perm(desc) or perm(Builders)" + locks = "cmd:perm(desc) or perm(Builder)" help_category = "Building" def edit_handler(self): @@ -567,11 +590,12 @@ class CmdDesc(COMMAND_DEFAULT_CLASS): self.caller.db.evmenu_target = obj # launch the editor - EvEditor(self.caller, loadfunc=_desc_load, savefunc=_desc_save, quitfunc=_desc_quit, key="desc", persistent=True) + EvEditor(self.caller, loadfunc=_desc_load, savefunc=_desc_save, + quitfunc=_desc_quit, key="desc", persistent=True) return def func(self): - "Define command" + """Define command""" caller = self.caller if not self.args and 'edit' not in self.switches: @@ -609,76 +633,118 @@ class CmdDestroy(COMMAND_DEFAULT_CLASS): switches: override - The @destroy command will usually avoid accidentally - destroying player objects. This switch overrides this safety. + destroying account objects. This switch overrides this safety. + force - destroy without confirmation. examples: @destroy house, roof, door, 44-78 @destroy 5-10, flower, 45 + @destroy/force north Destroys one or many objects. If dbrefs are used, a range to delete can be - given, e.g. 4-10. Also the end points will be deleted. + given, e.g. 4-10. Also the end points will be deleted. This command + displays a confirmation before destroying, to make sure of your choice. + You can specify the /force switch to bypass this confirmation. """ key = "@destroy" aliases = ["@delete", "@del"] - locks = "cmd:perm(destroy) or perm(Builders)" + locks = "cmd:perm(destroy) or perm(Builder)" help_category = "Building" + confirm = True # set to False to always bypass confirmation + default_confirm = 'yes' # what to assume if just pressing enter (yes/no) + def func(self): - "Implements the command." + """Implements the command.""" caller = self.caller + delete = True if not self.args or not self.lhslist: caller.msg("Usage: @destroy[/switches] [obj, obj2, obj3, [dbref-dbref],...]") - return "" + delete = False - def delobj(objname, byref=False): + def delobj(obj): # helper function for deleting a single object string = "" - obj = caller.search(objname) - if not obj: - self.caller.msg(" (Objects to destroy must either be local or specified with a unique #dbref.)") - return "" - objname = obj.name - if not (obj.access(caller, "control") or obj.access(caller, 'delete')): - return "\nYou don't have permission to delete %s." % objname - if obj.player and not 'override' in self.switches: - return "\nObject %s is controlled by an active player. Use /override to delete anyway." % objname - if obj.dbid == int(settings.DEFAULT_HOME.lstrip("#")): - return "\nYou are trying to delete |c%s|n, which is set as DEFAULT_HOME. " \ + if not obj.pk: + string = "\nObject %s was already deleted." % obj.db_key + else: + objname = obj.name + if not (obj.access(caller, "control") or obj.access(caller, 'delete')): + return "\nYou don't have permission to delete %s." % objname + if obj.account and 'override' not in self.switches: + return "\nObject %s is controlled by an active account. Use /override to delete anyway." % objname + if obj.dbid == int(settings.DEFAULT_HOME.lstrip("#")): + return "\nYou are trying to delete |c%s|n, which is set as DEFAULT_HOME. " \ "Re-point settings.DEFAULT_HOME to another " \ "object before continuing." % objname - had_exits = hasattr(obj, "exits") and obj.exits - had_objs = hasattr(obj, "contents") and any(obj for obj in obj.contents - if not (hasattr(obj, "exits") and obj not in obj.exits)) - # do the deletion - okay = obj.delete() - if not okay: - string += "\nERROR: %s not deleted, probably because delete() returned False." % objname - else: - string += "\n%s was destroyed." % objname - if had_exits: - string += " Exits to and from %s were destroyed as well." % objname - if had_objs: - string += " Objects inside %s were moved to their homes." % objname + had_exits = hasattr(obj, "exits") and obj.exits + had_objs = hasattr(obj, "contents") and any(obj for obj in obj.contents + if not (hasattr(obj, "exits") and obj not in obj.exits)) + # do the deletion + okay = obj.delete() + if not okay: + string += "\nERROR: %s not deleted, probably because delete() returned False." % objname + else: + string += "\n%s was destroyed." % objname + if had_exits: + string += " Exits to and from %s were destroyed as well." % objname + if had_objs: + string += " Objects inside %s were moved to their homes." % objname return string - result = [] + objs = [] for objname in self.lhslist: + if not delete: + continue + if '-' in objname: # might be a range of dbrefs dmin, dmax = [utils.dbref(part, reqhash=False) for part in objname.split('-', 1)] if dmin and dmax: for dbref in range(int(dmin), int(dmax + 1)): - result.append(delobj("#" + str(dbref), True)) + obj = caller.search("#" + str(dbref)) + if obj: + objs.append(obj) + continue else: - result.append(delobj(objname)) + obj = caller.search(objname) else: - result.append(delobj(objname, True)) - if result: - caller.msg("".join(result).strip()) + obj = caller.search(objname) + + if obj is None: + self.caller.msg(" (Objects to destroy must either be local or specified with a unique #dbref.)") + elif obj not in objs: + objs.append(obj) + + if objs and ("force" not in self.switches and type(self).confirm): + confirm = "Are you sure you want to destroy " + if len(objs) == 1: + confirm += objs[0].get_display_name(caller) + elif len(objs) < 5: + confirm += ", ".join([obj.get_display_name(caller) for obj in objs]) + else: + confirm += ", ".join(["#{}".format(obj.id) for obj in objs]) + confirm += " [yes]/no?" if self.default_confirm == 'yes' else " yes/[no]" + answer = "" + while answer.strip().lower() not in ("y", "yes", "n", "no"): + answer = yield(confirm) + answer = self.default_confirm if answer == '' else answer + + if answer.strip().lower() in ("n", "no"): + caller.msg("Cancelled: no object was destroyed.") + delete = False + + if delete: + results = [] + for obj in objs: + results.append(delobj(obj)) + + if results: + caller.msg("".join(results).strip()) class CmdDig(ObjManipCommand): @@ -705,17 +771,17 @@ class CmdDig(ObjManipCommand): would be 'north;no;n'. """ key = "@dig" - locks = "cmd:perm(dig) or perm(Builders)" + locks = "cmd:perm(dig) or perm(Builder)" help_category = "Building" # lockstring of newly created rooms, for easy overloading. # Will be formatted with the {id} of the creating object. - new_room_lockstring = "control:id({id}) or perm(Wizards); " \ - "delete:id({id}) or perm(Wizards); " \ - "edit:id({id}) or perm(Wizards)" + new_room_lockstring = "control:id({id}) or perm(Admin); " \ + "delete:id({id}) or perm(Admin); " \ + "edit:id({id}) or perm(Admin)" def func(self): - "Do the digging. Inherits variables from ObjManipCommand.parse()" + """Do the digging. Inherits variables from ObjManipCommand.parse()""" caller = self.caller @@ -748,8 +814,8 @@ class CmdDig(ObjManipCommand): alias_string = "" if new_room.aliases.all(): alias_string = " (%s)" % ", ".join(new_room.aliases.all()) - room_string = "Created room %s(%s)%s of type %s." % (new_room, - new_room.dbref, alias_string, typeclass) + room_string = "Created room %s(%s)%s of type %s." % ( + new_room, new_room.dbref, alias_string, typeclass) # create exit to room @@ -759,11 +825,9 @@ class CmdDig(ObjManipCommand): if self.rhs_objs: to_exit = self.rhs_objs[0] if not to_exit["name"]: - exit_to_string = \ - "\nNo exit created to new room." + exit_to_string = "\nNo exit created to new room." elif not location: - exit_to_string = \ - "\nYou cannot create an exit from a None-location." + exit_to_string = "\nYou cannot create an exit from a None-location." else: # Build the exit to the new room from the current one typeclass = to_exit["option"] @@ -792,22 +856,20 @@ class CmdDig(ObjManipCommand): # Building the exit back to the current room back_exit = self.rhs_objs[1] if not back_exit["name"]: - exit_back_string = \ - "\nNo back exit created." + exit_back_string = "\nNo back exit created." elif not location: - exit_back_string = \ - "\nYou cannot create an exit back to a None-location." + exit_back_string = "\nYou cannot create an exit back to a None-location." else: typeclass = back_exit["option"] if not typeclass: typeclass = settings.BASE_EXIT_TYPECLASS new_back_exit = create.create_object(typeclass, - back_exit["name"], - new_room, - aliases=back_exit["aliases"], - locks=lockstring, - destination=location, - report_to=caller) + back_exit["name"], + new_room, + aliases=back_exit["aliases"], + locks=lockstring, + destination=location, + report_to=caller) alias_string = "" if new_back_exit.aliases.all(): alias_string = " (%s)" % ", ".join(new_back_exit.aliases.all()) @@ -821,12 +883,13 @@ class CmdDig(ObjManipCommand): if new_room and ('teleport' in self.switches or "tel" in self.switches): caller.move_to(new_room) + class CmdTunnel(COMMAND_DEFAULT_CLASS): """ create new rooms in cardinal directions only Usage: - @tunnel[/switch] [= [;alias;alias;...][:typeclass]] + @tunnel[/switch] [:typeclass] [= [;alias;alias;...][:typeclass]] Switches: oneway - do not create an exit back to the current location @@ -850,7 +913,7 @@ class CmdTunnel(COMMAND_DEFAULT_CLASS): key = "@tunnel" aliases = ["@tun"] - locks = "cmd: perm(tunnel) or perm(Builders)" + locks = "cmd: perm(tunnel) or perm(Builder)" help_category = "Building" # store the direction, full name and its opposite @@ -868,23 +931,37 @@ class CmdTunnel(COMMAND_DEFAULT_CLASS): "o": ("out", "i")} def func(self): - "Implements the tunnel command" + """Implements the tunnel command""" if not self.args or not self.lhs: - string = "Usage: @tunnel[/switch] [= " \ + string = "Usage: @tunnel[/switch] [:typeclass] [= " \ "[;alias;alias;...][:typeclass]]" self.caller.msg(string) return - if self.lhs not in self.directions: - string = "@tunnel can only understand the following directions: %s." % ",".join(sorted(self.directions.keys())) + + # If we get a typeclass, we need to get just the exitname + exitshort = self.lhs.split(":")[0] + + if exitshort not in self.directions: + string = "@tunnel can only understand the following directions: %s." % ",".join( + sorted(self.directions.keys())) string += "\n(use @dig for more freedom)" self.caller.msg(string) return + # retrieve all input and parse it - exitshort = self.lhs exitname, backshort = self.directions[exitshort] backname = self.directions[backshort][0] + # if we recieved a typeclass for the exit, add it to the alias(short name) + if ":" in self.lhs: + # limit to only the first : character + exit_typeclass = ":" + self.lhs.split(":", 1)[-1] + # exitshort and backshort are the last part of the exit strings, + # so we add our typeclass argument after + exitshort += exit_typeclass + backshort += exit_typeclass + roomname = "Some place" if self.rhs: roomname = self.rhs # this may include aliases; that's fine. @@ -893,7 +970,7 @@ class CmdTunnel(COMMAND_DEFAULT_CLASS): if "tel" in self.switches: telswitch = "/teleport" backstring = "" - if not "oneway" in self.switches: + if "oneway" not in self.switches: backstring = ", %s;%s" % (backname, backshort) # build the string we will use to call @dig @@ -924,11 +1001,11 @@ class CmdLink(COMMAND_DEFAULT_CLASS): """ key = "@link" - locks = "cmd:perm(link) or perm(Builders)" + locks = "cmd:perm(link) or perm(Builder)" help_category = "Building" def func(self): - "Perform the link" + """Perform the link""" caller = self.caller if not self.args: @@ -942,7 +1019,6 @@ class CmdLink(COMMAND_DEFAULT_CLASS): if not obj: return - string = "" if self.rhs: # this means a target name was given target = caller.search(self.rhs, global_search=True) @@ -950,8 +1026,9 @@ class CmdLink(COMMAND_DEFAULT_CLASS): return string = "" + note = "Note: %s(%s) did not have a destination set before. Make sure you linked the right thing." if not obj.destination: - string += "Note: %s(%s) did not have a destination set before. Make sure you linked the right thing." % (obj.name,obj.dbref) + string = note % (obj.name, obj.dbref) if "twoway" in self.switches: if not (target.location and obj.location): string = "To create a two-way link, %s and %s must both have a location" % (obj, target) @@ -959,10 +1036,11 @@ class CmdLink(COMMAND_DEFAULT_CLASS): self.caller.msg(string) return if not target.destination: - string += "\nNote: %s(%s) did not have a destination set before. Make sure you linked the right thing." % (target.name, target.dbref) + string += note % (target.name, target.dbref) obj.destination = target.location target.destination = obj.location - string += "\nLink created %s (in %s) <-> %s (in %s) (two-way)." % (obj.name, obj.location, target.name, target.location) + string += "\nLink created %s (in %s) <-> %s (in %s) (two-way)." % ( + obj.name, obj.location, target.name, target.location) else: obj.destination = target string += "\nLink created %s -> %s (one way)." % (obj.name, target) @@ -1002,7 +1080,7 @@ class CmdUnLink(CmdLink): # this is just a child of CmdLink key = "@unlink" - locks = "cmd:perm(unlink) or perm(Builders)" + locks = "cmd:perm(unlink) or perm(Builder)" help_key = "Building" def func(self): @@ -1029,7 +1107,7 @@ class CmdSetHome(CmdLink): set an object's home location Usage: - @home [= ] + @sethome [= ] The "home" location is a "safety" location for objects; they will be moved there if their current location ceases to exist. All @@ -1039,15 +1117,14 @@ class CmdSetHome(CmdLink): If no location is given, just view the object's home location. """ - key = "@home" - aliases = "@sethome" - locks = "cmd:perm(@home) or perm(Builders)" + key = "@sethome" + locks = "cmd:perm(@sethome) or perm(Builder)" help_category = "Building" def func(self): - "implement the command" + """implement the command""" if not self.args: - string = "Usage: @home [= ]" + string = "Usage: @sethome [= ]" self.caller.msg(string) return @@ -1070,7 +1147,8 @@ class CmdSetHome(CmdLink): old_home = obj.home obj.home = new_home if old_home: - string = "%s's home location was changed from %s(%s) to %s(%s)." % (obj, old_home, old_home.dbref, new_home, new_home.dbref) + string = "%s's home location was changed from %s(%s) to %s(%s)." % ( + obj, old_home, old_home.dbref, new_home, new_home.dbref) else: string = "%s' home location was set to %s(%s)." % (obj, new_home, new_home.dbref) self.caller.msg(string) @@ -1088,11 +1166,11 @@ class CmdListCmdSets(COMMAND_DEFAULT_CLASS): """ key = "@cmdsets" aliases = "@listcmsets" - locks = "cmd:perm(listcmdsets) or perm(Builders)" + locks = "cmd:perm(listcmdsets) or perm(Builder)" help_category = "Building" def func(self): - "list the cmdsets" + """list the cmdsets""" caller = self.caller if self.arglist: @@ -1113,17 +1191,17 @@ class CmdName(ObjManipCommand): @name = ;alias1;alias2 Rename an object to something new. Use *obj to - rename a player. + rename an account. """ key = "@name" aliases = ["@rename"] - locks = "cmd:perm(rename) or perm(Builders)" + locks = "cmd:perm(rename) or perm(Builder)" help_category = "Building" def func(self): - "change the name" + """change the name""" caller = self.caller if not self.args: @@ -1134,22 +1212,22 @@ class CmdName(ObjManipCommand): if self.lhs_objs: objname = self.lhs_objs[0]['name'] if objname.startswith("*"): - # player mode - obj = caller.player.search(objname.lstrip("*")) + # account mode + obj = caller.account.search(objname.lstrip("*")) if obj: if self.rhs_objs[0]['aliases']: - caller.msg("Players can't have aliases.") + caller.msg("Accounts can't have aliases.") return newname = self.rhs if not newname: caller.msg("No name defined!") return if not (obj.access(caller, "control") or obj.access(caller, "edit")): - caller.msg("You don't have right to edit this player %s." % obj) + caller.msg("You don't have right to edit this account %s." % obj) return obj.username = newname obj.save() - caller.msg("Player's name changed to '%s'." % newname) + caller.msg("Account's name changed to '%s'." % newname) return # object search, also with * obj = caller.search(objname) @@ -1195,12 +1273,12 @@ class CmdOpen(ObjManipCommand): """ key = "@open" - locks = "cmd:perm(open) or perm(Builders)" + locks = "cmd:perm(open) or perm(Builder)" help_category = "Building" # a custom member method to chug out exits and do checks def create_exit(self, exit_name, location, destination, - exit_aliases=None, typeclass=None): + exit_aliases=None, typeclass=None): """ Helper function to avoid code duplication. At this point we know destination is a valid location @@ -1233,8 +1311,8 @@ class CmdOpen(ObjManipCommand): exit_obj.destination = destination if exit_aliases: [exit_obj.aliases.add(alias) for alias in exit_aliases] - string += " Rerouted its old destination '%s' to '%s' and changed aliases." % \ - (old_destination.name, destination.name) + string += " Rerouted its old destination '%s' to '%s' and changed aliases." % ( + old_destination.name, destination.name) else: string += " It already points to the correct place." @@ -1255,7 +1333,7 @@ class CmdOpen(ObjManipCommand): string = "Created new Exit '%s' from %s to %s%s." % ( exit_name, location.name, destination.name, string) else: - string = "Error: Exit '%s' not created." % (exit_name) + string = "Error: Exit '%s' not created." % exit_name # emit results caller.msg(string) return exit_obj @@ -1331,7 +1409,7 @@ def _convert_from_string(cmd, strobj): be converted to a string and a warning will be given. We need to convert like this since all data being sent over the - telnet connection by the Player is text - but we will want to + telnet connection by the Account is text - but we will want to store it as the "real" python type so we can do convenient comparisons later (e.g. obj.db.value = 2, if value is stored as a string this will always fail). @@ -1384,18 +1462,26 @@ def _convert_from_string(cmd, strobj): # nested lists/dicts) return rec_convert(strobj.strip()) + class CmdSetAttribute(ObjManipCommand): """ - set attribute on an object or player + set attribute on an object or account Usage: @set / = @set / = @set / - @set */ = + @set */attr = Switch: edit: Open the line editor (string values only) + script: If we're trying to set an attribute on a script + channel: If we're trying to set an attribute on a channel + account: If we're trying to set an attribute on an account + room: Setting an attribute on a room (global search) + exit: Setting an attribute on an exit (global search) + char: Setting an attribute on a character (global search) + character: Alias for char, as above. Sets attributes on objects. The second form clears a previously set attribute while the last form @@ -1417,14 +1503,14 @@ class CmdSetAttribute(ObjManipCommand): """ key = "@set" - locks = "cmd:perm(set) or perm(Builders)" + locks = "cmd:perm(set) or perm(Builder)" 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. + accounts. This function is expected to display its own error message. @@ -1478,26 +1564,58 @@ class CmdSetAttribute(ObjManipCommand): "dicts.|n") def edit_handler(self, obj, attr): - "Activate the line editor" + """Activate the line editor""" def load(caller): - "Called for the editor to load the buffer" + """Called for the editor to load the buffer""" old_value = obj.attributes.get(attr) if old_value is not None and not isinstance(old_value, basestring): typ = type(old_value).__name__ - self.caller.msg("|RWARNING! Saving this buffer will overwrite the "\ + self.caller.msg("|RWARNING! Saving this buffer will overwrite the " "current attribute (of type %s) with a string!|n" % typ) return str(old_value) return old_value + def save(caller, buf): - "Called when editor saves its buffer." + """Called when editor saves its buffer.""" obj.attributes.add(attr, buf) caller.msg("Saved Attribute %s." % attr) # start the editor EvEditor(self.caller, load, save, key="%s/%s" % (obj, attr)) + def search_for_obj(self, objname): + """ + Searches for an object matching objname. The object may be of different typeclasses. + Args: + objname: Name of the object we're looking for + + Returns: + A typeclassed object, or None if nothing is found. + """ + from evennia.utils.utils import variable_from_module + _AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1)) + caller = self.caller + if objname.startswith('*') or "account" in self.switches: + found_obj = caller.search_account(objname.lstrip('*')) + elif "script" in self.switches: + found_obj = _AT_SEARCH_RESULT(search.search_script(objname), caller) + elif "channel" in self.switches: + found_obj = _AT_SEARCH_RESULT(search.search_channel(objname), caller) + else: + global_search = True + if "char" in self.switches or "character" in self.switches: + typeclass = settings.BASE_CHARACTER_TYPECLASS + elif "room" in self.switches: + typeclass = settings.BASE_ROOM_TYPECLASS + elif "exit" in self.switches: + typeclass = settings.BASE_EXIT_TYPECLASS + else: + global_search = False + typeclass = None + found_obj = caller.search(objname, global_search=global_search, typeclass=typeclass) + return found_obj def func(self): - "Implement the set attribute - a limited form of @py." + """Implement the set attribute - a limited form of @py.""" caller = self.caller if not self.args: @@ -1509,10 +1627,7 @@ class CmdSetAttribute(ObjManipCommand): objname = self.lhs_objattr[0]['name'] attrs = self.lhs_objattr[0]['attrs'] - if objname.startswith('*'): - obj = caller.search_player(objname.lstrip('*')) - else: - obj = caller.search(objname) + obj = self.search_for_obj(objname) if not obj: return @@ -1523,7 +1638,7 @@ class CmdSetAttribute(ObjManipCommand): if "edit" in self.switches: # edit in the line editor if len(attrs) > 1: - caller.msg("The Line editor can only be applied " \ + caller.msg("The Line editor can only be applied " "to one attribute at a time.") return self.edit_handler(obj, attrs[0]) @@ -1538,7 +1653,7 @@ class CmdSetAttribute(ObjManipCommand): continue result.append(self.view_attr(obj, attr)) # we view it without parsing markup. - self.caller.msg("".join(result).strip(), options={"raw":True}) + self.caller.msg("".join(result).strip(), options={"raw": True}) return else: # deleting the attribute(s) @@ -1599,11 +1714,11 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS): key = "@typeclass" aliases = ["@type", "@parent", "@swap", "@update"] - locks = "cmd:perm(typeclass) or perm(Builders)" + locks = "cmd:perm(typeclass) or perm(Builder)" help_category = "Building" def func(self): - "Implements command" + """Implements command""" caller = self.caller @@ -1644,7 +1759,7 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS): return is_same = obj.is_typeclass(new_typeclass, exact=True) - if is_same and not 'force' in self.switches: + if is_same and 'force' not in self.switches: string = "%s already has the typeclass '%s'. Use /force to override." % (obj.name, new_typeclass) else: update = "update" in self.switches @@ -1654,14 +1769,14 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS): # we let this raise exception if needed obj.swap_typeclass(new_typeclass, clean_attributes=reset, - clean_cmdsets=reset, run_start_hooks=hooks) + clean_cmdsets=reset, run_start_hooks=hooks) if is_same: string = "%s updated its existing typeclass (%s).\n" % (obj.name, obj.path) else: string = "%s changed typeclass from %s to %s.\n" % (obj.name, - old_typeclass_path, - obj.typeclass_path) + old_typeclass_path, + obj.typeclass_path) if update: string += "Only the at_object_creation hook was run (update mode)." else: @@ -1689,7 +1804,7 @@ class CmdWipe(ObjManipCommand): matching the given attribute-wildcard search string. """ key = "@wipe" - locks = "cmd:perm(wipe) or perm(Builders)" + locks = "cmd:perm(wipe) or perm(Builder)" help_category = "Building" def func(self): @@ -1730,9 +1845,9 @@ class CmdLock(ObjManipCommand): assign a lock definition to an object Usage: - @lock [ = ] + @lock [ = ] or - @lock[/switch] / + @lock[/switch] / Switch: del - delete given access type @@ -1741,28 +1856,28 @@ class CmdLock(ObjManipCommand): If no lockstring is given, shows all locks on object. - Lockstring is on the form + Lockstring is of the form access_type:[NOT] func1(args)[ AND|OR][ NOT] func2(args) ...] Where func1, func2 ... valid lockfuncs with or without arguments. Separator expressions need not be capitalized. For example: - 'get: id(25) or perm(Wizards)' + 'get: id(25) or perm(Admin)' The 'get' access_type is checked by the get command and will an object locked with this string will only be possible to - pick up by Wizards or by object with id 25. + pick up by Admins or by object with id=25. You can add several access_types after one another by separating them by ';', i.e: - 'get:id(25);delete:perm(Builders)' + 'get:id(25);delete:perm(Builder)' """ key = "@lock" - aliases = ["@locks", "lock", "locks"] - locks = "cmd: perm(locks) or perm(Builders)" + aliases = ["@locks"] + locks = "cmd: perm(locks) or perm(Builder)" help_category = "Building" def func(self): - "Sets up the command" + """Sets up the command""" caller = self.caller if not self.args: @@ -1772,18 +1887,22 @@ class CmdLock(ObjManipCommand): return if '/' in self.lhs: - # call on the form @lock obj/access_type + # call of the form @lock obj/access_type objname, access_type = [p.strip() for p in self.lhs.split('/', 1)] - obj = caller.search(objname) + obj = None + if objname.startswith("*"): + obj = caller.search_account(objname.lstrip('*')) if not obj: + obj = caller.search(objname) + if not obj: + return + if not (obj.access(caller, 'control') or obj.access(caller, "edit")): + caller.msg("You are not allowed to do that.") return lockdef = obj.locks.get(access_type) - string = "" + if lockdef: if 'del' in self.switches: - if not (obj.access(caller, 'control') or obj.access(caller, "edit")): - caller.msg("You are not allowed to do that.") - return obj.locks.delete(access_type) string = "deleted lock %s" % lockdef else: @@ -1797,15 +1916,19 @@ class CmdLock(ObjManipCommand): # we have a = separator, so we are assigning a new lock if self.switches: swi = ", ".join(self.switches) - caller.msg("Switch(es) |w%s|n can not be used with a "\ - "lock assignment. Use e.g. " \ + caller.msg("Switch(es) |w%s|n can not be used with a " + "lock assignment. Use e.g. " "|w@lock/del objname/locktype|n instead." % swi) return objname, lockdef = self.lhs, self.rhs - obj = caller.search(objname) + obj = None + if objname.startswith("*"): + obj = caller.search_account(objname.lstrip('*')) if not obj: - return + obj = caller.search(objname) + if not obj: + return if not (obj.access(caller, 'control') or obj.access(caller, "edit")): caller.msg("You are not allowed to do that.") return @@ -1824,11 +1947,18 @@ class CmdLock(ObjManipCommand): caller.msg("Added lock '%s' to %s." % (lockdef, obj)) return - # if we get here, we are just viewing all locks - obj = caller.search(self.lhs) + # if we get here, we are just viewing all locks on obj + obj = None + if self.lhs.startswith("*"): + obj = caller.search_account(self.lhs.lstrip("*")) + if not obj: + obj = caller.search(self.lhs) if not obj: return - caller.msg(obj.locks.all()) + if not (obj.access(caller, 'control') or obj.access(caller, "edit")): + caller.msg("You are not allowed to do that.") + return + caller.msg("\n".join(obj.locks.all())) class CmdExamine(ObjManipCommand): @@ -1837,26 +1967,26 @@ class CmdExamine(ObjManipCommand): Usage: examine [[/attrname]] - examine [*[/attrname]] + examine [*[/attrname]] Switch: - player - examine a Player (same as adding *) + account - examine an Account (same as adding *) object - examine an Object (useful when OOC) The examine command shows detailed game info about an object and optionally a specific attribute on it. If object is not specified, the current location is examined. - Append a * before the search string to examine a player. + Append a * before the search string to examine an account. """ key = "@examine" - aliases = ["@ex","ex", "exam", "examine"] - locks = "cmd:perm(examine) or perm(Builders)" + aliases = ["@ex", "exam"] + locks = "cmd:perm(examine) or perm(Builder)" help_category = "Building" arg_regex = r"(/\w+?(\s|$))|\s|$" - player_mode = False + account_mode = False def list_attribute(self, crop, attr, value): """ @@ -1913,18 +2043,18 @@ class CmdExamine(ObjManipCommand): string += "\n|wAliases|n: %s" % (", ".join(utils.make_iter(str(obj.aliases)))) if hasattr(obj, "sessions") and obj.sessions.all(): string += "\n|wSession id(s)|n: %s" % (", ".join("#%i" % sess.sessid - for sess in obj.sessions.all())) + for sess in obj.sessions.all())) if hasattr(obj, "email") and obj.email: string += "\n|wEmail|n: |c%s|n" % obj.email - if hasattr(obj, "has_player") and obj.has_player: - string += "\n|wPlayer|n: |c%s|n" % obj.player.name - perms = obj.player.permissions.all() - if obj.player.is_superuser: + if hasattr(obj, "has_account") and obj.has_account: + string += "\n|wAccount|n: |c%s|n" % obj.account.name + perms = obj.account.permissions.all() + if obj.account.is_superuser: perms = [""] elif not perms: perms = [""] - string += "\n|wPlayer Perms|n: %s" % (", ".join(perms)) - if obj.player.attributes.has("_quell"): + string += "\n|wAccount Perms|n: %s" % (", ".join(perms)) + if obj.account.attributes.has("_quell"): string += " |r(quelled)|n" string += "\n|wTypeclass|n: %s (%s)" % (obj.typename, obj.typeclass_path) @@ -1957,43 +2087,41 @@ class CmdExamine(ObjManipCommand): locks_string = " Default" string += "\n|wLocks|n:%s" % locks_string - if not (len(obj.cmdset.all()) == 1 and obj.cmdset.current.key == "_EMPTY_CMDSET"): # all() returns a 'stack', so make a copy to sort. stored_cmdsets = sorted(obj.cmdset.all(), key=lambda x: x.priority, reverse=True) - string += "\n|wStored Cmdset(s)|n:\n %s" % ("\n ".join("%s [%s] (%s, prio %s)" % \ - (cmdset.path, cmdset.key, cmdset.mergetype, cmdset.priority) - for cmdset in stored_cmdsets if cmdset.key != "_EMPTY_CMDSET")) + string += "\n|wStored Cmdset(s)|n:\n %s" % ("\n ".join("%s [%s] (%s, prio %s)" % ( + cmdset.path, cmdset.key, cmdset.mergetype, cmdset.priority) for cmdset in stored_cmdsets + if cmdset.key != "_EMPTY_CMDSET")) # this gets all components of the currently merged set all_cmdsets = [(cmdset.key, cmdset) for cmdset in avail_cmdset.merged_from] - # we always at least try to add player- and session sets since these are ignored + # we always at least try to add account- and session sets since these are ignored # if we merge on the object level. - if hasattr(obj, "player") and obj.player: - all_cmdsets.extend([(cmdset.key, cmdset) for cmdset in obj.player.cmdset.all()]) + if hasattr(obj, "account") and obj.account: + all_cmdsets.extend([(cmdset.key, cmdset) for cmdset in obj.account.cmdset.all()]) if obj.sessions.count(): # if there are more sessions than one on objects it's because of multisession mode 3. # we only show the first session's cmdset here (it is -in principle- possible that # different sessions have different cmdsets but for admins who want such madness # it is better that they overload with their own CmdExamine to handle it). - all_cmdsets.extend([(cmdset.key, cmdset) for cmdset in obj.player.sessions.all()[0].cmdset.all()]) + all_cmdsets.extend([(cmdset.key, cmdset) for cmdset in obj.account.sessions.all()[0].cmdset.all()]) else: try: # we have to protect this since many objects don't have sessions. - all_cmdsets.extend([(cmdset.key, cmdset) for cmdset in obj.get_session(obj.sessions.get()).cmdset.all()]) + all_cmdsets.extend([(cmdset.key, cmdset) + for cmdset in obj.get_session(obj.sessions.get()).cmdset.all()]) except (TypeError, AttributeError): # an error means we are merging an object without a session pass all_cmdsets = [cmdset for cmdset in dict(all_cmdsets).values()] all_cmdsets.sort(key=lambda x: x.priority, reverse=True) - string += "\n|wMerged Cmdset(s)|n:\n %s" % ("\n ".join("%s [%s] (%s, prio %s)" % \ - (cmdset.path, cmdset.key, cmdset.mergetype, cmdset.priority) - for cmdset in all_cmdsets)) - + string += "\n|wMerged Cmdset(s)|n:\n %s" % ("\n ".join("%s [%s] (%s, prio %s)" % ( + cmdset.path, cmdset.key, cmdset.mergetype, cmdset.priority) for cmdset in all_cmdsets)) # list the commands available to this object avail_cmdset = sorted([cmd.key for cmd in avail_cmdset - if cmd.access(obj, "cmd")]) + if cmd.access(obj, "cmd")]) cmdsetstr = utils.fill(", ".join(avail_cmdset), indent=2) string += "\n|wCommands available to %s (result of Merged CmdSets)|n:\n %s" % (obj.key, cmdsetstr) @@ -2005,7 +2133,7 @@ class CmdExamine(ObjManipCommand): # display Tags tags_string = utils.fill(", ".join("%s[%s]" % (tag, category) - for tag, category in obj.tags.all(return_key_and_category=True)), indent=5) + for tag, category in obj.tags.all(return_key_and_category=True)), indent=5) if tags_string: string += "\n|wTags[category]|n: %s" % tags_string.strip() @@ -2017,7 +2145,7 @@ class CmdExamine(ObjManipCommand): for content in obj.contents: if content.destination: exits.append(content) - elif content.player: + elif content.account: pobjs.append(content) else: things.append(content) @@ -2029,11 +2157,11 @@ class CmdExamine(ObjManipCommand): string += "\n|wContents|n: %s" % ", ".join(["%s(%s)" % (cont.name, cont.dbref) for cont in obj.contents if cont not in exits and cont not in pobjs]) separator = "-" * _DEFAULT_WIDTH - #output info + # output info return '%s\n%s\n%s' % (separator, string.strip(), separator) def func(self): - "Process command" + """Process command""" caller = self.caller def get_cmdset_callback(cmdset): @@ -2053,11 +2181,12 @@ class CmdExamine(ObjManipCommand): if hasattr(caller, "location"): obj = caller.location if not obj.access(caller, 'examine'): - #If we don't have special info access, just look at the object instead. + # If we don't have special info access, just look at the object instead. self.msg(caller.at_look(obj)) return # using callback for printing result whenever function returns. - get_and_merge_cmdsets(obj, self.session, self.player, obj, "object", self.raw_string).addCallback(get_cmdset_callback) + get_and_merge_cmdsets(obj, self.session, self.account, obj, "object", + self.raw_string).addCallback(get_cmdset_callback) else: self.msg("You need to supply a target to examine.") return @@ -2069,21 +2198,21 @@ class CmdExamine(ObjManipCommand): obj_name = objdef['name'] obj_attrs = objdef['attrs'] - self.player_mode = utils.inherits_from(caller, "evennia.players.players.DefaultPlayer") or \ - "player" in self.switches or obj_name.startswith('*') - if self.player_mode: + self.account_mode = utils.inherits_from(caller, "evennia.accounts.accounts.DefaultAccount") or \ + "account" in self.switches or obj_name.startswith('*') + if self.account_mode: try: - obj = caller.search_player(obj_name.lstrip('*')) + obj = caller.search_account(obj_name.lstrip('*')) except AttributeError: - # this means we are calling examine from a player object - obj = caller.search(obj_name.lstrip('*'), search_object = 'object' in self.switches) + # this means we are calling examine from an account object + obj = caller.search(obj_name.lstrip('*'), search_object='object' in self.switches) else: obj = caller.search(obj_name) if not obj: continue if not obj.access(caller, 'examine'): - #If we don't have special info access, just look + # If we don't have special info access, just look # at the object instead. self.msg(caller.at_look(obj)) continue @@ -2095,12 +2224,12 @@ class CmdExamine(ObjManipCommand): else: if obj.sessions.count(): mergemode = "session" - elif self.player_mode: - mergemode = "player" + elif self.account_mode: + mergemode = "account" else: mergemode = "object" # using callback to print results whenever function returns. - get_and_merge_cmdsets(obj, self.session, self.player, obj, mergemode, self.raw_string).addCallback(get_cmdset_callback) + get_and_merge_cmdsets(obj, self.session, self.account, obj, mergemode, self.raw_string).addCallback(get_cmdset_callback) class CmdFind(COMMAND_DEFAULT_CLASS): @@ -2108,7 +2237,7 @@ class CmdFind(COMMAND_DEFAULT_CLASS): search the database for objects Usage: - @find[/switches] [= dbrefmin[-dbrefmax]] + @find[/switches] [= dbrefmin[-dbrefmax]] Switches: room - only look for rooms (location=None) @@ -2117,19 +2246,19 @@ class CmdFind(COMMAND_DEFAULT_CLASS): exact- only exact matches are returned. Searches the database for an object of a particular name or exact #dbref. - Use *playername to search for a player. The switches allows for + Use *accountname to search for an account. The switches allows for limiting object matches to certain game entities. Dbrefmin and dbrefmax limits matches to within the given dbrefs range, or above/below if only one is given. """ key = "@find" - aliases = "find, @search, search, @locate, locate" - locks = "cmd:perm(find) or perm(Builders)" + aliases = "@search, @locate" + locks = "cmd:perm(find) or perm(Builder)" help_category = "Building" def func(self): - "Search functionality" + """Search functionality""" caller = self.caller switches = self.switches @@ -2154,22 +2283,22 @@ class CmdFind(COMMAND_DEFAULT_CLASS): high = max(low, high) is_dbref = utils.dbref(searchstring) - is_player = searchstring.startswith("*") + is_account = searchstring.startswith("*") restrictions = "" if self.switches: restrictions = ", %s" % (",".join(self.switches)) - if is_dbref or is_player: + if is_dbref or is_account: if is_dbref: # a dbref search result = caller.search(searchstring, global_search=True, quiet=True) string = "|wExact dbref match|n(#%i-#%i%s):" % (low, high, restrictions) else: - # a player search + # an account search searchstring = searchstring.lstrip("*") - result = caller.search_player(searchstring, quiet=True) + result = caller.search_account(searchstring, quiet=True) string = "|wMatch|n(#%i-#%i%s):" % (low, high, restrictions) if "room" in switches: @@ -2182,17 +2311,17 @@ class CmdFind(COMMAND_DEFAULT_CLASS): if not result: string += "\n |RNo match found.|n" elif not low <= int(result[0].id) <= high: - string += "\n |RNo match found for '%s' in #dbref interval.|n" % (searchstring) + string += "\n |RNo match found for '%s' in #dbref interval.|n" % searchstring else: - result=result[0] + result = result[0] string += "\n|g %s - %s|n" % (result.get_display_name(caller), result.path) else: - # Not a player/dbref search but a wider search; build a queryset. + # Not an account/dbref search but a wider search; build a queryset. # Searchs for key and aliases if "exact" in switches: keyquery = Q(db_key__iexact=searchstring, id__gte=low, id__lte=high) aliasquery = Q(db_tags__db_key__iexact=searchstring, - db_tags__db_tagtype__iexact="alias",id__gte=low, id__lte=high) + db_tags__db_tagtype__iexact="alias", id__gte=low, id__lte=high) else: keyquery = Q(db_key__istartswith=searchstring, id__gte=low, id__lte=high) aliasquery = Q(db_tags__db_key__istartswith=searchstring, @@ -2251,16 +2380,17 @@ class CmdTeleport(COMMAND_DEFAULT_CLASS): Note that the only way to retrieve an object from a None location is by direct #dbref reference. A puppeted object cannot be moved to None. + loc - teleport object to the target's location instead of its contents Teleports an object somewhere. If no object is given, you yourself is teleported to the target location. """ key = "@tel" aliases = "@teleport" - locks = "cmd:perm(teleport) or perm(Builders)" + locks = "cmd:perm(teleport) or perm(Builder)" help_category = "Building" def func(self): - "Performs the teleport" + """Performs the teleport""" caller = self.caller args = self.args @@ -2270,6 +2400,7 @@ class CmdTeleport(COMMAND_DEFAULT_CLASS): # setting switches tel_quietly = "quiet" in switches to_none = "tonone" in switches + to_loc = "loc" in switches if to_none: # teleporting to None @@ -2280,22 +2411,22 @@ class CmdTeleport(COMMAND_DEFAULT_CLASS): if not obj_to_teleport: caller.msg("Did not find object to teleport.") return - if obj_to_teleport.has_player: + if obj_to_teleport.has_account: caller.msg("Cannot teleport a puppeted object " "(%s, puppeted by %s) to a None-location." % ( - obj_to_teleport.key, obj_to_teleport.player)) + obj_to_teleport.key, obj_to_teleport.account)) return caller.msg("Teleported %s -> None-location." % obj_to_teleport) if obj_to_teleport.location and not tel_quietly: obj_to_teleport.location.msg_contents("%s teleported %s into nothingness." % (caller, obj_to_teleport), exclude=caller) - obj_to_teleport.location=None + obj_to_teleport.location = None return # not teleporting to None location if not args and not to_none: - caller.msg("Usage: teleport[/switches] [ =] |home") + caller.msg("Usage: teleport[/switches] [ =] ||home") return if rhs: @@ -2311,6 +2442,11 @@ class CmdTeleport(COMMAND_DEFAULT_CLASS): if not destination: caller.msg("Destination not found.") return + if to_loc: + destination = destination.location + if not destination: + caller.msg("Destination has no location.") + return if obj_to_teleport == destination: caller.msg("You can't teleport an object inside of itself!") return @@ -2357,11 +2493,11 @@ class CmdScript(COMMAND_DEFAULT_CLASS): key = "@script" aliases = "@addscript" - locks = "cmd:perm(script) or perm(Builders)" + locks = "cmd:perm(script) or perm(Builder)" help_category = "Building" def func(self): - "Do stuff" + """Do stuff""" caller = self.caller @@ -2397,18 +2533,16 @@ class CmdScript(COMMAND_DEFAULT_CLASS): obj.get_display_name(caller))) script.stop() obj.scripts.validate() - else: # rhs exists + else: # rhs exists if not self.switches: # adding a new script, and starting it ok = obj.scripts.add(self.rhs, autostart=True) if not ok: result.append("\nScript %s could not be added and/or started on %s." % ( - self.rhs, obj.get_display_name(caller) - )) + self.rhs, obj.get_display_name(caller))) else: result.append("Script |w%s|n successfully added and started on %s." % ( - self.rhs, obj.get_display_name(caller) - )) + self.rhs, obj.get_display_name(caller))) else: paths = [self.rhs] + ["%s.%s" % (prefix, self.rhs) @@ -2458,12 +2592,12 @@ class CmdTag(COMMAND_DEFAULT_CLASS): key = "@tag" aliases = ["@tags"] - locks = "cmd:perm(tag) or perm(Builders)" + locks = "cmd:perm(tag) or perm(Builder)" help_category = "Building" arg_regex = r"(/\w+?(\s|$))|\s|$" def func(self): - "Implement the @tag functionality" + """Implement the @tag functionality""" if not self.args: self.caller.msg("Usage: @tag[/switches] [= [:]]") @@ -2478,16 +2612,16 @@ class CmdTag(COMMAND_DEFAULT_CLASS): nobjs = len(objs) if nobjs > 0: catstr = " (category: '|w%s|n')" % category if category else \ - ("" if nobjs == 1 else " (may have different tag categories)") + ("" if nobjs == 1 else " (may have different tag categories)") matchstr = ", ".join(o.get_display_name(self.caller) for o in objs) string = "Found |w%i|n object%s with tag '|w%s|n'%s:\n %s" % (nobjs, - "s" if nobjs > 1 else "", - tag, - catstr, matchstr) + "s" if nobjs > 1 else "", + tag, + catstr, matchstr) else: string = "No objects found with tag '%s%s'." % (tag, - " (category: %s)" % category if category else "") + " (category: %s)" % category if category else "") self.caller.msg(string) return if "del" in self.switches: @@ -2504,14 +2638,14 @@ class CmdTag(COMMAND_DEFAULT_CLASS): if obj.tags.get(tag, category=category): obj.tags.remove(tag, category=category) string = "Removed tag '%s'%s from %s." % ( - tag, - " (category: %s)" % category if category else "", - obj) + tag, + " (category: %s)" % category if category else "", + obj) else: string = "No tag '%s'%s to delete on %s." % ( - tag, - " (category: %s)" % category if category else "", - obj) + tag, + " (category: %s)" % category if category else "", + obj) else: # no tag specified, clear all tags old_tags = ["%s%s" % (tag, " (category: %s" % category if category else "") @@ -2525,7 +2659,7 @@ class CmdTag(COMMAND_DEFAULT_CLASS): return # no search/deletion if self.rhs: - # = is found, so we are on the form obj = tag + # = is found; command args are of the form obj = tag obj = self.caller.search(self.lhs, global_search=True) if not obj: return @@ -2550,7 +2684,7 @@ class CmdTag(COMMAND_DEFAULT_CLASS): categories = [" (category: %s)" % tup[1] if tup[1] else "" for tup in tagtuples] if ntags: string = "Tag%s on %s: %s" % ("s" if ntags > 1 else "", obj, - ", ".join("'%s'%s" % (tags[i], categories[i]) for i in range(ntags))) + ", ".join("'%s'%s" % (tags[i], categories[i]) for i in range(ntags))) else: string = "No tags attached to %s." % obj self.caller.msg(string) @@ -2561,6 +2695,7 @@ class CmdTag(COMMAND_DEFAULT_CLASS): # Reload the server and the prototypes should be available. # + class CmdSpawn(COMMAND_DEFAULT_CLASS): """ spawn objects from prototype @@ -2598,18 +2733,17 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): """ key = "@spawn" - aliases = ["spawn"] - locks = "cmd:perm(spawn) or perm(Builders)" + locks = "cmd:perm(spawn) or perm(Builder)" help_category = "Building" def func(self): - "Implements the spawner" + """Implements the spawner""" def _show_prototypes(prototypes): - "Helper to show a list of available prototypes" + """Helper to show a list of available prototypes""" prots = ", ".join(sorted(prototypes.keys())) - return "\nAvailable prototypes (case sensistive): %s" % \ - ("\n" + utils.fill(prots) if prots else "None") + return "\nAvailable prototypes (case sensitive): %s" % ( + "\n" + utils.fill(prots) if prots else "None") prototypes = spawn(return_prototypes=True) if not self.args: @@ -2629,7 +2763,6 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): self.caller.msg(string) return - if isinstance(prototype, basestring): # A prototype key keystr = prototype @@ -2640,17 +2773,16 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): return elif isinstance(prototype, dict): # we got the prototype on the command line. We must make sure to not allow - # the 'exec' key unless we are immortals or higher. - if "exec" in prototype and not self.caller.check_permstring("Immortals"): + # the 'exec' key unless we are developers or higher. + if "exec" in prototype and not self.caller.check_permstring("Developer"): self.caller.msg("Spawn aborted: You don't have access to use the 'exec' prototype key.") return else: self.caller.msg("The prototype must be a prototype key or a Python dictionary.") return - if not "noloc" in self.switches and not "location" in prototype: + if "noloc" not in self.switches and "location" not in prototype: prototype["location"] = self.caller.location for obj in spawn(prototype): self.caller.msg("Spawned %s." % obj.get_display_name(self.caller)) - diff --git a/evennia/commands/default/cmdset_player.py b/evennia/commands/default/cmdset_account.py similarity index 60% rename from evennia/commands/default/cmdset_player.py rename to evennia/commands/default/cmdset_account.py index 5ed27ff8a9..4e357a2ce0 100644 --- a/evennia/commands/default/cmdset_player.py +++ b/evennia/commands/default/cmdset_account.py @@ -1,8 +1,8 @@ """ -This is the cmdset for Player (OOC) commands. These are -stored on the Player object and should thus be able to handle getting -a Player object as caller rather than a Character. +This is the cmdset for Account (OOC) commands. These are +stored on the Account object and should thus be able to handle getting +an Account object as caller rather than a Character. Note - in order for session-rerouting (in MULTISESSION_MODE=2) to function, all commands in this cmdset should use the self.msg() @@ -11,33 +11,33 @@ command method rather than caller.msg(). from evennia.commands.cmdset import CmdSet from evennia.commands.default import help, comms, admin, system -from evennia.commands.default import building, player +from evennia.commands.default import building, account -class PlayerCmdSet(CmdSet): +class AccountCmdSet(CmdSet): """ - Implements the player command set. + Implements the account command set. """ - key = "DefaultPlayer" + key = "DefaultAccount" priority = -10 def at_cmdset_creation(self): "Populates the cmdset" - # Player-specific commands - self.add(player.CmdOOCLook()) - self.add(player.CmdIC()) - self.add(player.CmdOOC()) - self.add(player.CmdCharCreate()) - self.add(player.CmdCharDelete()) - #self.add(player.CmdSessions()) - self.add(player.CmdWho()) - self.add(player.CmdOption()) - self.add(player.CmdQuit()) - self.add(player.CmdPassword()) - self.add(player.CmdColorTest()) - self.add(player.CmdQuell()) + # Account-specific commands + self.add(account.CmdOOCLook()) + self.add(account.CmdIC()) + self.add(account.CmdOOC()) + self.add(account.CmdCharCreate()) + self.add(account.CmdCharDelete()) + # self.add(account.CmdSessions()) + self.add(account.CmdWho()) + self.add(account.CmdOption()) + self.add(account.CmdQuit()) + self.add(account.CmdPassword()) + self.add(account.CmdColorTest()) + self.add(account.CmdQuell()) # testing self.add(building.CmdExamine()) @@ -52,7 +52,7 @@ class PlayerCmdSet(CmdSet): self.add(system.CmdPy()) # Admin commands - self.add(admin.CmdDelPlayer()) + self.add(admin.CmdDelAccount()) self.add(admin.CmdNewPassword()) # Comm commands diff --git a/evennia/commands/default/cmdset_character.py b/evennia/commands/default/cmdset_character.py index bb0d2e343b..cfc8a30ca4 100644 --- a/evennia/commands/default/cmdset_character.py +++ b/evennia/commands/default/cmdset_character.py @@ -1,14 +1,15 @@ """ This module ties together all the commands default Character objects have available (i.e. IC commands). Note that some commands, such as -communication-commands are instead put on the player level, in the -Player cmdset. Player commands remain available also to Characters. +communication-commands are instead put on the account level, in the +Account cmdset. Account commands remain available also to Characters. """ 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): """ Implements the default command set. @@ -25,7 +26,7 @@ class CharacterCmdSet(CmdSet): self.add(general.CmdInventory()) self.add(general.CmdPose()) self.add(general.CmdNick()) - self.add(general.CmdDesc()) + self.add(general.CmdSetDesc()) self.add(general.CmdGet()) self.add(general.CmdDrop()) self.add(general.CmdGive()) @@ -41,12 +42,12 @@ class CharacterCmdSet(CmdSet): self.add(system.CmdPy()) self.add(system.CmdScripts()) self.add(system.CmdObjects()) - self.add(system.CmdPlayers()) + self.add(system.CmdAccounts()) self.add(system.CmdService()) self.add(system.CmdAbout()) self.add(system.CmdTime()) self.add(system.CmdServerLoad()) - #self.add(system.CmdPs()) + # self.add(system.CmdPs()) self.add(system.CmdTickers()) # Admin commands diff --git a/evennia/commands/default/cmdset_session.py b/evennia/commands/default/cmdset_session.py index 8095d3119c..c5a7821241 100644 --- a/evennia/commands/default/cmdset_session.py +++ b/evennia/commands/default/cmdset_session.py @@ -2,7 +2,8 @@ This module stores session-level commands. """ from evennia.commands.cmdset import CmdSet -from evennia.commands.default import player +from evennia.commands.default import account + class SessionCmdSet(CmdSet): """ @@ -13,4 +14,4 @@ class SessionCmdSet(CmdSet): def at_cmdset_creation(self): "Populate the cmdset" - self.add(player.CmdSessions()) + self.add(account.CmdSessions()) diff --git a/evennia/commands/default/comms.py b/evennia/commands/default/comms.py index e2945c700f..37a4934d16 100644 --- a/evennia/commands/default/comms.py +++ b/evennia/commands/default/comms.py @@ -2,16 +2,16 @@ Comsystem command module. Comm commands are OOC commands and intended to be made available to -the Player at all times (they go into the PlayerCmdSet). So we -make sure to homogenize self.caller to always be the player object +the Account at all times (they go into the AccountCmdSet). So we +make sure to homogenize self.caller to always be the account object for easy handling. """ from past.builtins import cmp from django.conf import settings from evennia.comms.models import ChannelDB, Msg -from evennia.players.models import PlayerDB -from evennia.players import bots +from evennia.accounts.models import AccountDB +from evennia.accounts import bots from evennia.comms.channelhandler import CHANNELHANDLER from evennia.locks.lockhandler import LockException from evennia.utils import create, utils, evtable @@ -69,14 +69,14 @@ class CmdAddCom(COMMAND_DEFAULT_CLASS): locks = "cmd:not pperm(channel_banned)" # this is used by the COMMAND_DEFAULT_CLASS parent - player_caller = True + account_caller = True def func(self): """Implement the command""" caller = self.caller args = self.args - player = caller + account = caller if not args: self.msg("Usage: addcom [alias =] channelname.") @@ -96,21 +96,21 @@ class CmdAddCom(COMMAND_DEFAULT_CLASS): return # check permissions - if not channel.access(player, 'listen'): + if not channel.access(account, 'listen'): self.msg("%s: You are not allowed to listen to this channel." % channel.key) return string = "" - if not channel.has_connection(player): + if not channel.has_connection(account): # we want to connect as well. - if not channel.connect(player): - # if this would have returned True, the player is connected + if not channel.connect(account): + # if this would have returned True, the account is connected self.msg("%s: You are not allowed to join this channel." % channel.key) return else: string += "You now listen to the channel %s. " % channel.key else: - if channel.unmute(player): + if channel.unmute(account): string += "You unmute channel %s." % channel.key else: string += "You are already connected to channel %s." % channel.key @@ -145,13 +145,13 @@ class CmdDelCom(COMMAND_DEFAULT_CLASS): locks = "cmd:not perm(channel_banned)" # this is used by the COMMAND_DEFAULT_CLASS parent - player_caller = True + account_caller = True def func(self): """Implementing the command. """ caller = self.caller - player = caller + account = caller if not self.args: self.msg("Usage: delcom ") @@ -161,7 +161,7 @@ class CmdDelCom(COMMAND_DEFAULT_CLASS): channel = find_channel(caller, ostring, silent=True, noaliases=True) if channel: # we have given a channel name - unsubscribe - if not channel.has_connection(player): + if not channel.has_connection(account): self.msg("You are not listening to that channel.") return chkey = channel.key.lower() @@ -171,7 +171,7 @@ class CmdDelCom(COMMAND_DEFAULT_CLASS): for nick in [nick for nick in make_iter(caller.nicks.get(category="channel", return_obj=True)) if nick and nick.pk and nick.value[3].lower() == chkey]: nick.delete() - disconnect = channel.disconnect(player) + disconnect = channel.disconnect(account) if disconnect: wipednicks = " Eventual aliases were removed." if delnicks else "" self.msg("You stop listening to channel '%s'.%s" % (channel.key, wipednicks)) @@ -209,7 +209,7 @@ class CmdAllCom(COMMAND_DEFAULT_CLASS): help_category = "Comms" # this is used by the COMMAND_DEFAULT_CLASS parent - player_caller = True + account_caller = True def func(self): """Runs the function""" @@ -268,12 +268,12 @@ class CmdChannels(COMMAND_DEFAULT_CLASS): Use addcom/delcom to join and leave channels """ key = "@channels" - aliases = ["@clist", "channels", "comlist", "chanlist", "channellist", "all channels"] + aliases = ["@clist", "comlist", "chanlist", "channellist", "all channels"] help_category = "Comms" locks = "cmd: not pperm(channel_banned)" # this is used by the COMMAND_DEFAULT_CLASS parent - player_caller = True + account_caller = True def func(self): """Implement function""" @@ -297,7 +297,7 @@ class CmdChannels(COMMAND_DEFAULT_CLASS): clower = chan.key.lower() nicks = caller.nicks.get(category="channel", return_obj=True) comtable.add_row(*["%s%s" % (chan.key, chan.aliases.all() and - "(%s)" % ",".join(chan.aliases.all()) or ""), + "(%s)" % ",".join(chan.aliases.all()) or ""), "%s" % ",".join(nick.db_key for nick in make_iter(nicks) if nick and nick.value[3].lower() == clower), chan.db.desc]) @@ -345,7 +345,7 @@ class CmdCdestroy(COMMAND_DEFAULT_CLASS): locks = "cmd: not pperm(channel_banned)" # this is used by the COMMAND_DEFAULT_CLASS parent - player_caller = True + account_caller = True def func(self): """Destroy objects cleanly.""" @@ -372,15 +372,15 @@ class CmdCdestroy(COMMAND_DEFAULT_CLASS): class CmdCBoot(COMMAND_DEFAULT_CLASS): """ - kick a player from a channel you control + kick an account from a channel you control Usage: - @cboot[/quiet] = [:reason] + @cboot[/quiet] = [:reason] Switches: quiet - don't notify the channel - Kicks a player or object from a channel you control. + Kicks an account or object from a channel you control. """ @@ -389,13 +389,13 @@ class CmdCBoot(COMMAND_DEFAULT_CLASS): help_category = "Comms" # this is used by the COMMAND_DEFAULT_CLASS parent - player_caller = True + account_caller = True def func(self): """implement the function""" if not self.args or not self.rhs: - string = "Usage: @cboot[/quiet] = [:reason]" + string = "Usage: @cboot[/quiet] = [:reason]" self.msg(string) return @@ -404,12 +404,12 @@ class CmdCBoot(COMMAND_DEFAULT_CLASS): return reason = "" if ":" in self.rhs: - playername, reason = self.rhs.rsplit(":", 1) - searchstring = playername.lstrip('*') + accountname, reason = self.rhs.rsplit(":", 1) + searchstring = accountname.lstrip('*') else: searchstring = self.rhs.lstrip('*') - player = self.caller.search(searchstring, player=True) - if not player: + account = self.caller.search(searchstring, account=True) + if not account: return if reason: reason = " (reason: %s)" % reason @@ -417,20 +417,20 @@ class CmdCBoot(COMMAND_DEFAULT_CLASS): string = "You don't control this channel." self.msg(string) return - if player not in channel.db_subscriptions.all(): - string = "Player %s is not connected to channel %s." % (player.key, channel.key) + if not channel.subscriptions.has(account): + string = "Account %s is not connected to channel %s." % (account.key, channel.key) self.msg(string) return if "quiet" not in self.switches: - string = "%s boots %s from channel.%s" % (self.caller, player.key, reason) + string = "%s boots %s from channel.%s" % (self.caller, account.key, reason) channel.msg(string) - # find all player's nicks linked to this channel and delete them + # find all account's nicks linked to this channel and delete them for nick in [nick for nick in - player.character.nicks.get(category="channel") or [] + account.character.nicks.get(category="channel") or [] if nick.value[3].lower() == channel.key]: nick.delete() - # disconnect player - channel.disconnect(player) + # disconnect account + channel.disconnect(account) CHANNELHANDLER.update() @@ -453,11 +453,11 @@ class CmdCemit(COMMAND_DEFAULT_CLASS): key = "@cemit" aliases = ["@cmsg"] - locks = "cmd: not pperm(channel_banned) and pperm(Players)" + locks = "cmd: not pperm(channel_banned) and pperm(Player)" help_category = "Comms" # this is used by the COMMAND_DEFAULT_CLASS parent - player_caller = True + account_caller = True def func(self): """Implement function""" @@ -496,7 +496,7 @@ class CmdCWho(COMMAND_DEFAULT_CLASS): help_category = "Comms" # this is used by the COMMAND_DEFAULT_CLASS parent - player_caller = True + account_caller = True def func(self): """implement function""" @@ -530,11 +530,11 @@ class CmdChannelCreate(COMMAND_DEFAULT_CLASS): key = "@ccreate" aliases = "channelcreate" - locks = "cmd:not pperm(channel_banned) and pperm(Players)" + locks = "cmd:not pperm(channel_banned) and pperm(Player)" help_category = "Comms" # this is used by the COMMAND_DEFAULT_CLASS parent - player_caller = True + account_caller = True def func(self): """Implement the command""" @@ -587,7 +587,7 @@ class CmdClock(COMMAND_DEFAULT_CLASS): help_category = "Comms" # this is used by the COMMAND_DEFAULT_CLASS parent - player_caller = True + account_caller = True def func(self): """run the function""" @@ -614,7 +614,7 @@ class CmdClock(COMMAND_DEFAULT_CLASS): # Try to add the lock try: channel.locks.add(self.rhs) - except LockException, err: + except LockException as err: self.msg(err) return string = "Lock(s) applied. " @@ -639,7 +639,7 @@ class CmdCdesc(COMMAND_DEFAULT_CLASS): help_category = "Comms" # this is used by the COMMAND_DEFAULT_CLASS parent - player_caller = True + account_caller = True def func(self): """Implement command""" @@ -666,10 +666,10 @@ class CmdCdesc(COMMAND_DEFAULT_CLASS): class CmdPage(COMMAND_DEFAULT_CLASS): """ - send a private message to another player + send a private message to another account Usage: - page[/switches] [,,... = ] + page[/switches] [,,... = ] tell '' page @@ -687,12 +687,12 @@ class CmdPage(COMMAND_DEFAULT_CLASS): help_category = "Comms" # this is used by the COMMAND_DEFAULT_CLASS parent - player_caller = True + account_caller = True def func(self): """Implement function using the Msg methods""" - # Since player_caller is set above, this will be a Player. + # Since account_caller is set above, this will be an Account. caller = self.caller # get the messages we've sent (not to channels) @@ -718,7 +718,7 @@ class CmdPage(COMMAND_DEFAULT_CLASS): try: number = int(self.args) except ValueError: - self.msg("Usage: tell [ = msg]") + self.msg("Usage: tell [ = msg]") return if len(pages) > number: @@ -767,7 +767,7 @@ class CmdPage(COMMAND_DEFAULT_CLASS): self.msg("Noone found to page.") return - header = "|wPlayer|n |c%s|n |wpages:|n" % caller.key + header = "|wAccount|n |c%s|n |wpages:|n" % caller.key message = self.rhs # if message begins with a :, we assume it is a 'page-pose' @@ -778,7 +778,7 @@ class CmdPage(COMMAND_DEFAULT_CLASS): create.create_message(caller, message, receivers=recobjs) - # tell the players they got a message. + # tell the accounts they got a message. received = [] rstrings = [] for pobj in recobjs: @@ -805,7 +805,7 @@ def _list_bots(): bots (str): A table of bots or an error message. """ - ircbots = [bot for bot in PlayerDB.objects.filter(db_is_bot=True, username__startswith="ircbot-")] + ircbots = [bot for bot in AccountDB.objects.filter(db_is_bot=True, username__startswith="ircbot-")] if ircbots: from evennia.utils.evtable import EvTable table = EvTable("|w#dbref|n", "|wbotname|n", "|wev-channel|n", @@ -836,7 +836,7 @@ class CmdIRC2Chan(COMMAND_DEFAULT_CLASS): Example: @irc2chan myircchan = irc.dalnet.net 6667 #mychannel evennia-bot - @irc2chan public = irc.freenode.net 6667 #evgaming #evbot:players.mybot.MyBot + @irc2chan public = irc.freenode.net 6667 #evgaming #evbot:accounts.mybot.MyBot This creates an IRC bot that connects to a given IRC network and channel. If a custom typeclass path is given, this will be used @@ -850,7 +850,7 @@ class CmdIRC2Chan(COMMAND_DEFAULT_CLASS): """ key = "@irc2chan" - locks = "cmd:serversetting(IRC_ENABLED) and pperm(Immortals)" + locks = "cmd:serversetting(IRC_ENABLED) and pperm(Developer)" help_category = "Comms" def func(self): @@ -868,11 +868,11 @@ class CmdIRC2Chan(COMMAND_DEFAULT_CLASS): if 'disconnect' in self.switches or 'remove' in self.switches or 'delete' in self.switches: botname = "ircbot-%s" % self.lhs - matches = PlayerDB.objects.filter(db_is_bot=True, username=botname) + matches = AccountDB.objects.filter(db_is_bot=True, username=botname) dbref = utils.dbref(self.lhs) if not matches and dbref: # try dbref match - matches = PlayerDB.objects.filter(db_is_bot=True, id=dbref) + matches = AccountDB.objects.filter(db_is_bot=True, id=dbref) if matches: matches[0].delete() self.msg("IRC connection destroyed.") @@ -890,7 +890,7 @@ class CmdIRC2Chan(COMMAND_DEFAULT_CLASS): self.rhs = self.rhs.replace('#', ' ') # to avoid Python comment issues try: irc_network, irc_port, irc_channel, irc_botname = \ - [part.strip() for part in self.rhs.split(None, 4)] + [part.strip() for part in self.rhs.split(None, 4)] irc_channel = "#%s" % irc_channel except Exception: string = "IRC bot definition '%s' is not valid." % self.rhs @@ -906,16 +906,16 @@ class CmdIRC2Chan(COMMAND_DEFAULT_CLASS): irc_ssl = "ssl" in self.switches # create a new bot - bot = PlayerDB.objects.filter(username__iexact=botname) + bot = AccountDB.objects.filter(username__iexact=botname) if bot: # re-use an existing bot bot = bot[0] if not bot.is_bot: - self.msg("Player '%s' already exists and is not a bot." % botname) + self.msg("Account '%s' already exists and is not a bot." % botname) return else: try: - bot = create.create_player(botname, None, None, typeclass=botclass) + bot = create.create_account(botname, None, None, typeclass=botclass) except Exception as err: self.msg("|rError, could not create the bot:|n '%s'." % err) return @@ -943,7 +943,7 @@ class CmdIRCStatus(COMMAND_DEFAULT_CLASS): """ key = "@ircstatus" - locks = "cmd:serversetting(IRC_ENABLED) and perm(ircstatus) or perm(Builders))" + locks = "cmd:serversetting(IRC_ENABLED) and perm(ircstatus) or perm(Builder))" help_category = "Comms" def func(self): @@ -963,7 +963,7 @@ class CmdIRCStatus(COMMAND_DEFAULT_CLASS): return matches = None if utils.dbref(botname): - matches = PlayerDB.objects.filter(db_is_bot=True, id=utils.dbref(botname)) + matches = AccountDB.objects.filter(db_is_bot=True, id=utils.dbref(botname)) if not matches: self.msg("No matching IRC-bot found. Use @ircstatus without arguments to list active bots.") return @@ -981,7 +981,7 @@ class CmdIRCStatus(COMMAND_DEFAULT_CLASS): # an asynchronous call. self.caller.msg("Requesting nicklist from %s (%s:%s)." % (channel, network, port)) ircbot.get_nicklist(self.caller) - elif self.caller.locks.check_lockstring(self.caller, "dummy:perm(ircstatus) or perm(Immortals)"): + elif self.caller.locks.check_lockstring(self.caller, "dummy:perm(ircstatus) or perm(Developer)"): # reboot the client self.caller.msg("Forcing a disconnect + reconnect of %s." % chtext) ircbot.reconnect() @@ -1016,7 +1016,7 @@ class CmdRSS2Chan(COMMAND_DEFAULT_CLASS): """ key = "@rss2chan" - locks = "cmd:serversetting(RSS_ENABLED) and pperm(Immortals)" + locks = "cmd:serversetting(RSS_ENABLED) and pperm(Developer)" help_category = "Comms" def func(self): @@ -1038,7 +1038,7 @@ class CmdRSS2Chan(COMMAND_DEFAULT_CLASS): if 'list' in self.switches: # show all connections - rssbots = [bot for bot in PlayerDB.objects.filter(db_is_bot=True, username__startswith="rssbot-")] + rssbots = [bot for bot in AccountDB.objects.filter(db_is_bot=True, username__startswith="rssbot-")] if rssbots: from evennia.utils.evtable import EvTable table = EvTable("|wdbid|n", "|wupdate rate|n", "|wev-channel", @@ -1052,10 +1052,10 @@ class CmdRSS2Chan(COMMAND_DEFAULT_CLASS): if 'disconnect' in self.switches or 'remove' in self.switches or 'delete' in self.switches: botname = "rssbot-%s" % self.lhs - matches = PlayerDB.objects.filter(db_is_bot=True, db_key=botname) + matches = AccountDB.objects.filter(db_is_bot=True, db_key=botname) if not matches: # try dbref match - matches = PlayerDB.objects.filter(db_is_bot=True, id=self.args.lstrip("#")) + matches = AccountDB.objects.filter(db_is_bot=True, id=self.args.lstrip("#")) if matches: matches[0].delete() self.msg("RSS connection destroyed.") @@ -1072,14 +1072,14 @@ class CmdRSS2Chan(COMMAND_DEFAULT_CLASS): botname = "rssbot-%s" % url # create a new bot - bot = PlayerDB.objects.filter(username__iexact=botname) + bot = AccountDB.objects.filter(username__iexact=botname) if bot: # re-use existing bot bot = bot[0] if not bot.is_bot: - self.msg("Player '%s' already exists and is not a bot." % botname) + self.msg("Account '%s' already exists and is not a bot." % botname) return else: - bot = create.create_player(botname, None, None, typeclass=bots.RSSBot) + bot = create.create_account(botname, None, None, typeclass=bots.RSSBot) bot.start(ev_channel=channel, rss_url=url, rss_rate=10) self.msg("RSS reporter created. Fetching RSS.") diff --git a/evennia/commands/default/general.py b/evennia/commands/default/general.py index 18bd5d0dde..81f7f12729 100644 --- a/evennia/commands/default/general.py +++ b/evennia/commands/default/general.py @@ -1,6 +1,7 @@ """ General Character commands usually available to all characters """ +import re from django.conf import settings from evennia.utils import utils, evtable from evennia.typeclasses.attributes import NickTemplateInvalid @@ -9,7 +10,7 @@ COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS) # limit symbol import for API __all__ = ("CmdHome", "CmdLook", "CmdNick", - "CmdInventory", "CmdGet", "CmdDrop", "CmdGive", + "CmdInventory", "CmdSetDesc", "CmdGet", "CmdDrop", "CmdGive", "CmdSay", "CmdWhisper", "CmdPose", "CmdAccess") @@ -24,7 +25,7 @@ class CmdHome(COMMAND_DEFAULT_CLASS): """ key = "home" - locks = "cmd:perm(home) or perm(Builders)" + locks = "cmd:perm(home) or perm(Builder)" arg_regex = r"$" def func(self): @@ -47,7 +48,7 @@ class CmdLook(COMMAND_DEFAULT_CLASS): Usage: look look - look * + look * Observes your location or objects in your vicinity. """ @@ -67,7 +68,7 @@ class CmdLook(COMMAND_DEFAULT_CLASS): caller.msg("You have no location to look at!") return else: - target = caller.search(self.args, use_dbref=caller.check_permstring("Builders")) + target = caller.search(self.args) if not target: return self.msg(caller.at_look(target)) @@ -75,37 +76,41 @@ class CmdLook(COMMAND_DEFAULT_CLASS): class CmdNick(COMMAND_DEFAULT_CLASS): """ - define a personal alias/nick + define a personal alias/nick by defining a string to + match and replace it with another on the fly Usage: nick[/switches] [= [replacement_string]] nick[/switches]