mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Merge branch 'develop' into global-scripts-raise
This commit is contained in:
commit
d1981bd15b
62 changed files with 1995 additions and 818 deletions
8
.github/ISSUE_TEMPLATE/bug-report-develop.md
vendored
8
.github/ISSUE_TEMPLATE/bug-report-develop.md
vendored
|
|
@ -8,7 +8,7 @@ assignees: ''
|
|||
---
|
||||
|
||||
#### Describe the bug
|
||||
(This is for bugs in the develop-branch only. Make sure you test with the latest version.)
|
||||
<!--(This is for bugs in the develop-branch only. Make sure you test with the latest version.)-->
|
||||
|
||||
#### To Reproduce
|
||||
Steps to reproduce the behavior:
|
||||
|
|
@ -18,10 +18,10 @@ Steps to reproduce the behavior:
|
|||
4. See error
|
||||
|
||||
#### Expected behavior
|
||||
(Replace with a clear and concise description of what you expected to happen.)
|
||||
<!--(Replace with a clear and concise description of what you expected to happen.-->)
|
||||
|
||||
#### Develop-branch commit
|
||||
(The commit-hash. If unsure, run `evennia -v` or get the first few lines of the `about` command in-game.)
|
||||
<!--(The commit-hash. If unsure, run `evennia -v` or get the first few lines of the `about` command in-game.)-->
|
||||
|
||||
#### Additional context
|
||||
(Replace with any other context about the problem, or ideas on how to solve.)
|
||||
<!--(Replace with any other context about the problem, or ideas on how to solve.)-->
|
||||
|
|
|
|||
10
.github/ISSUE_TEMPLATE/bug-report.md
vendored
10
.github/ISSUE_TEMPLATE/bug-report.md
vendored
|
|
@ -8,20 +8,20 @@ assignees: ''
|
|||
---
|
||||
|
||||
#### Describe the bug
|
||||
(Replace with a clear and concise description of what the bug is.)
|
||||
<!--(Replace with a clear and concise description of what the bug is.)-->
|
||||
|
||||
#### To Reproduce
|
||||
Steps to reproduce the behavior:
|
||||
1.
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4. See error
|
||||
|
||||
#### Expected behavior
|
||||
(Replace with a clear and concise description of what you expected to happen.)
|
||||
<!--(Replace with a clear and concise description of what you expected to happen.)-->
|
||||
|
||||
#### Environment, Evennia version, OS etc
|
||||
(Replace with info. If unsure, run `evennia -v` or get the first few lines of the `about` command in-game.)
|
||||
<!--(Replace with info. If unsure, run `evennia -v` or get the first few lines of the `about` command in-game.)-->
|
||||
|
||||
#### Additional context
|
||||
(Replace with any other context about the problem, or ideas on how to solve.)
|
||||
<!--(Replace with any other context about the problem, or ideas on how to solve.)-->
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ assignees: ''
|
|||
---
|
||||
|
||||
#### Existing page / new
|
||||
(Link to existing documentation page or proposed name of new page)
|
||||
<!--(Link to existing documentation page or proposed name of new page)-->
|
||||
|
||||
#### Documentation issue
|
||||
(Replace with the description of what the issue is or motivate a changes/additions)
|
||||
<!--(Replace with the description of what the issue is or motivate a changes/additions)-->
|
||||
|
||||
#### Suggested change
|
||||
(Enter the suggested change here)
|
||||
<!--(Enter the suggested change here)-->
|
||||
|
|
|
|||
8
.github/ISSUE_TEMPLATE/feature-request.md
vendored
8
.github/ISSUE_TEMPLATE/feature-request.md
vendored
|
|
@ -8,13 +8,13 @@ assignees: ''
|
|||
---
|
||||
|
||||
#### Is your feature request related to a problem? Please describe.
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
<!--A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]-->
|
||||
|
||||
#### Describe the solution you'd like
|
||||
A clear and concise description of what you want to happen.
|
||||
<!--A clear and concise description of what you want to happen.-->
|
||||
|
||||
#### Describe alternatives you've considered
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
<!--A clear and concise description of any alternative solutions or features you've considered.-->
|
||||
|
||||
#### Additional context
|
||||
Add any other context or screenshots about the feature request here.
|
||||
<!--Add any other context or screenshots about the feature request here.-->
|
||||
|
|
|
|||
28
.github/workflows/github_action_test_suite.yml
vendored
28
.github/workflows/github_action_test_suite.yml
vendored
|
|
@ -107,7 +107,6 @@ jobs:
|
|||
pip install psycopg2-binary==2.8.6 # fix issue for Django 2.2
|
||||
pip install mysqlclient
|
||||
pip install coveralls
|
||||
pip install codacy-coverage
|
||||
pip install tblib
|
||||
pip install -e .
|
||||
|
||||
|
|
@ -122,6 +121,8 @@ jobs:
|
|||
evennia migrate
|
||||
evennia collectstatic --noinput
|
||||
|
||||
# OBS - it's important to not run the coverage tests with --parallel, it messes up the coverage
|
||||
# calculation!
|
||||
- name: Run test suite with coverage
|
||||
if: matrix.TESTING_DB == 'sqlite3' && matrix.python-version == '3.10'
|
||||
working-directory: testing_mygame
|
||||
|
|
@ -131,12 +132,13 @@ jobs:
|
|||
--omit=*/migrations/*,*/urls.py,*/test*.py,*.sh,*.txt,*.md,*.pyc,*.service \
|
||||
../bin/unix/evennia test \
|
||||
--settings=settings \
|
||||
--keepdb \
|
||||
--parallel 4 \
|
||||
--timing \
|
||||
evennia
|
||||
coverage xml
|
||||
coverage --version
|
||||
coverage report | grep TOTAL
|
||||
|
||||
# For other runs, run tests in parallel
|
||||
- name: Run test suite
|
||||
if: matrix.TESTING_DB != 'sqlite3' || matrix.python-version != '3.10'
|
||||
working-directory: testing_mygame
|
||||
|
|
@ -148,7 +150,7 @@ jobs:
|
|||
--timing \
|
||||
evennia
|
||||
|
||||
# we only want to run coverall/codacy once, so we only do it for one of the matrix combinations
|
||||
# we only want to run coverall once, so we only do it for one of the matrix combinations
|
||||
# it's also not critical if pushing to either service fails (happens for PRs since env is not
|
||||
# available outside of the evennia org)
|
||||
- name: Send data to Coveralls
|
||||
|
|
@ -160,27 +162,19 @@ jobs:
|
|||
cd testing_mygame
|
||||
coveralls
|
||||
|
||||
- name: Send data to Codacy
|
||||
if: ${{ matrix.TESTING_DB == 'sqlite3' && matrix.python-version == '3.10' }}
|
||||
continue-on-error: true
|
||||
uses: codacy/codacy-coverage-reporter-action@master
|
||||
with:
|
||||
project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
|
||||
coverage-reports: ./testing_mygame/coverage.xml
|
||||
|
||||
# docker setup and push
|
||||
-
|
||||
name: Set up QEMU
|
||||
if: matrix.TESTING_DB == 'sqlite3' && matrix.python-version == '3.10'
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
if: matrix.TESTING_DB == 'sqlite3' && matrix.python-version == '3.10'
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: matrix.TESTING_DB == 'sqlite3' && matrix.python-version == '3.10' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop')
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
|
@ -188,7 +182,7 @@ jobs:
|
|||
name: Build and push for master
|
||||
if: matrix.TESTING_DB == 'sqlite3' && matrix.python-version == '3.9' && github.ref == 'refs/heads/master'
|
||||
id: docker_build_master
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
push: true
|
||||
tags: evennia/evennia:latest
|
||||
|
|
@ -196,7 +190,7 @@ jobs:
|
|||
name: Build and push for develop
|
||||
if: matrix.TESTING_DB == 'sqlite3' && matrix.python-version == '3.10' && github.ref == 'refs/heads/develop'
|
||||
id: docker_build_develop
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
push: true
|
||||
tags: evennia/evennia:develop
|
||||
|
|
|
|||
|
|
@ -205,6 +205,8 @@ Up requirements to Django 4.0+, Twisted 22+, Python 3.9 or 3.10
|
|||
- Make setting `MAX_NR_CHARACTERS` interact better with the new settings above.
|
||||
- Allow `$search` funcparser func to search tags and to accept kwargs for more
|
||||
powerful searches passed into the regular search functions.
|
||||
- `spawner.spawn` and linked methods now has a kwarg `protfunc_raise_errors`
|
||||
(default True) to disable strict errors on malformed/not-found protfuncs
|
||||
|
||||
## Evennia 0.9.5
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,17 @@
|
|||
|
||||
## 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.
|
||||
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:
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
|
|
@ -16,31 +22,54 @@ Examples of behavior that contributes to creating a positive environment include
|
|||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* 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
|
||||
* 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 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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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]
|
||||
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/
|
||||
|
|
|
|||
|
|
@ -1,7 +1,17 @@
|
|||
# Contributing to Evennia
|
||||
|
||||
Evennia utilizes GitHub for issue tracking and contributions:
|
||||
There are many ways you can contribute to Evennia development:
|
||||
|
||||
- Reporting Issues issues/bugs and making feature requests can be done [in the issue tracker](https://github.com/evennia/evennia/issues).
|
||||
- Evennia's documentation is a [wiki](https://github.com/evennia/evennia/wiki) that everyone can contribute to. Further
|
||||
instructions and details about contributing are found [here](https://github.com/evennia/evennia/wiki/Contributing).
|
||||
- You can help a lot by being active in the community. You can spread
|
||||
the word - by writing, talking, blogging etc about Evennia. Let
|
||||
others know text-based gaming is still a thing!
|
||||
- You can help by reporting any issues/bugs you find, and tell us of your
|
||||
feature requests [as an issue on github][issues]. This is also where you
|
||||
report typos or errors in the [the Evennia documentation][docs].
|
||||
- To help fixing (or expand) our docs, check out [how to contribute to docs][contribute-docs].
|
||||
- To contribute to Evennia itself, check out how to [help with code][helping-code].
|
||||
|
||||
[issues]: https://github.com/evennia/evennia/issues/new/choose
|
||||
[docs]: https://www.evennia.com/docs/1.0-dev/index.html
|
||||
[contribute-docs]: https://www.evennia.com/docs/1.0-dev/Contributing-Docs.html
|
||||
[helping-code]: https://www.evennia.com/docs/1.0-dev/Contributing.html#helping-with-code
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
|
||||
# Evennia installation
|
||||
|
||||
You can find the latest updated installation instructions and
|
||||
requirements [here](https://github.com/evennia/evennia/wiki/Getting-Started).
|
||||
You can find the latest updated installation instructions and
|
||||
requirements
|
||||
[here](https://www.evennia.com/docs/1.0-dev/Setup/Installation.html)
|
||||
|
|
|
|||
|
|
@ -203,6 +203,8 @@ Up requirements to Django 4.0+, Twisted 22+, Python 3.9 or 3.10
|
|||
- Add new setting `MAX_NR_SIMULTANEUS_PUPPETS` - how many puppets the account
|
||||
can run at the same time. Used to limit multi-playing.
|
||||
- Make setting `MAX_NR_CHARACTERS` interact better with the new settings above.
|
||||
- Allow `$search` funcparser func to search tags and to accept kwargs for more
|
||||
powerful searches passed into the regular search functions.
|
||||
|
||||
## Evennia 0.9.5
|
||||
|
||||
|
|
|
|||
|
|
@ -377,7 +377,7 @@ Here's how to tell Evennia to manage the script in settings:
|
|||
|
||||
GLOBAL_SCRIPTS = {
|
||||
"my_script": {
|
||||
"typeclass": "scripts.Weather",
|
||||
"typeclass": "typeclasses.scripts.Weather",
|
||||
"repeats": -1,
|
||||
"interval": 50,
|
||||
"desc": "Weather script"
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ updated after Sept 2022 will be missing some translations.
|
|||
+---------------+----------------------+--------------+
|
||||
| fr | French | Mar 2022 |
|
||||
+---------------+----------------------+--------------+
|
||||
| it | Italian | Feb 2015 |
|
||||
| it | Italian | Oct 2022 |
|
||||
+---------------+----------------------+--------------+
|
||||
| ko | Korean (simplified) | Sep 2019 |
|
||||
+---------------+----------------------+--------------+
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Custom gameime
|
||||
# Custom gametime
|
||||
|
||||
Contrib by vlgeoff, 2017 - based on Griatch's core original
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,13 @@
|
|||
|
||||
Contrib by Griatch 2022
|
||||
|
||||
|
||||
```{warning}
|
||||
NOTE - this tutorial is WIP and NOT complete! It was put on hold to focus on
|
||||
releasing Evennia 1.0. You will still learn things from it, but don't expect
|
||||
perfection.
|
||||
```
|
||||
|
||||
A complete example MUD using Evennia. This is the final result of what is
|
||||
implemented if you follow the Getting-Started tutorial. It's recommended
|
||||
that you follow the tutorial step by step and write your own code. But if
|
||||
|
|
|
|||
73
docs/source/Contribs/Contrib-Git-Integration.md
Normal file
73
docs/source/Contribs/Contrib-Git-Integration.md
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# In-game Git Integration
|
||||
|
||||
Contribution by helpme (2022)
|
||||
|
||||
A module to integrate a stripped-down version of git within the game, allowing developers to view their git status, change branches, and pull updated code of both their local mygame repo and Evennia core. After a successful pull or checkout, the git command will reload the game: Manual restarts may be required to to apply certain changes that would impact persistent scripts etc.
|
||||
|
||||
Once the contrib is set up, integrating remote changes is as simple as entering the following into your game:
|
||||
|
||||
```
|
||||
git pull
|
||||
```
|
||||
|
||||
The repositories you want to work with, be it only your local mygame repo, only Evennia core, or both, must be git directories for the command to function. If you are only interested in using this to get upstream Evennia changes, only the Evennia repository needs to be a git repository. [Get started with version control here.](https://www.evennia.com/docs/1.0-dev/Coding/Version-Control.html)
|
||||
|
||||
## Dependencies
|
||||
|
||||
This package requires the dependency "gitpython", a python library used to interact with git repositories. To install, it's easiest to install Evennia's extra requirements:
|
||||
|
||||
- Activate your `virtualenv`
|
||||
- `cd` to the root of the Evennia repository. There should be an `requirements_extra.txt` file here.
|
||||
- `pip install -r requirements_extra.txt`
|
||||
|
||||
## Installation
|
||||
|
||||
This utility adds a simple assortment of 'git' commands. Import the module into your commands and add it to your command set to make it available.
|
||||
|
||||
Specifically, in `mygame/commands/default_cmdsets.py`:
|
||||
|
||||
```python
|
||||
...
|
||||
from evennia.contrib.utils.git_integration import GitCmdSet # <---
|
||||
|
||||
class CharacterCmdset(default_cmds.Character_CmdSet):
|
||||
...
|
||||
def at_cmdset_creation(self):
|
||||
...
|
||||
self.add(GitCmdSet) # <---
|
||||
|
||||
```
|
||||
|
||||
Then `reload` to make the git command available.
|
||||
|
||||
## Usage
|
||||
|
||||
This utility will only work if the directory you wish to work with is a git directory. If they are not, you will be prompted to initiate your directory as a git repository using the following commands in your terminal:
|
||||
|
||||
```
|
||||
git init
|
||||
git remote add origin 'link to your repository'
|
||||
```
|
||||
|
||||
By default, the git commands are only available to those with Developer permissions and higher. You can change this by overriding the command and setting its locks from "cmd:pperm(Developer)" to the lock of your choice.
|
||||
|
||||
The supported commands are:
|
||||
* git status: An overview of your git repository, which files have been changed locally, and the commit you're on.
|
||||
* git branch: What branches are available for you to check out.
|
||||
* git checkout 'branch': Checkout a branch.
|
||||
* git pull: Pull the latest code from your current branch.
|
||||
|
||||
* All of these commands are also available with 'evennia', to serve the same functionality related to your Evennia directory. So:
|
||||
* git evennia status
|
||||
* git evennia branch
|
||||
* git evennia checkout 'branch'
|
||||
* git evennia pull: Pull the latest Evennia code.
|
||||
|
||||
## Settings Used
|
||||
|
||||
The utility uses the existing GAME_DIR and EVENNIA_DIR settings from settings.py. You should not need to alter these if you have a standard directory setup, they ought to exist without any setup required from you.
|
||||
|
||||
----
|
||||
|
||||
<small>This document page is generated from `evennia/contrib/utils/git_integration/README.md`. Changes to this
|
||||
file will be overwritten, so edit that file rather than this one.</small>
|
||||
|
|
@ -53,28 +53,31 @@ Exits: northeast and east
|
|||
(then go back to your mygame/ folder)
|
||||
|
||||
This will install all optional requirements of Evennia.
|
||||
2. Import and add the `evennia.contrib.commands.XYZGridCmdSet` to the
|
||||
2. Import and [add] the `evennia.contrib.grid.xyzgrid.commands.XYZGridCmdSet` to the
|
||||
`CharacterCmdset` cmdset in `mygame/commands.default_cmds.py`. Reload
|
||||
the server. This makes the `map`, `goto/path` and the modified `teleport` and
|
||||
`open` commands available in-game.
|
||||
|
||||
[add]: ../Components/Command-Sets
|
||||
|
||||
3. Edit `mygame/server/conf/settings.py` and add
|
||||
|
||||
EXTRA_LAUNCHER_COMMANDS['xyzgrid'] = 'evennia.contrib.launchcmd.xyzcommand'
|
||||
|
||||
and
|
||||
|
||||
PROTOTYPE_MODULES += [’evennia.contrib.grid.xyzgrid.prototypes’]
|
||||
EXTRA_LAUNCHER_COMMANDS['xyzgrid'] = 'evennia.contrib.grid.xyzgrid.launchcmd.xyzcommand'
|
||||
PROTOTYPE_MODULES += ['evennia.contrib.grid.xyzgrid.prototypes']
|
||||
|
||||
This will add the new ability to enter `evennia xyzgrid <option>` on the
|
||||
command line. It will also make the `xyz_room` and `xyz_exit` prototypes
|
||||
available for use as prototype-parents when spawning the grid.
|
||||
|
||||
4. Run `evennia xyzgrid help` for available options.
|
||||
|
||||
5. (Optional): By default, the xyzgrid will only spawn module-based
|
||||
[prototypes](../Components/Prototypes.md). This is an optimization and usually makes sense
|
||||
[prototypes]. This is an optimization and usually makes sense
|
||||
since the grid is entirely defined outside the game anyway. If you want to
|
||||
also make use of in-game (db-) created prototypes, add
|
||||
`XYZGRID_USE_DB_PROTOTYPES = True` to settings.
|
||||
|
||||
[prototypes]: ../Components/Prototypes
|
||||
|
||||
## Overview
|
||||
|
||||
|
|
|
|||
|
|
@ -609,11 +609,12 @@ character make small verbal observations at irregular intervals.
|
|||
|
||||
_Contrib by Griatch 2022_
|
||||
|
||||
A complete example MUD using Evennia. This is the final result of what is
|
||||
implemented if you follow the Getting-Started tutorial. It's recommended
|
||||
that you follow the tutorial step by step and write your own code. But if
|
||||
you prefer you can also pick apart or use this as a starting point for your
|
||||
own game.
|
||||
|
||||
```{warning}
|
||||
NOTE - this tutorial is WIP and NOT complete! It was put on hold to focus on
|
||||
releasing Evennia 1.0. You will still learn things from it, but don't expect
|
||||
perfection.
|
||||
```
|
||||
|
||||
[Read the documentation](./Contrib-Evadventure.md) - [Browse the Code](evennia.contrib.tutorials.evadventure)
|
||||
|
||||
|
|
@ -680,6 +681,7 @@ and more._
|
|||
|
||||
Contrib-Auditing.md
|
||||
Contrib-Fieldfill.md
|
||||
Contrib-Git-Integration.md
|
||||
Contrib-Name-Generator.md
|
||||
Contrib-Random-String-Generator.md
|
||||
Contrib-Tree-Select.md
|
||||
|
|
@ -714,6 +716,16 @@ to any callable of your choice.
|
|||
|
||||
|
||||
|
||||
### Contrib: `git_integration`
|
||||
|
||||
_Contribution by helpme (2022)_
|
||||
|
||||
A module to integrate a stripped-down version of git within the game, allowing developers to view their git status, change branches, and pull updated code of both their local mygame repo and Evennia core. After a successful pull or checkout, the git command will reload the game: Manual restarts may be required to to apply certain changes that would impact persistent scripts etc.
|
||||
|
||||
[Read the documentation](./Contrib-Git-Integration.md) - [Browse the Code](evennia.contrib.utils.git_integration)
|
||||
|
||||
|
||||
|
||||
### Contrib: `name_generator`
|
||||
|
||||
_Contribution by InspectorCaracal (2022)_
|
||||
|
|
|
|||
|
|
@ -230,12 +230,12 @@ displaying them differently. See next section.
|
|||
As pointed out earlier, the `@time` command is meant to be used with a standard calendar, not a
|
||||
custom one. We can easily create a new command though. We'll call it `time`, as is often the case
|
||||
on other MU*. Here's an example of how we could write it (for the example, you can create a file
|
||||
`showtime.py` in your `commands` directory and paste this code in it):
|
||||
`gametime.py` in your `commands` directory and paste this code in it):
|
||||
|
||||
```python
|
||||
# in a file mygame/commands/gametime.py
|
||||
|
||||
from evennia.contrib import custom_gametime
|
||||
from evennia.contrib.base_systems import custom_gametime
|
||||
|
||||
from commands.command import Command
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# Installing with GIT
|
||||
|
||||
This installs and runs Evennia from its sources. This is required if you want to contribute to Evennia
|
||||
itself or have an easier time exploring the code. See the basic [Installation](./Installation.md) for
|
||||
a quick installation of the library. See the [troubleshooting](./Installation-Troubleshooting.md) if you run
|
||||
This installs and runs Evennia from its sources. This is required if you want to contribute to Evennia
|
||||
itself or have an easier time exploring the code. See the basic [Installation](./Installation.md) for
|
||||
a quick installation of the library. See the [troubleshooting](./Installation-Troubleshooting.md) if you run
|
||||
into trouble.
|
||||
|
||||
```{important}
|
||||
|
|
@ -14,6 +14,12 @@ If you are converting an existing game from a previous version, [see here](./Ins
|
|||
For the impatient. If you have trouble with a step, you should jump on to the
|
||||
more detailed instructions for your platform.
|
||||
|
||||
```{warning}
|
||||
Currently, these instructions will install 'latest' (stable) Evennia, which is
|
||||
the 0.9.5 version. To install 1.0-dev, you need to add a step `git checkout develop` between steps
|
||||
3 and 4 below.
|
||||
```
|
||||
|
||||
1. Install Python, GIT and python-virtualenv. Start a Console/Terminal.
|
||||
2. `cd` to some place you want to do your development (like a folder
|
||||
`/home/anna/muddev/` on Linux or a folder in your personal user directory on Windows).
|
||||
|
|
@ -25,7 +31,7 @@ more detailed instructions for your platform.
|
|||
8. `cd mygame`
|
||||
9. `evennia migrate`
|
||||
10. `evennia start` (make sure to make a superuser when asked)
|
||||
|
||||
|
||||
Evennia should now be running and you can connect to it by pointing a web browser to
|
||||
`http://localhost:4001` or a MUD telnet client to `localhost:4000` (use `127.0.0.1` if your OS does
|
||||
not recognize `localhost`).
|
||||
|
|
@ -99,12 +105,12 @@ Next you can continue initializing your game from the regular [Installation inst
|
|||
The Evennia server is a terminal program. Open the terminal e.g. from
|
||||
*Applications->Utilities->Terminal*. [Here is an introduction to the Mac
|
||||
terminal](https://blog.teamtreehouse.com/introduction-to-the-mac-os-x-command-line)
|
||||
if you are unsure how it works.
|
||||
if you are unsure how it works.
|
||||
|
||||
* Python should already be installed but you must make sure it's a high enough version - go for
|
||||
* Python should already be installed but you must make sure it's a high enough version - go for
|
||||
3.10.
|
||||
([This](https://docs.python-guide.org/en/latest/starting/install/osx/) discusses
|
||||
how you may upgrade it).
|
||||
how you may upgrade it).
|
||||
* GIT can be obtained with
|
||||
[git-osx-installer](https://code.google.com/p/git-osx-installer/) or via MacPorts [as described
|
||||
here](https://git-scm.com/book/en/Getting-Started-Installing-Git#Installing-on-Mac).
|
||||
|
|
@ -133,7 +139,7 @@ Python practice to install into a _virtualenv_. If you are unsure about what a
|
|||
virtualenv is and why it's useful, see the [Glossary entry on virtualenv](../Glossary.md#virtualenv).
|
||||
|
||||
```
|
||||
python3.10 -m venv evenv
|
||||
python3.10 -m venv evenv
|
||||
```
|
||||
A new folder `evenv` will appear (we could have called it anything). This
|
||||
folder will hold a self-contained setup of Python packages without interfering
|
||||
|
|
@ -221,7 +227,7 @@ Python practice to install into a _virtualenv_. If you are unsure about what a
|
|||
virtualenv is and why it's useful, see the [Glossary entry on virtualenv](../Glossary.md#virtualenv).
|
||||
|
||||
```
|
||||
python3.10 -m venv evenv
|
||||
python3.10 -m venv evenv
|
||||
```
|
||||
A new folder `evenv` will appear (we could have called it anything). This
|
||||
folder will hold a self-contained setup of Python packages without interfering
|
||||
|
|
@ -251,4 +257,4 @@ pip install -e evennia
|
|||
|
||||
Test that you can run the `evennia` command everywhere while your virtualenv (evenv) is active.
|
||||
|
||||
Next you can continue initializing your game from the regular [Installation instructions](./Installation.md).
|
||||
Next you can continue initializing your game from the regular [Installation instructions](./Installation.md).
|
||||
|
|
|
|||
|
|
@ -172,7 +172,8 @@ HTTP_LOG_FILE = os.path.join(LOG_DIR, "http_requests.log")
|
|||
LOCKWARNING_LOG_FILE = os.path.join(LOG_DIR, "lockwarnings.log")
|
||||
# Number of lines to append to rotating channel logs when they rotate
|
||||
CHANNEL_LOG_NUM_TAIL_LINES = 20
|
||||
# Max size (in bytes) of channel log files before they rotate
|
||||
# Max size (in bytes) of channel log files before they rotate.
|
||||
# Minimum is 1000 (1kB) but should usually be larger.
|
||||
CHANNEL_LOG_ROTATE_SIZE = 1000000
|
||||
# Unused by default, but used by e.g. the MapSystem contrib. A place for storing
|
||||
# semi-permanent data and avoid it being rebuilt over and over. It is created
|
||||
|
|
@ -605,7 +606,7 @@ OPTIONS_ACCOUNT_DEFAULT = {
|
|||
"column_names_color": ("Table column header text.", "Color", "w"),
|
||||
"help_category_color": ("Help category names.", "Color", "n"),
|
||||
"help_entry_color": ("Help entry names.", "Color", "n"),
|
||||
"timezone": ("Timezone for dates. @tz for a list.", "Timezone", "UTC"),
|
||||
"timezone": ("Timezone for dates.", "Timezone", "UTC"),
|
||||
}
|
||||
# Modules holding Option classes, responsible for serializing the option and
|
||||
# calling validator functions on it. Same-named functions in modules added
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
```{eval-rst}
|
||||
evennia.contrib.utils.git\_integration.git\_integration
|
||||
==============================================================
|
||||
|
||||
.. automodule:: evennia.contrib.utils.git_integration.git_integration
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
```
|
||||
18
docs/source/api/evennia.contrib.utils.git_integration.md
Normal file
18
docs/source/api/evennia.contrib.utils.git_integration.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
```{eval-rst}
|
||||
evennia.contrib.utils.git\_integration
|
||||
==============================================
|
||||
|
||||
.. automodule:: evennia.contrib.utils.git_integration
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 6
|
||||
|
||||
evennia.contrib.utils.git_integration.git_integration
|
||||
evennia.contrib.utils.git_integration.tests
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
```{eval-rst}
|
||||
evennia.contrib.utils.git\_integration.tests
|
||||
===================================================
|
||||
|
||||
.. automodule:: evennia.contrib.utils.git_integration.tests
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
```
|
||||
|
|
@ -13,6 +13,7 @@ evennia.contrib.utils
|
|||
|
||||
evennia.contrib.utils.auditing
|
||||
evennia.contrib.utils.fieldfill
|
||||
evennia.contrib.utils.git_integration
|
||||
evennia.contrib.utils.name_generator
|
||||
evennia.contrib.utils.random_string_generator
|
||||
evennia.contrib.utils.tree_select
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ class AccountDBManager(TypedObjectManager, UserManager):
|
|||
|
||||
Keyword Args:
|
||||
typeclass (str): The typeclass to use for the account.
|
||||
is_superuser (bool): Wether or not this account is to be a superuser
|
||||
is_superuser (bool): Whether or not this account is to be a superuser
|
||||
locks (str): Lockstring.
|
||||
permission (list): List of permission strings.
|
||||
tags (list): List of Tags on form `(key, category[, data])`
|
||||
|
|
|
|||
|
|
@ -748,7 +748,7 @@ def cmdhandler(
|
|||
)
|
||||
if suggestions:
|
||||
sysarg += _(" Maybe you meant {command}?").format(
|
||||
command=utils.list_to_string(suggestions, _("or"), addquote=True)
|
||||
command=utils.list_to_string(suggestions, endsep=_("or"), addquote=True)
|
||||
)
|
||||
else:
|
||||
sysarg += _(' Type "help" for help.')
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ class CmdCharCreate(COMMAND_DEFAULT_CLASS):
|
|||
# check if this Character already exists. Note that we are only
|
||||
# searching the base character typeclass here, not any child
|
||||
# classes.
|
||||
self.msg("|rA character named '|w%s|r' already exists.|n" % key)
|
||||
self.msg(f"|rA character named '|w{key}|r' already exists.|n")
|
||||
return
|
||||
|
||||
# create the character
|
||||
|
|
@ -190,12 +190,10 @@ class CmdCharCreate(COMMAND_DEFAULT_CLASS):
|
|||
elif not new_character.db.desc:
|
||||
new_character.db.desc = "This is a character."
|
||||
self.msg(
|
||||
"Created new character %s. Use |wic %s|n to enter the game as this character."
|
||||
% (new_character.key, new_character.key)
|
||||
f"Created new character {new_character.key}. Use |wic {new_character.key}|n to enter the game as this character."
|
||||
)
|
||||
logger.log_sec(
|
||||
"Character Created: %s (Caller: %s, IP: %s)."
|
||||
% (new_character, account, self.session.address)
|
||||
f"Character Created: {new_character} (Caller: {account}, IP: {self.session.address})."
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -248,10 +246,9 @@ class CmdCharDelete(COMMAND_DEFAULT_CLASS):
|
|||
pc for pc in caller.db._playable_characters if pc != delobj
|
||||
]
|
||||
delobj.delete()
|
||||
self.msg("Character '%s' was permanently deleted." % key)
|
||||
self.msg(f"Character '{key}' was permanently deleted.")
|
||||
logger.log_sec(
|
||||
"Character Deleted: %s (Caller: %s, IP: %s)."
|
||||
% (key, account, self.session.address)
|
||||
f"Character Deleted: {key} (Caller: {account}, IP: {self.session.address})."
|
||||
)
|
||||
else:
|
||||
self.msg("Deletion was aborted.")
|
||||
|
|
@ -372,14 +369,12 @@ class CmdIC(COMMAND_DEFAULT_CLASS):
|
|||
account.puppet_object(session, new_character)
|
||||
account.db._last_puppet = new_character
|
||||
logger.log_sec(
|
||||
"Puppet Success: (Caller: %s, Target: %s, IP: %s)."
|
||||
% (account, new_character, self.session.address)
|
||||
f"Puppet Success: (Caller: {account}, Target: {new_character}, IP: {self.session.address})."
|
||||
)
|
||||
except RuntimeError as exc:
|
||||
self.msg("|rYou cannot become |C%s|n: %s" % (new_character.name, exc))
|
||||
self.msg(f"|rYou cannot become |C{new_character.name}|n: {exc}")
|
||||
logger.log_sec(
|
||||
"Puppet Failed: %s (Caller: %s, Target: %s, IP: %s)."
|
||||
% (exc, account, new_character, self.session.address)
|
||||
f"Puppet Failed: %s (Caller: {account}, Target: {new_character}, IP: {self.session.address})."
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -432,7 +427,7 @@ class CmdOOC(MuxAccountLookCommand):
|
|||
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))
|
||||
self.msg(f"|rCould not unpuppet from |c{old_char}|n: {exc}")
|
||||
|
||||
|
||||
class CmdSessions(COMMAND_DEFAULT_CLASS):
|
||||
|
|
@ -469,7 +464,7 @@ class CmdSessions(COMMAND_DEFAULT_CLASS):
|
|||
char and str(char) or "None",
|
||||
char and str(char.location) or "N/A",
|
||||
)
|
||||
self.msg("|wYour current session(s):|n\n%s" % table)
|
||||
self.msg(f"|wYour current session(s):|n\n{table}")
|
||||
|
||||
|
||||
class CmdWho(COMMAND_DEFAULT_CLASS):
|
||||
|
|
@ -640,7 +635,7 @@ class CmdOption(COMMAND_DEFAULT_CLASS):
|
|||
)
|
||||
row.append("%s%s" % (saved, changed))
|
||||
table.add_row(*row)
|
||||
self.msg("|wClient settings (%s):|n\n%s|n" % (self.session.protocol_key, table))
|
||||
self.msg(f"|wClient settings ({self.session.protocol_key}):|n\n{table}|n")
|
||||
|
||||
return
|
||||
|
||||
|
|
@ -655,7 +650,7 @@ class CmdOption(COMMAND_DEFAULT_CLASS):
|
|||
try:
|
||||
codecs_lookup(new_encoding)
|
||||
except LookupError:
|
||||
raise RuntimeError("The encoding '|w%s|n' is invalid. " % new_encoding)
|
||||
raise RuntimeError(f"The encoding '|w{new_encoding}|n' is invalid. ")
|
||||
return val
|
||||
|
||||
def validate_size(new_size):
|
||||
|
|
@ -670,16 +665,13 @@ class CmdOption(COMMAND_DEFAULT_CLASS):
|
|||
old_val = flags.get(new_name, False)
|
||||
new_val = validator(new_val)
|
||||
if old_val == new_val:
|
||||
self.msg("Option |w%s|n was kept as '|w%s|n'." % (new_name, old_val))
|
||||
self.msg(f"Option |w{new_name}|n was kept as '|w{old_val}|n'.")
|
||||
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)
|
||||
)
|
||||
self.msg(f"Option |w{new_name}|n was changed from '|w{old_val}|n' to '|w{new_val}|n'.")
|
||||
return {new_name: new_val}
|
||||
except Exception as err:
|
||||
self.msg("|rCould not set option |w%s|r:|n %s" % (new_name, err))
|
||||
self.msg(f"|rCould not set option |w{new_name}|r:|n {err}")
|
||||
return False
|
||||
|
||||
validators = {
|
||||
|
|
@ -719,12 +711,12 @@ class CmdOption(COMMAND_DEFAULT_CLASS):
|
|||
saved_options.update(optiondict)
|
||||
self.account.attributes.add("_saved_protocol_flags", saved_options)
|
||||
for key in optiondict:
|
||||
self.msg("|gSaved option %s.|n" % key)
|
||||
self.msg(f"|gSaved option {key}.|n")
|
||||
if "clear" in self.switches:
|
||||
# clear this save
|
||||
for key in optiondict:
|
||||
self.account.attributes.get("_saved_protocol_flags", {}).pop(key, None)
|
||||
self.msg("|gCleared saved %s." % key)
|
||||
self.msg(f"|gCleared saved {key}.")
|
||||
self.session.update_flags(**optiondict)
|
||||
|
||||
|
||||
|
|
@ -767,10 +759,7 @@ class CmdPassword(COMMAND_DEFAULT_CLASS):
|
|||
account.set_password(newpass)
|
||||
account.save()
|
||||
self.msg("Password changed.")
|
||||
logger.log_sec(
|
||||
"Password Changed: %s (Caller: %s, IP: %s)."
|
||||
% (account, account, self.session.address)
|
||||
)
|
||||
logger.log_sec(f"Password Changed: {account} (Caller: {account}, IP: {self.session.address}).")
|
||||
|
||||
|
||||
class CmdQuit(COMMAND_DEFAULT_CLASS):
|
||||
|
|
@ -997,27 +986,27 @@ class CmdQuell(COMMAND_DEFAULT_CLASS):
|
|||
)
|
||||
if self.cmdstring in ("unquell", "unquell"):
|
||||
if not account.attributes.get("_quell"):
|
||||
self.msg("Already using normal Account permissions %s." % permstr)
|
||||
self.msg(f"Already using normal Account permissions {permstr}.")
|
||||
else:
|
||||
account.attributes.remove("_quell")
|
||||
self.msg("Account permissions %s restored." % permstr)
|
||||
self.msg(f"Account permissions {permstr} restored.")
|
||||
else:
|
||||
if account.attributes.get("_quell"):
|
||||
self.msg("Already quelling Account %s permissions." % permstr)
|
||||
self.msg(f"Already quelling Account {permstr} permissions.")
|
||||
return
|
||||
account.attributes.add("_quell", True)
|
||||
puppet = self.session.puppet if self.session else None
|
||||
if puppet:
|
||||
cpermstr = "(%s)" % ", ".join(puppet.permissions.all())
|
||||
cpermstr = "Quelling to current puppet's permissions %s." % cpermstr
|
||||
cpermstr = f"Quelling to current puppet's permissions {cpermstr}."
|
||||
cpermstr += (
|
||||
"\n(Note: If this is higher than Account permissions %s,"
|
||||
" the lowest of the two will be used.)" % permstr
|
||||
f"\n(Note: If this is higher than Account permissions {permstr},"
|
||||
" the lowest of the two will be used.)"
|
||||
)
|
||||
cpermstr += "\nUse unquell to return to normal permission usage."
|
||||
self.msg(cpermstr)
|
||||
else:
|
||||
self.msg("Quelling Account permissions%s. Use unquell to get them back." % permstr)
|
||||
self.msg(f"Quelling Account permissions{permstr}. Use unquell to get them back.")
|
||||
self._recache_locks(account)
|
||||
|
||||
|
||||
|
|
@ -1058,4 +1047,4 @@ class CmdStyle(COMMAND_DEFAULT_CLASS):
|
|||
except ValueError as e:
|
||||
self.msg(str(e))
|
||||
return
|
||||
self.msg("Style %s set to %s" % (self.lhs, result))
|
||||
self.msg(f"Style {self.lhs} set to {result}")
|
||||
|
|
|
|||
|
|
@ -76,12 +76,11 @@ class CmdBoot(COMMAND_DEFAULT_CLASS):
|
|||
# Boot by account object
|
||||
pobj = search.account_search(args)
|
||||
if not pobj:
|
||||
caller.msg("Account %s was not found." % args)
|
||||
caller.msg(f"Account {args} was not found.")
|
||||
return
|
||||
pobj = pobj[0]
|
||||
if not pobj.access(caller, "boot"):
|
||||
string = "You don't have the permission to boot %s." % (pobj.key,)
|
||||
caller.msg(string)
|
||||
caller.msg(f"You don't have the permission to boot {pobj.key}.")
|
||||
return
|
||||
# we have a bootable object with a connected user
|
||||
matches = SESSIONS.sessions_from_account(pobj)
|
||||
|
|
@ -96,9 +95,9 @@ class CmdBoot(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
feedback = None
|
||||
if "quiet" not in self.switches:
|
||||
feedback = "You have been disconnected by %s.\n" % caller.name
|
||||
feedback = f"You have been disconnected by {caller.name}.\n"
|
||||
if reason:
|
||||
feedback += "\nReason given: %s" % reason
|
||||
feedback += f"\nReason given: {reason}"
|
||||
|
||||
for session in boot_list:
|
||||
session.msg(feedback)
|
||||
|
|
@ -106,8 +105,7 @@ class CmdBoot(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
if pobj and boot_list:
|
||||
logger.log_sec(
|
||||
"Booted: %s (Reason: %s, Caller: %s, IP: %s)."
|
||||
% (pobj, reason, caller, self.session.address)
|
||||
f"Booted: {pobj} (Reason: {reason}, Caller: {caller}, IP: {self.session.address})."
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -130,7 +128,7 @@ def list_bans(cmd, banlist):
|
|||
table = cmd.styled_table("|wid", "|wname/ip", "|wdate", "|wreason")
|
||||
for inum, ban in enumerate(banlist):
|
||||
table.add_row(str(inum + 1), ban[0] and ban[0] or ban[1], ban[3], ban[4])
|
||||
return "|wActive bans:|n\n%s" % table
|
||||
return f"|wActive bans:|n\n{table}"
|
||||
|
||||
|
||||
class CmdBan(COMMAND_DEFAULT_CLASS):
|
||||
|
|
@ -227,7 +225,7 @@ class CmdBan(COMMAND_DEFAULT_CLASS):
|
|||
ServerConfig.objects.conf("server_bans", banlist)
|
||||
self.caller.msg(f"{typ}-ban '|w{ban}|n' was added. Use |wunban|n to reinstate.")
|
||||
logger.log_sec(
|
||||
"Banned {typ}: {ban.strip()} (Caller: {self.caller}, IP: {self.session.address})."
|
||||
f"Banned {typ}: {ban.strip()} (Caller: {self.caller}, IP: {self.session.address})."
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -267,7 +265,7 @@ class CmdUnban(COMMAND_DEFAULT_CLASS):
|
|||
if not banlist:
|
||||
self.caller.msg("There are no bans to clear.")
|
||||
elif not (0 < num < len(banlist) + 1):
|
||||
self.caller.msg("Ban id |w%s|x was not found." % self.args)
|
||||
self.caller.msg(f"Ban id |w{self.args}|n was not found.")
|
||||
else:
|
||||
# all is ok, ask, then clear ban
|
||||
ban = banlist[num - 1]
|
||||
|
|
@ -282,7 +280,7 @@ class CmdUnban(COMMAND_DEFAULT_CLASS):
|
|||
ServerConfig.objects.conf("server_bans", banlist)
|
||||
self.caller.msg(f"Cleared ban {num}: '{value}'")
|
||||
logger.log_sec(
|
||||
"Unbanned: {value.strip()} (Caller: {self.caller}, IP: {self.session.address})."
|
||||
f"Unbanned: {value.strip()} (Caller: {self.caller}, IP: {self.session.address})."
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -351,20 +349,20 @@ class CmdEmit(COMMAND_DEFAULT_CLASS):
|
|||
if not obj:
|
||||
return
|
||||
if rooms_only and obj.location is not None:
|
||||
caller.msg("%s is not a room. Ignored." % objname)
|
||||
caller.msg(f"{objname} is not a room. Ignored.")
|
||||
continue
|
||||
if accounts_only and not obj.has_account:
|
||||
caller.msg("%s has no active account. Ignored." % objname)
|
||||
caller.msg(f"{objname} has no active account. Ignored.")
|
||||
continue
|
||||
if obj.access(caller, "tell"):
|
||||
obj.msg(message)
|
||||
if send_to_contents and hasattr(obj, "msg_contents"):
|
||||
obj.msg_contents(message)
|
||||
caller.msg("Emitted to %s and contents:\n%s" % (objname, message))
|
||||
caller.msg(f"Emitted to {objname} and contents:\n{message}")
|
||||
else:
|
||||
caller.msg("Emitted to %s:\n%s" % (objname, message))
|
||||
caller.msg(f"Emitted to {objname}:\n{message}")
|
||||
else:
|
||||
caller.msg("You are not allowed to emit to %s." % objname)
|
||||
caller.msg(f"You are not allowed to emit to {objname}.")
|
||||
|
||||
|
||||
class CmdNewPassword(COMMAND_DEFAULT_CLASS):
|
||||
|
|
@ -407,11 +405,11 @@ class CmdNewPassword(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
account.set_password(newpass)
|
||||
account.save()
|
||||
self.msg("%s - new password set to '%s'." % (account.name, newpass))
|
||||
self.msg(f"{account.name} - new password set to '{newpass}'.")
|
||||
if account.character != caller:
|
||||
account.msg("%s has changed your password to '%s'." % (caller.name, newpass))
|
||||
account.msg(f"{caller.name} has changed your password to '{newpass}'.")
|
||||
logger.log_sec(
|
||||
"Password Changed: %s (Caller: %s, IP: %s)." % (account, caller, self.session.address)
|
||||
f"Password Changed: {account} (Caller: {caller}, IP: {self.session.address})."
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -464,7 +462,7 @@ class CmdPerm(COMMAND_DEFAULT_CLASS):
|
|||
caller.msg("You are not allowed to examine this object.")
|
||||
return
|
||||
|
||||
string = "Permissions on |w%s|n: " % obj.key
|
||||
string = f"Permissions on |{obj.key}|n: "
|
||||
if not obj.permissions.all():
|
||||
string += "<None>"
|
||||
else:
|
||||
|
|
@ -482,10 +480,8 @@ class CmdPerm(COMMAND_DEFAULT_CLASS):
|
|||
# we supplied an argument on the form obj = perm
|
||||
locktype = "edit" if accountmode else "control"
|
||||
if not obj.access(caller, locktype):
|
||||
caller.msg(
|
||||
"You are not allowed to edit this %s's permissions."
|
||||
% ("account" if accountmode else "object")
|
||||
)
|
||||
accountstr = 'account' if accountmode else 'object'
|
||||
caller.msg(f"You are not allowed to edit this {accountstr}'s permissions.")
|
||||
return
|
||||
|
||||
caller_result = []
|
||||
|
|
@ -496,18 +492,17 @@ class CmdPerm(COMMAND_DEFAULT_CLASS):
|
|||
obj.permissions.remove(perm)
|
||||
if obj.permissions.get(perm):
|
||||
caller_result.append(
|
||||
"\nPermissions %s could not be removed from %s." % (perm, obj.name)
|
||||
f"\nPermissions {perm} could not be removed from {obj.name}."
|
||||
)
|
||||
else:
|
||||
caller_result.append(
|
||||
"\nPermission %s removed from %s (if they existed)." % (perm, obj.name)
|
||||
f"\nPermission {perm} removed from {obj.name} (if they existed)."
|
||||
)
|
||||
target_result.append(
|
||||
"\n%s revokes the permission(s) %s from you." % (caller.name, perm)
|
||||
f"\n{caller.name} revokes the permission(s) {perm} from you."
|
||||
)
|
||||
logger.log_sec(
|
||||
"Permissions Deleted: %s, %s (Caller: %s, IP: %s)."
|
||||
% (perm, obj, caller, self.session.address)
|
||||
f"Permissions Deleted: {perm}, {obj} (Caller: {caller}, IP: {self.session.address})."
|
||||
)
|
||||
else:
|
||||
# add a new permission
|
||||
|
|
@ -518,7 +513,7 @@ class CmdPerm(COMMAND_DEFAULT_CLASS):
|
|||
# don't allow to set a permission higher in the hierarchy than
|
||||
# the one the caller has (to prevent self-escalation)
|
||||
if perm.lower() in PERMISSION_HIERARCHY and not obj.locks.check_lockstring(
|
||||
caller, "dummy:perm(%s)" % perm
|
||||
caller, f"dummy:perm({perm})"
|
||||
):
|
||||
caller.msg(
|
||||
"You cannot assign a permission higher than the one you have yourself."
|
||||
|
|
@ -527,21 +522,19 @@ class CmdPerm(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
if perm in permissions:
|
||||
caller_result.append(
|
||||
"\nPermission '%s' is already defined on %s." % (perm, obj.name)
|
||||
f"\nPermission '{perm}' is already defined on {obj.name}."
|
||||
)
|
||||
else:
|
||||
obj.permissions.add(perm)
|
||||
plystring = "the Account" if accountmode else "the Object/Character"
|
||||
caller_result.append(
|
||||
"\nPermission '%s' given to %s (%s)." % (perm, obj.name, plystring)
|
||||
f"\nPermission '{perm}' given to {obj.name} ({plystring})."
|
||||
)
|
||||
target_result.append(
|
||||
"\n%s gives you (%s, %s) the permission '%s'."
|
||||
% (caller.name, obj.name, plystring, perm)
|
||||
f"\n{caller.name} gives you ({obj.name}, {plystring}) the permission '{perm}'."
|
||||
)
|
||||
logger.log_sec(
|
||||
"Permissions Added: %s, %s (Caller: %s, IP: %s)."
|
||||
% (obj, perm, caller, self.session.address)
|
||||
f"Permissions Added: {perm}, {obj} (Caller: {caller}, IP: {self.session.address})."
|
||||
)
|
||||
|
||||
caller.msg("".join(caller_result).strip())
|
||||
|
|
@ -569,7 +562,7 @@ class CmdWall(COMMAND_DEFAULT_CLASS):
|
|||
if not self.args:
|
||||
self.caller.msg("Usage: wall <message>")
|
||||
return
|
||||
message = '%s shouts "%s"' % (self.caller.name, self.args)
|
||||
message = f'{self.caller.name} shouts "{self.args}"'
|
||||
self.msg("Announcing to all connected sessions ...")
|
||||
SESSIONS.announce_all(message)
|
||||
|
||||
|
|
@ -599,7 +592,7 @@ class CmdForce(COMMAND_DEFAULT_CLASS):
|
|||
if not targ:
|
||||
return
|
||||
if not targ.access(self.caller, self.perm_used):
|
||||
self.caller.msg("You don't have permission to force them to execute commands.")
|
||||
self.caller.msg(f"You don't have permission to force {targ} to execute commands.")
|
||||
return
|
||||
targ.execute_cmd(self.rhs)
|
||||
self.caller.msg("You have forced %s to: %s" % (targ, self.rhs))
|
||||
self.caller.msg(f"You have forced {targ} to: {self.rhs}")
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS):
|
|||
)
|
||||
)
|
||||
else:
|
||||
caller.msg("No aliases exist for '%s'." % obj.get_display_name(caller))
|
||||
caller.msg(f"No aliases exist for '{obj.get_display_name(caller)}'.")
|
||||
return
|
||||
|
||||
if not (obj.access(caller, "control") or obj.access(caller, "edit")):
|
||||
|
|
@ -350,13 +350,9 @@ class CmdCopy(ObjManipCommand):
|
|||
new_aliases=to_obj_aliases,
|
||||
)
|
||||
if copiedobj:
|
||||
string = "Copied %s to '%s' (aliases: %s)." % (
|
||||
from_obj_name,
|
||||
to_obj_name,
|
||||
to_obj_aliases,
|
||||
)
|
||||
string = f"Copied {from_obj_name} to '{to_obj_name}' (aliases: {to_obj_aliases})."
|
||||
else:
|
||||
string = "There was an error copying %s to '%s'." % (from_obj_name, to_obj_name)
|
||||
string = f"There was an error copying {from_obj_name} to '{to_obj_name}'."
|
||||
# we are done, echo to user
|
||||
caller.msg(string)
|
||||
|
||||
|
|
@ -415,7 +411,7 @@ class CmdCpAttr(ObjManipCommand):
|
|||
required and verify an object has an attribute.
|
||||
"""
|
||||
if not obj.attributes.has(attr):
|
||||
self.caller.msg("%s doesn't have an attribute %s." % (obj.name, attr))
|
||||
self.caller.msg(f"{obj.name} doesn't have an attribute {attr}.")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
|
@ -479,7 +475,7 @@ class CmdCpAttr(ObjManipCommand):
|
|||
to_obj_attrs = to_obj["attrs"]
|
||||
to_obj = caller.search(to_obj_name)
|
||||
if not to_obj:
|
||||
result.append("\nCould not find object '%s'" % to_obj_name)
|
||||
result.append(f"\nCould not find object '{to_obj_name}'")
|
||||
continue
|
||||
for inum, from_attr in enumerate(from_obj_attrs):
|
||||
try:
|
||||
|
|
@ -495,13 +491,11 @@ class CmdCpAttr(ObjManipCommand):
|
|||
if clear and not (from_obj == to_obj and from_attr == to_attr):
|
||||
from_obj.attributes.remove(from_attr)
|
||||
result.append(
|
||||
"\nMoved %s.%s -> %s.%s. (value: %s)"
|
||||
% (from_obj.name, from_attr, to_obj_name, to_attr, repr(value))
|
||||
f"\nMoved {from_obj.name}.{from_attr} -> {to_obj_name}.{to_attr}. (value: {repr(value)})"
|
||||
)
|
||||
else:
|
||||
result.append(
|
||||
"\nCopied %s.%s -> %s.%s. (value: %s)"
|
||||
% (from_obj.name, from_attr, to_obj_name, to_attr, repr(value))
|
||||
f"\nCopied {from_obj.name}.{from_attr} -> {to_obj.name}.{to_attr}. (value: {repr(value)})"
|
||||
)
|
||||
caller.msg("".join(result))
|
||||
|
||||
|
|
@ -615,11 +609,9 @@ class CmdCreate(ObjManipCommand):
|
|||
if not obj:
|
||||
continue
|
||||
if aliases:
|
||||
string = "You create a new %s: %s (aliases: %s)."
|
||||
string = string % (obj.typename, obj.name, ", ".join(aliases))
|
||||
string = f"You create a new {obj.typename}: {obj.name} (aliases: {', '.join(aliases)})."
|
||||
else:
|
||||
string = "You create a new %s: %s."
|
||||
string = string % (obj.typename, obj.name)
|
||||
string = f"You create a new {obj.typename}: {obj.name}."
|
||||
# set a default desc
|
||||
if not obj.db.desc:
|
||||
obj.db.desc = "You see nothing special."
|
||||
|
|
@ -681,7 +673,7 @@ class CmdDesc(COMMAND_DEFAULT_CLASS):
|
|||
return
|
||||
|
||||
if not (obj.access(self.caller, "control") or obj.access(self.caller, "edit")):
|
||||
self.caller.msg("You don't have permission to edit the description of %s." % obj.key)
|
||||
self.caller.msg(f"You don't have permission to edit the description of {obj.key}.")
|
||||
return
|
||||
|
||||
self.caller.db.evmenu_target = obj
|
||||
|
|
@ -721,9 +713,9 @@ class CmdDesc(COMMAND_DEFAULT_CLASS):
|
|||
desc = self.args
|
||||
if obj.access(self.caller, "control") or obj.access(self.caller, "edit"):
|
||||
obj.db.desc = desc
|
||||
caller.msg("The description was set on %s." % obj.get_display_name(caller))
|
||||
caller.msg(f"The description was set on {obj.get_display_name(caller)}.")
|
||||
else:
|
||||
caller.msg("You don't have permission to edit the description of %s." % obj.key)
|
||||
caller.msg(f"You don't have permission to edit the description of {obj.key}.")
|
||||
|
||||
|
||||
class CmdDestroy(COMMAND_DEFAULT_CLASS):
|
||||
|
|
@ -771,21 +763,20 @@ class CmdDestroy(COMMAND_DEFAULT_CLASS):
|
|||
# helper function for deleting a single object
|
||||
string = ""
|
||||
if not obj.pk:
|
||||
string = "\nObject %s was already deleted." % obj.db_key
|
||||
string = f"\nObject {obj.db_key} was already deleted."
|
||||
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
|
||||
return f"\nYou don't have permission to delete {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
|
||||
f"\nObject {objname} is controlled by an active account. Use /override to delete anyway."
|
||||
)
|
||||
if obj.dbid == int(settings.DEFAULT_HOME.lstrip("#")):
|
||||
return (
|
||||
"\nYou are trying to delete |c%s|n, which is set as DEFAULT_HOME. "
|
||||
f"\nYou are trying to delete |c{objname}|n, which is set as DEFAULT_HOME. "
|
||||
"Re-point settings.DEFAULT_HOME to another "
|
||||
"object before continuing." % objname
|
||||
"object before continuing."
|
||||
)
|
||||
|
||||
had_exits = hasattr(obj, "exits") and obj.exits
|
||||
|
|
@ -798,15 +789,14 @@ class CmdDestroy(COMMAND_DEFAULT_CLASS):
|
|||
okay = obj.delete()
|
||||
if not okay:
|
||||
string += (
|
||||
"\nERROR: %s not deleted, probably because delete() returned False."
|
||||
% objname
|
||||
f"\nERROR: {objname} not deleted, probably because delete() returned False."
|
||||
)
|
||||
else:
|
||||
string += "\n%s was destroyed." % objname
|
||||
string += f"\n{objname} was destroyed."
|
||||
if had_exits:
|
||||
string += " Exits to and from %s were destroyed as well." % objname
|
||||
string += f" Exits to and from {objname} were destroyed as well."
|
||||
if had_objs:
|
||||
string += " Objects inside %s were moved to their homes." % objname
|
||||
string += f" Objects inside {objname} were moved to their homes."
|
||||
return string
|
||||
|
||||
objs = []
|
||||
|
|
@ -936,12 +926,7 @@ 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 = f"Created room {new_room}({new_room.dbref}){alias_string} of type {typeclass}."
|
||||
|
||||
# create exit to room
|
||||
|
||||
|
|
@ -972,14 +957,7 @@ class CmdDig(ObjManipCommand):
|
|||
alias_string = ""
|
||||
if new_to_exit.aliases.all():
|
||||
alias_string = " (%s)" % ", ".join(new_to_exit.aliases.all())
|
||||
exit_to_string = "\nCreated Exit from %s to %s: %s(%s)%s."
|
||||
exit_to_string = exit_to_string % (
|
||||
location.name,
|
||||
new_room.name,
|
||||
new_to_exit,
|
||||
new_to_exit.dbref,
|
||||
alias_string,
|
||||
)
|
||||
exit_to_string = "\nCreated Exit from {location.name} to {new_room.name}: {new_to_exit}({new_to_exit.dbref}){alias_string}."
|
||||
|
||||
# Create exit back from new room
|
||||
|
||||
|
|
@ -1006,15 +984,8 @@ class CmdDig(ObjManipCommand):
|
|||
alias_string = ""
|
||||
if new_back_exit.aliases.all():
|
||||
alias_string = " (%s)" % ", ".join(new_back_exit.aliases.all())
|
||||
exit_back_string = "\nCreated Exit back from %s to %s: %s(%s)%s."
|
||||
exit_back_string = exit_back_string % (
|
||||
new_room.name,
|
||||
location.name,
|
||||
new_back_exit,
|
||||
new_back_exit.dbref,
|
||||
alias_string,
|
||||
)
|
||||
caller.msg("%s%s%s" % (room_string, exit_to_string, exit_back_string))
|
||||
exit_back_string = f"\nCreated Exit back from {new_room.name} to {location.name}: {new_back_exit}({new_back_exit.dbref}){alias_string}."
|
||||
caller.msg(f"{room_string}{exit_to_string}{exit_back_string}")
|
||||
if new_room and "teleport" in self.switches:
|
||||
caller.move_to(new_room, move_type="teleport")
|
||||
|
||||
|
|
@ -1112,10 +1083,10 @@ class CmdTunnel(COMMAND_DEFAULT_CLASS):
|
|||
telswitch = "/teleport"
|
||||
backstring = ""
|
||||
if "oneway" not in self.switches:
|
||||
backstring = ", %s;%s" % (backname, backshort)
|
||||
backstring = f", {backname};{backshort}"
|
||||
|
||||
# build the string we will use to call dig
|
||||
digstring = "dig%s %s = %s;%s%s" % (telswitch, roomname, exitname, exitshort, backstring)
|
||||
digstring = f"dig{telswitch} {roomname} = {exitname};{exitshort}{backstring}"
|
||||
self.execute_cmd(digstring)
|
||||
|
||||
|
||||
|
|
@ -1182,10 +1153,7 @@ class CmdLink(COMMAND_DEFAULT_CLASS):
|
|||
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,
|
||||
)
|
||||
string = f"To create a two-way link, {obj} and {target} must both have a location"
|
||||
string += " (i.e. they cannot be rooms, but should be exits)."
|
||||
self.caller.msg(string)
|
||||
return
|
||||
|
|
@ -1193,15 +1161,10 @@ class CmdLink(COMMAND_DEFAULT_CLASS):
|
|||
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 += f"\nLink created {obj.name} (in {obj.location}) <-> {target.name} (in {target.location}) (two-way)."
|
||||
else:
|
||||
obj.destination = target
|
||||
string += "\nLink created %s -> %s (one way)." % (obj.name, target)
|
||||
string += f"\nLink created {obj.name} -> {target} (one way)."
|
||||
|
||||
elif self.rhs is None:
|
||||
# this means that no = was given (otherwise rhs
|
||||
|
|
@ -1209,18 +1172,18 @@ class CmdLink(COMMAND_DEFAULT_CLASS):
|
|||
# the home/destination on object
|
||||
dest = obj.destination
|
||||
if dest:
|
||||
string = "%s is an exit to %s." % (obj.name, dest.name)
|
||||
string = f"{obj.name} is an exit to {dest.name}."
|
||||
else:
|
||||
string = "%s is not an exit. Its home location is %s." % (obj.name, obj.home)
|
||||
string = f"{obj.name} is not an exit. Its home location is {obj.home}."
|
||||
|
||||
else:
|
||||
# We gave the command link 'obj = ' which means we want to
|
||||
# clear destination.
|
||||
if obj.destination:
|
||||
obj.destination = None
|
||||
string = "Former exit %s no longer links anywhere." % obj.name
|
||||
string = f"Former exit {obj.name} no longer links anywhere."
|
||||
else:
|
||||
string = "%s had no destination to unlink." % obj.name
|
||||
string = f"{obj.name} had no destination to unlink."
|
||||
# give feedback
|
||||
caller.msg(string.strip())
|
||||
|
||||
|
|
@ -1297,7 +1260,7 @@ class CmdSetHome(CmdLink):
|
|||
if not home:
|
||||
string = "This object has no home location set!"
|
||||
else:
|
||||
string = "%s's current home is %s(%s)." % (obj, home, home.dbref)
|
||||
string = f"{obj}'s current home is {home}({home.dbref})."
|
||||
else:
|
||||
# set a home location
|
||||
new_home = self.caller.search(self.rhs, global_search=True)
|
||||
|
|
@ -1306,15 +1269,9 @@ class CmdSetHome(CmdLink):
|
|||
old_home = obj.home
|
||||
obj.home = new_home
|
||||
if old_home:
|
||||
string = "Home location of %s was changed from %s(%s) to %s(%s)." % (
|
||||
obj,
|
||||
old_home,
|
||||
old_home.dbref,
|
||||
new_home,
|
||||
new_home.dbref,
|
||||
)
|
||||
string = f"Home location of {obj} was changed from {old_home}({old_home.dbref} to {new_home}({new_home.dbref})."
|
||||
else:
|
||||
string = "Home location of %s was set to %s(%s)." % (obj, new_home, new_home.dbref)
|
||||
string = f"Home location of {obj} was set to {new_home}({new_home.dbref})."
|
||||
self.caller.msg(string)
|
||||
|
||||
|
||||
|
|
@ -1343,7 +1300,7 @@ class CmdListCmdSets(COMMAND_DEFAULT_CLASS):
|
|||
return
|
||||
else:
|
||||
obj = caller
|
||||
string = "%s" % obj.cmdset
|
||||
string = f"{obj.cmdset}"
|
||||
caller.msg(string)
|
||||
|
||||
|
||||
|
|
@ -1387,11 +1344,11 @@ class CmdName(ObjManipCommand):
|
|||
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 account %s." % obj)
|
||||
caller.msg(f"You don't have right to edit this account {obj}.")
|
||||
return
|
||||
obj.username = newname
|
||||
obj.save()
|
||||
caller.msg("Account's name changed to '%s'." % newname)
|
||||
caller.msg(f"Account's name changed to '{newname}'.")
|
||||
return
|
||||
# object search, also with *
|
||||
obj = caller.search(objname)
|
||||
|
|
@ -1407,7 +1364,7 @@ class CmdName(ObjManipCommand):
|
|||
caller.msg("No names or aliases defined!")
|
||||
return
|
||||
if not (obj.access(caller, "control") or obj.access(caller, "edit")):
|
||||
caller.msg("You don't have the right to edit %s." % obj)
|
||||
caller.msg(f"You don't have the right to edit {obj}.")
|
||||
return
|
||||
# change the name and set aliases:
|
||||
if newname:
|
||||
|
|
@ -1419,7 +1376,7 @@ class CmdName(ObjManipCommand):
|
|||
# fix for exits - we need their exit-command to change name too
|
||||
if obj.destination:
|
||||
obj.flush_from_cache(force=True)
|
||||
caller.msg("Object's name changed to '%s'%s." % (newname, astring))
|
||||
caller.msg(f"Object's name changed to '{newname}'{astring}.")
|
||||
|
||||
|
||||
class CmdOpen(ObjManipCommand):
|
||||
|
|
@ -1463,25 +1420,20 @@ class CmdOpen(ObjManipCommand):
|
|||
exit_obj = exit_obj[0]
|
||||
if not exit_obj.destination:
|
||||
# we are trying to link a non-exit
|
||||
string = "'%s' already exists and is not an exit!\nIf you want to convert it "
|
||||
string += (
|
||||
"to an exit, you must assign an object to the 'destination' property first."
|
||||
)
|
||||
caller.msg(string % exit_name)
|
||||
caller.msg(f"'{exit_name}' already exists and is not an exit!\nIf you want to convert it "
|
||||
"to an exit, you must assign an object to the 'destination' property first."
|
||||
)
|
||||
return None
|
||||
# we are re-linking an old exit.
|
||||
old_destination = exit_obj.destination
|
||||
if old_destination:
|
||||
string = "Exit %s already exists." % exit_name
|
||||
string = f"Exit {exit_name} already exists."
|
||||
if old_destination.id != destination.id:
|
||||
# reroute the old exit.
|
||||
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 += f" Rerouted its old destination '{old_destination.name}' to '{destination.name}' and changed aliases."
|
||||
else:
|
||||
string += " It already points to the correct place."
|
||||
|
||||
|
|
@ -1506,14 +1458,9 @@ class CmdOpen(ObjManipCommand):
|
|||
if not exit_aliases
|
||||
else " (aliases: %s)" % (", ".join([str(e) for e in exit_aliases]))
|
||||
)
|
||||
string = "Created new Exit '%s' from %s to %s%s." % (
|
||||
exit_name,
|
||||
location.name,
|
||||
destination.name,
|
||||
string,
|
||||
)
|
||||
string = f"Created new Exit '{exit_name}' from {location.name} to {destination.name}{string}."
|
||||
else:
|
||||
string = "Error: Exit '%s' not created." % exit_name
|
||||
string = f"Error: Exit '{exit.name}' not created."
|
||||
# emit results
|
||||
caller.msg(string)
|
||||
return exit_obj
|
||||
|
|
@ -1596,13 +1543,13 @@ def _convert_from_string(cmd, strobj):
|
|||
# treat as string
|
||||
strobj = utils.to_str(strobj)
|
||||
string = (
|
||||
'|RNote: name "|r%s|R" was converted to a string. '
|
||||
"Make sure this is acceptable." % strobj
|
||||
f'|RNote: name "|r{strobj}|R" was converted to a string. '
|
||||
"Make sure this is acceptable."
|
||||
)
|
||||
cmd.caller.msg(string)
|
||||
return strobj
|
||||
except Exception as err:
|
||||
string = "|RUnknown error in evaluating Attribute: {}".format(err)
|
||||
string = f"|RUnknown error in evaluating Attribute: {err}"
|
||||
return string
|
||||
|
||||
|
||||
|
|
@ -1853,7 +1800,7 @@ class CmdSetAttribute(ObjManipCommand):
|
|||
def save(caller, buf):
|
||||
"""Called when editor saves its buffer."""
|
||||
obj.attributes.add(attr, buf)
|
||||
caller.msg("Saved Attribute %s." % attr)
|
||||
caller.msg(f"Saved Attribute {attr}.")
|
||||
|
||||
# check non-strings before activating editor
|
||||
try:
|
||||
|
|
@ -1931,7 +1878,7 @@ class CmdSetAttribute(ObjManipCommand):
|
|||
if "edit" in self.switches:
|
||||
# edit in the line editor
|
||||
if not (obj.access(self.caller, "control") or obj.access(self.caller, "edit")):
|
||||
caller.msg("You don't have permission to edit %s." % obj.key)
|
||||
caller.msg(f"You don't have permission to edit {obj.key}.")
|
||||
return
|
||||
|
||||
if len(attrs) > 1:
|
||||
|
|
@ -1963,7 +1910,7 @@ class CmdSetAttribute(ObjManipCommand):
|
|||
else:
|
||||
# deleting the attribute(s)
|
||||
if not (obj.access(self.caller, "control") or obj.access(self.caller, "edit")):
|
||||
caller.msg("You don't have permission to edit %s." % obj.key)
|
||||
caller.msg(f"You don't have permission to edit {obj.key}.")
|
||||
return
|
||||
for attr in attrs:
|
||||
if not self.check_attr(obj, attr, category):
|
||||
|
|
@ -1982,7 +1929,7 @@ class CmdSetAttribute(ObjManipCommand):
|
|||
)
|
||||
|
||||
if not (obj.access(self.caller, "control") or obj.access(self.caller, "edit")):
|
||||
caller.msg("You don't have permission to edit %s." % obj.key)
|
||||
caller.msg(f"You don't have permission to edit {obj.key}.")
|
||||
return
|
||||
for attr in attrs:
|
||||
if not self.check_attr(obj, attr, category):
|
||||
|
|
@ -1997,7 +1944,7 @@ class CmdSetAttribute(ObjManipCommand):
|
|||
or parsed_value.access(self.caller, "edit")
|
||||
):
|
||||
caller.msg(
|
||||
"You don't have permission to set " f"object with identifier '{value}'."
|
||||
f"You don't have permission to set object with identifier '{value}'."
|
||||
)
|
||||
continue
|
||||
value = parsed_value
|
||||
|
|
@ -2166,21 +2113,17 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
|
|||
)
|
||||
)
|
||||
elif not matches:
|
||||
caller.msg("No object or typeclass path found to match '{}'".format(oquery))
|
||||
caller.msg(f"No object or typeclass path found to match '{oquery}'")
|
||||
else:
|
||||
# one match found
|
||||
caller.msg(
|
||||
"Docstring for typeclass '{}':\n{}".format(oquery, matches[0][1].__doc__)
|
||||
)
|
||||
caller.msg(f"Docstring for typeclass '{oquery}': \n{matches[0][1].__doc__}")
|
||||
else:
|
||||
# do the search again to get the error handling in case of multi-match
|
||||
obj = caller.search(oquery)
|
||||
if not obj:
|
||||
return
|
||||
caller.msg(
|
||||
"{}'s current typeclass is '{}.{}'".format(
|
||||
obj.name, obj.__class__.__module__, obj.__class__.__name__
|
||||
)
|
||||
f"{obj.name}'s current typeclass is '{obj.__class__.__module__}.{obj.__class__.__name__}'"
|
||||
)
|
||||
return
|
||||
|
||||
|
|
@ -2211,14 +2154,13 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
|
|||
prototype = prototype[0]
|
||||
else:
|
||||
# no match
|
||||
caller.msg("No prototype '{}' was found.".format(key))
|
||||
caller.msg(f"No prototype '{key}' was found.")
|
||||
return
|
||||
new_typeclass = prototype["typeclass"]
|
||||
self.switches.append("force")
|
||||
|
||||
if "show" in self.switches or "examine" in self.switches:
|
||||
string = "%s's current typeclass is %s." % (obj.name, obj.__class__)
|
||||
caller.msg(string)
|
||||
caller.msg(f"{obj.name}'s current typeclass is '{obj.__class__}'")
|
||||
return
|
||||
|
||||
if self.cmdstring in ("swap", "@swap"):
|
||||
|
|
@ -2267,8 +2209,7 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
|
|||
diff, _ = spawner.prototype_diff_from_object(prototype, obj)
|
||||
txt = spawner.format_diff(diff)
|
||||
prompt = (
|
||||
"Applying prototype '%s' over '%s' will cause the follow changes:\n%s\n"
|
||||
% (prototype["key"], obj.name, txt)
|
||||
f"Applying prototype '{prototype['key']}' over '{obj.name}' will cause the follow changes:\n{txt}\n"
|
||||
)
|
||||
if not reset:
|
||||
prompt += "\n|yWARNING:|n Use the /reset switch to apply the prototype over a blank state."
|
||||
|
|
@ -2289,16 +2230,12 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
|
|||
)
|
||||
prototype_success = modified > 0
|
||||
if not prototype_success:
|
||||
caller.msg("Prototype %s failed to apply." % prototype["key"])
|
||||
caller.msg(f"Prototype {prototype['key']} failed to apply.")
|
||||
|
||||
if is_same:
|
||||
string = "%s updated its existing typeclass (%s).\n" % (obj.name, obj.path)
|
||||
string = f"{obj.name} updated its existing typeclass ({obj.path}).\n"
|
||||
else:
|
||||
string = "%s changed typeclass from %s to %s.\n" % (
|
||||
obj.name,
|
||||
old_typeclass_path,
|
||||
obj.typeclass_path,
|
||||
)
|
||||
string = f"{obj.name} changed typeclass from {old_typeclass_path} to {obj.typeclass_path}.\n"
|
||||
if update:
|
||||
string += "Only the at_object_creation hook was run (update mode)."
|
||||
else:
|
||||
|
|
@ -2309,8 +2246,7 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
|
|||
string += " Attributes set before swap were not removed\n(use `swap` or `type/reset` to clear all)."
|
||||
if "prototype" in self.switches and prototype_success:
|
||||
string += (
|
||||
" Prototype '%s' was successfully applied over the object type."
|
||||
% prototype["key"]
|
||||
f" Prototype '{prototype['key']}' was successfully applied over the object type."
|
||||
)
|
||||
|
||||
caller.msg(string)
|
||||
|
|
@ -2359,12 +2295,11 @@ class CmdWipe(ObjManipCommand):
|
|||
if not attrs:
|
||||
# wipe everything
|
||||
obj.attributes.clear()
|
||||
string = "Wiped all attributes on %s." % obj.name
|
||||
string = f"Wiped all attributes on {obj.name}."
|
||||
else:
|
||||
for attrname in attrs:
|
||||
obj.attributes.remove(attrname)
|
||||
string = "Wiped attributes %s on %s."
|
||||
string = string % (",".join(attrs), obj.name)
|
||||
string = f"Wiped attributes {','.join(attrs)} on {obj.name}."
|
||||
caller.msg(string)
|
||||
|
||||
|
||||
|
|
@ -2445,7 +2380,7 @@ class CmdLock(ObjManipCommand):
|
|||
else:
|
||||
string = lockdef
|
||||
else:
|
||||
string = "%s has no lock of access type '%s'." % (obj, access_type)
|
||||
string = f"{obj} has no lock of access type '{access_type}'."
|
||||
caller.msg(string)
|
||||
return
|
||||
|
||||
|
|
@ -2454,9 +2389,9 @@ class CmdLock(ObjManipCommand):
|
|||
if self.switches:
|
||||
swi = ", ".join(self.switches)
|
||||
caller.msg(
|
||||
"Switch(es) |w%s|n can not be used with a "
|
||||
f"Switch(es) |w{swi}|n can not be used with a "
|
||||
"lock assignment. Use e.g. "
|
||||
"|wlock/del objname/locktype|n instead." % swi
|
||||
"|wlock/del objname/locktype|n instead."
|
||||
)
|
||||
return
|
||||
|
||||
|
|
@ -2484,7 +2419,7 @@ class CmdLock(ObjManipCommand):
|
|||
# update on them unless their cmdsets are rebuilt.
|
||||
obj.at_init()
|
||||
if ok:
|
||||
caller.msg("Added lock '%s' to %s." % (lockdef, obj))
|
||||
caller.msg(f"Added lock '{lockdef}' to {obj}.")
|
||||
return
|
||||
|
||||
# if we get here, we are just viewing all locks on obj
|
||||
|
|
@ -3167,14 +3102,12 @@ 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 += f"\n |RNo match found for '{searchstring}' in #dbref interval.|n"
|
||||
else:
|
||||
result = result[0]
|
||||
string += "\n|g %s - %s|n" % (result.get_display_name(caller), result.path)
|
||||
string += f"\n|g {result.get_display_name(caller)} - {result.path}|n"
|
||||
if "loc" in self.switches and not is_account and result.location:
|
||||
string += " (|wlocation|n: |g{}|n)".format(
|
||||
result.location.get_display_name(caller)
|
||||
)
|
||||
string += f" (|wlocation|n: |g{result.location.get_display_name(caller)}|n)"
|
||||
else:
|
||||
# Not an account/dbref search but a wider search; build a queryset.
|
||||
# Searches for key and aliases
|
||||
|
|
@ -3675,14 +3608,13 @@ class CmdTeleport(COMMAND_DEFAULT_CLASS):
|
|||
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.account)
|
||||
f"({obj_to_teleport.key}, puppeted by {obj_to_teleport.account}) to a None-location."
|
||||
)
|
||||
return
|
||||
caller.msg("Teleported %s -> None-location." % obj_to_teleport)
|
||||
caller.msg(f"Teleported {obj_to_teleport} -> None-location.")
|
||||
if obj_to_teleport.location and "quiet" not in self.switches:
|
||||
obj_to_teleport.location.msg_contents(
|
||||
"%s teleported %s into nothingness." % (caller, obj_to_teleport), exclude=caller
|
||||
f"{caller} teleported {obj_to_teleport} into nothingness.", exclude=caller
|
||||
)
|
||||
obj_to_teleport.location = None
|
||||
return
|
||||
|
|
@ -3710,7 +3642,7 @@ class CmdTeleport(COMMAND_DEFAULT_CLASS):
|
|||
return
|
||||
|
||||
if obj_to_teleport.location and obj_to_teleport.location == destination:
|
||||
caller.msg("%s is already at %s." % (obj_to_teleport, destination))
|
||||
caller.msg(f"{obj_to_teleport} is already at {destination}.")
|
||||
return
|
||||
|
||||
# check any locks
|
||||
|
|
@ -3886,7 +3818,7 @@ class CmdTag(COMMAND_DEFAULT_CLASS):
|
|||
", ".join(sorted("'%s'%s" % (tags[i], categories[i]) for i in range(ntags))),
|
||||
)
|
||||
else:
|
||||
string = "No tags attached to %s." % obj
|
||||
string = f"No tags attached to {obj}."
|
||||
self.caller.msg(string)
|
||||
|
||||
|
||||
|
|
@ -4049,7 +3981,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
|||
"funcparser callables ($funcs) in the strings."
|
||||
)
|
||||
else:
|
||||
string = "Expected {}, got {}.".format(expect, type(prototype))
|
||||
string = f"Expected {expect}, got {type(prototype)}."
|
||||
self.caller.msg(string)
|
||||
return
|
||||
|
||||
|
|
@ -4404,4 +4336,4 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
|||
# time we try to update objects with this prototype in the future.
|
||||
obj.location = caller.location
|
||||
except RuntimeError as err:
|
||||
caller.msg(err)
|
||||
caller.msg(err)
|
||||
|
|
@ -547,7 +547,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
|
|||
if message:
|
||||
channel.msg(message, senders=caller, bypass_mute=True)
|
||||
channel.delete()
|
||||
logger.log_sec("Channel {} was deleted by {}".format(channel_key, caller))
|
||||
logger.log_sec(f"Channel {channel_key} was deleted by {caller}")
|
||||
|
||||
def set_lock(self, channel, lockstring):
|
||||
"""
|
||||
|
|
@ -1390,9 +1390,9 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
|
|||
self.msg("Who do you want page?")
|
||||
return
|
||||
|
||||
header = "|wAccount|n |c%s|n |wpages:|n" % caller.key
|
||||
header = f"|wAccount|n |c{caller.key}|n |wpages:|n"
|
||||
if message.startswith(":"):
|
||||
message = "%s %s" % (caller.key, message.strip(":").strip())
|
||||
message = f"{caller.key} {message.strip(':').strip()}"
|
||||
|
||||
# create the persistent message object
|
||||
create.create_message(caller, message, receivers=targets)
|
||||
|
|
@ -1468,7 +1468,7 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
|
|||
lastpages = "\n ".join(listing)
|
||||
|
||||
if lastpages:
|
||||
string = "Your latest pages:\n %s" % lastpages
|
||||
string = f"Your latest pages:\n {lastpages}"
|
||||
else:
|
||||
string = "You haven't paged anyone yet."
|
||||
self.msg(string)
|
||||
|
|
@ -1565,7 +1565,7 @@ class CmdIRC2Chan(COMMAND_DEFAULT_CLASS):
|
|||
return
|
||||
|
||||
if "disconnect" in self.switches or "remove" in self.switches or "delete" in self.switches:
|
||||
botname = "ircbot-%s" % self.lhs
|
||||
botname = f"ircbot-{self.lhs}"
|
||||
matches = AccountDB.objects.filter(db_is_bot=True, username=botname)
|
||||
dbref = utils.dbref(self.lhs)
|
||||
if not matches and dbref:
|
||||
|
|
@ -1592,7 +1592,7 @@ class CmdIRC2Chan(COMMAND_DEFAULT_CLASS):
|
|||
irc_network, irc_port, irc_channel, irc_botname = [
|
||||
part.strip() for part in self.rhs.split(None, 4)
|
||||
]
|
||||
irc_channel = "#%s" % irc_channel
|
||||
irc_channel = f"#{irc_channel}"
|
||||
except Exception:
|
||||
string = "IRC bot definition '%s' is not valid." % self.rhs
|
||||
self.msg(string)
|
||||
|
|
@ -1601,7 +1601,7 @@ class CmdIRC2Chan(COMMAND_DEFAULT_CLASS):
|
|||
botclass = None
|
||||
if ":" in irc_botname:
|
||||
irc_botname, botclass = [part.strip() for part in irc_botname.split(":", 2)]
|
||||
botname = "ircbot-%s" % irc_botname
|
||||
botname = f"ircbot-{irc_botname}"
|
||||
# If path given, use custom bot otherwise use default.
|
||||
botclass = botclass if botclass else bots.IRCBot
|
||||
irc_ssl = "ssl" in self.switches
|
||||
|
|
@ -1612,13 +1612,13 @@ class CmdIRC2Chan(COMMAND_DEFAULT_CLASS):
|
|||
# re-use an existing bot
|
||||
bot = bot[0]
|
||||
if not bot.is_bot:
|
||||
self.msg("Account '%s' already exists and is not a bot." % botname)
|
||||
self.msg(f"Account '{botname}' already exists and is not a bot.")
|
||||
return
|
||||
else:
|
||||
try:
|
||||
bot = create.create_account(botname, None, None, typeclass=botclass)
|
||||
except Exception as err:
|
||||
self.msg("|rError, could not create the bot:|n '%s'." % err)
|
||||
self.msg(f"|rError, could not create the bot:|n '{err}'.")
|
||||
return
|
||||
bot.start(
|
||||
ev_channel=channel,
|
||||
|
|
@ -1681,26 +1681,21 @@ class CmdIRCStatus(COMMAND_DEFAULT_CLASS):
|
|||
channel = ircbot.db.irc_channel
|
||||
network = ircbot.db.irc_network
|
||||
port = ircbot.db.irc_port
|
||||
chtext = "IRC bot '%s' on channel %s (%s:%s)" % (
|
||||
ircbot.db.irc_botname,
|
||||
channel,
|
||||
network,
|
||||
port,
|
||||
)
|
||||
chtext = f"IRC bot '{ircbot.db.irc_botname}' on channel {channel} ({network}:{port})"
|
||||
if option == "ping":
|
||||
# check connection by sending outself a ping through the server.
|
||||
self.caller.msg("Pinging through %s." % chtext)
|
||||
self.caller.msg(f"Pinging through {chtext}.")
|
||||
ircbot.ping(self.caller)
|
||||
elif option in ("users", "nicklist", "who"):
|
||||
# retrieve user list. The bot must handles the echo since it's
|
||||
# an asynchronous call.
|
||||
self.caller.msg("Requesting nicklist from %s (%s:%s)." % (channel, network, port))
|
||||
self.caller.msg(f"Requesting nicklist from {channel} ({network}:{port}).")
|
||||
ircbot.get_nicklist(self.caller)
|
||||
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)
|
||||
self.caller.msg(f"Forcing a disconnect + reconnect of {chtext}.")
|
||||
ircbot.reconnect()
|
||||
else:
|
||||
self.caller.msg("You don't have permission to force-reload the IRC bot.")
|
||||
|
|
@ -1782,7 +1777,7 @@ class CmdRSS2Chan(COMMAND_DEFAULT_CLASS):
|
|||
return
|
||||
|
||||
if "disconnect" in self.switches or "remove" in self.switches or "delete" in self.switches:
|
||||
botname = "rssbot-%s" % self.lhs
|
||||
botname = f"rssbot-{self.lhs}"
|
||||
matches = AccountDB.objects.filter(db_is_bot=True, db_key=botname)
|
||||
if not matches:
|
||||
# try dbref match
|
||||
|
|
@ -1801,13 +1796,13 @@ class CmdRSS2Chan(COMMAND_DEFAULT_CLASS):
|
|||
channel = self.lhs
|
||||
url = self.rhs
|
||||
|
||||
botname = "rssbot-%s" % url
|
||||
botname = f"rssbot-{url}"
|
||||
bot = AccountDB.objects.filter(username__iexact=botname)
|
||||
if bot:
|
||||
# re-use existing bot
|
||||
bot = bot[0]
|
||||
if not bot.is_bot:
|
||||
self.msg("Account '%s' already exists and is not a bot." % botname)
|
||||
self.msg("Account '{botname}' already exists and is not a bot.")
|
||||
return
|
||||
else:
|
||||
# create a new bot
|
||||
|
|
@ -1875,7 +1870,7 @@ class CmdGrapevine2Chan(COMMAND_DEFAULT_CLASS):
|
|||
return
|
||||
|
||||
if "disconnect" in self.switches or "remove" in self.switches or "delete" in self.switches:
|
||||
botname = "grapevinebot-%s" % self.lhs
|
||||
botname = f"grapevinebot-{self.lhs}"
|
||||
matches = AccountDB.objects.filter(db_is_bot=True, db_key=botname)
|
||||
|
||||
if not matches:
|
||||
|
|
@ -1902,10 +1897,10 @@ class CmdGrapevine2Chan(COMMAND_DEFAULT_CLASS):
|
|||
# re-use existing bot
|
||||
bot = bot[0]
|
||||
if not bot.is_bot:
|
||||
self.msg("Account '%s' already exists and is not a bot." % botname)
|
||||
self.msg(f"Account '{botname}' already exists and is not a bot.")
|
||||
return
|
||||
else:
|
||||
self.msg("Reusing bot '%s' (%s)" % (botname, bot.dbref))
|
||||
self.msg(f"Reusing bot '{botname}' ({bot.dbref})")
|
||||
else:
|
||||
# create a new bot
|
||||
bot = create.create_account(botname, None, None, typeclass=bots.GrapevineBot)
|
||||
|
|
|
|||
|
|
@ -221,9 +221,7 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
|
|||
_, _, old_nickstring, old_replstring = oldnick.value
|
||||
caller.nicks.remove(old_nickstring, category=nicktype)
|
||||
caller.msg(
|
||||
"%s removed: '|w%s|n' -> |w%s|n."
|
||||
% (nicktypestr, old_nickstring, old_replstring)
|
||||
)
|
||||
f"{nicktypestr} removed: '|w{old_nickstring}|n' -> |w{old_replstring}|n.")
|
||||
else:
|
||||
caller.msg("No matching nicks to remove.")
|
||||
return
|
||||
|
|
@ -245,12 +243,12 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
|
|||
_, _, nick, repl = nick.value
|
||||
if nick.startswith(self.lhs):
|
||||
strings.append(
|
||||
"{}-nick: '{}' -> '{}'".format(nicktype.capitalize(), nick, repl)
|
||||
f"{nicktype.capitalize()}-nick: '{nick}' -> '{repl}'"
|
||||
)
|
||||
if strings:
|
||||
caller.msg("\n".join(strings))
|
||||
else:
|
||||
caller.msg("No nicks found matching '{}'".format(self.lhs))
|
||||
caller.msg(f"No nicks found matching '{self,lhs}'")
|
||||
return
|
||||
|
||||
if not self.rhs and self.lhs:
|
||||
|
|
@ -268,12 +266,12 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
|
|||
_, _, nick, repl = nick.value
|
||||
if nick.startswith(self.lhs):
|
||||
strings.append(
|
||||
"{}-nick: '{}' -> '{}'".format(nicktype.capitalize(), nick, repl)
|
||||
f"{nicktype.capitalize()}-nick: '{nick}' -> '{repl}'"
|
||||
)
|
||||
if strings:
|
||||
caller.msg("\n".join(strings))
|
||||
else:
|
||||
caller.msg("No nicks found matching '{}'".format(self.lhs))
|
||||
caller.msg(f"No nicks found matching '{self.lhs}'")
|
||||
return
|
||||
|
||||
if not self.rhs and self.lhs:
|
||||
|
|
@ -291,12 +289,12 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
|
|||
_, _, nick, repl = nick.value
|
||||
if nick.startswith(self.lhs):
|
||||
strings.append(
|
||||
"{}-nick: '{}' -> '{}'".format(nicktype.capitalize(), nick, repl)
|
||||
f"{nicktype.capitalize()}-nick: '{nick}' -> '{repl}'"
|
||||
)
|
||||
if strings:
|
||||
caller.msg("\n".join(strings))
|
||||
else:
|
||||
caller.msg("No nicks found matching '{}'".format(self.lhs))
|
||||
caller.msg(f"No nicks found matching '{self.lhs}'")
|
||||
return
|
||||
|
||||
if not self.args or not self.lhs:
|
||||
|
|
@ -316,7 +314,7 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
|
|||
errstring = ""
|
||||
string = ""
|
||||
for nicktype in nicktypes:
|
||||
nicktypestr = "%s-nick" % nicktype.capitalize()
|
||||
nicktypestr = f"{nicktype.capitalize()}-nick"
|
||||
old_nickstring = None
|
||||
old_replstring = None
|
||||
|
||||
|
|
@ -328,19 +326,11 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
|
|||
errstring = ""
|
||||
if oldnick:
|
||||
if replstring == old_replstring:
|
||||
string += "\nIdentical %s already set." % nicktypestr.lower()
|
||||
string += f"\nIdentical {nicktypestr.lower()} already set."
|
||||
else:
|
||||
string += "\n%s '|w%s|n' updated to map to '|w%s|n'." % (
|
||||
nicktypestr,
|
||||
old_nickstring,
|
||||
replstring,
|
||||
)
|
||||
string += f"\n{nicktypestr} '|w{old_nickstring}|n' updated to map to '|w{replstring}|n'."
|
||||
else:
|
||||
string += "\n%s '|w%s|n' mapped to '|w%s|n'." % (
|
||||
nicktypestr,
|
||||
nickstring,
|
||||
replstring,
|
||||
)
|
||||
string += f"\n{nicktypestr} '|w{nickstring}|n' mapped to '|w{replstring}|n'."
|
||||
try:
|
||||
caller.nicks.add(nickstring, replstring, category=nicktype)
|
||||
except NickTemplateInvalid:
|
||||
|
|
@ -350,11 +340,7 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
|
|||
return
|
||||
elif old_nickstring and old_replstring:
|
||||
# just looking at the nick
|
||||
string += "\n%s '|w%s|n' maps to '|w%s|n'." % (
|
||||
nicktypestr,
|
||||
old_nickstring,
|
||||
old_replstring,
|
||||
)
|
||||
string += f"\n{nicktypestr} '|w{old_nickstring}|n' maps to '|w{old_replstring}|n'."
|
||||
errstring = ""
|
||||
string = errstring if errstring else string
|
||||
caller.msg(_cy(string))
|
||||
|
|
@ -439,10 +425,8 @@ class CmdGet(COMMAND_DEFAULT_CLASS):
|
|||
if not success:
|
||||
caller.msg("This can't be picked up.")
|
||||
else:
|
||||
caller.msg("You pick up %s." % obj.name)
|
||||
caller.location.msg_contents(
|
||||
"%s picks up %s." % (caller.name, obj.name), exclude=caller
|
||||
)
|
||||
caller.msg(f"You pick up {obj.name}.")
|
||||
caller.location.msg_contents(f"{caller.name} picks up {obj.name}.", exclude=caller)
|
||||
# calling at_get hook method
|
||||
obj.at_get(caller)
|
||||
|
||||
|
|
@ -475,8 +459,8 @@ class CmdDrop(COMMAND_DEFAULT_CLASS):
|
|||
obj = caller.search(
|
||||
self.args,
|
||||
location=caller,
|
||||
nofound_string="You aren't carrying %s." % self.args,
|
||||
multimatch_string="You carry more than one %s:" % self.args,
|
||||
nofound_string=f"You aren't carrying {self.args}.",
|
||||
multimatch_string=f"You carry more than one {self.args}:",
|
||||
)
|
||||
if not obj:
|
||||
return
|
||||
|
|
@ -490,7 +474,7 @@ class CmdDrop(COMMAND_DEFAULT_CLASS):
|
|||
caller.msg("This couldn't be dropped.")
|
||||
else:
|
||||
caller.msg("You drop %s." % (obj.name,))
|
||||
caller.location.msg_contents("%s drops %s." % (caller.name, obj.name), exclude=caller)
|
||||
caller.location.msg_contents(f"{caller.name} drops {obj.name}.", exclude=caller)
|
||||
# Call the object script's at_drop() method.
|
||||
obj.at_drop(caller)
|
||||
|
||||
|
|
@ -521,17 +505,17 @@ class CmdGive(COMMAND_DEFAULT_CLASS):
|
|||
to_give = caller.search(
|
||||
self.lhs,
|
||||
location=caller,
|
||||
nofound_string="You aren't carrying %s." % self.lhs,
|
||||
multimatch_string="You carry more than one %s:" % self.lhs,
|
||||
nofound_string=f"You aren't carrying {self.lhs}.",
|
||||
multimatch_string=f"You carry more than one {self.lhs}:",
|
||||
)
|
||||
target = caller.search(self.rhs)
|
||||
if not (to_give and target):
|
||||
return
|
||||
if target == caller:
|
||||
caller.msg("You keep %s to yourself." % to_give.key)
|
||||
caller.msg(f"You keep {to_give.key} to yourself.")
|
||||
return
|
||||
if not to_give.location == caller:
|
||||
caller.msg("You are not holding %s." % to_give.key)
|
||||
caller.msg(f"You are not holding {to_give.key}.")
|
||||
return
|
||||
|
||||
# calling at_pre_give hook method
|
||||
|
|
@ -541,10 +525,10 @@ class CmdGive(COMMAND_DEFAULT_CLASS):
|
|||
# give object
|
||||
success = to_give.move_to(target, quiet=True, move_type="give")
|
||||
if not success:
|
||||
caller.msg("This could not be given.")
|
||||
caller.msg(f"You could not give {to_give.key}.")
|
||||
else:
|
||||
caller.msg("You give %s to %s." % (to_give.key, target.key))
|
||||
target.msg("%s gives you %s." % (caller.key, to_give.key))
|
||||
caller.msg(f"You give {to_give.key} to {target.key}.")
|
||||
target.msg(f"{caller.key} gives you {to_give.key}.")
|
||||
# Call the object script's at_give() method.
|
||||
to_give.at_give(caller, target)
|
||||
|
||||
|
|
@ -702,7 +686,7 @@ class CmdPose(COMMAND_DEFAULT_CLASS):
|
|||
msg = "What do you want to do?"
|
||||
self.caller.msg(msg)
|
||||
else:
|
||||
msg = "%s%s" % (self.caller.name, self.args)
|
||||
msg = f"{self.caller.name}{self.args}"
|
||||
self.caller.location.msg_contents(text=(msg, {"type": "pose"}), from_obj=self.caller)
|
||||
|
||||
|
||||
|
|
@ -737,7 +721,7 @@ class CmdAccess(COMMAND_DEFAULT_CLASS):
|
|||
pperms = ", ".join(caller.account.permissions.all())
|
||||
|
||||
string += "\n|wYour access|n:"
|
||||
string += "\nCharacter |c%s|n: %s" % (caller.key, cperms)
|
||||
string += f"\nCharacter |c{caller.key}|n: {cperms}"
|
||||
if hasattr(caller, "account"):
|
||||
string += "\nAccount |c%s|n: %s" % (caller.account.key, pperms)
|
||||
caller.msg(string)
|
||||
string += f"\nAccount |c{caller.account.key}|n: {pperms}"
|
||||
caller.msg(string)
|
||||
|
|
@ -962,7 +962,7 @@ class CmdSetHelp(CmdHelp):
|
|||
if "append" in switches or "merge" in switches or "extend" in switches:
|
||||
# merge/append operations
|
||||
if not old_entry:
|
||||
self.msg("Could not find topic '%s'. You must give an exact name." % topicstr)
|
||||
self.msg(f"Could not find topic '{topicstr}'. You must give an exact name.")
|
||||
return
|
||||
if not self.rhs:
|
||||
self.msg("You must supply text to append/merge.")
|
||||
|
|
@ -972,16 +972,16 @@ class CmdSetHelp(CmdHelp):
|
|||
else:
|
||||
old_entry.entrytext += "\n%s" % self.rhs
|
||||
old_entry.aliases.add(aliases)
|
||||
self.msg("Entry updated:\n%s%s" % (old_entry.entrytext, aliastxt))
|
||||
self.msg(f"Entry updated:\n{old_entry.entrytext}{aliastxt}")
|
||||
return
|
||||
|
||||
if "delete" in switches or "del" in switches:
|
||||
# delete the help entry
|
||||
if not old_entry:
|
||||
self.msg("Could not find topic '%s'%s." % (topicstr, aliastxt))
|
||||
self.msg(f"Could not find topic '{topicstr}'{aliastxt}.")
|
||||
return
|
||||
old_entry.delete()
|
||||
self.msg("Deleted help entry '%s'%s." % (topicstr, aliastxt))
|
||||
self.msg(f"Deleted help entry '{topicstr}'{aliastxt}.")
|
||||
return
|
||||
|
||||
# at this point it means we want to add a new help entry.
|
||||
|
|
@ -998,7 +998,7 @@ class CmdSetHelp(CmdHelp):
|
|||
old_entry.locks.add(lockstring)
|
||||
old_entry.aliases.add(aliases)
|
||||
old_entry.save()
|
||||
self.msg("Overwrote the old topic '%s'%s." % (topicstr, aliastxt))
|
||||
self.msg(f"Overwrote the old topic '{topicstr}'{aliastxt}.")
|
||||
else:
|
||||
self.msg(
|
||||
f"Topic '{topicstr}'{aliastxt} already exists. Use /edit to open in editor, or "
|
||||
|
|
|
|||
|
|
@ -216,30 +216,30 @@ Command {self} has no defined `func()` - showing on-command variables: No child
|
|||
self.caller.msg(string)
|
||||
# a simple test command to show the available properties
|
||||
string = "-" * 50
|
||||
string += "\n|w%s|n - Command variables from evennia:\n" % self.key
|
||||
string += f"\n|w{self.key}|n - Command variables from evennia:\n"
|
||||
string += "-" * 50
|
||||
string += "\nname of cmd (self.key): |w%s|n\n" % self.key
|
||||
string += "cmd aliases (self.aliases): |w%s|n\n" % self.aliases
|
||||
string += "cmd locks (self.locks): |w%s|n\n" % self.locks
|
||||
string += "help category (self.help_category): |w%s|n\n" % self.help_category
|
||||
string += "object calling (self.caller): |w%s|n\n" % self.caller
|
||||
string += "object storing cmdset (self.obj): |w%s|n\n" % self.obj
|
||||
string += "command string given (self.cmdstring): |w%s|n\n" % self.cmdstring
|
||||
string += f"\nname of cmd (self.key): |w{self.key}|n\n"
|
||||
string += f"cmd aliases (self.aliases): |w{self.aliases}|n\n"
|
||||
string += f"cmd locks (self.locks): |w{self.locks}|n\n"
|
||||
string += f"help category (self.help_category): |w{self.help_category}|n\n"
|
||||
string += f"object calling (self.caller): |w{self.caller}|n\n"
|
||||
string += f"object storing cmdset (self.obj): |w{self.obj}|n\n"
|
||||
string += f"command string given (self.cmdstring): |w{self.cmdstring}|n\n"
|
||||
# show cmdset.key instead of cmdset to shorten output
|
||||
string += utils.fill("current cmdset (self.cmdset): |w%s|n\n" % self.cmdset)
|
||||
string += utils.fill(f"current cmdset (self.cmdset): |w{self.cmdset}|n\n")
|
||||
string += "\n" + "-" * 50
|
||||
string += "\nVariables from MuxCommand baseclass\n"
|
||||
string += "-" * 50
|
||||
string += "\nraw argument (self.raw): |w%s|n \n" % self.raw
|
||||
string += "cmd args (self.args): |w%s|n\n" % self.args
|
||||
string += "cmd switches (self.switches): |w%s|n\n" % self.switches
|
||||
string += "cmd options (self.switch_options): |w%s|n\n" % self.switch_options
|
||||
string += "cmd parse left/right using (self.rhs_split): |w%s|n\n" % self.rhs_split
|
||||
string += "space-separated arg list (self.arglist): |w%s|n\n" % self.arglist
|
||||
string += "lhs, left-hand side of '=' (self.lhs): |w%s|n\n" % self.lhs
|
||||
string += "lhs, comma separated (self.lhslist): |w%s|n\n" % self.lhslist
|
||||
string += "rhs, right-hand side of '=' (self.rhs): |w%s|n\n" % self.rhs
|
||||
string += "rhs, comma separated (self.rhslist): |w%s|n\n" % self.rhslist
|
||||
string += f"\nraw argument (self.raw): |w{self.raw}|n \n"
|
||||
string += f"cmd args (self.args): |w{self.args}|n\n"
|
||||
string += f"cmd switches (self.switches): |w{self.switches}|n\n"
|
||||
string += f"cmd options (self.switch_options): |w{self.switch_options}|n\n"
|
||||
string += f"cmd parse left/right using (self.rhs_split): |w{self.rhs_split}|n\n"
|
||||
string += f"space-separated arg list (self.arglist): |w{self.arglist}|n\n"
|
||||
string += f"lhs, left-hand side of '=' (self.lhs): |w{self.lhs}|n\n"
|
||||
string += f"lhs, comma separated (self.lhslist): |w{self.lhslist}|n\n"
|
||||
string += f"rhs, right-hand side of '=' (self.rhs): |w{self.rhs}|n\n"
|
||||
string += f"rhs, comma separated (self.rhslist): |w{self.rhslist}|n\n"
|
||||
string += "-" * 50
|
||||
self.caller.msg(string)
|
||||
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ class CmdReload(COMMAND_DEFAULT_CLASS):
|
|||
if self.args:
|
||||
reason = "(Reason: %s) " % self.args.rstrip(".")
|
||||
if _BROADCAST_SERVER_RESTART_MESSAGES:
|
||||
SESSIONS.announce_all(" Server restart initiated %s..." % reason)
|
||||
SESSIONS.announce_all(f" Server restart initiated {reason}...")
|
||||
SESSIONS.portal_restart_server()
|
||||
|
||||
|
||||
|
|
@ -135,7 +135,7 @@ class CmdShutdown(COMMAND_DEFAULT_CLASS):
|
|||
announcement = "\nServer is being SHUT DOWN!\n"
|
||||
if self.args:
|
||||
announcement += "%s\n" % self.args
|
||||
logger.log_info("Server shutdown by %s." % self.caller.name)
|
||||
logger.log_info(f"Server shutdown by {self.caller.name}.")
|
||||
SESSIONS.announce_all(announcement)
|
||||
SESSIONS.portal_shutdown()
|
||||
|
||||
|
|
@ -482,13 +482,12 @@ class CmdAccounts(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
# Boot the account then delete it.
|
||||
self.msg("Informing and disconnecting account ...")
|
||||
string = "\nYour account '%s' is being *permanently* deleted.\n" % username
|
||||
string = f"\nYour account '{username}' is being *permanently* deleted.\n"
|
||||
if reason:
|
||||
string += " Reason given:\n '%s'" % reason
|
||||
account.msg(string)
|
||||
logger.log_sec(
|
||||
"Account Deleted: %s (Reason: %s, Caller: %s, IP: %s)."
|
||||
% (account, reason, caller, self.session.address)
|
||||
f"Account Deleted: {account} (Reason: {reason}, Caller: {caller}, IP: {self.session.address})."
|
||||
)
|
||||
account.delete()
|
||||
self.msg("Account %s was successfully deleted." % username)
|
||||
|
|
@ -519,8 +518,8 @@ class CmdAccounts(COMMAND_DEFAULT_CLASS):
|
|||
utils.datetime_format(ply.date_created), ply.dbref, ply.key, ply.path
|
||||
)
|
||||
|
||||
string = "\n|wAccount typeclass distribution:|n\n%s" % typetable
|
||||
string += "\n|wLast %s Accounts created:|n\n%s" % (min(naccounts, nlim), latesttable)
|
||||
string = f"\n|wAccount typeclass distribution:|n\n{typetable}"
|
||||
string += f"\n|wLast {min(naccounts, nlim)} Accounts created:|n\n{latesttable}"
|
||||
caller.msg(string)
|
||||
|
||||
|
||||
|
|
@ -610,7 +609,7 @@ class CmdService(COMMAND_DEFAULT_CLASS):
|
|||
if delmode:
|
||||
service.stopService()
|
||||
service_collection.removeService(service)
|
||||
caller.msg("|gStopped and removed service '%s'.|n" % self.args)
|
||||
caller.msg(f"|gStopped and removed service '{self.args}'.|n")
|
||||
else:
|
||||
caller.msg(f"Stopping service '{self.args}'...")
|
||||
try:
|
||||
|
|
@ -621,7 +620,7 @@ class CmdService(COMMAND_DEFAULT_CLASS):
|
|||
"If there are remaining problems, try reloading "
|
||||
"or rebooting the server."
|
||||
)
|
||||
caller.msg("|g... Stopped service '%s'.|n" % self.args)
|
||||
caller.msg(f"|g... Stopped service '{self.args}'.|n")
|
||||
return
|
||||
|
||||
if switches[0] == "start":
|
||||
|
|
|
|||
|
|
@ -53,28 +53,31 @@ Exits: northeast and east
|
|||
(then go back to your mygame/ folder)
|
||||
|
||||
This will install all optional requirements of Evennia.
|
||||
2. Import and add the `evennia.contrib.commands.XYZGridCmdSet` to the
|
||||
2. Import and [add] the `evennia.contrib.grid.xyzgrid.commands.XYZGridCmdSet` to the
|
||||
`CharacterCmdset` cmdset in `mygame/commands.default_cmds.py`. Reload
|
||||
the server. This makes the `map`, `goto/path` and the modified `teleport` and
|
||||
`open` commands available in-game.
|
||||
|
||||
[add]: docs/source/Command-Sets.md#defining-command-sets
|
||||
|
||||
3. Edit `mygame/server/conf/settings.py` and add
|
||||
|
||||
EXTRA_LAUNCHER_COMMANDS['xyzgrid'] = 'evennia.contrib.launchcmd.xyzcommand'
|
||||
|
||||
and
|
||||
|
||||
PROTOTYPE_MODULES += [’evennia.contrib.grid.xyzgrid.prototypes’]
|
||||
EXTRA_LAUNCHER_COMMANDS['xyzgrid'] = 'evennia.contrib.grid.xyzgrid.launchcmd.xyzcommand'
|
||||
PROTOTYPE_MODULES += ['evennia.contrib.grid.xyzgrid.prototypes']
|
||||
|
||||
This will add the new ability to enter `evennia xyzgrid <option>` on the
|
||||
command line. It will also make the `xyz_room` and `xyz_exit` prototypes
|
||||
available for use as prototype-parents when spawning the grid.
|
||||
|
||||
4. Run `evennia xyzgrid help` for available options.
|
||||
|
||||
5. (Optional): By default, the xyzgrid will only spawn module-based
|
||||
[prototypes](../Components/Prototypes.md). This is an optimization and usually makes sense
|
||||
[prototypes]. This is an optimization and usually makes sense
|
||||
since the grid is entirely defined outside the game anyway. If you want to
|
||||
also make use of in-game (db-) created prototypes, add
|
||||
`XYZGRID_USE_DB_PROTOTYPES = True` to settings.
|
||||
|
||||
[prototypes]: ../Components/Prototypes.md
|
||||
|
||||
## Overview
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
Tests for the XYZgrid system.
|
||||
|
||||
"""
|
||||
from unittest import mock
|
||||
|
||||
from random import randint
|
||||
from parameterized import parameterized
|
||||
|
|
@ -1415,3 +1416,78 @@ class TestBuildExampleGrid(BaseEvenniaTest):
|
|||
self.assertTrue(room2a.db.desc.startswith("This is the entrance to"))
|
||||
self.assertEqual(room2b.key, "North-west corner of the atrium")
|
||||
self.assertTrue(room2b.db.desc.startswith("Sunlight sifts down"))
|
||||
|
||||
|
||||
mock_room_callbacks = mock.MagicMock()
|
||||
mock_exit_callbacks = mock.MagicMock()
|
||||
|
||||
class TestXyzRoom(xyzroom.XYZRoom):
|
||||
def at_object_creation(self):
|
||||
mock_room_callbacks.at_object_creation()
|
||||
|
||||
class TestXyzExit(xyzroom.XYZExit):
|
||||
def at_object_creation(self):
|
||||
mock_exit_callbacks.at_object_creation()
|
||||
|
||||
MAP_DATA = {
|
||||
"map": """
|
||||
|
||||
+ 0 1
|
||||
|
||||
0 #-#
|
||||
|
||||
+ 0 1
|
||||
|
||||
""",
|
||||
"zcoord": "map1",
|
||||
"prototypes": {
|
||||
("*", "*"): {
|
||||
"key": "room",
|
||||
"desc": "A room.",
|
||||
"prototype_parent": "xyz_room",
|
||||
},
|
||||
("*", "*", "*"): {
|
||||
"desc": "A passage.",
|
||||
"prototype_parent": "xyz_exit",
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"map_visual_range": 1,
|
||||
"map_mode": "scan",
|
||||
}
|
||||
}
|
||||
|
||||
class TestCallbacks(BaseEvenniaTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
mock_room_callbacks.reset_mock()
|
||||
mock_exit_callbacks.reset_mock()
|
||||
|
||||
def setup_grid(self, map_data):
|
||||
self.grid, err = xyzgrid.XYZGrid.create("testgrid")
|
||||
|
||||
def _log(msg):
|
||||
print(msg)
|
||||
self.grid.log = _log
|
||||
|
||||
self.map_data = map_data
|
||||
self.grid.add_maps(map_data)
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
self.grid.delete()
|
||||
|
||||
def test_typeclassed_xyzroom_and_xyzexit_with_at_object_creation_are_called(self):
|
||||
map_data = dict(MAP_DATA)
|
||||
for prototype_key, prototype_value in map_data["prototypes"].items():
|
||||
if len(prototype_key) == 2:
|
||||
prototype_value["typeclass"] = "evennia.contrib.grid.xyzgrid.tests.TestXyzRoom"
|
||||
if len(prototype_key) == 3:
|
||||
prototype_value["typeclass"] = "evennia.contrib.grid.xyzgrid.tests.TestXyzExit"
|
||||
self.setup_grid(map_data)
|
||||
|
||||
self.grid.spawn()
|
||||
|
||||
# Two rooms and 2 exits, Each one should have gotten one `at_object_creation` callback.
|
||||
self.assertEqual(mock_room_callbacks.at_object_creation.mock_calls, [mock.call(), mock.call()])
|
||||
self.assertEqual(mock_exit_callbacks.at_object_creation.mock_calls, [mock.call(), mock.call()])
|
||||
|
|
|
|||
|
|
@ -22,8 +22,9 @@ from collections import defaultdict
|
|||
|
||||
from django.core import exceptions as django_exceptions
|
||||
from evennia.prototypes import spawner
|
||||
from evennia.utils.utils import class_from_module
|
||||
|
||||
from .utils import MAPSCAN, REVERSE_DIRECTIONS, MapParserError, BIGVAL
|
||||
from .utils import MAPSCAN, REVERSE_DIRECTIONS, MapParserError, BIGVAL, MapError
|
||||
|
||||
NodeTypeclass = None
|
||||
ExitTypeclass = None
|
||||
|
|
@ -316,13 +317,14 @@ class MapNode:
|
|||
try:
|
||||
nodeobj = NodeTypeclass.objects.get_xyz(xyz=xyz)
|
||||
except django_exceptions.ObjectDoesNotExist:
|
||||
# create a new entity with proper coordinates etc
|
||||
tclass = self.prototype["typeclass"]
|
||||
tclass = (
|
||||
f" ({tclass})" if tclass != "evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom" else ""
|
||||
)
|
||||
self.log(f" spawning room at xyz={xyz}{tclass}")
|
||||
nodeobj, err = NodeTypeclass.create(self.prototype.get("key", "An empty room"), xyz=xyz)
|
||||
# create a new entity, using the specified typeclass (if there's one) and
|
||||
# with proper coordinates etc
|
||||
typeclass = self.prototype.get("typeclass")
|
||||
if typeclass is None:
|
||||
raise MapError(f"The prototype {self.prototype} for this node has no 'typeclass' key.", self)
|
||||
self.log(f" spawning room at xyz={xyz} ({typeclass})")
|
||||
Typeclass = class_from_module(typeclass)
|
||||
nodeobj, err = Typeclass.create(self.prototype.get("key", "An empty room"), xyz=xyz)
|
||||
if err:
|
||||
raise RuntimeError(err)
|
||||
else:
|
||||
|
|
@ -400,7 +402,14 @@ class MapNode:
|
|||
continue
|
||||
|
||||
exitnode = self.links[direction]
|
||||
exi, err = ExitTypeclass.create(
|
||||
prot = maplinks[key.lower()][3].prototype
|
||||
typeclass = prot.get("typeclass")
|
||||
if typeclass is None:
|
||||
raise MapError(f"The prototype {self.prototype} for this node has no 'typeclass' key.", self)
|
||||
self.log(f" spawning/updating exit xyz={xyz}, direction={key} ({typeclass})")
|
||||
|
||||
Typeclass = class_from_module(typeclass)
|
||||
exi, err = Typeclass.create(
|
||||
key,
|
||||
xyz=xyz,
|
||||
xyz_destination=exitnode.get_spawn_xyz(),
|
||||
|
|
@ -408,15 +417,8 @@ class MapNode:
|
|||
)
|
||||
if err:
|
||||
raise RuntimeError(err)
|
||||
|
||||
linkobjs[key.lower()] = exi
|
||||
prot = maplinks[key.lower()][3].prototype
|
||||
tclass = prot["typeclass"]
|
||||
tclass = (
|
||||
f" ({tclass})"
|
||||
if tclass != "evennia.contrib.grid.xyzgrid.xyzroom.XYZExit"
|
||||
else ""
|
||||
)
|
||||
self.log(f" spawning/updating exit xyz={xyz}, direction={key}{tclass}")
|
||||
|
||||
# apply prototypes to catch any changes
|
||||
for key, linkobj in linkobjs.items():
|
||||
|
|
|
|||
|
|
@ -7,9 +7,10 @@ used as stand-alone XYZ-coordinate-aware rooms.
|
|||
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
from evennia.objects.objects import DefaultRoom, DefaultExit
|
||||
from evennia.objects.manager import ObjectManager
|
||||
from evennia.objects.objects import DefaultExit, DefaultRoom
|
||||
|
||||
# name of all tag categories. Note that the Z-coordinate is
|
||||
# the `map_name` of the XYZgrid
|
||||
|
|
@ -23,6 +24,8 @@ MAP_ZDEST_TAG_CATEGORY = "exit_dest_z_coordinate"
|
|||
|
||||
GET_XYZGRID = None
|
||||
|
||||
CLIENT_DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
|
||||
|
||||
|
||||
class XYZManager(ObjectManager):
|
||||
"""
|
||||
|
|
@ -229,7 +232,7 @@ class XYZExitManager(XYZManager):
|
|||
f"{key}={val}" for key, val in kwargs.items()
|
||||
)
|
||||
raise self.model.DoesNotExist(
|
||||
f"{self.model.__name__} " f"matching query {inp} does not exist."
|
||||
f"{self.model.__name__} matching query {inp} does not exist."
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -458,7 +461,8 @@ class XYZRoom(DefaultRoom):
|
|||
xymap.options.get("map_separator_char", self.map_separator_char),
|
||||
)
|
||||
|
||||
client_width, _ = looker.sessions.get()[0].get_client_size()
|
||||
sessions = looker.sessions.get()
|
||||
client_width, _ = sessions[0].get_client_size() if sessions else CLIENT_DEFAULT_WIDTH
|
||||
|
||||
map_width = xymap.max_x
|
||||
|
||||
|
|
|
|||
68
evennia/contrib/utils/git_integration/README.md
Normal file
68
evennia/contrib/utils/git_integration/README.md
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# In-game Git Integration
|
||||
|
||||
Contribution by helpme (2022)
|
||||
|
||||
A module to integrate a stripped-down version of git within the game, allowing developers to view their git status, change branches, and pull updated code of both their local mygame repo and Evennia core. After a successful pull or checkout, the git command will reload the game: Manual restarts may be required to to apply certain changes that would impact persistent scripts etc.
|
||||
|
||||
Once the contrib is set up, integrating remote changes is as simple as entering the following into your game:
|
||||
|
||||
```
|
||||
git pull
|
||||
```
|
||||
|
||||
The repositories you want to work with, be it only your local mygame repo, only Evennia core, or both, must be git directories for the command to function. If you are only interested in using this to get upstream Evennia changes, only the Evennia repository needs to be a git repository. [Get started with version control here.](https://www.evennia.com/docs/1.0-dev/Coding/Version-Control.html)
|
||||
|
||||
## Dependencies
|
||||
|
||||
This package requires the dependency "gitpython", a python library used to interact with git repositories. To install, it's easiest to install Evennia's extra requirements:
|
||||
|
||||
- Activate your `virtualenv`
|
||||
- `cd` to the root of the Evennia repository. There should be an `requirements_extra.txt` file here.
|
||||
- `pip install -r requirements_extra.txt`
|
||||
|
||||
## Installation
|
||||
|
||||
This utility adds a simple assortment of 'git' commands. Import the module into your commands and add it to your command set to make it available.
|
||||
|
||||
Specifically, in `mygame/commands/default_cmdsets.py`:
|
||||
|
||||
```python
|
||||
...
|
||||
from evennia.contrib.utils.git_integration import GitCmdSet # <---
|
||||
|
||||
class CharacterCmdset(default_cmds.Character_CmdSet):
|
||||
...
|
||||
def at_cmdset_creation(self):
|
||||
...
|
||||
self.add(GitCmdSet) # <---
|
||||
|
||||
```
|
||||
|
||||
Then `reload` to make the git command available.
|
||||
|
||||
## Usage
|
||||
|
||||
This utility will only work if the directory you wish to work with is a git directory. If they are not, you will be prompted to initiate your directory as a git repository using the following commands in your terminal:
|
||||
|
||||
```
|
||||
git init
|
||||
git remote add origin 'link to your repository'
|
||||
```
|
||||
|
||||
By default, the git commands are only available to those with Developer permissions and higher. You can change this by overriding the command and setting its locks from "cmd:pperm(Developer)" to the lock of your choice.
|
||||
|
||||
The supported commands are:
|
||||
* git status: An overview of your git repository, which files have been changed locally, and the commit you're on.
|
||||
* git branch: What branches are available for you to check out.
|
||||
* git checkout 'branch': Checkout a branch.
|
||||
* git pull: Pull the latest code from your current branch.
|
||||
|
||||
* All of these commands are also available with 'evennia', to serve the same functionality related to your Evennia directory. So:
|
||||
* git evennia status
|
||||
* git evennia branch
|
||||
* git evennia checkout 'branch'
|
||||
* git evennia pull: Pull the latest Evennia code.
|
||||
|
||||
## Settings Used
|
||||
|
||||
The utility uses the existing GAME_DIR and EVENNIA_DIR settings from settings.py. You should not need to alter these if you have a standard directory setup, they ought to exist without any setup required from you.
|
||||
5
evennia/contrib/utils/git_integration/__init__.py
Normal file
5
evennia/contrib/utils/git_integration/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
"""
|
||||
Git integration - helpme 2022
|
||||
"""
|
||||
|
||||
from .git_integration import GitCmdSet # noqa
|
||||
196
evennia/contrib/utils/git_integration/git_integration.py
Normal file
196
evennia/contrib/utils/git_integration/git_integration.py
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
from django.conf import settings
|
||||
from evennia import CmdSet, InterruptCommand
|
||||
from evennia.utils.utils import list_to_string
|
||||
from evennia.commands.default.muxcommand import MuxCommand
|
||||
from evennia.server.sessionhandler import SESSIONS
|
||||
|
||||
import git
|
||||
import datetime
|
||||
|
||||
class GitCommand(MuxCommand):
|
||||
"""
|
||||
The shared functionality between git/git evennia
|
||||
"""
|
||||
|
||||
def parse(self):
|
||||
"""
|
||||
Parse the arguments, set default arg to 'status' and check for existence of currently targeted repo
|
||||
"""
|
||||
super().parse()
|
||||
|
||||
if self.args:
|
||||
split_args = self.args.strip().split(" ", 1)
|
||||
self.action = split_args[0]
|
||||
if len(split_args) > 1:
|
||||
self.args = ''.join(split_args[1:])
|
||||
else:
|
||||
self.args = ''
|
||||
else:
|
||||
self.action = "status"
|
||||
self.args = ""
|
||||
|
||||
self.err_msgs = ["|rInvalid Git Repository|n:",
|
||||
"The {repo_type} repository is not recognized as a git directory.",
|
||||
"In order to initialize it as a git directory, you will need to access your terminal and run the following commands from within your directory:",
|
||||
" git init",
|
||||
" git remote add origin {remote_link}"]
|
||||
|
||||
try:
|
||||
self.repo = git.Repo(self.directory, search_parent_directories=True)
|
||||
except git.exc.InvalidGitRepositoryError:
|
||||
err_msg = '\n'.join(self.err_msgs).format(repo_type=self.repo_type, remote_link=self.remote_link)
|
||||
self.caller.msg(err_msg)
|
||||
raise InterruptCommand
|
||||
|
||||
self.commit = self.repo.head.commit
|
||||
|
||||
try:
|
||||
self.branch = self.repo.active_branch.name
|
||||
except TypeError as type_err:
|
||||
self.caller.msg(type_err)
|
||||
raise InterruptCommand
|
||||
|
||||
def short_sha(self, repo, hexsha):
|
||||
"""
|
||||
Utility: Get the short SHA of a commit.
|
||||
"""
|
||||
short_sha = repo.git.rev_parse(hexsha, short=True)
|
||||
return short_sha
|
||||
|
||||
def get_status(self):
|
||||
"""
|
||||
Retrieves the status of the active git repository, displaying unstaged changes/untracked files.
|
||||
"""
|
||||
time_of_commit = datetime.datetime.fromtimestamp(self.commit.committed_date)
|
||||
status_msg = '\n'.join([f"Branch: |w{self.branch}|n ({self.repo.git.rev_parse(self.commit.hexsha, short=True)}) ({time_of_commit})",
|
||||
f"By {self.commit.author.email}: {self.commit.message}"])
|
||||
|
||||
changedFiles = { item.a_path for item in self.repo.index.diff(None) }
|
||||
if changedFiles:
|
||||
status_msg += f"Unstaged/uncommitted changes:|/ |g{'|/ '.join(changedFiles)}|n|/"
|
||||
if len(self.repo.untracked_files) > 0:
|
||||
status_msg += f"Untracked files:|/ |x{'|/ '.join(self.repo.untracked_files)}|n"
|
||||
return status_msg
|
||||
|
||||
def get_branches(self):
|
||||
"""
|
||||
Display current and available branches.
|
||||
"""
|
||||
remote_refs = self.repo.remote().refs
|
||||
branch_msg = f"Current branch: |w{self.branch}|n. Branches available: {list_to_string(remote_refs)}"
|
||||
return branch_msg
|
||||
|
||||
def checkout(self):
|
||||
"""
|
||||
Check out a specific branch.
|
||||
"""
|
||||
remote_refs = self.repo.remote().refs
|
||||
to_branch = self.args.strip().removeprefix('origin/') # Slightly hacky, but git tacks on the origin/
|
||||
|
||||
if to_branch not in remote_refs:
|
||||
self.caller.msg(f"Branch '{to_branch}' not available.")
|
||||
return False
|
||||
elif to_branch == self.branch:
|
||||
self.caller.msg(f"Already on |w{to_branch}|n. Maybe you want <git pull>?")
|
||||
return False
|
||||
else:
|
||||
try:
|
||||
self.repo.git.checkout(to_branch)
|
||||
except git.exc.GitCommandError as err:
|
||||
self.caller.msg("Couldn't checkout {} ({})".format(to_branch, err.stderr.strip()))
|
||||
return False
|
||||
self.msg(f"Checked out |w{to_branch}|n successfully. Server restart initiated.")
|
||||
return True
|
||||
|
||||
def pull(self):
|
||||
"""
|
||||
Attempt to pull new code.
|
||||
"""
|
||||
old_commit = self.commit
|
||||
try:
|
||||
self.repo.remotes.origin.pull()
|
||||
except git.exc.GitCommandError as err:
|
||||
self.caller.msg("Couldn't pull {} ({})".format(self.branch, err.stderr.strip()))
|
||||
return False
|
||||
if old_commit == self.repo.head.commit:
|
||||
self.caller.msg("No new code to pull, no need to reset.\n")
|
||||
return False
|
||||
else:
|
||||
self.caller.msg(f"You have pulled new code. Server restart initiated.|/Head now at {self.repo.git.rev_parse(self.repo.head.commit.hexsha, short=True)}.|/Author: {self.repo.head.commit.author.name} ({self.repo.head.commit.author.email})|/{self.repo.head.commit.message.strip()}")
|
||||
return True
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
Provide basic Git functionality within the game.
|
||||
"""
|
||||
caller = self.caller
|
||||
|
||||
if self.action == "status":
|
||||
caller.msg(self.get_status())
|
||||
elif self.action == "branch" or (self.action == "checkout" and not self.args):
|
||||
caller.msg(self.get_branches())
|
||||
elif self.action == "checkout":
|
||||
if self.checkout():
|
||||
SESSIONS.portal_restart_server()
|
||||
elif self.action == "pull":
|
||||
if self.pull():
|
||||
SESSIONS.portal_restart_server()
|
||||
else:
|
||||
caller.msg("You can only git status, git branch, git checkout, or git pull.")
|
||||
return
|
||||
|
||||
class CmdGitEvennia(GitCommand):
|
||||
"""
|
||||
Pull the latest code from the evennia core or checkout a different branch.
|
||||
|
||||
Usage:
|
||||
git evennia status - View an overview of the evennia repository status.
|
||||
git evennia branch - View available branches in evennia.
|
||||
git evennia checkout <branch> - Checkout a different branch in evennia.
|
||||
git evennia pull - Pull the latest evennia code.
|
||||
|
||||
For updating your local mygame repository, the same commands are available with 'git'.
|
||||
|
||||
If there are any conflicts encountered, the command will abort. The command will reload your game after pulling new code automatically, but for some changes involving persistent scripts etc, you may need to manually restart.
|
||||
"""
|
||||
|
||||
key = "git evennia"
|
||||
locks = "cmd:pperm(Developer)"
|
||||
help_category = "System"
|
||||
directory = settings.EVENNIA_DIR
|
||||
repo_type = "Evennia"
|
||||
remote_link = "https://github.com/evennia/evennia.git"
|
||||
|
||||
|
||||
class CmdGit(GitCommand):
|
||||
"""
|
||||
Pull the latest code from your repository or checkout a different branch.
|
||||
|
||||
Usage:
|
||||
git status - View an overview of your git repository.
|
||||
git branch - View available branches.
|
||||
git checkout main - Checkout the main branch of your code.
|
||||
git pull - Pull the latest code from your current branch.
|
||||
|
||||
For updating evennia code, the same commands are available with 'git evennia'.
|
||||
|
||||
If there are any conflicts encountered, the command will abort. The command will reload your game after pulling new code automatically, but for changes involving persistent scripts etc, you may need to manually restart.
|
||||
"""
|
||||
|
||||
key = "git"
|
||||
locks = "cmd:pperm(Developer)"
|
||||
help_category = "System"
|
||||
directory = settings.GAME_DIR
|
||||
repo_type = "game"
|
||||
remote_link = "[your remote link]"
|
||||
|
||||
|
||||
# CmdSet for easily install all commands
|
||||
class GitCmdSet(CmdSet):
|
||||
"""
|
||||
The git command.
|
||||
"""
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdGit)
|
||||
self.add(CmdGitEvennia)
|
||||
69
evennia/contrib/utils/git_integration/tests.py
Normal file
69
evennia/contrib/utils/git_integration/tests.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
"""
|
||||
Tests of git.
|
||||
|
||||
"""
|
||||
|
||||
from evennia.utils.test_resources import EvenniaTest
|
||||
from evennia.contrib.utils.git_integration.git_integration import GitCommand
|
||||
from evennia.utils.utils import list_to_string
|
||||
|
||||
import git
|
||||
import mock
|
||||
import datetime
|
||||
|
||||
class TestGitIntegration(EvenniaTest):
|
||||
@mock.patch("git.Repo")
|
||||
@mock.patch("git.Git")
|
||||
@mock.patch("git.Actor")
|
||||
def setUp(self, mock_git, mock_repo, mock_author):
|
||||
super().setUp()
|
||||
|
||||
self.char1.msg = mock.Mock()
|
||||
|
||||
p = mock_git.return_value = False
|
||||
type(mock_repo.clone_from.return_value).bare = p
|
||||
mock_repo.index.add(["mock.txt"])
|
||||
mock_git.Repo.side_effect = git.exc.InvalidGitRepositoryError
|
||||
|
||||
mock_author.name = "Faux Author"
|
||||
mock_author.email = "a@email.com"
|
||||
|
||||
commit_date = datetime.datetime(2021, 2, 1)
|
||||
|
||||
mock_repo.index.commit(
|
||||
"Initial skeleton",
|
||||
author=mock_author,
|
||||
committer=mock_author,
|
||||
author_date=commit_date,
|
||||
commit_date=commit_date,
|
||||
)
|
||||
|
||||
test_cmd_git = GitCommand()
|
||||
self.repo = test_cmd_git.repo = mock_repo
|
||||
self.commit = test_cmd_git.commit = mock_git.head.commit
|
||||
self.branch = test_cmd_git.branch = mock_git.active_branch.name
|
||||
test_cmd_git.caller = self.char1
|
||||
test_cmd_git.args = "nonexistent_branch"
|
||||
self.test_cmd_git = test_cmd_git
|
||||
|
||||
def test_git_status(self):
|
||||
time_of_commit = datetime.datetime.fromtimestamp(self.test_cmd_git.commit.committed_date)
|
||||
status_msg = '\n'.join([f"Branch: |w{self.test_cmd_git.branch}|n ({self.test_cmd_git.repo.git.rev_parse(self.test_cmd_git.commit.hexsha, short=True)}) ({time_of_commit})",
|
||||
f"By {self.test_cmd_git.commit.author.email}: {self.test_cmd_git.commit.message}"])
|
||||
self.assertEqual(status_msg, self.test_cmd_git.get_status())
|
||||
|
||||
def test_git_branch(self):
|
||||
# View current branch
|
||||
remote_refs = self.test_cmd_git.repo.remote().refs
|
||||
branch_msg = f"Current branch: |w{self.test_cmd_git.branch}|n. Branches available: {list_to_string(remote_refs)}"
|
||||
self.assertEqual(branch_msg, self.test_cmd_git.get_branches())
|
||||
|
||||
def test_git_checkout(self):
|
||||
# Checkout no branch
|
||||
self.test_cmd_git.checkout()
|
||||
self.char1.msg.assert_called_with("Branch 'nonexistent_branch' not available.")
|
||||
|
||||
def test_git_pull(self):
|
||||
self.test_cmd_git.pull()
|
||||
self.char1.msg.assert_called_with(f"You have pulled new code. Server restart initiated.|/Head now at {self.repo.git.rev_parse(self.repo.head.commit.hexsha, short=True)}.|/Author: {self.repo.head.commit.author.name} ({self.repo.head.commit.author.email})|/{self.repo.head.commit.message.strip()}")
|
||||
|
||||
|
|
@ -689,13 +689,13 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
exclude (list, optional): A list of objects not to send to.
|
||||
from_obj (Object, optional): An object designated as the
|
||||
"sender" of the message. See `DefaultObject.msg()` for
|
||||
more info.
|
||||
more info. This will be used for `$You/you` if using funcparser inlines.
|
||||
mapping (dict, optional): A mapping of formatting keys
|
||||
`{"key":<object>, "key2":<object2>,...}.
|
||||
The keys must either match `{key}` or `$You(key)/$you(key)` markers
|
||||
in the `text` string. If `<object>` doesn't have a `get_display_name`
|
||||
method, it will be returned as a string. If not set, a key `you` will
|
||||
be auto-added to point to `from_obj` if given, otherwise to `self`.
|
||||
method, it will be returned as a string. Pass "you" to represent the caller,
|
||||
this can be skipped if `from_obj` is provided (that will then act as 'you').
|
||||
raise_funcparse_errors (bool, optional): If set, a failing `$func()` will
|
||||
lead to an outright error. If unset (default), the failing `$func()`
|
||||
will instead appear in output unparsed.
|
||||
|
|
@ -728,6 +728,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
|
||||
char.location.msg_contents(
|
||||
"$You() $conj(attack) $you(defender).",
|
||||
from_obj=player1,
|
||||
mapping={"defender": player2})
|
||||
|
||||
- player1 will see `You attack The Second girl.`
|
||||
|
|
@ -738,7 +739,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
|
||||
char.location.msg_contents(
|
||||
"{attacker} attacks {defender}.",
|
||||
mapping={"attacker:player1, "defender":player2})
|
||||
mapping={"attacker":player1, "defender":player2})
|
||||
|
||||
- player1 will see: 'Player1 attacks The Second girl.'
|
||||
- player2 will see: 'The First girl attacks Player2'
|
||||
|
|
@ -762,7 +763,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
for receiver in contents:
|
||||
|
||||
# actor-stance replacements
|
||||
inmessage = _MSG_CONTENTS_PARSER.parse(
|
||||
outmessage = _MSG_CONTENTS_PARSER.parse(
|
||||
inmessage,
|
||||
raise_errors=raise_funcparse_errors,
|
||||
return_string=True,
|
||||
|
|
@ -772,7 +773,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
)
|
||||
|
||||
# director-stance replacements
|
||||
outmessage = inmessage.format_map(
|
||||
outmessage = outmessage.format_map(
|
||||
{
|
||||
key: obj.get_display_name(looker=receiver)
|
||||
if hasattr(obj, "get_display_name")
|
||||
|
|
@ -1690,8 +1691,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
access_type (str): The type of access that was requested.
|
||||
|
||||
Keyword Args:
|
||||
Not used by default, added for possible expandability in a
|
||||
game.
|
||||
Unused by default, added for possible expandability in a game.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
|
@ -3133,3 +3133,20 @@ class DefaultExit(DefaultObject):
|
|||
|
||||
"""
|
||||
traversing_object.msg(_("You cannot go there."))
|
||||
|
||||
def get_return_exit(self, return_all=False):
|
||||
"""
|
||||
Get the exits that pair with this one in its destination room
|
||||
(i.e. returns to its location)
|
||||
|
||||
Args:
|
||||
return_all (bool): Whether to return available results as a
|
||||
list or single matching exit.
|
||||
|
||||
Returns:
|
||||
queryset or exit (Exit): The matching exit(s).
|
||||
"""
|
||||
query = ObjectDB.objects.filter(db_location=self.destination, db_destination=self.location)
|
||||
if return_all:
|
||||
return query
|
||||
return query.first()
|
||||
|
|
|
|||
|
|
@ -69,6 +69,23 @@ class DefaultObjectTest(BaseEvenniaTest):
|
|||
self.assertEqual(description, obj.db.desc)
|
||||
self.assertEqual(obj.db.creator_ip, self.ip)
|
||||
|
||||
def test_exit_get_return_exit(self):
|
||||
ex1, _ = DefaultExit.create("north", self.room1, self.room2, account=self.account)
|
||||
single_return_exit = ex1.get_return_exit()
|
||||
all_return_exit = ex1.get_return_exit(return_all=True)
|
||||
self.assertEqual(single_return_exit, None)
|
||||
self.assertEqual(len(all_return_exit), 0)
|
||||
|
||||
ex2, _ = DefaultExit.create("south", self.room2, self.room1, account=self.account)
|
||||
single_return_exit = ex1.get_return_exit()
|
||||
all_return_exit = ex1.get_return_exit(return_all=True)
|
||||
self.assertEqual(single_return_exit, ex2)
|
||||
self.assertEqual(len(all_return_exit), 1)
|
||||
|
||||
ex3, _ = DefaultExit.create("also_south", self.room2, self.room1, account=self.account)
|
||||
all_return_exit = ex1.get_return_exit(return_all=True)
|
||||
self.assertEqual(len(all_return_exit), 2)
|
||||
|
||||
def test_urls(self):
|
||||
"Make sure objects are returning URLs"
|
||||
self.assertTrue(self.char1.get_absolute_url())
|
||||
|
|
|
|||
|
|
@ -7,29 +7,29 @@ Handling storage of prototypes, both database-based ones (DBPrototypes) and thos
|
|||
|
||||
import hashlib
|
||||
import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext as _
|
||||
from evennia.scripts.scripts import DefaultScript
|
||||
from evennia.locks.lockhandler import check_lockstring, validate_lockstring
|
||||
from evennia.objects.models import ObjectDB
|
||||
from evennia.scripts.scripts import DefaultScript
|
||||
from evennia.typeclasses.attributes import Attribute
|
||||
from evennia.utils import dbserialize, logger
|
||||
from evennia.utils.create import create_script
|
||||
from evennia.utils.evmore import EvMore
|
||||
from evennia.utils.evtable import EvTable
|
||||
from evennia.utils.funcparser import FuncParser
|
||||
from evennia.utils.utils import (
|
||||
all_from_module,
|
||||
variable_from_module,
|
||||
make_iter,
|
||||
is_iter,
|
||||
dbid_to_obj,
|
||||
justify,
|
||||
class_from_module,
|
||||
dbid_to_obj,
|
||||
is_iter,
|
||||
justify,
|
||||
make_iter,
|
||||
variable_from_module,
|
||||
)
|
||||
from evennia.locks.lockhandler import validate_lockstring, check_lockstring
|
||||
from evennia.utils import logger
|
||||
from evennia.utils.funcparser import FuncParser
|
||||
from evennia.utils import dbserialize
|
||||
from evennia.utils.evtable import EvTable
|
||||
|
||||
_MODULE_PROTOTYPE_MODULES = {}
|
||||
_MODULE_PROTOTYPES = {}
|
||||
|
|
@ -925,7 +925,13 @@ def validate_prototype(
|
|||
|
||||
|
||||
def protfunc_parser(
|
||||
value, available_functions=None, testing=False, stacktrace=False, caller=None, **kwargs
|
||||
value,
|
||||
available_functions=None,
|
||||
testing=False,
|
||||
stacktrace=False,
|
||||
caller=None,
|
||||
raise_errors=True,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Parse a prototype value string for a protfunc and process it.
|
||||
|
|
@ -939,6 +945,7 @@ def protfunc_parser(
|
|||
available_functions (dict, optional): Mapping of name:protfunction to use for this parsing.
|
||||
If not set, use default sources.
|
||||
stacktrace (bool, optional): If set, print the stack parsing process of the protfunc-parser.
|
||||
raise_errors (bool, optional): Raise explicit errors from malformed/not found protfunc calls.
|
||||
|
||||
Keyword Args:
|
||||
session (Session): Passed to protfunc. Session of the entity spawning the prototype.
|
||||
|
|
@ -957,7 +964,7 @@ def protfunc_parser(
|
|||
if not isinstance(value, str):
|
||||
return value
|
||||
|
||||
result = FUNC_PARSER.parse_to_any(value, raise_errors=True, caller=caller, **kwargs)
|
||||
result = FUNC_PARSER.parse_to_any(value, raise_errors=raise_errors, caller=caller, **kwargs)
|
||||
|
||||
return result
|
||||
|
||||
|
|
@ -1100,7 +1107,9 @@ def check_permission(prototype_key, action, default=True):
|
|||
return default
|
||||
|
||||
|
||||
def init_spawn_value(value, validator=None, caller=None, prototype=None):
|
||||
def init_spawn_value(
|
||||
value, validator=None, caller=None, prototype=None, protfunc_raise_errors=True
|
||||
):
|
||||
"""
|
||||
Analyze the prototype value and produce a value useful at the point of spawning.
|
||||
|
||||
|
|
@ -1128,7 +1137,9 @@ def init_spawn_value(value, validator=None, caller=None, prototype=None):
|
|||
value = validator(value[0](*make_iter(args)))
|
||||
else:
|
||||
value = validator(value)
|
||||
result = protfunc_parser(value, caller=caller, prototype=prototype)
|
||||
result = protfunc_parser(
|
||||
value, caller=caller, prototype=prototype, raise_errors=protfunc_raise_errors
|
||||
)
|
||||
if result != value:
|
||||
return validator(result)
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -137,21 +137,19 @@ import copy
|
|||
import hashlib
|
||||
import time
|
||||
|
||||
import evennia
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
import evennia
|
||||
from evennia.objects.models import ObjectDB
|
||||
from evennia.utils import logger
|
||||
from evennia.utils.utils import make_iter, is_iter
|
||||
from evennia.prototypes import prototypes as protlib
|
||||
from evennia.prototypes.prototypes import (
|
||||
PROTOTYPE_TAG_CATEGORY,
|
||||
init_spawn_value,
|
||||
value_to_obj,
|
||||
value_to_obj_or_any,
|
||||
init_spawn_value,
|
||||
PROTOTYPE_TAG_CATEGORY,
|
||||
)
|
||||
|
||||
from evennia.utils import logger
|
||||
from evennia.utils.utils import is_iter, make_iter
|
||||
|
||||
_CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination")
|
||||
_PROTOTYPE_META_NAMES = (
|
||||
|
|
@ -634,7 +632,7 @@ def format_diff(diff, minimal=True):
|
|||
|
||||
|
||||
def batch_update_objects_with_prototype(
|
||||
prototype, diff=None, objects=None, exact=False, caller=None
|
||||
prototype, diff=None, objects=None, exact=False, caller=None, protfunc_raise_errors=True
|
||||
):
|
||||
"""
|
||||
Update existing objects with the latest version of the prototype.
|
||||
|
|
@ -653,6 +651,8 @@ def batch_update_objects_with_prototype(
|
|||
objects will be removed if they exist. This will lead to a more accurate 1:1 correlation
|
||||
between the object and the prototype but is usually impractical.
|
||||
caller (Object or Account, optional): This may be used by protfuncs to do permission checks.
|
||||
protfunc_raise_errors (bool): Have protfuncs raise explicit errors if malformed/not found.
|
||||
This is highly recommended.
|
||||
Returns:
|
||||
changed (int): The number of objects that had changes applied to them.
|
||||
|
||||
|
|
@ -704,7 +704,13 @@ def batch_update_objects_with_prototype(
|
|||
do_save = True
|
||||
|
||||
def _init(val, typ):
|
||||
return init_spawn_value(val, str, caller=caller, prototype=new_prototype)
|
||||
return init_spawn_value(
|
||||
val,
|
||||
str,
|
||||
caller=caller,
|
||||
prototype=new_prototype,
|
||||
protfunc_raise_errors=protfunc_raise_errors,
|
||||
)
|
||||
|
||||
if key == "key":
|
||||
obj.db_key = _init(val, str)
|
||||
|
|
@ -892,6 +898,8 @@ def spawn(*prototypes, caller=None, **kwargs):
|
|||
custom `prototype_parents` are given to this function.
|
||||
only_validate (bool): Only run validation of prototype/parents
|
||||
(no object creation) and return the create-kwargs.
|
||||
protfunc_raise_errors (bool): Raise explicit exceptions on a malformed/not-found
|
||||
protfunc. Defaults to True.
|
||||
|
||||
Returns:
|
||||
object (Object, dict or list): Spawned object(s). If `only_validate` is given, return
|
||||
|
|
@ -938,57 +946,55 @@ def spawn(*prototypes, caller=None, **kwargs):
|
|||
# extract the keyword args we need to create the object itself. If we get a callable,
|
||||
# call that to get the value (don't catch errors)
|
||||
create_kwargs = {}
|
||||
init_spawn_kwargs = dict(
|
||||
caller=caller,
|
||||
prototype=prototype,
|
||||
protfunc_raise_errors=kwargs.get("protfunc_raise_errors", True),
|
||||
)
|
||||
|
||||
# we must always add a key, so if not given we use a shortened md5 hash. There is a (small)
|
||||
# chance this is not unique but it should usually not be a problem.
|
||||
val = prot.pop(
|
||||
"key",
|
||||
"Spawned-{}".format(hashlib.md5(bytes(str(time.time()), "utf-8")).hexdigest()[:6]),
|
||||
)
|
||||
create_kwargs["db_key"] = init_spawn_value(val, str, caller=caller, prototype=prototype)
|
||||
create_kwargs["db_key"] = init_spawn_value(val, str, **init_spawn_kwargs)
|
||||
|
||||
val = prot.pop("location", None)
|
||||
create_kwargs["db_location"] = init_spawn_value(
|
||||
val, value_to_obj, caller=caller, prototype=prototype
|
||||
)
|
||||
create_kwargs["db_location"] = init_spawn_value(val, value_to_obj, **init_spawn_kwargs)
|
||||
|
||||
val = prot.pop("home", None)
|
||||
if val:
|
||||
create_kwargs["db_home"] = init_spawn_value(
|
||||
val, value_to_obj, caller=caller, prototype=prototype
|
||||
)
|
||||
create_kwargs["db_home"] = init_spawn_value(val, value_to_obj, **init_spawn_kwargs)
|
||||
else:
|
||||
try:
|
||||
create_kwargs["db_home"] = init_spawn_value(
|
||||
settings.DEFAULT_HOME, value_to_obj, caller=caller, prototype=prototype
|
||||
settings.DEFAULT_HOME, value_to_obj, **init_spawn_kwargs
|
||||
)
|
||||
except ObjectDB.DoesNotExist:
|
||||
# settings.DEFAULT_HOME not existing is common for unittests
|
||||
pass
|
||||
|
||||
val = prot.pop("destination", None)
|
||||
create_kwargs["db_destination"] = init_spawn_value(
|
||||
val, value_to_obj, caller=caller, prototype=prototype
|
||||
)
|
||||
create_kwargs["db_destination"] = init_spawn_value(val, value_to_obj, **init_spawn_kwargs)
|
||||
|
||||
val = prot.pop("typeclass", settings.BASE_OBJECT_TYPECLASS)
|
||||
create_kwargs["db_typeclass_path"] = init_spawn_value(
|
||||
val, str, caller=caller, prototype=prototype
|
||||
)
|
||||
create_kwargs["db_typeclass_path"] = init_spawn_value(val, str, **init_spawn_kwargs)
|
||||
|
||||
# extract calls to handlers
|
||||
val = prot.pop("permissions", [])
|
||||
permission_string = init_spawn_value(val, make_iter, caller=caller, prototype=prototype)
|
||||
permission_string = init_spawn_value(val, make_iter, **init_spawn_kwargs)
|
||||
val = prot.pop("locks", "")
|
||||
lock_string = init_spawn_value(val, str, caller=caller, prototype=prototype)
|
||||
lock_string = init_spawn_value(val, str, **init_spawn_kwargs)
|
||||
val = prot.pop("aliases", [])
|
||||
alias_string = init_spawn_value(val, make_iter, caller=caller, prototype=prototype)
|
||||
alias_string = init_spawn_value(val, make_iter, **init_spawn_kwargs)
|
||||
|
||||
val = prot.pop("tags", [])
|
||||
tags = []
|
||||
for (tag, category, *data) in val:
|
||||
tags.append(
|
||||
(
|
||||
init_spawn_value(tag, str, caller=caller, prototype=prototype),
|
||||
init_spawn_value(tag, str, **init_spawn_kwargs),
|
||||
category,
|
||||
data[0] if data else None,
|
||||
)
|
||||
|
|
@ -1000,13 +1006,13 @@ def spawn(*prototypes, caller=None, **kwargs):
|
|||
tags.append((prototype_key, PROTOTYPE_TAG_CATEGORY))
|
||||
|
||||
val = prot.pop("exec", "")
|
||||
execs = init_spawn_value(val, make_iter, caller=caller, prototype=prototype)
|
||||
execs = init_spawn_value(val, make_iter, **init_spawn_kwargs)
|
||||
|
||||
# extract ndb assignments
|
||||
nattributes = dict(
|
||||
(
|
||||
key.split("_", 1)[1],
|
||||
init_spawn_value(val, value_to_obj, caller=caller, prototype=prototype),
|
||||
init_spawn_value(val, value_to_obj, **init_spawn_kwargs),
|
||||
)
|
||||
for key, val in prot.items()
|
||||
if key.startswith("ndb_")
|
||||
|
|
@ -1019,7 +1025,7 @@ def spawn(*prototypes, caller=None, **kwargs):
|
|||
attributes.append(
|
||||
(
|
||||
attrname,
|
||||
init_spawn_value(value, caller=caller, prototype=prototype),
|
||||
init_spawn_value(value, **init_spawn_kwargs),
|
||||
rest[0] if rest else None,
|
||||
rest[1] if len(rest) > 1 else None,
|
||||
)
|
||||
|
|
@ -1036,9 +1042,7 @@ def spawn(*prototypes, caller=None, **kwargs):
|
|||
simple_attributes.append(
|
||||
(
|
||||
key,
|
||||
init_spawn_value(
|
||||
value, value_to_obj_or_any, caller=caller, prototype=prototype
|
||||
),
|
||||
init_spawn_value(value, value_to_obj_or_any, **init_spawn_kwargs),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,19 +3,20 @@ Unit tests for the prototypes and spawner
|
|||
|
||||
"""
|
||||
|
||||
from random import randint, sample
|
||||
import mock
|
||||
import uuid
|
||||
from random import randint, sample
|
||||
from time import time
|
||||
|
||||
import mock
|
||||
from anything import Something
|
||||
from django.test.utils import override_settings
|
||||
from evennia.prototypes import menus as olc_menus
|
||||
from evennia.prototypes import protfuncs as protofuncs
|
||||
from evennia.prototypes import prototypes as protlib
|
||||
from evennia.prototypes import spawner
|
||||
from evennia.prototypes.prototypes import _PROTOTYPE_TAG_META_CATEGORY
|
||||
from evennia.utils.test_resources import BaseEvenniaTest
|
||||
from evennia.utils.tests.test_evmenu import TestEvMenu
|
||||
from evennia.prototypes import spawner, prototypes as protlib
|
||||
from evennia.prototypes import menus as olc_menus
|
||||
from evennia.prototypes import protfuncs as protofuncs, spawner
|
||||
|
||||
from evennia.prototypes.prototypes import _PROTOTYPE_TAG_META_CATEGORY
|
||||
|
||||
_PROTPARENTS = {
|
||||
"NOBODY": {},
|
||||
|
|
@ -43,6 +44,11 @@ _PROTPARENTS = {
|
|||
"key": "goblin archwizard",
|
||||
"prototype_parent": ("GOBLIN_WIZARD", "ARCHWIZARD"),
|
||||
},
|
||||
"ISSUE2908": {
|
||||
"typeclass": "evennia.objects.objects.DefaultObject",
|
||||
"key": "testobject_isse2909",
|
||||
"location": "$choice($objlist(",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -163,10 +169,12 @@ class TestUtils(BaseEvenniaTest):
|
|||
"key": "Obj",
|
||||
"home": Something,
|
||||
"location": Something,
|
||||
"locks": "call:true();control:perm(Developer);delete:perm(Admin);"
|
||||
"drop:holds();"
|
||||
"edit:perm(Admin);examine:perm(Builder);get:all();"
|
||||
"puppet:pperm(Developer);tell:perm(Admin);view:all()",
|
||||
"locks": (
|
||||
"call:true();control:perm(Developer);delete:perm(Admin);"
|
||||
"drop:holds();"
|
||||
"edit:perm(Admin);examine:perm(Builder);get:all();"
|
||||
"puppet:pperm(Developer);tell:perm(Admin);view:all()"
|
||||
),
|
||||
"prototype_desc": "Built from Obj",
|
||||
"prototype_key": Something,
|
||||
"prototype_locks": "spawn:all();edit:all()",
|
||||
|
|
@ -183,10 +191,12 @@ class TestUtils(BaseEvenniaTest):
|
|||
"home": Something,
|
||||
"key": "Obj",
|
||||
"location": Something,
|
||||
"locks": "call:true();control:perm(Developer);delete:perm(Admin);"
|
||||
"drop:holds();"
|
||||
"edit:perm(Admin);examine:perm(Builder);get:all();"
|
||||
"puppet:pperm(Developer);tell:perm(Admin);view:all()",
|
||||
"locks": (
|
||||
"call:true();control:perm(Developer);delete:perm(Admin);"
|
||||
"drop:holds();"
|
||||
"edit:perm(Admin);examine:perm(Builder);get:all();"
|
||||
"puppet:pperm(Developer);tell:perm(Admin);view:all()"
|
||||
),
|
||||
"new": "new_val",
|
||||
"permissions": ["Builder"],
|
||||
"prototype_desc": "New version of prototype",
|
||||
|
|
@ -962,3 +972,24 @@ class TestPartialTagAttributes(BaseEvenniaTest):
|
|||
def test_partial_spawn(self):
|
||||
obj = spawner.spawn(self.prot)
|
||||
self.assertEqual(obj[0].key, self.prot["key"])
|
||||
|
||||
|
||||
class TestIssue2908(BaseEvenniaTest):
|
||||
"""
|
||||
Test spawning a prototype with a nested protfunc, as per issue #2908.
|
||||
|
||||
"""
|
||||
|
||||
def test_spawn_with_protfunc(self):
|
||||
|
||||
self.room1.tags.add("beach", category="zone")
|
||||
|
||||
prot = {
|
||||
"prototype_key": "rock",
|
||||
"typeclass": "evennia.objects.objects.DefaultObject",
|
||||
"key": "a rock",
|
||||
"location": "$choice($objlist(beach,category=zone,type=tag))",
|
||||
}
|
||||
|
||||
obj = spawner.spawn(prot, caller=self.char1)
|
||||
self.assertEqual(obj[0].location, self.room1)
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ def create_objects():
|
|||
"examine:perm(Developer);edit:false();delete:false();boot:false();msg:all();puppet:false()"
|
||||
)
|
||||
# we set this low so that quelling is more useful
|
||||
superuser_character.permissions.add("Player")
|
||||
superuser_character.permissions.add("Developer")
|
||||
superuser_character.save()
|
||||
|
||||
superuser.attributes.add("_first_login", True)
|
||||
|
|
|
|||
|
|
@ -153,7 +153,8 @@ HTTP_LOG_FILE = os.path.join(LOG_DIR, "http_requests.log")
|
|||
LOCKWARNING_LOG_FILE = os.path.join(LOG_DIR, "lockwarnings.log")
|
||||
# Number of lines to append to rotating channel logs when they rotate
|
||||
CHANNEL_LOG_NUM_TAIL_LINES = 20
|
||||
# Max size (in bytes) of channel log files before they rotate
|
||||
# Max size (in bytes) of channel log files before they rotate.
|
||||
# Minimum is 1000 (1kB) but should usually be larger.
|
||||
CHANNEL_LOG_ROTATE_SIZE = 1000000
|
||||
# Unused by default, but used by e.g. the MapSystem contrib. A place for storing
|
||||
# semi-permanent data and avoid it being rebuilt over and over. It is created
|
||||
|
|
@ -584,9 +585,7 @@ OPTIONS_ACCOUNT_DEFAULT = {
|
|||
"footer_text_color": ("Text inside Footer Lines.", "Color", "n"),
|
||||
"footer_fill": ("Fill for Footer Lines.", "Text", "="),
|
||||
"column_names_color": ("Table column header text.", "Color", "w"),
|
||||
"help_category_color": ("Help category names.", "Color", "n"),
|
||||
"help_entry_color": ("Help entry names.", "Color", "n"),
|
||||
"timezone": ("Timezone for dates. @tz for a list.", "Timezone", "UTC"),
|
||||
"timezone": ("Timezone for dates.", "Timezone", "UTC"),
|
||||
}
|
||||
# Modules holding Option classes, responsible for serializing the option and
|
||||
# calling validator functions on it. Same-named functions in modules added
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@ all Attributes and TypedObjects).
|
|||
|
||||
"""
|
||||
import shlex
|
||||
from django.db.models import F, Q, Count, ExpressionWrapper, FloatField
|
||||
|
||||
from django.db.models import Count, ExpressionWrapper, F, FloatField, Q
|
||||
from django.db.models.functions import Cast
|
||||
from evennia.utils import idmapper
|
||||
from evennia.utils.utils import make_iter, variable_from_module
|
||||
from evennia.typeclasses.attributes import Attribute
|
||||
from evennia.typeclasses.tags import Tag
|
||||
from evennia.utils import idmapper
|
||||
from evennia.utils.utils import class_from_module, make_iter, variable_from_module
|
||||
|
||||
__all__ = ("TypedObjectManager",)
|
||||
_GA = object.__getattribute__
|
||||
|
|
@ -196,6 +197,8 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager):
|
|||
query.append(("db_key", key))
|
||||
if category:
|
||||
query.append(("db_category", category))
|
||||
else:
|
||||
query.append(("db_category", None))
|
||||
return _Tag.objects.filter(**dict(query))
|
||||
else:
|
||||
# search only among tags stored on on this model
|
||||
|
|
@ -537,15 +540,14 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager):
|
|||
|
||||
def typeclass_search(self, typeclass, include_children=False, include_parents=False):
|
||||
"""
|
||||
Searches through all objects returning those which has a
|
||||
certain typeclass. If location is set, limit search to objects
|
||||
in that location.
|
||||
Searches through all objects returning those which are of the
|
||||
specified typeclass.
|
||||
|
||||
Args:
|
||||
typeclass (str or class): A typeclass class or a python path to a typeclass.
|
||||
include_children (bool, optional): Return objects with
|
||||
given typeclass *and* all children inheriting from this
|
||||
typeclass. Mutuall exclusive to `include_parents`.
|
||||
typeclass. Mutually exclusive to `include_parents`.
|
||||
include_parents (bool, optional): Return objects with
|
||||
given typeclass *and* all parents to this typeclass.
|
||||
Mutually exclusive to `include_children`.
|
||||
|
|
@ -553,35 +555,28 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager):
|
|||
Returns:
|
||||
objects (list): The objects found with the given typeclasses.
|
||||
|
||||
Raises:
|
||||
ImportError: If the provided `typeclass` is not a valid typeclass or the
|
||||
path to an existing typeclass.
|
||||
|
||||
"""
|
||||
|
||||
if callable(typeclass):
|
||||
cls = typeclass.__class__
|
||||
typeclass = "%s.%s" % (cls.__module__, cls.__name__)
|
||||
elif not isinstance(typeclass, str) and hasattr(typeclass, "path"):
|
||||
typeclass = typeclass.path
|
||||
|
||||
# query objects of exact typeclass
|
||||
query = Q(db_typeclass_path__exact=typeclass)
|
||||
if not callable(typeclass):
|
||||
typeclass = class_from_module(typeclass)
|
||||
|
||||
if include_children:
|
||||
# build requests for child typeclass objects
|
||||
clsmodule, clsname = typeclass.rsplit(".", 1)
|
||||
cls = variable_from_module(clsmodule, clsname)
|
||||
subclasses = cls.__subclasses__()
|
||||
if subclasses:
|
||||
for child in (child for child in subclasses if hasattr(child, "path")):
|
||||
query = query | Q(db_typeclass_path__exact=child.path)
|
||||
elif include_parents:
|
||||
# build requests for parent typeclass objects
|
||||
clsmodule, clsname = typeclass.rsplit(".", 1)
|
||||
cls = variable_from_module(clsmodule, clsname)
|
||||
parents = cls.__mro__
|
||||
query = typeclass.objects.all_family()
|
||||
else:
|
||||
query = typeclass.objects.all()
|
||||
|
||||
if include_parents:
|
||||
parents = typeclass.__mro__
|
||||
if parents:
|
||||
parent_queries = []
|
||||
for parent in (parent for parent in parents if hasattr(parent, "path")):
|
||||
query = query | Q(db_typeclass_path__exact=parent.path)
|
||||
# actually query the database
|
||||
return super().filter(query)
|
||||
parent_queries.append(super().filter(db_typeclass_path__exact=parent.path))
|
||||
query = query.union(*parent_queries)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
class TypeclassManager(TypedObjectManager):
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ Unit tests for typeclass base system
|
|||
"""
|
||||
|
||||
from django.test import override_settings
|
||||
from evennia.typeclasses import attributes
|
||||
from evennia.objects.objects import DefaultObject
|
||||
from evennia.utils.test_resources import BaseEvenniaTest, EvenniaTestCase
|
||||
from mock import patch
|
||||
from parameterized import parameterized
|
||||
|
|
@ -213,6 +213,82 @@ class TestTypedObjectManager(BaseEvenniaTest):
|
|||
self.assertEqual(tagobj.db_data, "data4")
|
||||
|
||||
|
||||
# setting up testing typeclass with child- and parent class
|
||||
class TestSearchManagerTypeclassParent(DefaultObject):
|
||||
pass
|
||||
|
||||
|
||||
class TestSearchManagerTypeclass(TestSearchManagerTypeclassParent):
|
||||
pass
|
||||
|
||||
|
||||
class TestSearchManagerTypeclassChild(TestSearchManagerTypeclass):
|
||||
pass
|
||||
|
||||
|
||||
class TestSearchTypeclassFamily(EvenniaTestCase):
|
||||
"""
|
||||
Test the manager method for searching for inheriting typeclasses.
|
||||
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.obj_parent, _ = TestSearchManagerTypeclassParent.create(key="obj_parent")
|
||||
self.obj1, _ = TestSearchManagerTypeclass.create(key="obj1")
|
||||
self.obj2, _ = TestSearchManagerTypeclass.create(key="obj2")
|
||||
self.obj_child, _ = TestSearchManagerTypeclassChild.create(key="obj_child")
|
||||
|
||||
def test_typeclass_search__inputs(self):
|
||||
"""Test basic functionality"""
|
||||
|
||||
res1 = self.obj1.__class__.objects.typeclass_search(self.obj1.__class__)
|
||||
res2 = self.obj1.__class__.objects.typeclass_search(
|
||||
"evennia.typeclasses.tests.TestSearchManagerTypeclass"
|
||||
)
|
||||
self.assertEqual(set(res1), {self.obj1, self.obj2})
|
||||
self.assertEqual(set(res2), {self.obj1, self.obj2})
|
||||
|
||||
def test_typeclass_search__children_and_parents(self):
|
||||
"""Test getting parents/child classes"""
|
||||
|
||||
# just the objects of this typeclass
|
||||
res1 = self.obj1.__class__.objects.typeclass_search(self.obj1.__class__)
|
||||
res2 = self.obj2.__class__.objects.typeclass_search(self.obj2.__class__)
|
||||
|
||||
# these objects + children
|
||||
res3 = self.obj1.__class__.objects.typeclass_search(
|
||||
self.obj1.__class__, include_children=True
|
||||
)
|
||||
# these objects + parents
|
||||
res4 = self.obj1.__class__.objects.typeclass_search(
|
||||
self.obj1.__class__, include_parents=True
|
||||
)
|
||||
# these objects + parents + children
|
||||
res5 = self.obj1.__class__.objects.typeclass_search(
|
||||
self.obj1.__class__, include_children=True, include_parents=True
|
||||
)
|
||||
|
||||
self.assertEqual(set(res1), {self.obj1, self.obj2})
|
||||
self.assertEqual(set(res2), {self.obj1, self.obj2})
|
||||
self.assertEqual(set(res3), {self.obj1, self.obj2, self.obj_child})
|
||||
self.assertEqual(set(res4), {self.obj1, self.obj2, self.obj_parent})
|
||||
self.assertEqual(set(res5), {self.obj1, self.obj2, self.obj_child, self.obj_parent})
|
||||
|
||||
def test_typeclass_search__nested(self):
|
||||
"""Test several levels deep searches"""
|
||||
# check all children of the parent
|
||||
res1 = self.obj1.__class__.objects.typeclass_search(
|
||||
self.obj_parent.__class__, include_children=True
|
||||
)
|
||||
# check all parents of the child
|
||||
res2 = self.obj1.__class__.objects.typeclass_search(
|
||||
self.obj_child.__class__, include_parents=True
|
||||
)
|
||||
|
||||
self.assertEqual(set(res1), {self.obj_parent, self.obj1, self.obj2, self.obj_child})
|
||||
self.assertEqual(set(res2), {self.obj_parent, self.obj1, self.obj2, self.obj_child})
|
||||
|
||||
|
||||
class TestTags(BaseEvenniaTest):
|
||||
def test_has_tag_key_only(self):
|
||||
self.obj1.tags.add("tagC")
|
||||
|
|
@ -238,6 +314,23 @@ class TestTags(BaseEvenniaTest):
|
|||
self.obj1.tags.add("tagC", "categoryC")
|
||||
self.assertFalse(self.obj1.tags.has(category="categoryD"))
|
||||
|
||||
def test_tag_add_no_category__issue_2688(self):
|
||||
"""
|
||||
Adding a tag without a category should create a new tag:None tag
|
||||
rather than trying to update an existing tag:category tag.
|
||||
"""
|
||||
# adding tag+category, creates tag+category entry
|
||||
self.obj1.tags.add("testing", category="testing_category")
|
||||
self.assertEqual(
|
||||
self.obj1.tags.all(return_key_and_category=True), [("testing", "testing_category")]
|
||||
)
|
||||
# adding a new tag with no category should create a tag+None entry
|
||||
self.obj1.tags.add("testing")
|
||||
self.assertEqual(
|
||||
self.obj1.tags.all(return_key_and_category=True),
|
||||
[("testing", "testing_category"), ("testing", None)],
|
||||
)
|
||||
|
||||
|
||||
class TestNickHandler(BaseEvenniaTest):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -61,8 +61,9 @@ Use as follows:
|
|||
# create a new form from the template
|
||||
form = EvForm("path/to/testform.py")
|
||||
|
||||
(EvForm can also take a dictionary holding
|
||||
the required keys FORMCHAR, TABLECHAR and FORM)
|
||||
# EvForm can also take a dictionary instead of a filepath, as long
|
||||
# as the dict contains the keys FORMCHAR, TABLECHAR and FORM
|
||||
# form = EvForm(form=form_dict)
|
||||
|
||||
# add data to each tagged form cell
|
||||
form.map(cells={1: "Tom the Bouncer",
|
||||
|
|
|
|||
|
|
@ -354,7 +354,6 @@ class FuncParser:
|
|||
|
||||
if curr_func:
|
||||
# we are starting a nested funcdef
|
||||
return_str = True
|
||||
if len(callstack) > _MAX_NESTING:
|
||||
# stack full - ignore this function
|
||||
if raise_errors:
|
||||
|
|
@ -799,7 +798,7 @@ def funcparser_callable_round(*args, **kwargs):
|
|||
num, *significant = args
|
||||
significant = significant[0] if significant else 0
|
||||
try:
|
||||
round(num, significant)
|
||||
return round(num, significant)
|
||||
except Exception:
|
||||
if kwargs.get("raise_errors"):
|
||||
raise
|
||||
|
|
@ -867,22 +866,33 @@ def funcparser_callable_choice(*args, **kwargs):
|
|||
Args:
|
||||
listing (list): A list of items to randomly choose between.
|
||||
This will be converted from a string to a real list.
|
||||
*args: If multiple args are given, will pick one randomly from them.
|
||||
|
||||
Returns:
|
||||
any: The randomly chosen element.
|
||||
|
||||
Example:
|
||||
- `$choice([key, flower, house])`
|
||||
- `$choice(key, flower, house)`
|
||||
- `$choice([1, 2, 3, 4])`
|
||||
|
||||
"""
|
||||
if not args:
|
||||
return ""
|
||||
args, _ = safe_convert_to_types(("py", {}), *args, **kwargs)
|
||||
if not args[0]:
|
||||
|
||||
nargs = len(args)
|
||||
if nargs == 1:
|
||||
# this needs to be a list/tuple for this to make sense
|
||||
args, _ = safe_convert_to_types(("py", {}), args[0], **kwargs)
|
||||
args = make_iter(args[0]) if args else None
|
||||
else:
|
||||
# separate arg per entry
|
||||
converters = ["py" for _ in range(nargs)]
|
||||
args, _ = safe_convert_to_types((converters, {}), *args, **kwargs)
|
||||
|
||||
if not args:
|
||||
return ""
|
||||
try:
|
||||
return random.choice(args[0])
|
||||
return random.choice(args)
|
||||
except Exception:
|
||||
if kwargs.get("raise_errors"):
|
||||
raise
|
||||
|
|
@ -1153,7 +1163,7 @@ def funcparser_callable_search(*args, caller=None, access="control", **kwargs):
|
|||
if not targets:
|
||||
if return_list:
|
||||
return []
|
||||
raise ParsingError(f"$search: Query '{query}' gave no matches.")
|
||||
raise ParsingError(f"$search: Query '{args[0]}' gave no matches.")
|
||||
|
||||
if len(targets) > 1 and not return_list:
|
||||
raise ParsingError(
|
||||
|
|
@ -1162,7 +1172,7 @@ def funcparser_callable_search(*args, caller=None, access="control", **kwargs):
|
|||
)
|
||||
|
||||
for target in targets:
|
||||
if not target.access(caller, target, access):
|
||||
if not target.access(caller, access):
|
||||
raise ParsingError("$search Cannot add found entity - access failure.")
|
||||
|
||||
return list(targets) if return_list else targets[0]
|
||||
|
|
|
|||
|
|
@ -384,7 +384,7 @@ class EvenniaLogFile(logfile.LogFile):
|
|||
from django.conf import settings
|
||||
|
||||
_CHANNEL_LOG_NUM_TAIL_LINES = settings.CHANNEL_LOG_NUM_TAIL_LINES
|
||||
num_lines_to_append = _CHANNEL_LOG_NUM_TAIL_LINES
|
||||
num_lines_to_append = max(1, _CHANNEL_LOG_NUM_TAIL_LINES)
|
||||
|
||||
def rotate(self, num_lines_to_append=None):
|
||||
"""
|
||||
|
|
@ -463,7 +463,7 @@ def _open_log_file(filename):
|
|||
from django.conf import settings
|
||||
|
||||
_LOGDIR = settings.LOG_DIR
|
||||
_LOG_ROTATE_SIZE = settings.CHANNEL_LOG_ROTATE_SIZE
|
||||
_LOG_ROTATE_SIZE = max(1000, settings.CHANNEL_LOG_ROTATE_SIZE)
|
||||
|
||||
filename = os.path.join(_LOGDIR, filename)
|
||||
if filename in _LOG_FILE_HANDLES:
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ __all__ = (
|
|||
"search_script_tag",
|
||||
"search_account_tag",
|
||||
"search_channel_tag",
|
||||
"search_typeclass",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -362,3 +363,35 @@ def search_channel_tag(key=None, category=None, tagtype=None, **kwargs):
|
|||
|
||||
# search for tag objects (not the objects they are attached to
|
||||
search_tag_object = ObjectDB.objects.get_tag
|
||||
|
||||
|
||||
# Locate Objects by Typeclass
|
||||
|
||||
# search_objects_by_typeclass(typeclass="", include_children=False, include_parents=False) (also search_typeclass works)
|
||||
# This returns the objects of the given typeclass
|
||||
|
||||
|
||||
def search_objects_by_typeclass(typeclass, include_children=False, include_parents=False):
|
||||
"""
|
||||
Searches through all objects returning those of a certain typeclass.
|
||||
|
||||
Args:
|
||||
typeclass (str or class): A typeclass class or a python path to a typeclass.
|
||||
include_children (bool, optional): Return objects with
|
||||
given typeclass *and* all children inheriting from this
|
||||
typeclass. Mutuall exclusive to `include_parents`.
|
||||
include_parents (bool, optional): Return objects with
|
||||
given typeclass *and* all parents to this typeclass.
|
||||
Mutually exclusive to `include_children`.
|
||||
|
||||
Returns:
|
||||
objects (list): The objects found with the given typeclasses.
|
||||
"""
|
||||
return ObjectDB.objects.typeclass_search(
|
||||
typeclass=typeclass,
|
||||
include_children=include_children,
|
||||
include_parents=include_parents,
|
||||
)
|
||||
|
||||
|
||||
search_typeclass = search_objects_by_typeclass
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ Test the funcparser module.
|
|||
"""
|
||||
|
||||
import time
|
||||
import unittest
|
||||
from ast import literal_eval
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
|
@ -77,6 +78,10 @@ def _lsum_callable(*args, **kwargs):
|
|||
return ""
|
||||
|
||||
|
||||
def _raises_callable(*args, **kwargs):
|
||||
raise RuntimeError("Test exception raised by test callable")
|
||||
|
||||
|
||||
_test_callables = {
|
||||
"foo": _test_callable,
|
||||
"bar": _test_callable,
|
||||
|
|
@ -89,6 +94,7 @@ _test_callables = {
|
|||
"add": _add_callable,
|
||||
"lit": _lit_callable,
|
||||
"sum": _lsum_callable,
|
||||
"raise": _raises_callable,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -102,6 +108,22 @@ class TestFuncParser(TestCase):
|
|||
|
||||
self.parser = funcparser.FuncParser(_test_callables)
|
||||
|
||||
def test_constructor_wrong_args(self):
|
||||
# Given list argument doesn't contain modules or paths.
|
||||
with self.assertRaises(AttributeError):
|
||||
parser = funcparser.FuncParser(["foo", _test_callable])
|
||||
|
||||
def test_constructor_ignore_non_callables(self):
|
||||
# Ignores callables that aren't actual functions.
|
||||
parser = funcparser.FuncParser({"foo": 1, "bar": "baz"})
|
||||
|
||||
@patch("evennia.utils.funcparser.variable_from_module")
|
||||
def test_constructor_raises(self, patched_variable_from_module):
|
||||
# Patched variable from module returns FUNCPARSER_CALLABLES that isn't dict.
|
||||
patched_variable_from_module.return_value = ["foo"]
|
||||
with self.assertRaises(funcparser.ParsingError):
|
||||
parser = funcparser.FuncParser("foo.module")
|
||||
|
||||
@parameterized.expand(
|
||||
[
|
||||
("Test normal string", "Test normal string"),
|
||||
|
|
@ -216,13 +238,49 @@ class TestFuncParser(TestCase):
|
|||
# print(f"time: {(t1-t0)*1000} ms")
|
||||
self.assertEqual(expected, ret)
|
||||
|
||||
def test_parse_raise(self):
|
||||
@parameterized.expand(
|
||||
(
|
||||
"Test malformed This is $dummy(a, b) and $bar(",
|
||||
"Test $funcNotFound()",
|
||||
)
|
||||
)
|
||||
def test_parse_raise_unparseable(self, unparseable):
|
||||
"""
|
||||
Make sure error is raised if told to do so.
|
||||
|
||||
"""
|
||||
string = "Test malformed This is $dummy(a, b) and $bar("
|
||||
with self.assertRaises(funcparser.ParsingError):
|
||||
self.parser.parse(unparseable, raise_errors=True)
|
||||
|
||||
@patch("evennia.utils.funcparser._MAX_NESTING", 2)
|
||||
def test_parse_max_nesting(self):
|
||||
"""
|
||||
Make sure it is an error if the max nesting value is reached.
|
||||
|
||||
TODO: Does this make sense? When it sees the first function, len(callstack)
|
||||
is 0. It doesn't raise until the stack length is greater than the
|
||||
_MAX_NESTING value, which means you can nest 4 values with a value of
|
||||
2, as demonstrated by this test.
|
||||
"""
|
||||
string = "$add(1, $add(1, $add(1, $toint(42))))"
|
||||
ret = self.parser.parse(string)
|
||||
|
||||
# TODO: Does this return value actually make sense?
|
||||
# It removed the spaces from the calls.
|
||||
self.assertEqual("$add(1,$add(1,$add(1,$toint(42))))", ret)
|
||||
|
||||
with self.assertRaises(funcparser.ParsingError):
|
||||
self.parser.parse(string, raise_errors=True)
|
||||
|
||||
def test_parse_underlying_exception(self):
|
||||
string = "test $add(1, 1) $raise()"
|
||||
ret = self.parser.parse(string)
|
||||
|
||||
# TODO: Does this return value actually make sense?
|
||||
# It completed the first function call.
|
||||
self.assertEqual("test 2 $raise()", ret)
|
||||
|
||||
with self.assertRaises(RuntimeError):
|
||||
self.parser.parse(string, raise_errors=True)
|
||||
|
||||
def test_parse_strip(self):
|
||||
|
|
@ -234,6 +292,12 @@ class TestFuncParser(TestCase):
|
|||
ret = self.parser.parse(string, strip=True)
|
||||
self.assertEqual("Test and things", ret)
|
||||
|
||||
@unittest.skip("broken due to https://github.com/evennia/evennia/issues/2927")
|
||||
def test_parse_whitespace_preserved(self):
|
||||
string = "The answer is $add(1, x)"
|
||||
ret = self.parser.parse(string)
|
||||
self.assertEqual("The answer is $add(1, x)", ret)
|
||||
|
||||
def test_parse_escape(self):
|
||||
"""
|
||||
Test the parser's escape functionality.
|
||||
|
|
@ -368,8 +432,7 @@ class TestDefaultCallables(TestCase):
|
|||
)
|
||||
def test_conjugate(self, string, expected_you, expected_them):
|
||||
"""
|
||||
Test callables with various input strings
|
||||
|
||||
Test the $conj(), $you() and $pron callables with various input strings.
|
||||
"""
|
||||
mapping = {"char1": self.obj1, "char2": self.obj2}
|
||||
ret = self.parser.parse(
|
||||
|
|
@ -381,6 +444,46 @@ class TestDefaultCallables(TestCase):
|
|||
)
|
||||
self.assertEqual(expected_them, ret)
|
||||
|
||||
def test_conjugate_missing_args(self):
|
||||
string = "You $conj(smile)"
|
||||
with self.assertRaises(funcparser.ParsingError):
|
||||
self.parser.parse(string, raise_errors=True)
|
||||
|
||||
@parameterized.expand(
|
||||
[
|
||||
("male", "Char1 smiles at himself"),
|
||||
("female", "Char1 smiles at herself"),
|
||||
("neutral", "Char1 smiles at itself"),
|
||||
("plural", "Char1 smiles at itself"),
|
||||
]
|
||||
)
|
||||
def test_pronoun_gender(self, gender, expected):
|
||||
string = "Char1 smiles at $pron(yourself)"
|
||||
|
||||
self.obj1.gender = gender
|
||||
ret = self.parser.parse(string, caller=self.obj1, raise_errors=True)
|
||||
self.assertEqual(expected, ret)
|
||||
|
||||
self.obj1.gender = lambda: gender
|
||||
ret = self.parser.parse(string, caller=self.obj1, raise_errors=True)
|
||||
self.assertEqual(expected, ret)
|
||||
|
||||
def test_pronoun_viewpoint(self):
|
||||
string = "Char1 smiles at $pron(I)"
|
||||
|
||||
ret = self.parser.parse(string, caller=self.obj1, viewpoint="op", raise_errors=True)
|
||||
self.assertEqual("Char1 smiles at it", ret)
|
||||
|
||||
def test_pronoun_capitalize(self):
|
||||
string = "Char1 smiles at $pron(I)"
|
||||
|
||||
ret = self.parser.parse(string, caller=self.obj1, capitalize=True, raise_errors=True)
|
||||
self.assertEqual("Char1 smiles at It", ret)
|
||||
|
||||
string = "Char1 smiles at $Pron(I)"
|
||||
ret = self.parser.parse(string, caller=self.obj1, capitalize=True, raise_errors=True)
|
||||
self.assertEqual("Char1 smiles at It", ret)
|
||||
|
||||
@parameterized.expand(
|
||||
[
|
||||
("Test $pad(Hello, 20, c, -) there", "Test -------Hello-------- there"),
|
||||
|
|
@ -396,6 +499,7 @@ class TestDefaultCallables(TestCase):
|
|||
("Some $mult(3, 2) things", "Some 6 things"),
|
||||
("Some $div(6, 2) things", "Some 3.0 things"),
|
||||
("Some $toint(6) things", "Some 6 things"),
|
||||
("Some $toint(3 + 3) things", "Some 6 things"),
|
||||
("Some $ljust(Hello, 30)", "Some Hello "),
|
||||
("Some $rjust(Hello, 30)", "Some Hello"),
|
||||
("Some $rjust(Hello, width=30)", "Some Hello"),
|
||||
|
|
@ -415,6 +519,33 @@ class TestDefaultCallables(TestCase):
|
|||
("There is $an(thing) here", "There is a thing here"),
|
||||
("Some $eval(\"'-'*20\")Hello", "Some --------------------Hello"),
|
||||
('$crop("spider\'s silk", 5)', "spide"),
|
||||
("$an(apple)", "an apple"),
|
||||
# These two are broken because of https://github.com/evennia/evennia/issues/2912
|
||||
# ("$round(2.9) apples", "3.0 apples"),
|
||||
# ("$round(2.967, 1) apples", "3.0 apples"),
|
||||
# Degenerate cases
|
||||
("$int2str() apples", " apples"),
|
||||
("$int2str(x) apples", "x apples"),
|
||||
("$int2str(1 + 1) apples", "1 + 1 apples"),
|
||||
("$int2str(13) apples", "13 apples"),
|
||||
("$toint([1, 2, 3]) apples", "[1, 2, 3] apples"),
|
||||
("$an() foo bar", " foo bar"),
|
||||
("$add(1) apple", " apple"),
|
||||
("$add(1, [1, 2]) apples", " apples"),
|
||||
("$round() apples", " apples"),
|
||||
("$choice() apple", " apple"),
|
||||
("A $pad() apple", "A apple"),
|
||||
("A $pad(tasty, 13, x, -) apple", "A ----tasty---- apple"),
|
||||
("A $crop() apple", "A apple"),
|
||||
("A $space() apple", "A apple"),
|
||||
("A $justify() apple", "A apple"),
|
||||
("A $clr() apple", "A apple"),
|
||||
("A $clr(red) apple", "A red apple"),
|
||||
("10 $pluralize()", "10 "),
|
||||
("10 $pluralize(apple, 10)", "10 apples"),
|
||||
("1 $pluralize(apple)", "1 apple"),
|
||||
("You $conj()", "You "),
|
||||
("$pron() smiles", " smiles"),
|
||||
]
|
||||
)
|
||||
def test_other_callables(self, string, expected):
|
||||
|
|
@ -426,6 +557,9 @@ class TestDefaultCallables(TestCase):
|
|||
self.assertEqual(expected, ret)
|
||||
|
||||
def test_random(self):
|
||||
"""
|
||||
Test random callable, with ranges of expected values.
|
||||
"""
|
||||
string = "$random(1,10)"
|
||||
for i in range(100):
|
||||
ret = self.parser.parse_to_any(string, raise_errors=True)
|
||||
|
|
@ -436,12 +570,52 @@ class TestDefaultCallables(TestCase):
|
|||
ret = self.parser.parse_to_any(string, raise_errors=True)
|
||||
self.assertTrue(0 <= ret <= 1)
|
||||
|
||||
string = "$random(2)"
|
||||
for i in range(100):
|
||||
ret = self.parser.parse_to_any(string, raise_errors=True)
|
||||
self.assertTrue(0 <= ret <= 2)
|
||||
|
||||
string = "$random(1.0, 3.0)"
|
||||
for i in range(100):
|
||||
ret = self.parser.parse_to_any(string, raise_errors=True)
|
||||
self.assertTrue(isinstance(ret, float))
|
||||
self.assertTrue(1.0 <= ret <= 3.0)
|
||||
|
||||
string = "$random([1,2]) apples"
|
||||
ret = self.parser.parse_to_any(string)
|
||||
self.assertEqual(" apples", ret)
|
||||
with self.assertRaises(TypeError):
|
||||
ret = self.parser.parse_to_any(string, raise_errors=True)
|
||||
|
||||
# @unittest.skip("underlying function seems broken")
|
||||
def test_choice(self):
|
||||
"""
|
||||
Test choice callable, where output could be either choice.
|
||||
"""
|
||||
string = "$choice(red, green) apple"
|
||||
ret = self.parser.parse(string)
|
||||
self.assertIn(ret, ("red apple", "green apple"))
|
||||
|
||||
string = "$choice([red, green]) apple"
|
||||
ret = self.parser.parse(string)
|
||||
self.assertIn(ret, ("red apple", "green apple"))
|
||||
|
||||
string = "$choice(['red', 'green']) apple"
|
||||
ret = self.parser.parse(string)
|
||||
self.assertIn(ret, ("red apple", "green apple"))
|
||||
|
||||
string = "$choice([1, 2])"
|
||||
ret = self.parser.parse(string)
|
||||
self.assertIn(ret, ("1", "2"))
|
||||
ret = self.parser.parse_to_any(string)
|
||||
self.assertIn(ret, (1, 2))
|
||||
|
||||
string = "$choice(1, 2)"
|
||||
ret = self.parser.parse(string)
|
||||
self.assertIn(ret, ("1", "2"))
|
||||
ret = self.parser.parse_to_any(string)
|
||||
self.assertIn(ret, (1, 2))
|
||||
|
||||
def test_randint(self):
|
||||
string = "$randint(1.0, 3.0)"
|
||||
ret = self.parser.parse_to_any(string, raise_errors=True)
|
||||
|
|
@ -528,6 +702,7 @@ class TestCallableSearch(test_resources.BaseEvenniaTest):
|
|||
"""
|
||||
string = "$search(TestAccount, type=account)"
|
||||
expected = self.account
|
||||
self.account.locks.add("control:id(%s)" % self.char1.dbref)
|
||||
|
||||
ret = self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=True)
|
||||
self.assertEqual(expected, ret)
|
||||
|
|
@ -539,6 +714,7 @@ class TestCallableSearch(test_resources.BaseEvenniaTest):
|
|||
"""
|
||||
string = "$search(Script, type=script)"
|
||||
expected = self.script
|
||||
self.script.locks.add("control:id(%s)" % self.char1.dbref)
|
||||
|
||||
ret = self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=True)
|
||||
self.assertEqual(expected, ret)
|
||||
|
|
@ -553,3 +729,86 @@ class TestCallableSearch(test_resources.BaseEvenniaTest):
|
|||
|
||||
ret = self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=True)
|
||||
self.assertEqual(expected, ret)
|
||||
|
||||
def test_search_tag(self):
|
||||
"""
|
||||
Test searching for a tag
|
||||
"""
|
||||
self.char1.tags.add("foo")
|
||||
|
||||
string = "This is $search(foo, type=tag)"
|
||||
expected = "This is %s" % str(self.char1)
|
||||
|
||||
ret = self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=True)
|
||||
self.assertEqual(expected, ret)
|
||||
|
||||
def test_search_not_found(self):
|
||||
string = "$search(foo)"
|
||||
with self.assertRaises(funcparser.ParsingError):
|
||||
self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=True)
|
||||
|
||||
ret = self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=False)
|
||||
self.assertEqual("$search(foo)", ret)
|
||||
|
||||
ret = self.parser.parse_to_any(
|
||||
string, caller=self.char1, return_list=True, raise_errors=False
|
||||
)
|
||||
self.assertEqual([], ret)
|
||||
|
||||
def test_search_multiple_results_no_list(self):
|
||||
"""
|
||||
Test exception when search returns multiple results but list is not requested
|
||||
"""
|
||||
string = "$search(BaseObject)"
|
||||
with self.assertRaises(funcparser.ParsingError):
|
||||
self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=True)
|
||||
|
||||
def test_search_no_access(self):
|
||||
string = "Go to $search(Room)"
|
||||
with self.assertRaises(funcparser.ParsingError):
|
||||
self.parser.parse(string, caller=self.char2, return_list=True, raise_errors=True)
|
||||
|
||||
def test_search_no_caller(self):
|
||||
string = "$search(Char)"
|
||||
with self.assertRaises(funcparser.ParsingError):
|
||||
self.parser.parse(string, caller=None, raise_errors=True)
|
||||
|
||||
def test_search_no_args(self):
|
||||
string = "$search()"
|
||||
ret = self.parser.parse(string, caller=self.char1, return_list=False, raise_errors=True)
|
||||
self.assertEqual("None", ret)
|
||||
|
||||
ret = self.parser.parse(string, caller=self.char1, return_list=True, raise_errors=True)
|
||||
self.assertEqual("[]", ret)
|
||||
|
||||
def test_search_nested__issue2902(self):
|
||||
"""
|
||||
Search for objects by-tag, check that the result is a valid object
|
||||
|
||||
"""
|
||||
# we
|
||||
parser = funcparser.FuncParser(
|
||||
{**funcparser.SEARCHING_CALLABLES, **funcparser.FUNCPARSER_CALLABLES}
|
||||
)
|
||||
|
||||
# set up search targets
|
||||
self.obj1.tags.add("beach", category="zone")
|
||||
self.obj2.tags.add("beach", category="zone")
|
||||
|
||||
# first a plain search
|
||||
string = "$objlist(beach,category=zone,type=tag)"
|
||||
ret = parser.parse_to_any(string, caller=self.char1, raise_errors=True)
|
||||
|
||||
self.assertEqual(ret, [self.obj1, self.obj2])
|
||||
|
||||
# get random result from the possible matches
|
||||
string = "$choice($objlist(beach,category=zone,type=tag))"
|
||||
ret = parser.parse_to_any(string, caller=self.char1, raise_errors=True)
|
||||
|
||||
self.assertIn(ret, [self.obj1, self.obj2])
|
||||
|
||||
# test wrapping in $obj(), should just pass object through
|
||||
string = "$obj($choice($objlist(beach,category=zone,type=tag)))"
|
||||
ret = parser.parse_to_any(string, caller=self.char1, raise_errors=True)
|
||||
|
||||
self.assertIn(ret, [self.obj1, self.obj2])
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
from evennia import DefaultObject, DefaultRoom
|
||||
from evennia.objects.models import ObjectDB
|
||||
from evennia.scripts.scripts import DefaultScript
|
||||
from evennia.utils.search import (
|
||||
search_script,
|
||||
search_script_attribute,
|
||||
search_script_tag,
|
||||
search_typeclass,
|
||||
)
|
||||
from evennia.utils.test_resources import EvenniaTest
|
||||
from evennia.utils.search import search_script_attribute, search_script_tag, search_script
|
||||
|
||||
|
||||
class TestSearch(EvenniaTest):
|
||||
|
|
@ -61,3 +68,15 @@ class TestSearch(EvenniaTest):
|
|||
script, errors = DefaultScript.create("a-script")
|
||||
found = search_script("wrong_key")
|
||||
self.assertEqual(len(found), 0, errors)
|
||||
|
||||
def test_search_typeclass(self):
|
||||
"""Check that an object can be found by typeclass"""
|
||||
DefaultObject.create("test_obj")
|
||||
found = search_typeclass("evennia.objects.objects.DefaultObject")
|
||||
self.assertEqual(len(found), 1)
|
||||
|
||||
def test_search_wrong_typeclass(self):
|
||||
"""Check that an object cannot be found by wrong typeclass"""
|
||||
DefaultObject.create("test_obj_2")
|
||||
with self.assertRaises(ImportError):
|
||||
search_typeclass("not.a.typeclass")
|
||||
|
|
|
|||
|
|
@ -66,8 +66,12 @@ class TestListToString(TestCase):
|
|||
[1,2,3] -> '1, 2, 3'
|
||||
with sep==';' and endsep==';':
|
||||
[1,2,3] -> '1; 2; 3'
|
||||
with sep=='or':
|
||||
[1,2,3] -> '1 or 2, and 3'
|
||||
with endsep=='and':
|
||||
[1,2,3] -> '1, 2 and 3'
|
||||
with endsep=='; and':
|
||||
[1,2,3] -> '1, 2; and 3'
|
||||
with endsep=='':
|
||||
[1,2,3] -> '1, 2 3'
|
||||
with addquote and endsep="and"
|
||||
|
|
@ -80,6 +84,8 @@ class TestListToString(TestCase):
|
|||
self.assertEqual("1, 2 and 3", utils.list_to_string([1, 2, 3], endsep="and"))
|
||||
self.assertEqual("1, 2 3", utils.list_to_string([1, 2, 3], endsep=""))
|
||||
self.assertEqual("1; 2; 3", utils.list_to_string([1, 2, 3], sep=";", endsep=";"))
|
||||
self.assertEqual("1 or 2, and 3", utils.list_to_string([1, 2, 3], sep="or"))
|
||||
self.assertEqual("1, 2; and 3", utils.list_to_string([1, 2, 3], endsep="; and"))
|
||||
self.assertEqual(
|
||||
'"1", "2", "3"', utils.list_to_string([1, 2, 3], endsep=",", addquote=True)
|
||||
)
|
||||
|
|
@ -696,3 +702,36 @@ class TestDelay(BaseEvenniaTest):
|
|||
timedelay
|
||||
) # Clock must advance to trigger, even if past timedelay
|
||||
self.assertEqual(self.char1.ndb.dummy_var, "dummy_func ran")
|
||||
|
||||
|
||||
class TestIntConversions(TestCase):
|
||||
def test_int2str(self):
|
||||
self.assertEqual("three", utils.int2str(3))
|
||||
# special adjective conversion
|
||||
self.assertEqual("3rd", utils.int2str(3, adjective=True))
|
||||
# generic adjective conversion
|
||||
self.assertEqual("5th", utils.int2str(5, adjective=True))
|
||||
# No mapping return int as str
|
||||
self.assertEqual("15", utils.int2str(15))
|
||||
|
||||
def test_str2int(self):
|
||||
# simple conversions
|
||||
self.assertEqual(5, utils.str2int("5"))
|
||||
|
||||
# basic mapped numbers
|
||||
self.assertEqual(3, utils.str2int("three"))
|
||||
self.assertEqual(20, utils.str2int("twenty"))
|
||||
|
||||
# multi-place numbers
|
||||
self.assertEqual(2345, utils.str2int("two thousand, three hundred and forty-five"))
|
||||
|
||||
# ordinal numbers
|
||||
self.assertEqual(1, utils.str2int("1st"))
|
||||
self.assertEqual(1, utils.str2int("first"))
|
||||
self.assertEqual(4, utils.str2int("fourth"))
|
||||
# ordinal sound-change conversions
|
||||
self.assertEqual(5, utils.str2int("fifth"))
|
||||
self.assertEqual(20, utils.str2int("twentieth"))
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
utils.str2int("not a number")
|
||||
|
|
@ -24,6 +24,7 @@ from ast import literal_eval
|
|||
from collections import OrderedDict, defaultdict
|
||||
from inspect import getmembers, getmodule, getmro, ismodule, trace
|
||||
from os.path import join as osjoin
|
||||
from string import punctuation
|
||||
from unicodedata import east_asian_width
|
||||
|
||||
from django.apps import apps
|
||||
|
|
@ -409,12 +410,17 @@ def iter_to_str(iterable, sep=",", endsep=", and", addquote=False):
|
|||
else:
|
||||
iterable = tuple(str(val) for val in iterable)
|
||||
|
||||
if endsep.startswith(sep):
|
||||
# oxford comma alternative
|
||||
endsep = endsep[1:] if len_iter < 3 else endsep
|
||||
elif endsep:
|
||||
# normal space-separated end separator
|
||||
endsep = " " + str(endsep).strip()
|
||||
if endsep:
|
||||
if endsep.startswith(sep):
|
||||
# oxford comma alternative
|
||||
endsep = endsep[1:] if len_iter < 3 else endsep
|
||||
elif endsep[0] not in punctuation:
|
||||
# add a leading space if endsep is a word
|
||||
endsep = " " + str(endsep).strip()
|
||||
|
||||
# also add a leading space if separator is a word
|
||||
if sep not in punctuation:
|
||||
sep = " " + sep
|
||||
|
||||
if len_iter == 1:
|
||||
return str(iterable[0])
|
||||
|
|
@ -2281,14 +2287,17 @@ def at_search_result(matches, caller, query="", quiet=False, **kwargs):
|
|||
)
|
||||
|
||||
for num, result in enumerate(matches):
|
||||
# we need to consider Commands, where .aliases is a list
|
||||
aliases = result.aliases.all() if hasattr(result.aliases, "all") else result.aliases
|
||||
# remove any pluralization aliases
|
||||
aliases = [
|
||||
alias
|
||||
for alias in aliases
|
||||
if hasattr(alias, "category") and alias.category not in ("plural_key",)
|
||||
]
|
||||
# we need to consider that result could be a Command, where .aliases
|
||||
# is a list of strings
|
||||
if hasattr(result.aliases, "all"):
|
||||
# result is a typeclassed entity where `.aliases` is an AliasHandler.
|
||||
aliases = result.aliases.all(return_objs=True)
|
||||
# remove pluralization aliases
|
||||
aliases = [alias for alias in aliases if alias.category not in ("plural_key",)]
|
||||
else:
|
||||
# result is likely a Command, where `.aliases` is a list of strings.
|
||||
aliases = result.aliases
|
||||
|
||||
error += _MULTIMATCH_TEMPLATE.format(
|
||||
number=num + 1,
|
||||
name=result.get_display_name(caller)
|
||||
|
|
@ -2563,6 +2572,14 @@ def safe_convert_to_types(converters, *args, raise_errors=True, **kwargs):
|
|||
# ...
|
||||
|
||||
"""
|
||||
container_end_char = {"(": ")", "[": "]", "{": "}"} # tuples, lists, sets
|
||||
|
||||
def _manual_parse_containers(inp):
|
||||
startchar = inp[0]
|
||||
endchar = inp[-1]
|
||||
if endchar != container_end_char.get(startchar):
|
||||
return
|
||||
return [str(part).strip() for part in inp[1:-1].split(",")]
|
||||
|
||||
def _safe_eval(inp):
|
||||
if not inp:
|
||||
|
|
@ -2570,16 +2587,21 @@ def safe_convert_to_types(converters, *args, raise_errors=True, **kwargs):
|
|||
if not isinstance(inp, str):
|
||||
# already converted
|
||||
return inp
|
||||
|
||||
try:
|
||||
return literal_eval(inp)
|
||||
try:
|
||||
return literal_eval(inp)
|
||||
except ValueError:
|
||||
parts = _manual_parse_containers(inp)
|
||||
if not parts:
|
||||
raise
|
||||
return parts
|
||||
|
||||
except Exception as err:
|
||||
literal_err = f"{err.__class__.__name__}: {err}"
|
||||
try:
|
||||
return simple_eval(inp)
|
||||
except Exception as err:
|
||||
simple_err = f"{str(err.__class__.__name__)}: {err}"
|
||||
pass
|
||||
|
||||
if raise_errors:
|
||||
from evennia.utils.funcparser import ParsingError
|
||||
|
|
@ -2590,6 +2612,9 @@ def safe_convert_to_types(converters, *args, raise_errors=True, **kwargs):
|
|||
f"simple_eval raised {simple_err}"
|
||||
)
|
||||
raise ParsingError(err)
|
||||
else:
|
||||
# fallback - convert to str
|
||||
return str(inp)
|
||||
|
||||
# handle an incomplete/mixed set of input converters
|
||||
if not converters:
|
||||
|
|
@ -2755,3 +2780,110 @@ def int2str(number, adjective=False):
|
|||
if adjective:
|
||||
return _INT2STR_MAP_ADJ.get(number, f"{number}th")
|
||||
return _INT2STR_MAP_NOUN.get(number, str(number))
|
||||
|
||||
|
||||
_STR2INT_MAP = {
|
||||
"one": 1,
|
||||
"two": 2,
|
||||
"three": 3,
|
||||
"four": 4,
|
||||
"five": 5,
|
||||
"six": 6,
|
||||
"seven": 7,
|
||||
"eight": 8,
|
||||
"nine": 9,
|
||||
"ten": 10,
|
||||
"eleven": 11,
|
||||
"twelve": 12,
|
||||
"thirteen": 13,
|
||||
"fourteen": 14,
|
||||
"fifteen": 15,
|
||||
"sixteen": 16,
|
||||
"seventeen": 17,
|
||||
"eighteen": 18,
|
||||
"nineteen": 19,
|
||||
"twenty": 20,
|
||||
"thirty": 30,
|
||||
"forty": 40,
|
||||
"fifty": 50,
|
||||
"sixty": 60,
|
||||
"seventy": 70,
|
||||
"eighty": 80,
|
||||
"ninety": 90,
|
||||
"hundred": 100,
|
||||
"thousand": 1000,
|
||||
}
|
||||
_STR2INT_ADJS = {
|
||||
"first": 1,
|
||||
"second": 2,
|
||||
"third": 3,
|
||||
}
|
||||
|
||||
|
||||
def str2int(number):
|
||||
"""
|
||||
Converts a string to an integer.
|
||||
|
||||
Args:
|
||||
number (str): The string to convert. It can be a digit such as "1", or a number word such as "one".
|
||||
|
||||
Returns:
|
||||
int: The string represented as an integer.
|
||||
"""
|
||||
number = str(number)
|
||||
original_input = number
|
||||
try:
|
||||
# it's a digit already
|
||||
return int(number)
|
||||
except:
|
||||
# if it's an ordinal number such as "1st", it'll convert to int with the last two characters chopped off
|
||||
try:
|
||||
return int(number[:-2])
|
||||
except:
|
||||
pass
|
||||
|
||||
# convert sound changes for generic ordinal numbers
|
||||
if number[-2:] == "th":
|
||||
# remove "th"
|
||||
number = number[:-2]
|
||||
if number[-1] == "f":
|
||||
# e.g. twelfth, fifth
|
||||
number = number[:-1] + "ve"
|
||||
elif number[-2:] == "ie":
|
||||
# e.g. twentieth, fortieth
|
||||
number = number[:-2] + "y"
|
||||
# custom case for ninth
|
||||
elif number[-3:] == "nin":
|
||||
number += "e"
|
||||
|
||||
if i := _STR2INT_MAP.get(number):
|
||||
# it's a single number, return it
|
||||
return i
|
||||
|
||||
# remove optional "and"s
|
||||
number = number.replace(" and ", " ")
|
||||
|
||||
# split number words by spaces, hyphens and commas, to accommodate multiple styles
|
||||
numbers = [word.lower() for word in re.split(r"[-\s\,]", number) if word]
|
||||
sums = []
|
||||
for word in numbers:
|
||||
# check if it's a known number-word
|
||||
if i := _STR2INT_MAP.get(word):
|
||||
if not len(sums):
|
||||
# initialize the list with the current value
|
||||
sums = [i]
|
||||
else:
|
||||
# if the previous number was smaller, it's a multiplier
|
||||
# e.g. the "two" in "two hundred"
|
||||
if sums[-1] < i:
|
||||
sums[-1] = sums[-1] * i
|
||||
# otherwise, it's added on, like the "five" in "twenty five"
|
||||
else:
|
||||
sums.append(i)
|
||||
elif i := _STR2INT_ADJS.get(word):
|
||||
# it's a special adj word; ordinal case will never be a multiplier
|
||||
sums.append(i)
|
||||
else:
|
||||
# invalid number-word, raise ValueError
|
||||
raise ValueError(f"String {original_input} cannot be converted to int.")
|
||||
return sum(sums)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
English pronoun mapping between 1st/2nd person and 3rd person perspective (and vice-versa).
|
||||
|
||||
This file is released under the Evennia regular BSD License.
|
||||
(Griatch 2021)
|
||||
(Griatch 2021) - revised by InspectorCaracal 2022
|
||||
|
||||
Pronouns are words you use instead of a proper name, such as 'him', 'herself', 'theirs' etc. These
|
||||
look different depending on who sees the outgoing string. This mapping maps between 1st/2nd case and
|
||||
|
|
@ -21,227 +21,266 @@ viewpoint/pronouns Subject Object Possessive Possessive Reflexive
|
|||
|
||||
3rd person male he him his his himself
|
||||
3rd person female she her her hers herself
|
||||
3rd person neutral it it its theirs* itself
|
||||
3rd person neutral it it its its itself
|
||||
3rd person plural they them their theirs themselves
|
||||
==================== ======= ======== ========== ========== ===========
|
||||
|
||||
> `*`) Not formally used, we use `theirs` here as a filler.
|
||||
|
||||
"""
|
||||
from evennia.utils.utils import copy_word_case
|
||||
from evennia.utils.utils import copy_word_case, is_iter
|
||||
|
||||
DEFAULT_PRONOUN_TYPE = "object_pronoun"
|
||||
DEFAULT_PRONOUN_TYPE = "subject pronoun"
|
||||
DEFAULT_VIEWPOINT = "2nd person"
|
||||
DEFAULT_GENDER = "neutral"
|
||||
|
||||
PRONOUN_TYPES = [
|
||||
"subject pronoun",
|
||||
"object pronoun",
|
||||
"possessive adjective",
|
||||
"possessive pronoun",
|
||||
"reflexive pronoun",
|
||||
]
|
||||
VIEWPOINTS = ["1st person", "2nd person", "3rd person"]
|
||||
GENDERS = ["male", "female", "neutral", "plural"]
|
||||
|
||||
PRONOUN_MAPPING = {
|
||||
# 1st/2nd person -> 3rd person mappings
|
||||
"I": {"subject pronoun": {"3rd person": {"male": "he", "female": "she", "neutral": "it"}}},
|
||||
"me": {"object pronoun": {"3rd person": {"male": "him", "female": "her", "neutral": "it"}}},
|
||||
"my": {
|
||||
"possessive adjective": {"3rd person": {"male": "his", "female": "her", "neutral": "its"}}
|
||||
},
|
||||
"mine": {
|
||||
"possessive pronoun": {
|
||||
"3rd person": {
|
||||
"male": "his",
|
||||
"female": "hers",
|
||||
"neutral": "theirs", # colloqial,
|
||||
}
|
||||
}
|
||||
},
|
||||
"myself": {
|
||||
"reflexive_pronoun": {
|
||||
"3rd person": {
|
||||
"male": "himself",
|
||||
"female": "herself",
|
||||
"neutral": "itself",
|
||||
"plural": "themselves",
|
||||
}
|
||||
}
|
||||
},
|
||||
"you": {
|
||||
"1st person": {
|
||||
"subject pronoun": {
|
||||
"3rd person": {
|
||||
"male": "he",
|
||||
"female": "she",
|
||||
"neutral": "it",
|
||||
"plural": "they",
|
||||
}
|
||||
"neutral": "I",
|
||||
"plural": "we",
|
||||
},
|
||||
"object pronoun": {
|
||||
"3rd person": {
|
||||
"male": "him",
|
||||
"female": "her",
|
||||
"neutral": "it",
|
||||
"plural": "them",
|
||||
}
|
||||
"neutral": "me",
|
||||
"plural": "us",
|
||||
},
|
||||
},
|
||||
"your": {
|
||||
"possessive adjective": {
|
||||
"3rd person": {
|
||||
"male": "his",
|
||||
"female": "her",
|
||||
"neutral": "its",
|
||||
"plural": "their",
|
||||
}
|
||||
}
|
||||
},
|
||||
"yours": {
|
||||
"possessive pronoun": {
|
||||
"3rd person": {
|
||||
"male": "his",
|
||||
"female": "hers",
|
||||
"neutral": "theirs", # colloqial
|
||||
"plural": "theirs",
|
||||
}
|
||||
}
|
||||
},
|
||||
"yourself": {
|
||||
"reflexive_pronoun": {
|
||||
"3rd person": {
|
||||
"male": "himself",
|
||||
"female": "herself",
|
||||
"neutral": "itself",
|
||||
}
|
||||
}
|
||||
},
|
||||
"we": {"subject pronoun": {"3rd person": {"plural": "they"}}},
|
||||
"us": {"object pronoun": {"3rd person": {"plural": "them"}}},
|
||||
"our": {"possessive adjective": {"3rd person": {"plural": "their"}}},
|
||||
"ours": {"possessive pronoun": {"3rd person": {"plural": "theirs"}}},
|
||||
"ourselves": {"reflexive pronoun": {"3rd person": {"plural": "themselves"}}},
|
||||
"ours": {"possessive pronoun": {"3rd person": {"plural": "theirs"}}},
|
||||
"ourselves": {"reflexive pronoun": {"3rd person": {"plural": "themselves"}}},
|
||||
"yourselves": {"reflexive_pronoun": {"3rd person": {"plural": "themselves"}}},
|
||||
# 3rd person to 1st/second person mappings
|
||||
"he": {
|
||||
"subject pronoun": {
|
||||
"1st person": {"neutral": "I", "plural": "we"}, # pluralis majestatis
|
||||
"2nd person": {"neutral": "you", "plural": "you"}, # pluralis majestatis
|
||||
}
|
||||
},
|
||||
"him": {
|
||||
"object pronoun": {
|
||||
"1st person": {"neutral": "me", "plural": "us"}, # pluralis majestatis
|
||||
"2nd person": {"neutral": "you", "plural": "you"}, # pluralis majestatis
|
||||
}
|
||||
},
|
||||
"his": {
|
||||
"possessive adjective": {
|
||||
"1st person": {"neutral": "my", "plural": "our"}, # pluralis majestatis
|
||||
"2nd person": {"neutral": "your", "plural": "your"}, # pluralis majestatis
|
||||
"neutral": "my",
|
||||
"plural": "our",
|
||||
},
|
||||
"possessive pronoun": {
|
||||
"1st person": {"neutral": "mine", "plural": "ours"}, # pluralis majestatis
|
||||
"2nd person": {"neutral": "yours", "plural": "yours"}, # pluralis majestatis
|
||||
"neutral": "mine",
|
||||
"plural": "ours",
|
||||
},
|
||||
},
|
||||
"himself": {
|
||||
"reflexive pronoun": {
|
||||
"1st person": {"neutral": "myself", "plural": "ourselves"}, # pluralis majestatis
|
||||
"2nd person": {"neutral": "yours", "plural": "yours"}, # pluralis majestatis
|
||||
},
|
||||
},
|
||||
"she": {
|
||||
"subject pronoun": {
|
||||
"1st person": {"neutral": "I", "plural": "you"}, # pluralis majestatis
|
||||
"2nd person": {"neutral": "you", "plural": "we"}, # pluralis majestatis
|
||||
"neutral": "myself",
|
||||
"plural": "ourselves"
|
||||
}
|
||||
},
|
||||
"her": {
|
||||
"2nd person": {
|
||||
"subject pronoun": {
|
||||
"neutral": "you",
|
||||
},
|
||||
"object pronoun": {
|
||||
"1st person": {"neutral": "me", "plural": "us"}, # pluralis majestatis
|
||||
"2nd person": {"neutral": "you", "plural": "you"}, # pluralis majestatis
|
||||
"neutral": "you",
|
||||
},
|
||||
"possessive adjective": {
|
||||
"1st person": {"neutral": "my", "plural": "our"}, # pluralis majestatis
|
||||
"2nd person": {"neutral": "your", "plural": "your"}, # pluralis majestatis
|
||||
"neutral": "your",
|
||||
},
|
||||
},
|
||||
"hers": {
|
||||
"possessive pronoun": {
|
||||
"1st person": {"neutral": "mine", "plural": "ours"}, # pluralis majestatis
|
||||
"2nd person": {"neutral": "yours", "plural": "yours"}, # pluralis majestatis
|
||||
"neutral": "yours",
|
||||
},
|
||||
"reflexive pronoun": {
|
||||
"neutral": "yourself",
|
||||
"plural": "yourselves",
|
||||
}
|
||||
},
|
||||
"herself": {
|
||||
"reflexive pronoun": {
|
||||
"1st person": {"neutral": "myself", "plural": "ourselves"}, # pluralis majestatis
|
||||
"2nd person": {"neutral": "yourself", "plural": "yourselves"}, # pluralis majestatis
|
||||
},
|
||||
},
|
||||
"it": {
|
||||
"3rd person": {
|
||||
"subject pronoun": {
|
||||
"1st person": {"neutral": "I", "plural": "we"}, # pluralis majestatis
|
||||
"2nd person": {"neutral": "you", "plural": "you"}, # pluralis majestatis
|
||||
"male": "he",
|
||||
"female": "she",
|
||||
"neutral": "it",
|
||||
"plural": "they"
|
||||
},
|
||||
"object pronoun": {
|
||||
"1st person": {"neutral": "me", "plural": "us"}, # pluralis majestatis
|
||||
"2nd person": {"neutral": "you", "plural": "you"}, # pluralis majestatis
|
||||
"male": "him",
|
||||
"female": "her",
|
||||
"neutral": "it",
|
||||
"plural": "them"
|
||||
},
|
||||
},
|
||||
"its": {
|
||||
"possessive adjective": {
|
||||
"1st person": {"neutral": "my", "plural": "our"}, # pluralis majestatis
|
||||
"2nd person": {"neutral": "your", "plural": "your"}, # pluralis majestatis
|
||||
}
|
||||
},
|
||||
"theirs": {
|
||||
"male": "his",
|
||||
"female": "her",
|
||||
"neutral": "its",
|
||||
"plural": "their"
|
||||
},
|
||||
"possessive pronoun": {
|
||||
"1st person": {"neutral": "mine", "plural": "ours"}, # pluralis majestatis
|
||||
"2nd person": {"neutral": "yours", "plural": "yours"}, # pluralis majestatis
|
||||
}
|
||||
},
|
||||
"itself": {
|
||||
"reflexive pronoun": {
|
||||
"1st person": {"neutral": "myself", "plural": "ourselves"}, # pluralis majestatis
|
||||
"2nd person": {"neutral": "yourself", "plural": "yourselves"}, # pluralis majestatis
|
||||
"male": "his",
|
||||
"female": "hers",
|
||||
"neutral": "its",
|
||||
"plural": "theirs",
|
||||
},
|
||||
},
|
||||
"they": {
|
||||
"subject pronoun": {
|
||||
"1st person": {
|
||||
"plural": "we",
|
||||
},
|
||||
"2nd person": {
|
||||
"plural": "you",
|
||||
},
|
||||
}
|
||||
},
|
||||
"them": {
|
||||
"object pronoun": {
|
||||
"1st person": {
|
||||
"plural": "us",
|
||||
},
|
||||
"2nd person": {
|
||||
"plural": "you",
|
||||
},
|
||||
}
|
||||
},
|
||||
"their": {
|
||||
"possessive adjective": {
|
||||
"1st person": {
|
||||
"plural": "our",
|
||||
},
|
||||
"2nd person": {
|
||||
"plural": "your",
|
||||
},
|
||||
}
|
||||
},
|
||||
"themselves": {
|
||||
"reflexive pronoun": {
|
||||
"1st person": {
|
||||
"plural": "ourselves",
|
||||
},
|
||||
"2nd person": {
|
||||
"plural": "yourselves",
|
||||
},
|
||||
}
|
||||
},
|
||||
"male": "himself",
|
||||
"female": "herself",
|
||||
"neutral": "itself",
|
||||
"plural": "themselves",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
PRONOUN_TABLE = {
|
||||
"I": (
|
||||
"1st person",
|
||||
("neutral", "male", "female"),
|
||||
"subject pronoun"
|
||||
),
|
||||
"me": (
|
||||
"1st person",
|
||||
("neutral", "male", "female"),
|
||||
"object pronoun"
|
||||
),
|
||||
"my": (
|
||||
"1st person",
|
||||
("neutral", "male", "female"),
|
||||
"possessive adjective"
|
||||
),
|
||||
"mine": (
|
||||
"1st person",
|
||||
("neutral", "male", "female"),
|
||||
"possessive pronoun"
|
||||
),
|
||||
"myself": (
|
||||
"1st person",
|
||||
("neutral", "male", "female"),
|
||||
"reflexive pronoun"
|
||||
),
|
||||
|
||||
"we": (
|
||||
"1st person",
|
||||
"plural",
|
||||
"subject pronoun"
|
||||
),
|
||||
"us": (
|
||||
"1st person",
|
||||
"plural",
|
||||
"object pronoun"
|
||||
),
|
||||
"our": (
|
||||
"1st person",
|
||||
"plural",
|
||||
"possessive adjective"
|
||||
),
|
||||
"ours": (
|
||||
"1st person",
|
||||
"plural",
|
||||
"possessive pronoun"
|
||||
),
|
||||
"ourselves": (
|
||||
"1st person",
|
||||
"plural",
|
||||
"reflexive pronoun"
|
||||
),
|
||||
"you": (
|
||||
"2nd person",
|
||||
("neutral", "male", "female", "plural"),
|
||||
("subject pronoun", "object pronoun")
|
||||
),
|
||||
"your": (
|
||||
"2nd person",
|
||||
("neutral", "male", "female", "plural"),
|
||||
"possessive adjective"
|
||||
),
|
||||
"yours": (
|
||||
"2nd person",
|
||||
("neutral", "male", "female", "plural"),
|
||||
"possessive pronoun"
|
||||
),
|
||||
"yourself": (
|
||||
"2nd person",
|
||||
("neutral", "male", "female"),
|
||||
"reflexive pronoun"
|
||||
),
|
||||
"yourselves": (
|
||||
"2nd person",
|
||||
"plural",
|
||||
"reflexive pronoun"
|
||||
),
|
||||
"he": (
|
||||
"3rd person",
|
||||
"male",
|
||||
"subject pronoun"
|
||||
),
|
||||
"him": (
|
||||
"3rd person",
|
||||
"male",
|
||||
"object pronoun"
|
||||
),
|
||||
"his":(
|
||||
"3rd person",
|
||||
"male",
|
||||
("possessive pronoun","possessive adjective"),
|
||||
),
|
||||
"himself": (
|
||||
"3rd person",
|
||||
"male",
|
||||
"reflexive pronoun"
|
||||
),
|
||||
"she": (
|
||||
"3rd person",
|
||||
"female",
|
||||
"subject pronoun"
|
||||
),
|
||||
"her": (
|
||||
"3rd person",
|
||||
"female",
|
||||
("object pronoun", "possessive adjective"),
|
||||
),
|
||||
"hers": (
|
||||
"3rd person",
|
||||
"female",
|
||||
"possessive pronoun"
|
||||
),
|
||||
"herself": (
|
||||
"3rd person",
|
||||
"female",
|
||||
"reflexive pronoun"
|
||||
),
|
||||
"it": (
|
||||
"3rd person",
|
||||
"neutral",
|
||||
("subject pronoun", "object pronoun"),
|
||||
),
|
||||
"its": (
|
||||
"3rd person",
|
||||
"neutral",
|
||||
("possessive pronoun", "possessive adjective"),
|
||||
),
|
||||
"itself": (
|
||||
"3rd person",
|
||||
"neutral",
|
||||
"reflexive pronoun"
|
||||
),
|
||||
"they": (
|
||||
"3rd person",
|
||||
"plural",
|
||||
"subject pronoun"
|
||||
),
|
||||
"them": (
|
||||
"3rd person",
|
||||
"plural",
|
||||
"object pronoun"
|
||||
),
|
||||
"their": (
|
||||
"3rd person",
|
||||
"plural",
|
||||
"possessive adjective"
|
||||
),
|
||||
"theirs": (
|
||||
"3rd person",
|
||||
"plural",
|
||||
"possessive pronoun"
|
||||
),
|
||||
"themselves": (
|
||||
"3rd person",
|
||||
"plural",
|
||||
"reflexive pronoun"
|
||||
),
|
||||
}
|
||||
|
||||
# define the default viewpoint conversions
|
||||
VIEWPOINT_CONVERSION = {
|
||||
"1st person": "3rd person",
|
||||
"2nd person": "3rd person",
|
||||
"3rd person": ("1st person", "2nd person"),
|
||||
}
|
||||
|
||||
ALIASES = {
|
||||
"m": "male",
|
||||
|
|
@ -263,19 +302,9 @@ ALIASES = {
|
|||
"pp": "possessive pronoun",
|
||||
}
|
||||
|
||||
PRONOUN_TYPES = [
|
||||
"subject pronoun",
|
||||
"object pronoun",
|
||||
"possessive adjective",
|
||||
"possessive pronoun",
|
||||
"reflexive pronoun",
|
||||
]
|
||||
VIEWPOINTS = ["1st person", "2nd person", "3rd person"]
|
||||
GENDERS = ["male", "female", "neutral", "plural"] # including plural as a gender for simplicity
|
||||
|
||||
|
||||
def pronoun_to_viewpoints(
|
||||
pronoun, options=None, pronoun_type="object_pronoun", gender="neutral", viewpoint="2nd person"
|
||||
pronoun, options=None, pronoun_type=DEFAULT_PRONOUN_TYPE, gender=DEFAULT_GENDER, viewpoint=DEFAULT_VIEWPOINT
|
||||
):
|
||||
"""
|
||||
Access function for determining the forms of a pronount from different viewpoints.
|
||||
|
|
@ -292,7 +321,7 @@ def pronoun_to_viewpoints(
|
|||
- `subject pronoun`/`subject`/`sp` (I, you, he, they)
|
||||
- `object pronoun`/`object/`/`op` (me, you, him, them)
|
||||
- `possessive adjective`/`adjective`/`pa` (my, your, his, their)
|
||||
- `possessive pronoun`/`pronoun`/`pp` (mine, yours, his, theirs)
|
||||
- `possessive pronoun`/`pronoun`/`pp` (mine, yours, his, theirs)
|
||||
|
||||
gender (str, optional): Specific gender to use (plural counts a gender for this purpose).
|
||||
A gender specified in `options` takes precedence. Values and aliases are:
|
||||
|
|
@ -323,18 +352,20 @@ def pronoun_to_viewpoints(
|
|||
|
||||
pronoun_lower = "I" if pronoun == "I" else pronoun.lower()
|
||||
|
||||
if pronoun_lower not in PRONOUN_MAPPING:
|
||||
if pronoun_lower not in PRONOUN_TABLE:
|
||||
return pronoun
|
||||
|
||||
# differentiators
|
||||
# get the default data for the input pronoun
|
||||
source_viewpoint, source_gender, source_type = PRONOUN_TABLE[pronoun_lower]
|
||||
|
||||
# differentiators
|
||||
if pronoun_type not in PRONOUN_TYPES:
|
||||
pronoun_type = DEFAULT_PRONOUN_TYPE
|
||||
if viewpoint not in VIEWPOINTS:
|
||||
viewpoint = DEFAULT_VIEWPOINT
|
||||
if gender not in GENDERS:
|
||||
gender = DEFAULT_GENDER
|
||||
|
||||
|
||||
if options:
|
||||
# option string/list will override the kwargs differentiators given
|
||||
if isinstance(options, str):
|
||||
|
|
@ -350,44 +381,35 @@ def pronoun_to_viewpoints(
|
|||
elif opt in GENDERS:
|
||||
gender = opt
|
||||
|
||||
# step down into the mapping, using differentiators as needed
|
||||
pronoun_types = PRONOUN_MAPPING[pronoun_lower]
|
||||
# this has one or more pronoun-types
|
||||
if len(pronoun_types) == 1:
|
||||
pronoun_type, viewpoints = next(iter(pronoun_types.items()))
|
||||
elif pronoun_type in pronoun_types:
|
||||
viewpoints = pronoun_types[pronoun_type]
|
||||
elif DEFAULT_PRONOUN_TYPE in pronoun_types:
|
||||
pronoun_type = DEFAULT_PRONOUN_TYPE
|
||||
viewpoints = pronoun_types[pronoun_type]
|
||||
# check if pronoun maps to multiple options and differentiate
|
||||
# but don't allow invalid differentiators
|
||||
if is_iter(source_type):
|
||||
pronoun_type = pronoun_type if pronoun_type in source_type else source_type[0]
|
||||
else:
|
||||
# not enough info - grab the first of the mappings
|
||||
pronoun_type, viewpoints = next(iter(pronoun_types.items()))
|
||||
|
||||
# we have one or more viewpoints at this point
|
||||
if len(viewpoints) == 1:
|
||||
viewpoint, genders = next(iter(viewpoints.items()))
|
||||
elif viewpoint in viewpoints:
|
||||
genders = viewpoints[viewpoint]
|
||||
elif DEFAULT_VIEWPOINT in viewpoints:
|
||||
viewpoint = DEFAULT_VIEWPOINT
|
||||
genders = viewpoints[viewpoint]
|
||||
pronoun_type = source_type
|
||||
target_viewpoint = VIEWPOINT_CONVERSION[source_viewpoint]
|
||||
if is_iter(target_viewpoint):
|
||||
viewpoint = viewpoint if viewpoint in target_viewpoint else target_viewpoint[0]
|
||||
else:
|
||||
# not enough info - grab first of mappings
|
||||
viewpoint, genders = next(iter(viewpoints.items()))
|
||||
viewpoint = target_viewpoint
|
||||
|
||||
# we have one or more possible genders (including plural forms)
|
||||
if len(genders) == 1:
|
||||
gender, mapped_pronoun = next(iter(genders.items()))
|
||||
elif gender in genders:
|
||||
mapped_pronoun = genders[gender]
|
||||
elif DEFAULT_GENDER in genders:
|
||||
gender = DEFAULT_GENDER
|
||||
mapped_pronoun = genders[gender]
|
||||
# special handling for the royal "we"
|
||||
if is_iter(source_gender):
|
||||
gender_opts = list(source_gender)
|
||||
else:
|
||||
# not enough info - grab first mapping
|
||||
gender, mapped_pronoun = next(iter(genders.items()))
|
||||
gender_opts = [source_gender]
|
||||
if viewpoint == "1st person":
|
||||
# make sure plural is always an option when converting to 1st person
|
||||
# it doesn't matter if it's in the list twice, so don't bother checking
|
||||
gender_opts.append("plural")
|
||||
# if the gender is still not in the extended options, fall back to source pronoun's default
|
||||
gender = gender if gender in gender_opts else gender_opts[0]
|
||||
|
||||
# step down into the mapping
|
||||
viewpoint_map = PRONOUN_MAPPING[viewpoint]
|
||||
pronouns = viewpoint_map.get(pronoun_type, viewpoint_map[DEFAULT_PRONOUN_TYPE])
|
||||
mapped_pronoun = pronouns.get(gender, pronouns[DEFAULT_GENDER])
|
||||
|
||||
# keep the same capitalization as the original
|
||||
if pronoun != "I":
|
||||
# don't remap I, since this is always capitalized.
|
||||
|
|
@ -396,10 +418,10 @@ def pronoun_to_viewpoints(
|
|||
mapped_pronoun = mapped_pronoun.upper()
|
||||
|
||||
if viewpoint == "3rd person":
|
||||
# the remapped viewpoing is in 3rd person, meaning the ingoing viewpoing
|
||||
# the desired viewpoint is 3rd person, meaning the incoming viewpoint
|
||||
# must have been 1st or 2nd person.
|
||||
return pronoun, mapped_pronoun
|
||||
else:
|
||||
# the remapped viewpoint is 1st or 2nd person, so ingoing must have been
|
||||
# the desired viewpoint is 1st or 2nd person, so incoming must have been
|
||||
# in 3rd person form.
|
||||
return mapped_pronoun, pronoun
|
||||
|
|
|
|||
|
|
@ -279,7 +279,7 @@ class TestPronounMapping(TestCase):
|
|||
("you", "m", "you", "he"),
|
||||
("you", "f op", "you", "her"),
|
||||
("I", "", "I", "it"),
|
||||
("I", "p", "I", "it"), # plural is invalid
|
||||
("I", "p", "I", "it"), # plural is invalid
|
||||
("I", "m", "I", "he"),
|
||||
("Me", "n", "Me", "It"),
|
||||
("your", "p", "your", "their"),
|
||||
|
|
@ -295,7 +295,6 @@ class TestPronounMapping(TestCase):
|
|||
("her", "p", "you", "her"),
|
||||
("her", "pa", "your", "her"),
|
||||
("their", "pa", "your", "their"),
|
||||
("their", "pa", "your", "their"),
|
||||
("itself", "", "yourself", "itself"),
|
||||
("themselves", "", "yourselves", "themselves"),
|
||||
("herself", "", "yourself", "herself"),
|
||||
|
|
@ -311,6 +310,5 @@ class TestPronounMapping(TestCase):
|
|||
received_1st_or_2nd_person, received_3rd_person = pronouns.pronoun_to_viewpoints(
|
||||
pronoun, options
|
||||
)
|
||||
|
||||
self.assertEqual(expected_1st_or_2nd_person, received_1st_or_2nd_person)
|
||||
self.assertEqual(expected_3rd_person, received_3rd_person)
|
||||
|
|
|
|||
|
|
@ -22,3 +22,6 @@ django-extensions >= 3.1.0
|
|||
|
||||
# xyzroom contrib
|
||||
scipy<1.9
|
||||
|
||||
# Git contrib
|
||||
gitpython >= 3.1.27
|
||||
Loading…
Add table
Add a link
Reference in a new issue