Merge branch 'evennia:main' into hex_colors

This commit is contained in:
Michael Faith 2024-04-07 00:18:17 -07:00 committed by GitHub
commit 716807aea3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
207 changed files with 2521 additions and 1108 deletions

View file

@ -9,13 +9,14 @@ echo " 2. On main branch, update CHANGELOG.md."
echo " 3. Make sure pyproject.toml is set to the same major.minor.patch version as evennia/VERSION.txt ($VERSION)."
echo " 4. If major release:"
echo " a. Update docs/sources/conf.py, Add '[MAJOR_VERSION].x' to 'legacy_versions' and 'v$VERSION' to 'legacy_branches'."
echo " b. Make sure all changes are committed, e.g. as 'Evennia $VERSION major/minor/patch release'."
echo " c. Check out a new branch v$VERSION."
echo " d. Push the v$VERSION branch to github."
echo " e. On the v$VERSION branch, temporarily set 'current_is_legacy=True' in source/conf.py, then (re)build "
echo " b. Update 'SECURITY.md' with latest new version."
echo " c. Make sure all changes are committed, e.g. as 'Evennia $VERSION major/minor/patch release'."
echo " d. Check out a new branch v$VERSION."
echo " e. Push the v$VERSION branch to github."
echo " f. On the v$VERSION branch, temporarily set 'current_is_legacy=True' in source/conf.py, then (re)build "
echo " the docs for this release with 'make local' and old-version warning headers. Throw away git changes after."
echo " f. Rename the created build/html folder to '[MAJOR_VERSION].x'. Manually copy it to the gh-pages branch's build/ folder."
echo " g. Add the folder, commit and push to the gh-pages branch. Then checkout main branch again."
echo " g. Rename the created build/html folder to '[MAJOR_VERSION].x'. Manually copy it to the gh-pages branch's build/ folder."
echo " h. Add the folder, commit and push to the gh-pages branch. Then checkout main branch again."
echo " 5. Run 'make local' in docs/ to update dynamic docs (like Changelog.md) and autodocstrings (may have to run twice)."
echo " 6. Make sure all changes are committed (if not already), e.g. as 'Evennia $VERSION major/minor/patch release' (un-staged files will be wiped)."
echo " 7. Make sure all unit tests pass!"

View file

@ -2,16 +2,120 @@
## Main branch
- [Feature][pull3470]: New `exit_order` kwarg to
`DefaultObject.get_display_exits` to easier customize the order in which
standard exits are displayed in a room (chiizujin)
[pull3470]: https://github.com/evennia/evennia/pull/3470
## Evennia 4.1.1
April 6, 2024
- [Fix][pull3438]: Error with 'you' mapping in third-person style of
`msg_contents` (InspectorCaracal)
- [Fix][pull3472]: The new `filter_visible` didn't exclude oneself by default
(InspectorCaracal)
- Fix: `find #dbref` results didn't include the results of
`.get_extra_display_name_info` (the #dbref display by default) (Griatch)
- Fix: Add `DefaultAccount.get_extra_display_name_info` method for API
compliance with `DefaultObject` in commands. (Griatch)
- Fix: Show `XYZRoom` subclass when repr() it. (Griatch)
- [Fix][pull3485]: Typo in `sethome` message (chiizujin)
- [Fix][pull3487]: Fix traceback when using `get`,`drop` and `give` with no
arguments (InspectorCaracal)
- [Fix][issue3476]: Don't ignore EvEditor commands with wrong capitalization (Griatch)
- [Fix][issue3477]: The `at_server_reload_start()` hook was not firing on
a reload (regression).
- [Fix][issue3488]: `AttributeProperty(<default>, autocreate=False)`, where
`<default>` was mutable would not update/save properly in-place (Griatch)
- [Docs] Added new [Server-Lifecycle][doc-server-lifecycle] page to describe
the hooks called on server start/stop/reload (Griatch)
- [Docs] Doc typo fixes (Griatch, chiizujin)
[pull3438]: https://github.com/evennia/evennia/pull/3446
[pull3485]: https://github.com/evennia/evennia/pull/3485
[pull3487]: https://github.com/evennia/evennia/pull/3487
[issue3476]: https://github.com/evennia/evennia/issues/3476
[issue3477]: https://github.com/evennia/evennia/issues/3477
[issue3488]: https://github.com/evennia/evennia/issues/3488
[doc-server-lifecycle]: https://www.evennia.com/docs/latest/Concepts/Server-Lifecycle.html
## Evennia 4.1.0
April 1, 2024
- [Deprecation]: `DefaultObject.get_visible_contents` - unused in core, will be
removed. Use the new `.filter_visible` together with the `.get_display_*` methods instead..
- [Deprecation]: `DefaultObject.get_content_names` - unused in core, will be
removed. Use the `DefaultObject.get_display_*` methods instead.
- [Feature][pull3421]: New `utils.compress_whitespace` utility used with
default object's `.format_appearance` to make it easier to overload without
adding line breaks in hook returns. (InspectorCaracal)
- [Feature][pull3458]: New `sethelp/category` switch to change a help topic's
category after it was created (chiizujin)
- [Feature][pull3467]: Add `alias/delete` switch for removing object aliases
from in-game with default command (chiizujin)
- [Feature][issue3450]: The default `page` command now tags its `Msg` objects
with tag 'page' (category 'comms') and also checks the `Msg`' 'read' lock.
made backwards compatible for old pages (Griatch)
- [Feature][pull3466]: Add optional `no_article` kwarg to
`DefaultObject.get_numbered_name` for the system to skip adding automatic
articles. (chiizujin)
- [Feature][pull3433]: Add ability to default get/drop to affect stacks of
items, such as `get/drop 3 rock` by a custom class parent (InspectorCaracal)
- Feature: Clean up the default Command variable list shown when a command has
no `func()` defined (Griatch)
- [Feature][issue3461]: Add `DefaultObject.filter_display_visible` helper method
to make it easier to customize object visibility rules. (Griatch)
- [Fix][pull3446]: Use plural ('no apples') instead of singular ('no apple') in
`get_numbered_name` for better grammatical form (InspectorCaracal)
- Doc: Added Beginner Tutorial lessons for AI and Procedural dungeon (Griatch)
- Doc fixes (Griatch, InspectorCaracal)
- [Fix][pull3453]: Object aliases not showing in search multi-match
disambiguation display (chiizujin)
- [Fix][pull3455]: `sethelp/edit <topic>` without a `= text` created a `None`
entry that would lose the edit. (chiiziujin)
- [Fix][pull3456]: `format_grid` utility used for `help` command caused commands
to disappear for wider client widths (chiizujin)
- [Fix][pull3457]: Help topic categories with different case would appear as
duplicates (chiizujin)
- [Fix][pull3454]: Traceback in crafting contrib's `recipe.msg`
(InspectorCaracal)
- [Fix][pull3459]: EvEditor line-echo compacted whitespace erroneously (chiizujin)
- [Fix][pull3463]: EvEditor :help described the :paste operation in the wrong
way (chiizujin)
- [Fix][pull3464]: EvEditor range:range specification didn't return correct
range (chiizujin)
- [Fix][issue3462]: EvEditor :UU and :DD etc commands were not properly
differentiating from their lower-case alternatives (Griatch)
- [Fix][issue3460]: The `menu_login` contrib regression caused it to error out
when creating a new character (Griatch)
- Doc: Added Beginner Tutorial lessons for [Monster and NPC AI][docAI],
[Quests][docQuests] and [Making a Procedural dungeon][docDungeon] (Griatch)
- Doc fixes (Griatch, InspectorCaracal, homeofpoe)
[pull3421]: https://github.com/evennia/evennia/pull/3421
[pull3446]: https://github.com/evennia/evennia/pull/3446
[pull3453]: https://github.com/evennia/evennia/pull/3453
[pull3455]: https://github.com/evennia/evennia/pull/3455
[pull3456]: https://github.com/evennia/evennia/pull/3456
[pull3457]: https://github.com/evennia/evennia/pull/3457
[pull3458]: https://github.com/evennia/evennia/pull/3458
[pull3454]: https://github.com/evennia/evennia/pull/3454
[pull3459]: https://github.com/evennia/evennia/pull/3459
[pull3463]: https://github.com/evennia/evennia/pull/3463
[pull3464]: https://github.com/evennia/evennia/pull/3464
[pull3466]: https://github.com/evennia/evennia/pull/3466
[pull3467]: https://github.com/evennia/evennia/pull/3467
[pull3433]: https://github.com/evennia/evennia/pull/3433
[issue3450]: https://github.com/evennia/evennia/issues/3450
[issue3462]: https://github.com/evennia/evennia/issues/3462
[issue3460]: https://github.com/evennia/evennia/issues/3460
[issue3461]: https://github.com/evennia/evennia/issues/3461
[docAI]: https://www.evennia.com/docs/latest/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-AI.html
[docQuests]: https://www.evennia.com/docs/latest/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Quests.html
[docDungeon]: https://www.evennia.com/docs/latest/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Dungeon.html
## Evennia 4.0.0

View file

@ -7,7 +7,8 @@ to use the latest minor/patch version within each major version.
| Version | Supported |
| ------- | ------------------ |
| 3.x | :white_check_mark: |
| 4.x | :white_check_mark: |
| 3.x | :x: |
| 2.x | :x: |
| 1.x | :x: |
| < 1.0 | :x: |

View file

@ -1,16 +1,113 @@
# Changelog
## Main branch
## Evennia 4.1.1
April 6, 2024
- [Fix][pull3438]: Error with 'you' mapping in third-person style of
`msg_contents` (InspectorCaracal)
- [Fix][pull3472]: The new `filter_visible` didn't exclude oneself by default
(InspectorCaracal)
- Fix: `find #dbref` results didn't include the results of
`.get_extra_display_name_info` (the #dbref display by default) (Griatch)
- Fix: Add `DefaultAccount.get_extra_display_name_info` method for API
compliance with `DefaultObject` in commands. (Griatch)
- Fix: Show `XYZRoom` subclass when repr() it. (Griatch)
- [Fix][pull3485]: Typo in `sethome` message (chiizujin)
- [Fix][pull3487]: Fix traceback when using `get`,`drop` and `give` with no
arguments (InspectorCaracal)
- [Fix][issue3476]: Don't ignore EvEditor commands with wrong capitalization (Griatch)
- [Fix][issue3477]: The `at_server_reload_start()` hook was not firing on
a reload (regression).
- [Fix][issue3488]: `AttributeProperty(<default>, autocreate=False)`, where
`<default>` was mutable would not update/save properly in-place (Griatch)
- [Docs] Added new [Server-Lifecycle][doc-server-lifecycle] page to describe
the hooks called on server start/stop/reload (Griatch)
- [Docs] Doc typo fixes (Griatch, chiizujin)
[pull3438]: https://github.com/evennia/evennia/pull/3446
[pull3485]: https://github.com/evennia/evennia/pull/3485
[pull3487]: https://github.com/evennia/evennia/pull/3487
[issue3476]: https://github.com/evennia/evennia/issues/3476
[issue3477]: https://github.com/evennia/evennia/issues/3477
[issue3488]: https://github.com/evennia/evennia/issues/3488
[doc-server-lifecycle]: https://www.evennia.com/docs/latest/Concepts/Server-Lifecycle.html
## Evennia 4.1.0
April 1, 2024
- [Deprecation]: `DefaultObject.get_visible_contents` - unused in core, will be
removed. Use the new `.filter_visible` together with the `.get_display_*` methods instead..
- [Deprecation]: `DefaultObject.get_content_names` - unused in core, will be
removed. Use the `DefaultObject.get_display_*` methods instead.
- [Feature][pull3421]: New `utils.compress_whitespace` utility used with
default object's `.format_appearance` to make it easier to overload without
adding line breaks in hook returns. (InspectorCaracal)
- [Feature][pull3458]: New `sethelp/category` switch to change a help topic's
category after it was created (chiizujin)
- [Feature][pull3467]: Add `alias/delete` switch for removing object aliases
from in-game with default command (chiizujin)
- [Feature][issue3450]: The default `page` command now tags its `Msg` objects
with tag 'page' (category 'comms') and also checks the `Msg`' 'read' lock.
made backwards compatible for old pages (Griatch)
- [Feature][pull3466]: Add optional `no_article` kwarg to
`DefaultObject.get_numbered_name` for the system to skip adding automatic
articles. (chiizujin)
- [Feature][pull3433]: Add ability to default get/drop to affect stacks of
items, such as `get/drop 3 rock` by a custom class parent (InspectorCaracal)
- Feature: Clean up the default Command variable list shown when a command has
no `func()` defined (Griatch)
- [Feature][issue3461]: Add `DefaultObject.filter_display_visible` helper method
to make it easier to customize object visibility rules. (Griatch)
- [Fix][pull3446]: Use plural ('no apples') instead of singular ('no apple') in
`get_numbered_name` for better grammatical form (InspectorCaracal)
- Doc fixes (Griatch, InspectorCaracal)
- [Fix][pull3453]: Object aliases not showing in search multi-match
disambiguation display (chiizujin)
- [Fix][pull3455]: `sethelp/edit <topic>` without a `= text` created a `None`
entry that would lose the edit. (chiiziujin)
- [Fix][pull3456]: `format_grid` utility used for `help` command caused commands
to disappear for wider client widths (chiizujin)
- [Fix][pull3457]: Help topic categories with different case would appear as
duplicates (chiizujin)
- [Fix][pull3454]: Traceback in crafting contrib's `recipe.msg`
(InspectorCaracal)
- [Fix][pull3459]: EvEditor line-echo compacted whitespace erroneously (chiizujin)
- [Fix][pull3463]: EvEditor :help described the :paste operation in the wrong
way (chiizujin)
- [Fix][pull3464]: EvEditor range:range specification didn't return correct
range (chiizujin)
- [Fix][issue3462]: EvEditor :UU and :DD etc commands were not properly
differentiating from their lower-case alternatives (Griatch)
- [Fix][issue3460]: The `menu_login` contrib regression caused it to error out
when creating a new character (Griatch)
- Doc: Added Beginner Tutorial lessons for [Monster and NPC AI][docAI],
[Quests][docQuests] and [Making a Procedural dungeon][docDungeon] (Griatch)
- Doc fixes (Griatch, InspectorCaracal, homeofpoe)
[pull3421]: https://github.com/evennia/evennia/pull/3421
[pull3446]: https://github.com/evennia/evennia/pull/3446
[pull3453]: https://github.com/evennia/evennia/pull/3453
[pull3455]: https://github.com/evennia/evennia/pull/3455
[pull3456]: https://github.com/evennia/evennia/pull/3456
[pull3457]: https://github.com/evennia/evennia/pull/3457
[pull3458]: https://github.com/evennia/evennia/pull/3458
[pull3454]: https://github.com/evennia/evennia/pull/3454
[pull3459]: https://github.com/evennia/evennia/pull/3459
[pull3463]: https://github.com/evennia/evennia/pull/3463
[pull3464]: https://github.com/evennia/evennia/pull/3464
[pull3466]: https://github.com/evennia/evennia/pull/3466
[pull3467]: https://github.com/evennia/evennia/pull/3467
[pull3433]: https://github.com/evennia/evennia/pull/3433
[issue3450]: https://github.com/evennia/evennia/issues/3450
[issue3462]: https://github.com/evennia/evennia/issues/3462
[issue3460]: https://github.com/evennia/evennia/issues/3460
[issue3461]: https://github.com/evennia/evennia/issues/3461
[docAI]: https://www.evennia.com/docs/latest/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-AI.html
[docQuests]: https://www.evennia.com/docs/latest/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Quests.html
[docDungeon]: https://www.evennia.com/docs/latest/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Dungeon.html
## Evennia 4.0.0

View file

@ -134,7 +134,7 @@ the in-editor help command (`:h`).
:y <l> - yank (copy) line <l> to the copy buffer
:x <l> - cut line <l> and store it in the copy buffer
:p <l> - put (paste) previously copied line directly after <l>
:p <l> - put (paste) previously copied line directly before <l>
:i <l> <txt> - insert new text <txt> at line <l>. Old line will move down
:r <l> <txt> - replace line <l> with text <txt>
:I <l> <txt> - insert text at the beginning of line <l>

View file

@ -37,6 +37,7 @@ Banning.md
```{toctree}
:maxdepth: 2
Server-Lifecycle
Protocols.md
Models.md
Zones.md

View file

@ -0,0 +1,80 @@
# Evennia Server Lifecycle
As part of your game design you may want to change how Evennia behaves when starting or stopping. A common use case would be to start up some piece of custom code you want to always have available once the server is up.
Evennia has three main life cycles, all of which you can add custom behavior for:
- **Database life cycle**: Evennia uses a database. This exists in parallel to the code changes you do. The database exists until you choose to reset or delete it. Doing so doesn't require re-downloading Evennia.
- **Reboot life cycle**: From When Evennia starts to it being fully shut down, which means both Portal and Server are stopped. At the end of this cycle, all players are disconnected.
- **Reload life cycle:** This is the main runtime, until a "reload" event. Reloads refreshes game code but do not kick any players.
## When Evennia starts for the first time
This is the beginning of the **Database life cycle**, just after the database is created and migrated for the first time (or after it was deleted and re-built). See [Choosing a Database](../Setup/Choosing-a-Database.md) for instructions on how to reset a database, should you want to re-run this sequence after the first time.
Hooks called, in sequence:
1. `evennia.server.initial_setup.handle_setup(last_step=None)`: Evennia's core initialization function. This is what creates the #1 Character (tied to the superuser account) and `Limbo` room. It calls the next hook below and also understands to restart at the last failed step if there was some issue. You should normally not override this function unless you _really_ know what you are doing. To override, change `settings.INITIAL_SETUP_MODULE` to your own module with a `handle_setup` function in it.
2. `mygame/server/conf/at_initial_setup.py` contains a single function, `at_initial_setup()`, which will be called without arguments. It's called last in the setup sequence by the above function. Use this to add your own custom behavior or to tweak the initialization. If you for example wanted to change the auto-generated Limbo room, you should do it from here. If you want to change where this function is found, you can do so by changing `settings.AT_INITIAL_SETUP_HOOK_MODULE`.
## When Evennia starts and shutdowns
This is part of the **Reboot life cycle**. Evennia consists of two main processes, the [Portal and the Server](../Components/Portal-And-Server.md). On a reboot or shutdown, both Portal and Server shuts down, which means all players are disconnected.
Each process call a series of hooks located in `mygame/server/conf/at_server_startstop.py`. You can customize the module used with `settings.AT_SERVER_STARTSTOP_MODULE` - this can even be a list of modules, if so, the appropriately-named functions will be called from each module, in sequence.
All hooks are called without arguments.
> The use of the term 'server' in the hook-names indicate the whole of Evennia, not just the `Server` component.
### Server cold start
Starting the server from zero, after a full stop. This is done with `evennia start` from the terminal.
1. `at_server_init()` - Always called first in the startup sequence.
2. `at_server_cold_start()` - Only called on cold starts.
3. `at_server_start()` - Always called last in the startup sequece.
### Server cold shutdown
Shutting everything down. Done with `shutdown` in-game or `evennia stop` from the terminal.
1. `at_server_cold_stop()` - Only called on cold stops.
2. `at_server_stop()` - Always called last in the stopping sequence.
### Server reboots
This is done with `evennia reboot` and effectively constitutes an automatic cold shutdown followed by a cold start controlled from the `evennia` launcher. There are no special `reboot` hooks for this, instead it looks like you'd expect:
1. `at_server_cold_stop()`
2. `at_server_stop()` (after this, both `Server` + `Portal` have both shut down)
3. `at_server_init()` (like a cold start)
4. `at_server_cold_start()`
5. `at_server_start()`
## When Evennia reloads and resets
This is the **Reload life cycle**. As mentioned above, Evennia consists of two components, the [Portal and Server](../Components/Portal-And-Server.md). During a reload, only the `Server` component is shut down and restarted. Since the Portal stays up, players are not disconnected.
All hooks are called without arguments.
### Server reload
Reloads are initiated with the `reload` command in-game, or with `evennia reload` from the terminal.
1. `at_server_reload_stop()` - Only called on reload stops.
2. `at_server_stop` - Always called last in the stopping sequence.
3. `at_server_init()` - Always called first in startup sequence.
4. `at_server_reload_start()` - Only called on a reload (re)start.
5. `at_server_start()` - Always called last in the startup sequence.
### Server reset
A 'reset' is a hybrid reload state, where the reload is treated as a cold shutdown only for the sake of running hooks (players are not disconnected). It's run with `reset` in-game or with `evennia reset` from the terminal.
1. `at_server_cold_stop()`
2. `at_server_stop()` (after this, only `Server` has shut down)
3. `at_server_init()` (`Server` coming back up)
4. `at_server_cold_start()`
5. `at_server_start()`

View file

@ -23,12 +23,25 @@ at the root of `evennia/docs/source/`.
result in Evennia. This is often on a tutorial or FAQ form and will refer to the rest of the documentation for further reading.
- `source/Howtos/Beginner-Tutorial/` holds all documents part of the initial tutorial sequence.
Other files and folders:
- `source/api/` contains the auto-generated API documentation as `.html` files. Don't edit these files manually, they are auto-generated from sources.
- `source/_templates` and `source/_static` hold files for the doc itself. They should only be modified if wanting to change the look and structure of the documentation generation itself.
- `conf.py` holds the Sphinx configuration. It should usually not be modified except to update the Evennia version on a new branch.
## Automatically generated doc pages
Some doc pages are automatically generated. Changes to their generated markdown file will be overwritten. Instead they must be modified at the point the automation reads the text from.
- All API docs under `source/api` are built from the doc strings of Evennia core code. Documentation fixes for these needs to be done in the doc strings of the relevant module, function, class or method.
- [Contribs/Contribs-Overview.md](Contribs/Contribs-Overview.md) is completely generated from scratch when building the docs, by the script `evennia/docs/pylib/contrib_readmes2docs.py`.
- All contrib blurbs on the above page are taken from the first paragraph of each contrib's `README.md`, found under `evennia/contrib/*/*/README.md`.
- Similarly, all contrib documentation linked from the above page is generated from each contrib's `README.md` file.
- [Components/Default-Commands.md](Components/Default-Commands.md) is generated from the command classes found under `evennia/commands/default/`.
- [Coding/Evennia-Code-Style.md](Coding/Evennia-Code-Style.md) is generated from `evennia/CODING_STYLE.md`.
- [Coding/Changelog.md](Coding/Changelog.md) is generated from `evennia/CHANGELOG.md`
- [Setup/Settings-Default.md](Setup/Settings-Default.md) is generated from the default settings file `evennia/default_settings.py`
Most auto-generated pages have a warning in the header indicating that it's auto-generated.
## Editing syntax

View file

@ -91,47 +91,34 @@ The `me.cmdset` is the store of all cmdsets stored on us. By giving the path to
Now try
> echo
Command echo has no defined `func()` - showing on-command variables:
...
...
Command "echo" has no defined `func()`. Available properties ...
...(lots of stuff)...
`echo` works! You should be getting a long list of outputs. The reason for this is that your `echo` function is not really "doing" anything yet and the default function is then to show all useful resources available to you when you use your Command. Let's look at some of those listed:
`echo` works! You should be getting a long list of outputs. Your `echo` function is not really "doing" anything yet and the default function is then to show all useful resources available to you when you use your Command. Let's look at some of those listed:
Command echo has no defined `func()` - showing on-command variables:
obj (<class 'typeclasses.characters.Character'>): YourName
lockhandler (<class 'evennia.locks.lockhandler.LockHandler'>): cmd:all()
caller (<class 'typeclasses.characters.Character'>): YourName
cmdname (<class 'str'>): echo
raw_cmdname (<class 'str'>): echo
cmdstring (<class 'str'>): echo
args (<class 'str'>):
cmdset (<class 'evennia.commands.cmdset.CmdSet'>): @mail, about, access, accounts, addcom, alias, allcom, ban, batchcode, batchcommands, boot, cboot, ccreate,
cdesc, cdestroy, cemit, channels, charcreate, chardelete, checklockstring, clientwidth, clock, cmdbare, cmdsets, color, copy, cpattr, create, cwho, delcom,
desc, destroy, dig, dolphin, drop, echo, emit, examine, find, force, get, give, grapevine2chan, help, home, ic, inventory, irc2chan, ircstatus, link, lock,
look, menutest, mudinfo, mvattr, name, nick, objects, ooc, open, option, page, password, perm, pose, public, py, quell, quit, reload, reset, rss2chan, say,
script, scripts, server, service, sessions, set, setdesc, sethelp, sethome, shutdown, spawn, style, tag, tel, test2010, test2028, testrename, testtable,
tickers, time, tunnel, typeclass, unban, unlink, up, up, userpassword, wall, whisper, who, wipe
session (<class 'evennia.server.serversession.ServerSession'>): Griatch(#1)@1:2:7:.:0:.:0:.:1
account (<class 'typeclasses.accounts.Account'>): Griatch(account 1)
raw_string (<class 'str'>): echo
--------------------------------------------------
echo - Command variables from evennia:
--------------------------------------------------
name of cmd (self.key): echo
cmd aliases (self.aliases): []
cmd locks (self.locks): cmd:all();
help category (self.help_category): General
object calling (self.caller): Griatch
object storing cmdset (self.obj): Griatch
command string given (self.cmdstring): echo
current cmdset (self.cmdset): ChannelCmdSet
```
Command "echo" has no defined `func()` method. Available properties on this command are:
self.key (<class 'str'>): "echo"
self.cmdname (<class 'str'>): "echo"
self.raw_cmdname (<class 'str'>): "echo"
self.raw_string (<class 'str'>): "echo
"
self.aliases (<class 'list'>): []
self.args (<class 'str'>): ""
self.caller (<class 'typeclasses.characters.Character'>): YourName
self.obj (<class 'typeclasses.characters.Character'>): YourName
self.session (<class 'evennia.server.serversession.ServerSession'>): YourName(#1)@1:2:7:.:0:.:0:.:1
self.locks (<class 'str'>): "cmd:all();"
self.help_category (<class 'str'>): "general"
self.cmdset (... a long list of commands ...)
```
These are all properties you can access with `.` on the Command instance, such as `.key`, `.args` and so on. Evennia makes these available to you and they will be different every time a command is run. The most important ones we will make use of now are:
- `caller` - this is 'you', the person calling the command.
- `args` - this is all arguments to the command. Now it's empty, but if you tried `echo foo bar` you'd find that this would be `" foo bar"`.
- `args` - this is all arguments to the command. Now it's empty, but if you tried `echo foo bar` you'd find that this would be `" foo bar"` (including the extra space between `echo` and `foo` that you may want to strip away).
- `obj` - this is object on which this Command (and CmdSet) "sits". So you, in this case.
- `raw_string` is not commonly used, but it's the completely unmodified input from the user. It even includes the line break used to send to the command to the server (that's why the end-quotes appear on the next line).
The reason our command doesn't do anything yet is because it's missing a `func` method. This is what Evennia looks for to figure out what a Command actually does. Modify your `CmdEcho` class:
@ -238,7 +225,7 @@ Tweak this file as follows:
```python
# in mygame/commands/default_cmdsets.py
# ,..
# ...
from . import mycommands # <-------
@ -297,10 +284,9 @@ And Bob would see
Still in `mygame/commands/mycommands.py`, add a new class, between `CmdEcho` and `MyCmdSet`.
```{code-block} python
# in mygame/commands/mycommands.py
:linenos:
:emphasize-lines: 3,4,11,14,15,17,18,19,21
:emphasize-lines: 5,6,13,16,19,20,21,23
# in mygame/commands/mycommands.py
# ...
@ -324,18 +310,17 @@ class CmdHit(Command):
return
self.caller.msg(f"You hit {target.key} with full force!")
target.msg(f"You got hit by {self.caller.key} with full force!")
# ...
# ...
```
A lot of things to dissect here:
- **Line 3**: The normal `class` header. We inherit from `Command` which we imported at the top of this file.
- **Lines 4-10**: The docstring and help-entry for the command. You could expand on this as much as you wanted.
- **Line 11**: We want to write `hit` to use this command.
- **Line 14**: We strip the whitespace from the argument like before. Since we don't want to have to do `self.args.strip()` over and over, we store the stripped version in a _local variable_ `args`. Note that we don't modify `self.args` by doing this, `self.args` will still have the whitespace and is not the same as `args` in this example.
- **Line 5**: The normal `class` header. We inherit from `Command` which we imported at the top of this file.
- **Lines 6-12**: The docstring and help-entry for the command. You could expand on this as much as you wanted.
- **Line 13**: We want to write `hit` to use this command.
- **Line 16**: We strip the whitespace from the argument like before. Since we don't want to have to do `self.args.strip()` over and over, we store the stripped version in a _local variable_ `args`. Note that we don't modify `self.args` by doing this, `self.args` will still have the whitespace and is not the same as `args` in this example.
```{sidebar} if-statements
The full form of the if statement is
if condition:
@ -346,9 +331,9 @@ The full form of the if statement is
...
There can be any number of `elifs` to mark when different branches of the code should run. If `else` is provided, it will run if none of the other conditions were truthy.
```
- **Line 15** has our first _conditional_, an `if` statement. This is written on the form `if <condition>:` and only if that condition is 'truthy' will the indented code block under the `if` statement run. To learn what is truthy in Python it's usually easier to learn what is "falsy":
- **Line 17** has our first _conditional_, an `if` statement. This is written on the form `if <condition>:` and only if that condition is 'truthy' will the indented code block under the `if` statement run. To learn what is truthy in Python it's usually easier to learn what is "falsy":
- `False` - this is a reserved boolean word in Python. The opposite is `True`.
- `None` - another reserved word. This represents nothing, a null-result or value.
- `0` or `0.0`
@ -356,12 +341,20 @@ There can be any number of `elifs` to mark when different branches of the code s
- Empty _iterables_ we haven't used yet, like empty lists `[]`, empty tuples `()` and empty dicts `{}`.
- Everything else is "truthy".
- **Line 16**'s condition is `not args`. The `not` _inverses_ the result, so if `args` is the empty string (falsy), the whole conditional becomes truthy. Let's continue in the code:
The conditional on **Line 16**'s condition is `not args`. The `not` _inverses_ the result, so if `args` is the empty string (falsy), the whole conditional becomes truthy. Let's continue in the code:
```{sidebar} Errors in your code
With longer code snippets to try, it gets more and more likely you'll
make an error and get a `traceback` when you reload. This will either appear
directly in-game or in your log (view it with `evennia -l` in a terminal).
Don't panic - tracebacks are your friends! They are to be read bottom-up and usually describe exactly where your problem is. Refer to [The Python introduction lesson](./Beginner-Tutorial-Python-basic-introduction.md) for more hints. If you get stuck, reach out to the Evennia community for help.
```
- **Lines 16-17**: This code will only run if the `if` statement is truthy, in this case if `args` is the empty string.
- **Line 17**: `return` is a reserved Python word that exits `func` immediately.
- **Line 18**: We use `self.caller.search` to look for the target in the current location.
- **Lines 19-20**: A feature of `.search` is that it will already inform `self.caller` if it couldn't find the target. In that case, `target` will be `None` and we should just directly `return`.
- **Lines 21-22**: At this point we have a suitable target and can send our punching strings to each.
- **Line 19**: `return` is a reserved Python word that exits `func` immediately.
- **Line 20**: We use `self.caller.search` to look for the target in the current location.
- **Lines 21-22**: A feature of `.search` is that it will already inform `self.caller` if it couldn't find the target. In that case, `target` will be `None` and we should just directly `return`.
- **Lines 23-24**: At this point we have a suitable target and can send our punching strings to each.
Finally we must also add this to a CmdSet. Let's add it to `MyCmdSet`.
@ -377,14 +370,6 @@ class MyCmdSet(CmdSet):
```
```{sidebar} Errors in your code
With longer code snippets to try, it gets more and more likely you'll
make an error and get a `traceback` when you reload. This will either appear
directly in-game or in your log (view it with `evennia -l` in a terminal).
Don't panic; tracebacks are your friends - they are to be read bottom-up and usually describe exactly where your problem is. Refer to [The Python introduction lesson](./Beginner-Tutorial-Python-basic-introduction.md) for more hints. If you get stuck, reach out to the Evennia community for help.
```
Note that since we did `py self.cmdset.remove("commands.mycommands.MyCmdSet")` earlier, this cmdset is no longer available on our Character. Instead we will add these commands directly to our default cmdset.
```python
@ -439,4 +424,4 @@ You won't see the second string. Only Smaug sees that (and is not amused).
In this lesson we learned how to create our own Command, add it to a CmdSet and then to ourselves. We also upset a dragon.
In the next lesson we'll learn how to hit Smaug with different weapons. We'll also
get into how we replace and extend Evennia's default Commands.
get into how we replace and extend Evennia's default Commands.

View file

@ -149,6 +149,54 @@ If you you really want all matches to the search parameters you specify. In othe
There are equivalent search functions for all the main resources. You can find a listing of them [in the Search functions section](../../../Evennia-API.md) of the API front page.
## Understanding object relationships
It's important to understand how objects relate to one another when searching.
Let's consider a `chest` with a `coin` inside it. The chest stands in a `dungeon` room. In the dungeon is also a `door` (an exit leading outside).
```
┌───────────────────────┐
│dungeon │
│ ┌─────────┐ │
│ │chest │ ┌────┐ │
│ │ ┌────┐ │ │door│ │
│ │ │coin│ │ └────┘ │
│ │ └────┘ │ │
│ │ │ │
│ └─────────┘ │
│ │
└───────────────────────┘
```
If you have access to any in-game Object, you can find related objects by use if its `.location` and `.contents` properties.
- `coin.location` is `chest`.
- `chest.location` is `dungeon`.
- `door.location` is `dungeon`.
- `room.location` is `None` since it's not inside something else.
One can use this to find what is inside what. For example, `coin.location.location` is the `dungeon`.
- `room.contents` is `[chest, door]`
- `chest.contents` is `[coin]`
- `coin.contents` is `[]`, the empty list since there's nothing 'inside' the coin.
- `door.contents` is `[]` too.
A convenient helper is `.contents_get` - this allows to restrict what is returned:
- `room.contents_get(exclude=chest)` - this returns everything in the room except the chest (maybe it's hidden?)
There is a special property for finding exits:
- `room.exits` is `[door]`
- `coin.exits` is `[]` since it has no exits (same for all the other objects)
There is a property `.destination` which is only used by exits:
- `door.destination` is `outside` (or wherever the door leads)
- `room.destination` is `None` (same for all the other non-exit objects)
## What can be searched for
These are the main database entities one can search for:
@ -162,6 +210,8 @@ These are the main database entities one can search for:
Most of the time you'll likely spend your time searching for Objects and the occasional Accounts.
Most search methods are available directly from `evennia`. But there are also a lot of useful search helpers found via `evennia.search`.
So to find an entity, what can be searched for?
### Search by key
@ -206,9 +256,9 @@ However, using `search_object` will find the rose wherever it's located:
> py evennia.search_object("rose")
<QuerySet [Rose]>
However, if you demand that the room is in the current room, it won't be found:
The `evennia.search_object` method doesn't have a `location` argument. What you do instead is to limit the search by setting its `candidates` keyword to the `.contents` of the current location. This is the same as a location search, since it will only accept matches among those in the room. In this example we'll (correctly) find the rose is not in the room.
> py evennia.search_object("rose", location=here)
> py evennia.search_object("rose", candidate=here.contents)
<QuerySet []>
In general, the `Object.search` is a shortcut for doing the very common searches of things in the same location, whereas the `search_object` finds objects anywhere.
@ -267,7 +317,7 @@ For example, let's say our plants have a 'growth state' that updates as it grows
Now we can find the things that have a given growth state:
> py evennia.search_object_attribute("growth_state", "withering")
> py evennia.search_object("withering", attribute_name="growth_state")
<QuerySet [Rose]>
> Searching by Attribute can be very practical. But if you want to group entities or search very often, using Tags and search by Tags is faster and more resource-efficient.
@ -317,53 +367,11 @@ In legacy code bases you may be used to relying a lot on #dbrefs to find and tra
```
## Finding objects relative each other
## Summary
It's important to understand how objects relate to one another when searching.
Let's consider a `chest` with a `coin` inside it. The chest stands in a room `dungeon`. In the dungeon is also a `door`. This is an exit leading outside.
Knowing how to find things is important and the tools from this section will serve you well. These tools will cover most of your regular needs.
```
┌───────────────────────┐
│dungeon │
│ ┌─────────┐ │
│ │chest │ ┌────┐ │
│ │ ┌────┐ │ │door│ │
│ │ │coin│ │ └────┘ │
│ │ └────┘ │ │
│ │ │ │
│ └─────────┘ │
│ │
└───────────────────────┘
```
- `coin.location` is `chest`.
- `chest.location` is `dungeon`.
- `door.location` is `dungeon`.
- `room.location` is `None` since it's not inside something else.
One can use this to find what is inside what. For example, `coin.location.location` is the `dungeon`.
We can also find what is inside each object. This is a list of things.
- `room.contents` is `[chest, door]`
- `chest.contents` is `[coin]`
- `coin.contents` is `[]`, the empty list since there's nothing 'inside' the coin.
- `door.contents` is `[]` too.
A convenient helper is `.contents_get` - this allows to restrict what is returned:
- `room.contents_get(exclude=chest)` - this returns everything in the room except the chest (maybe it's hidden?)
There is a special property for finding exits:
- `room.exits` is `[door]`
- `coin.exits` is `[]` (same for all the other objects)
There is a property `.destination` which is only used by exits:
- `door.destination` is `outside` (or wherever the door leads)
- `room.destination` is `None` (same for all the other non-exit objects)
You can also include this information in searches:
Not always though. If we go back to the example of a coin in a chest from before, you _could_ use the following to dynamically figure out if there are any chests in the room with coins inside:
```python
from evennia import search_object
@ -372,13 +380,9 @@ from evennia import search_object
dungeons = search_object("dungeon", typeclass="typeclasses.rooms.Room")
chests = search_object("chest", location=dungeons[0])
# find if there are any skulls in the chest
skulls = search_object("Skull", candidates=chests[0].contents)
coins = search_object("coin", candidates=chests[0].contents)
```
More advanced, nested queries like this can however often be made more efficient by using the hints in the next lesson.
This would work but is both quite inefficient, fragile and a lot to type. This kind of thing is better done by directly querying the database.
## Summary
Knowing how to find things is important and the tools from this section will serve you well. These tools will cover most of your needs ...
... but not always. In the next lesson we will dive further into more complex searching when we look at Django queries and querysets in earnest.
In the next lesson we will dive further into more complex searching when we look at Django queries and querysets in earnest.

View file

@ -40,6 +40,8 @@ You can find an AIHandler implemented in `evennia/contrib/tutorials`, in [evadve
```
This is the core logic for managing AI states. Create a new file `evadventure/ai.py`.
> Create a new file `evadventure/ai.py`.
```{code-block} python
:linenos:
:emphasize-lines: 10,11-13,16,23

View file

@ -1,13 +1,11 @@
# Player Characters
In the [previous lesson about rules and dice rolling](./Beginner-Tutorial-Rules.md) we made some
assumptions about the "Player Character" entity:
In the [previous lesson about rules and dice rolling](./Beginner-Tutorial-Rules.md) we made some assumptions about the "Player Character" entity:
- It should store Abilities on itself as `character.strength`, `character.constitution` etc.
- It should have a `.heal(amount)` method.
So we have some guidelines of how it should look! A Character is a database entity with values that
should be able to be changed over time. It makes sense to base it off Evennia's
So we have some guidelines of how it should look! A Character is a database entity with values that should be able to be changed over time. It makes sense to base it off Evennia's
[DefaultCharacter Typeclass](../../../Components/Typeclasses.md). The Character class is like a 'character sheet' in a tabletop
RPG, it will hold everything relevant to that PC.
@ -16,8 +14,7 @@ RPG, it will hold everything relevant to that PC.
Player Characters (PCs) are not the only "living" things in our world. We also have _NPCs_
(like shopkeepers and other friendlies) as well as _monsters_ (mobs) that can attack us.
In code, there are a few ways we could structure this. If NPCs/monsters were just special cases of PCs,
we could use a class inheritance like this:
In code, there are a few ways we could structure this. If NPCs/monsters were just special cases of PCs, we could use a class inheritance like this:
```python
from evennia import DefaultCharacter
@ -34,9 +31,7 @@ class EvAdventureMob(EvAdventureNPC):
All code we put on the `Character` class would now be inherited to `NPC` and `Mob` automatically.
However, in _Knave_, NPCs and particularly monsters are _not_ using the same rules as PCs - they are
simplified to use a Hit-Die (HD) concept. So while still character-like, NPCs should be separate from
PCs like this:
However, in _Knave_, NPCs and particularly monsters are _not_ using the same rules as PCs - they are simplified to use a Hit-Die (HD) concept. So while still character-like, NPCs should be separate from PCs like this:
```python
from evennia import DefaultCharacter
@ -60,8 +55,7 @@ Nevertheless, there are some things that _should_ be common for all 'living thin
- All can loot their fallen foes.
- All can get looted when defeated.
We don't want to code this separately for every class but we no longer have a common parent
class to put it on. So instead we'll use the concept of a _mixin_ class:
We don't want to code this separately for every class but we no longer have a common parent class to put it on. So instead we'll use the concept of a _mixin_ class:
```python
from evennia import DefaultCharacter
@ -83,10 +77,7 @@ class EvAdventureMob(LivingMixin, EvadventureNPC):
In [evennia/contrib/tutorials/evadventure/characters.py](../../../api/evennia.contrib.tutorials.evadventure.characters.md)
is an example of a character class structure.
```
Above, the `LivingMixin` class cannot work on its own - it just 'patches' the other classes with some
extra functionality all living things should be able to do. This is an example of
_multiple inheritance_. It's useful to know about, but one should not over-do multiple inheritance
since it can also get confusing to follow the code.
Above, the `LivingMixin` class cannot work on its own - it just 'patches' the other classes with some extra functionality all living things should be able to do. This is an example of _multiple inheritance_. It's useful to know about, but one should not over-do multiple inheritance since it can also get confusing to follow the code.
## Living mixin class
@ -178,7 +169,6 @@ Most of these are empty since they will behave differently for characters and np
Once we create more of our game, we will need to remember to actually call these hook methods so they serve a purpose. For example, once we implement combat, we must remember to call `at_attacked` as well as the other methods involving taking damage, getting defeated or dying.
## Character class
We will now start making the basic Character class, based on what we need from _Knave_.
@ -234,8 +224,7 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
We make an assumption about our rooms here - that they have a property `.allow_death`. We need to make a note to actually add such a property to rooms later!
In our `Character` class we implement all attributes we want to simulate from the _Knave_ ruleset.
The `AttributeProperty` is one way to add an Attribute in a field-like way; these will be accessible on every character in several ways:
In our `Character` class we implement all attributes we want to simulate from the _Knave_ ruleset. The `AttributeProperty` is one way to add an Attribute in a field-like way; these will be accessible on every character in several ways:
- As `character.strength`
- As `character.db.strength`
@ -249,7 +238,7 @@ We implement the Player Character versions of `at_defeat` and `at_death`. We als
### Funcparser inlines
This piece of code is worth some more explanation:
This piece of code in the `at_defeat` method above is worth some more extra explanation:
```python
self.location.msg_contents(
@ -259,8 +248,7 @@ self.location.msg_contents(
Remember that `self` is the Character instance here. So `self.location.msg_contents` means "send a message to everything inside my current location". In other words, send a message to everyone in the same place as the character.
The `$You() $conj(collapse)` are [FuncParser inlines](../../../Components/FuncParser.md). These are functions that
execute in the string. The resulting string may look different for different audiences. The `$You()` inline function will use `from_obj` to figure out who 'you' are and either show your name or 'You'. The `$conj()` (verb conjugator) will tweak the (English) verb to match.
The `$You() $conj(collapse)` are [FuncParser inlines](../../../Components/FuncParser.md). These are functions that execute in the string. The resulting string may look different for different audiences. The `$You()` inline function will use `from_obj` to figure out who 'you' are and either show your name or 'You'. The `$conj()` (verb conjugator) will tweak the (English) verb to match.
- You will see: `"You collapse in a heap, alive but beaten."`
- Others in the room will see: `"Thomas collapses in a heap, alive but beaten."`
@ -303,10 +291,7 @@ You can easily make yourself an `EvAdventureCharacter` in-game by using the
You can now do `examine self` to check your type updated.
If you want _all_ new Characters to be of this type you need to tell Evennia about it. Evennia
uses a global setting `BASE_CHARACTER_TYPECLASS` to know which typeclass to use when creating
Characters (when logging in, for example). This defaults to `typeclasses.characters.Character` (that is,
the `Character` class in `mygame/typeclasses/characters.py`).
If you want _all_ new Characters to be of this type you need to tell Evennia about it. Evennia uses a global setting `BASE_CHARACTER_TYPECLASS` to know which typeclass to use when creating Characters (when logging in, for example). This defaults to `typeclasses.characters.Character` (that is, the `Character` class in `mygame/typeclasses/characters.py`).
There are thus two ways to weave your new Character class into Evennia:
@ -327,8 +312,7 @@ instead.
> Create a new module `mygame/evadventure/tests/test_characters.py`
For testing, we just need to create a new EvAdventure character and check
that calling the methods on it doesn't error out.
For testing, we just need to create a new EvAdventure character and check that calling the methods on it doesn't error out.
```python
# mygame/evadventure/tests/test_characters.py
@ -368,22 +352,18 @@ class TestCharacters(BaseEvenniaTest):
# tests for other methods ...
```
If you followed the previous lessons, these tests should look familiar. Consider adding
tests for other methods as practice. Refer to previous lessons for details.
If you followed the previous lessons, these tests should look familiar. Consider adding tests for other methods as practice. Refer to previous lessons for details.
For running the tests you do:
evennia test --settings settings.py .evadventure.tests.test_character
evennia test --settings settings.py .evadventure.tests.test_characters
## About races and classes
_Knave_ doesn't have any D&D-style _classes_ (like Thief, Fighter etc). It also does not bother with
_races_ (like dwarves, elves etc). This makes the tutorial shorter, but you may ask yourself how you'd
add these functions.
_Knave_ doesn't have any D&D-style _classes_ (like Thief, Fighter etc). It also does not bother with _races_ (like dwarves, elves etc). This makes the tutorial shorter, but you may ask yourself how you'd add these functions.
In the framework we have sketched out for _Knave_, it would be simple - you'd add your race/class as
an Attribute on your Character:
In the framework we have sketched out for _Knave_, it would be simple - you'd add your race/class as an Attribute on your Character:
```python
# mygame/evadventure/characters.py
@ -399,8 +379,7 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
charrace = AttributeProperty("Human")
```
We use `charclass` rather than `class` here, because `class` is a reserved Python keyword. Naming
`race` as `charrace` thus matches in style.
We use `charclass` rather than `class` here, because `class` is a reserved Python keyword. Naming `race` as `charrace` thus matches in style.
We'd then need to expand our [rules module](./Beginner-Tutorial-Rules.md) (and later
[character generation](./Beginner-Tutorial-Chargen.md) to check and include what these classes mean.
@ -409,23 +388,16 @@ We'd then need to expand our [rules module](./Beginner-Tutorial-Rules.md) (and l
## Summary
With the `EvAdventureCharacter` class in place, we have a better understanding of how our PCs will look
like under _Knave_.
With the `EvAdventureCharacter` class in place, we have a better understanding of how our PCs will look like under _Knave_.
For now, we only have bits and pieces and haven't been testing this code in-game. But if you want
you can swap yourself into `EvAdventureCharacter` right now. Log into your game and run
the command
For now, we only have bits and pieces and haven't been testing this code in-game. But if you want you can swap yourself into `EvAdventureCharacter` right now. Log into your game and run the command
type self = evadventure.characters.EvAdventureCharacter
If all went well, `ex self` will now show your typeclass as being `EvAdventureCharacter`.
Check out your strength with
If all went well, `ex self` will now show your typeclass as being `EvAdventureCharacter`. Check out your strength with
py self.strength = 3
```{important}
When doing `ex self` you will _not_ see all your Abilities listed yet. That's because
Attributes added with `AttributeProperty` are not available until they have been accessed at
least once. So once you set (or look at) `.strength` above, `strength` will show in `examine` from
then on.
When doing `ex self` you will _not_ see all your Abilities listed yet. That's because Attributes added with `AttributeProperty` are not available until they have been accessed at least once. So once you set (or look at) `.strength` above, `strength` will show in `examine` from then on.
```

View file

@ -16,7 +16,7 @@ We will design a base combat system that supports both styles.
> Create a new module `evadventure/combat_base.py`
```{sidebar}
In [evennia/contrib/tutorials/evadventure/combat_base.py](evennia.contrib.tutorials.evadventure.combat_base) you'll find a complete implementation of the base combat module.
Under `evennia/contrib/tutorials/evadventure/`, in [combat_base.py](evennia.contrib.tutorials.evadventure.combat_base) you'll find a complete implementation of the base combat module.
```
Our "Combat Handler" will handle the administration around combat. It needs to be _persistent_ (even is we reload the server your combat should keep going).
@ -718,7 +718,7 @@ We rely on the [Equipment handler](./Beginner-Tutorial-Equipment.md) we created
> Create a module `evadventure/tests/test_combat.py`.
```{sidebar}
See [evennia/contrib/tutorials/evadventure/tests/test_combat.py](evennia.contrib.tutorials.evadventure.tests.test_combat) for ready-made combat unit tests.
Look under `evennia/contrib/tutorials/evadventure/`, in [tests/test_combat.py](evennia.contrib.tutorials.evadventure.tests.test_combat) for ready-made combat unit tests.
```
Unit testing the combat base classes can seem impossible because we have not yet implemented most of it. We can however get very far by the use of [Mocks](https://docs.python.org/3/library/unittest.mock.html). The idea of a Mock is that you _replace_ a piece of code with a dummy object (a 'mock') that can be called to return some specific value.

View file

@ -80,7 +80,7 @@ The advantage of using a menu is that you have all possible actions directly ava
## General Principle
```{sidebar}
An example of an implemented Turnbased combat system can be found in [evennia/contrib/tutorials/evadventure/combat_turnbased.py](evennia.contrib.tutorials.evadventure.combat_turnbased).
An example of an implemented Turnbased combat system can be found under `evennia/contrib/tutorials/evadventure/`, in [combat_turnbased.py](evennia.contrib.tutorials.evadventure.combat_turnbased).
```
Here is the general principle of the Turnbased combat handler:

View file

@ -92,7 +92,7 @@ class EvAdventureObject(DefaultObject):
"""The top of the description"""
return ""
def get_display_desc(self, looker, **kwargs)
def get_display_desc(self, looker, **kwargs):
"""The main display - show object stats"""
return get_obj_stats(self, owner=looker)
@ -216,7 +216,7 @@ class EvAdventureConsumable(EvAdventureObject):
"""Called when using the item"""
pass
def at_post_use(self. user, *args, **kwargs):
def at_post_use(self, user, *args, **kwargs):
"""Called after using the item"""
# detract a usage, deleting the item if used up.
self.uses -= 1
@ -452,7 +452,7 @@ _BARE_HANDS = None
# ...
class WeaponBareHands(EvAdventureWeapon)
class WeaponBareHands(EvAdventureWeapon):
obj_type = ObjType.WEAPON
inventory_use_slot = WieldLocation.WEAPON_HAND
attack_type = Ability.STR

View file

@ -1,5 +1,402 @@
# Game Quests
```{warning}
This part of the Beginner tutorial is still being developed.
```
A _quest_ is a common feature of games. From classic fetch-quests like retrieving 10 flowers to complex quest chains involving drama and intrigue, quests need to be properly tracked in our game.
A quest follows a specific development:
1. The quest is _started_. This normally involves the player accepting the quest, from a quest-giver, job board or other source. But the quest could also be thrust on the player ("save the family from the burning house before it collapses!")
2. Once a quest has been accepted and assigned to a character, it is either either `Started` (that is, 'in progress'), `Abandoned`, `Failed` or `Complete`.
3. A quest may consist of one or more 'steps'. Each step has its own set of finish conditions.
4. At suitable times the quest's _progress_ is checked. This could happen on a timer or when trying to 'hand in' the quest. When checking, the current 'step' is checked against its finish conditions. If ok, that step is closed and the next step is checked until it either hits a step that is not yet complete, or there are no more steps, in which case the entire quest is complete.
```{sidebar}
An example implementation of quests is found under `evennia/contrib/tutorials`, in [evadvanture/quests.py](evennia.contrib.tutorials.evadventure.quests).
```
To represent quests in code, we need
- A convenient flexible way to code how we check the status and current steps of the quest. We want this scripting to be as flexible as possible. Ideally we want to be able to code the quests's logic in full Python.
- Persistence. The fact that we accepted the quest, as well as its status and other flags must be saved in the database and survive a server reboot.
We'll accomplish this using two pieces of Python code:
- `EvAdventureQuest`: A Python class with helper methods that we can call to check current quest status, figure if a given quest-step is complete or not. We will create and script new quests by simply inheriting from this base class and implement new methods on it in a standardized way.
- `EvAdventureQuestHandler` will sit 'on' each Character as `character.quests`. It will hold all `EvAdventureQuest`s that the character is or has been involved in. It is also responsible for storing quest state using [Attributes](../../../Components/Attributes.md) on the Character.
## The Quest Handler
> Create a new module `evadventure/quests.py`.
We saw the implementation of an on-object handler back in the [lesson about NPC and monster AI](./Beginner-Tutorial-AI.md#the-aihandler) (the `AIHandler`).
```{code-block} python
:linenos:
:emphasize-lines: 9,10,11,14-18,21,24-28
# in evadventure/quests.py
class EvAdventureQuestHandler:
quest_storage_attribute_key = "_quests"
quest_storage_attribute_category = "evadventure"
def __init__(self, obj):
self.obj = obj
self.quest_classes = {}
self.quests = {}
self._load()
def _load(self):
self.quest_classes = self.obj.attributes.get(
self.quest_storage_attribute_key,
category=self.quest_storage_attribute_category,
default={},
)
# instantiate all quests
for quest_key, quest_class in self.quest_classes.items():
self.quests[quest_key] = quest_class(self.obj)
def _save(self):
self.obj.attributes.add(
self.quest_storage_attribute_key,
self.quest_classes,
category=self.quest_storage_attribute_category,
)
def get(self, quest_key):
return self.quests.get(quest_key)
def all(self):
return list(self.quests.values())
def add(self, quest_class):
self.quest_classes[quest_class.key] = quest_class
self.quests[quest_class.key] = quest_class(self.obj)
self._save()
def remove(self, quest_key):
quest = self.quests.pop(quest_key, None)
self.quest_classes.pop(quest_key, None)
self.quests.pop(quest_key, None)
self._save()
```
```{sidebar} Persistent handler pattern
Persistent handlers are commonly used throughout Evennia. You can read more about them in the [Making a Persistent object Handler](../../Tutorial-Persistent-Handler.md) tutorial.
```
- **Line 9**: We know that the quests themselves will be Python classes inheriting from `EvAdventureQuest` (which we haven't created yet). We will store those classes in `self.quest_classes` on the handler. Note that there is a difference between a class and an _instance_ of a class! The class cannot hold any _state_ on its own, such as the status of that quest is for this particular character. The class only holds python code.
- **Line 10**: We set aside another property on the handler - `self.quest` This is dictionary that will hold `EvAdventureQuest` _instances_.
- **Line 11**: Note that we call the `self._load()` method here, this loads up data from the database whenever this handler is accessed.
- **Lines 14-18**: We use `self.obj.attributes.get` to fetch an [Attribute](../../../Components/Attributes.md) on the Character named `_quests` and with a category of `evadventure`. If it doesn't exist yet (because we never started any quests), we just return an empty dict.
- **Line 21**: Here we loop over all the classes and instantiate them. We haven't defined how these quest-classes look yet, but by instantiating them with `self.obj` (the Character) we should be covered - from the Character class the quest will be able to get to everything else (this handler itself will be accessible as `obj.quests` from that quest instance after all).
- **Line 24**: Here we do the corresponding save operation.
The rest of the handler are just access methods for getting, adding and removing quests from the handler. We make one assumption in those code, namely that the quest class has a property `.key` being the unique quest-name.
This is how it would be used in practice:
```python
# in some questing code
from evennia import search_object
from evadventure import quests
class EvAdventureSuperQuest(quests.EvAdventureQuest):
key = "superquest"
# quest implementation here
def start_super_quest(character):
character.quests.add(EvAdventureSuperQuest)
```
```{sidebar} What can be saved in Attributes?
For more details, see [the Attributes documentation](../../../Components/Attributes.md#what-types-of-data-can-i-save-in-an-attribute) on the matter.
```
We chose to store classes and not instances of classes above. The reason for this has to do with what can be stored in a database `Attribute` - one limitation of an Attribute is that we can't save a class instance _with other database entities baked inside it_. If we saved quest instances as-is, it's highly likely they'd contain database entities 'hidden' inside them - a reference to the Character, maybe to objects required for the quest to be complete etc. Evennia would fail trying to save that data.
Instead we store only the classes, instantiate those classes with the Character, and let the quest store its state flags separately, like this:
```python
# in evadventure/quests.py
class EvAdventureQuestHandler:
# ...
quest_data_attribute_template = "_quest_data_{quest_key}"
quest_data_attribute_category = "evadventure"
# ...
def save_quest_data(self, quest_key):
quest = self.get(quest_key)
if quest:
self.obj.attributes.add(
self.quest_data_attribute_template.format(quest_key=quest_key),
quest.data,
category=self.quest_data_attribute_category,
)
def load_quest_data(self, quest_key):
return self.obj.attributes.get(
self.quest_data_attribute_template.format(quest_key=quest_key),
category=self.quest_data_attribute_category,
default={},
)
```
This works the same as the `_load` and `_save` methods, except it fetches a property `.data` (this will be a `dict`) on the quest instance and save it. As long as we make sure to call these methods from the quest the quest whenever that `.data` property is changed, all will be well - this is because Attributes know how to properly analyze a `dict` to find and safely serialize any database entities found within.
Our handler is ready. We created the `EvAdventureCharacter` class back in the [Character lesson](./Beginner-Tutorial-Characters.md) - let's add quest-support to it.
```python
# in evadventure/characters.py
# ...
from evennia.utils import lazy_property
from evadventure.quests import EvAdventureQuestHandler
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
# ...
@lazy_property
def quests(self):
return EvAdventureQuestHandler(self)
# ...
```
We also need a way to represent the quests themselves though!
## The Quest class
```{code-block} python
:linenos:
:emphasize-lines: 7,12,13,34-36
# in evadventure/quests.py
# ...
class EvAdventureQuest:
key = "base-quest"
desc = "Base quest"
start_step = "start"
def __init__(self, quester):
self.quester = quester
self.data = self.questhandler.load_quest_data(self.key)
self._current_step = self.get_data("current_step")
if not self.current_step:
self.current_step = self.start_step
def add_data(self, key, value):
self.data[key] = value
self.questhandler.save_quest_data(self.key)
def get_data(self, key, default=None):
return self.data.get(key, default)
def remove_data(self, key):
self.data.pop(key, None)
self.questhandler.save_quest_data(self.key)
@property
def questhandler(self):
return self.quester.quests
@property
def current_step(self):
return self._current_step
@current_step.setter
def current_step(self, step_name):
self._current_step = step_name
self.add_data("current_step", step_name)
```
- **Line 7**: Each class must have a `.key` property unquely identifying the quest. We depend on this in the quest-handler.
- **Line 12**: `quester` (the Character) is passed into this class when it is initiated inside `EvAdventureQuestHandler._load()`.
- **Line 13**: We load the quest data into `self.data` directly using the `questhandler.load_quest-data` method (which in turn loads it from an Attribute on the Character). Note that the `.questhandler` property is defined on **lines 34-36** as a shortcut to get to the handler.
The `add/get/remove_data` methods are convenient wrappers for getting data in and out of the database using the matching methods on the handler. When we implement a quest we should prefer to use `.get_data`, `add_data` and `remove_data` over manipulating `.data` directly, since the former will make sure to save said that to the database automatically.
The `current_step` tracks the current quest 'step' we are in; what this means is up to each Quest. We set up convenient properties for setting the `current_state` and also make sure to save it in the data dict as "current_step".
The quest can have a few possible statuses: "started", "completed", "abandoned" and "failed". We create a few properties and methods for easily control that, while saving everything under the hood:
```python
# in evadventure/quests.py
# ...
class EvAdventureQuest:
# ...
@property
def status(self):
return self.get_data("status", "started")
@status.setter
def status(self, value):
self.add_data("status", value)
@property
def is_completed(self):
return self.status == "completed"
@property
def is_abandoned(self):
return self.status == "abandoned"
@property
def is_failed(self):
return self.status == "failed"
def complete(self):
self.status = "completed"
def abandon(self):
self.status = "abandoned"
def fail(self):
self.status = "failed"
```
So far we have only added convenience functions for checking statuses. How will the actual "quest" aspect of this work?
What will happen when the system wants to check the progress of the quest, is that it will call a method `.progress()` on this class. Similarly, to get help for the current step, it will call a method `.help()`
```python
start_step = "start"
# help entries for quests (could also be methods)
help_start = "You need to start first"
help_end = "You need to end the quest"
def progress(self, *args, **kwargs):
getattr(self, f"step_{self.current_step}")(*args, **kwargs)
def help(self, *args, **kwargs):
if self.status in ("abandoned", "completed", "failed"):
help_resource = getattr(self, f"help_{self.status}",
f"You have {self.status} this quest.")
else:
help_resource = getattr(self, f"help_{self.current_step}", "No help available.")
if callable(help_resource):
# the help_* methods can be used to dynamically generate help
return help_resource(*args, **kwargs)
else:
# normally it's just a string
return str(help_resource)
```
```{sidebar} What's with the *args, **kwargs?
These are optional, but allow you to pass extra information into your quest-check. This could be very powerful if you want to add extra context to determine if a quest-step is currently complete or not.
```
Calling the `.progress(*args, **kwargs)` method will call a method named `step_<current_step>(*args, **kwargs)` on this class. That is, if we are on the _start_ step, the method called will be `self.step_start(*args, **kwargs)`. Where is this method? It has not been implemented yet! In fact, it's up to us to implement methods like this for each quest. By just adding a correctly added method, we will easily be able to add more steps to a quest.
Similarly, calling `.help(*args, **kwargs)` will try to find a property `help_<current_step>`. If this is a callable, it will be called as for example `self.help_start(*args, **kwargs)`. If it is given as a string, then the string will be returned as-is and the `*args, **kwargs` will be ignored.
### Example quest
```python
# in some quest module, like world/myquests.py
from evadventure.quests import EvAdventureQuest
class ShortQuest(EvAdventureQuest):
key = "simple-quest"
desc = "A very simple quest."
def step_start(self, *args, **kwargs):
"""Example step!"""
self.quester.msg("Quest started!")
self.current_step = "end"
def step_end(self, *args, **kwargs):
if not self.is_completed:
self.quester.msg("Quest ended!")
self.complete()
```
This is a very simple quest that will resolve on its own after two `.progress()` checks. Here's the full life cycle of this quest:
```python
# in some module somewhere, using evennia shell or in-game using py
from evennia import search_object
from world.myquests import ShortQuest
character = search_object("MyCharacterName")[0]
character.quests.add(ShortQuest)
# this will echo "Quest started!" to character
character.quests.get("short-quest").progress()
# this will echo "Quest ended!" to character
character.quests.get("short-quest").progress()
```
### A useful Command
The player must know which quests they have and be able to inspect them. Here's a simple `quests` command to handle this:
```python
# in evadventure/quests.py
class CmdQuests(Command):
"""
List all quests and their statuses as well as get info about the status of
a specific quest.
Usage:
quests
quest <questname>
"""
key = "quests"
aliases = ["quest"]
def parse(self):
self.quest_name = self.args.strip()
def func(self):
if self.quest_name:
quest = self.caller.quests.get(self.quest_name)
if not quest:
self.msg(f"Quest {self.quest_name} not found.")
return
self.msg(f"Quest {quest.key}: {quest.status}\n{quest.help()}")
return
quests = self.caller.quests.all()
if not quests:
self.msg("No quests.")
return
for quest in quests:
self.msg(f"Quest {quest.key}: {quest.status}")
```
Add this to the `CharacterCmdSet` in `mygame/commands/default_cmdsets.py`. Follow the [Adding a command lesson](../Part1/Beginner-Tutorial-Adding-Commands.md#add-the-echo-command-to-the-default-cmdset) if you are unsure how to do this. Reload and if you are playing as an `EvAdventureCharacter` you should be able to use `quests` to view your quests.
## Testing
> Create a new folder `evadventure/tests/test_quests.py`.
```{sidebar}
An example test suite for quests is found in `evennia/contrib/tutorials/evadventure`, as [tests/test_quests.py](evennia.contrib.tutorials.evadventure.tests.test_quests).
```
Testing of the quests means creating a test character, making a dummy quest, add it to the character's quest handler and making sure all methods work correcly. Create the testing quest so that it will automatically step forward when calling `.progress()`, so you can make sure it works as intended.
## Conclusions
What we created here is just the framework for questing. The actual complexity will come when creating the quests themselves (that is, implementing the `step_<current_step>(*args, **kwargs)` methods), which is something we'll get to later, in [Part 4](../Part4/Beginner-Tutorial-Part4-Overview.md) of this tutorial.

View file

@ -31,6 +31,7 @@ value - which may change as Evennia is developed. This way you can
always be sure of what you have changed and what is default behaviour.
"""
import os
import sys

View file

@ -1,7 +1,6 @@
# Changing Game Settings
Evennia runs out of the box without any changes to its settings. But there are several important
ways to customize the server and expand it with your own plugins.
Evennia runs out of the box without any changes to its settings. But there are several important ways to customize the server and expand it with your own plugins.
All game-specific settings are located in the `mygame/server/conf/` directory.
@ -17,13 +16,9 @@ heavily documented and up-to-date, so you should refer to this file directly for
Since `mygame/server/conf/settings.py` is a normal Python module, it simply imports
`evennia/settings_default.py` into itself at the top.
This means that if any setting you want to change were to depend on some *other* default setting,
you might need to copy & paste both in order to change them and get the effect you want (for most
commonly changed settings, this is not something you need to worry about).
This means that if any setting you want to change were to depend on some *other* default setting, you might need to copy & paste both in order to change them and get the effect you want (for most commonly changed settings, this is not something you need to worry about).
You should never edit `evennia/settings_default.py`. Rather you should copy&paste the select
variables you want to change into your `settings.py` and edit them there. This will overload the
previously imported defaults.
You should never edit `evennia/settings_default.py`. Rather you should copy&paste the select variables you want to change into your `settings.py` and edit them there. This will overload the previously imported defaults.
```{warning} Don't copy everything!
It may be tempting to copy *everything* from `settings_default.py` into your own settings file just to have it all in one place. Don't do this. By copying only what you need, you can easier track what you changed.
@ -41,45 +36,24 @@ In code, the settings is accessed through
Each setting appears as a property on the imported `settings` object. You can also explore all possible options with `evennia.settings_full` (this also includes advanced Django defaults that are not touched in default Evennia).
> When importing `settings` into your code like this, it will be *read
only*. You *cannot* edit your settings from your code! The only way to change an Evennia setting is
to edit `mygame/server/conf/settings.py` directly. You will also need to restart the server
(possibly also the Portal) before a changed setting becomes available.
> When importing `settings` into your code like this, it will be *read only*. You *cannot* edit your settings from your code! The only way to change an Evennia setting is to edit `mygame/server/conf/settings.py` directly. You will also need to restart the server (possibly also the Portal) before a changed setting becomes available.
## Other files in the `server/conf` directory
Apart from the main `settings.py` file,
- `at_initial_setup.py` - this allows you to add a custom startup method to be called (only) the
very first time Evennia starts (at the same time as user #1 and Limbo is created). It can be made to
start your own global scripts or set up other system/world-related things your game needs to have
running from the start.
- `at_server_startstop.py` - this module contains two functions that Evennia will call every time
the Server starts and stops respectively - this includes stopping due to reloading and resetting as
well as shutting down completely. It's a useful place to put custom startup code for handlers and
other things that must run in your game but which has no database persistence.
- `connection_screens.py` - all global string variables in this module are interpreted by Evennia as
a greeting screen to show when an Account first connects. If more than one string variable is
present in the module a random one will be picked.
- `at_initial_setup.py` - this allows you to add a custom startup method to be called (only) the very first time Evennia starts (at the same time as user #1 and Limbo is created). It can be made to start your own global scripts or set up other system/world-related things your game needs to have running from the start.
- `at_server_startstop.py` - this module contains functions that Evennia will call every time the Server starts and stops respectively - this includes stopping due to reloading and resetting as well as shutting down completely. It's a useful place to put custom startup code for handlers and other things that must run in your game but which has no database persistence.
- `connection_screens.py` - all global string variables in this module are interpreted by Evennia as a greeting screen to show when an Account first connects. If more than one string variable is present in the module a random one will be picked.
- `inlinefuncs.py` - this is where you can define custom [FuncParser functions](../Components/FuncParser.md).
- `inputfuncs.py` - this is where you define custom [Input functions](../Components/Inputfuncs.md) to handle data
from the client.
- `lockfuncs.py` - this is one of many possible modules to hold your own "safe" *lock functions* to
make available to Evennia's [Locks](../Components/Locks.md).
- `mssp.py` - this holds meta information about your game. It is used by MUD search engines (which
you often have to register with) in order to display what kind of game you are running along with
statistics such as number of online accounts and online status.
- `inputfuncs.py` - this is where you define custom [Input functions](../Components/Inputfuncs.md) to handle data from the client.
- `lockfuncs.py` - this is one of many possible modules to hold your own "safe" *lock functions* to make available to Evennia's [Locks](../Components/Locks.md).
- `mssp.py` - this holds meta information about your game. It is used by MUD search engines (which you often have to register with) in order to display what kind of game you are running along with statistics such as number of online accounts and online status.
- `oobfuncs.py` - in here you can define custom [OOB functions](../Concepts/OOB.md).
- `portal_services_plugin.py` - this allows for adding your own custom services/protocols to the
Portal. It must define one particular function that will be called by Evennia at startup. There can
be any number of service plugin modules, all will be imported and used if defined. More info can be
found [here](https://code.google.com/p/evennia/wiki/SessionProtocols#Adding_custom_Protocols).
- `server_services_plugin.py` - this is equivalent to the previous one, but used for adding new
services to the Server instead. More info can be found
[here](https://code.google.com/p/evennia/wiki/SessionProtocols#Adding_custom_Protocols).
- `portal_services_plugin.py` - this allows for adding your own custom services/protocols to the Portal. It must define one particular function that will be called by Evennia at startup. There can be any number of service plugin modules, all will be imported and used if defined. More info can be found [here](https://code.google.com/p/evennia/wiki/SessionProtocols#Adding_custom_Protocols).
- `server_services_plugin.py` - this is equivalent to the previous one, but used for adding new services to the Server instead. More info can be found [here](https://code.google.com/p/evennia/wiki/SessionProtocols#Adding_custom_Protocols).
Some other Evennia systems can be customized by plugin modules but has no explicit template in
`conf/`:
Some other Evennia systems can be customized by plugin modules but has no explicit template in `conf/`:
- *cmdparser.py* - a custom module can be used to totally replace Evennia's default command parser. All this does is to split the incoming string into "command name" and "the rest". It also handles things like error messages for no-matches and multiple-matches among other things that makes this more complex than it sounds. The default parser is *very* generic, so you are most often best served by modifying things further down the line (on the command parse level) than here.
- *at_search.py* - this allows for replacing the way Evennia handles search results. It allows to change how errors are echoed and how multi-matches are resolved and reported (like how the default understands that "2-ball" should match the second "ball" object if there are two of them in the room).

View file

@ -374,8 +374,12 @@ def setup(app):
# build toctree file
sys.path.insert(1, os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
from docs.pylib import (auto_link_remapper, contrib_readmes2docs,
update_default_cmd_index, update_dynamic_pages)
from docs.pylib import (
auto_link_remapper,
contrib_readmes2docs,
update_default_cmd_index,
update_dynamic_pages,
)
_no_autodoc = os.environ.get("NOAUTODOC")
update_default_cmd_index.run_update(no_autodoc=_no_autodoc)

View file

@ -1 +1 @@
4.0.0
4.1.1

View file

@ -16,6 +16,7 @@ to launch such a shell (using python or ipython depending on your install).
See www.evennia.com for full documentation.
"""
import evennia
# docstring header

View file

@ -10,6 +10,7 @@ character object, so you should customize that
instead for most things).
"""
import re
import time
import typing
@ -372,6 +373,18 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
session.protocol_flags.get("SCREENREADER") for session in self.sessions.all()
)
def get_extra_display_name_info(self, looker, **kwargs):
"""
Used in .get_display_name() to provide extra information to the looker. We split this
to be consistent with the Object version of this method.
This is used e.g. by the `find` command by default.
"""
if looker and self.locks.check_lockstring(looker, "perm(Admin)"):
return f"(#{self.id})"
return ""
def get_display_name(self, looker, **kwargs):
"""
This is used by channels and other OOC communications methods to give a
@ -1334,7 +1347,8 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
if isinstance(searchdata, str):
# handle wrapping of common terms
if searchdata.lower() in ("me", "*me", "self", "*self"):
return self
return [self] if quiet else self
searchdata = self.nicks.nickreplace(
searchdata, categories=("account",), include_account=False
)

View file

@ -15,6 +15,7 @@ persistently store attributes of its own. This is ideal for extra
account info and OOC account configuration variables etc.
"""
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.db import models

View file

@ -6,7 +6,6 @@ same inputs as the default one.
"""
import re
from django.conf import settings

View file

@ -26,6 +26,7 @@ Set theory.
to affect the low-priority cmdset. Ex: A1,A3 + B1,B2,B4,B5 = B2,B4,B5
"""
from weakref import WeakKeyDictionary
from django.utils.translation import gettext as _

View file

@ -64,6 +64,7 @@ example, you can have a 'On a boat' set, onto which you then tack on
the 'Fishing' set. Fishing from a boat? No problem!
"""
import sys
from importlib import import_module
from inspect import trace

View file

@ -4,6 +4,7 @@ The base Command class.
All commands in Evennia inherit from the 'Command' class in this module.
"""
import inspect
import math
import re
@ -21,7 +22,6 @@ CMD_IGNORE_PREFIXES = settings.CMD_IGNORE_PREFIXES
class InterruptCommand(Exception):
"""Cleanly interrupt a command."""
pass
@ -487,31 +487,29 @@ class Command(metaclass=CommandMeta):
purposes when making commands.
"""
variables = "\n".join(
" |w{}|n ({}): {}".format(key, type(val), val) for key, val in self.__dict__.items()
)
string = f"""
Command {self} has no defined `func()` - showing on-command variables:
{variables}
"""
# a simple test command to show the available properties
string += "-" * 50
string += "\n|w%s|n - Command variables from evennia:\n" % self.key
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.capitalize()
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
# show cmdset.key instead of cmdset to shorten output
string += fill(
"current cmdset (self.cmdset): |w%s|n\n"
% (self.cmdset.key if self.cmdset.key else self.cmdset.__class__)
)
output_string = """
Command \"{cmdname}\" has no defined `func()` method. Available properties on this command are:
self.msg(string)
{variables}"""
variables = [
" |w{}|n ({}): {}".format(key, type(val), f'"{val}"' if isinstance(val, str) else val)
for key, val in (
("self.key", self.key),
("self.cmdname", self.cmdstring),
("self.raw_cmdname", self.raw_cmdname),
("self.raw_string", self.raw_string),
("self.aliases", self.aliases),
("self.args", self.args),
("self.caller", self.caller),
("self.obj", self.obj),
("self.session", self.session),
("self.locks", self.locks),
("self.help_category", self.help_category),
("self.cmdset", self.cmdset),
)
]
output = output_string.format(cmdname=self.key, variables="\n ".join(variables))
self.msg(output)
def func(self):
"""

View file

@ -18,6 +18,7 @@ self.msg() and similar methods to reroute returns to the correct
method. Otherwise all text will be returned to all connected sessions.
"""
import time
from codecs import lookup as codecs_lookup

View file

@ -17,6 +17,7 @@ the Evennia API. It is also a severe security risk and should
therefore always be limited to superusers only.
"""
import re
from django.conf import settings

View file

@ -1,16 +1,17 @@
"""
Building and world design commands
"""
import re
import typing
import evennia
from django.conf import settings
from django.core.paginator import Paginator
from django.db.models import Max, Min, Q
import evennia
from evennia import InterruptCommand
from evennia.commands.cmdhandler import (generate_cmdset_providers,
get_and_merge_cmdsets)
from evennia.commands.cmdhandler import generate_cmdset_providers, get_and_merge_cmdsets
from evennia.locks.lockhandler import LockException
from evennia.objects.models import ObjectDB
from evennia.prototypes import menus as olc_menus
@ -23,10 +24,18 @@ from evennia.utils.dbserialize import deserialize
from evennia.utils.eveditor import EvEditor
from evennia.utils.evmore import EvMore
from evennia.utils.evtable import EvTable
from evennia.utils.utils import (class_from_module, crop, dbref, display_len,
format_grid, get_all_typeclasses,
inherits_from, interactive, list_to_string,
variable_from_module)
from evennia.utils.utils import (
class_from_module,
crop,
dbref,
display_len,
format_grid,
get_all_typeclasses,
inherits_from,
interactive,
list_to_string,
variable_from_module,
)
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
@ -68,7 +77,7 @@ __all__ = (
)
# used by set
from ast import literal_eval as _LITERAL_EVAL
from ast import literal_eval as _LITERAL_EVAL # noqa
LIST_APPEND_CHAR = "+"
@ -218,10 +227,13 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS):
alias <obj> [= [alias[,alias,alias,...]]]
alias <obj> =
alias/category <obj> = [alias[,alias,...]:<category>
alias/delete <obj> = <alias>
Switches:
category - requires ending input with :category, to store the
given aliases with the given category.
delete - deletes all occurrences of the given alias, regardless
of category
Assigns aliases to an object so it can be referenced by more
than one name. Assign empty to remove all aliases from object. If
@ -235,7 +247,7 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS):
key = "@alias"
aliases = "setobjalias"
switch_options = ("category",)
switch_options = ("category", "delete")
locks = "cmd:perm(setobjalias) or perm(Builder)"
help_category = "Building"
@ -252,12 +264,12 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS):
return
objname = self.lhs
# Find the object to receive aliases
# Find the object to receive/delete aliases
obj = caller.search(objname)
if not obj:
return
if self.rhs is None:
# no =, so we just list aliases on object.
if self.rhs is None and "delete" not in self.switches:
# no =, and not deleting, so we just list aliases on object.
aliases = obj.aliases.all(return_key_and_category=True)
if aliases:
caller.msg(
@ -280,7 +292,9 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS):
return
if not self.rhs:
# we have given an empty =, so delete aliases
# we have given an empty =, so delete aliases.
# as a side-effect, 'alias/delete obj' and 'alias/delete obj='
# will also be caught here, which is fine
old_aliases = obj.aliases.all()
if old_aliases:
caller.msg(
@ -292,6 +306,19 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS):
caller.msg("No aliases to clear.")
return
if "delete" in self.switches:
# delete all matching keys, regardless of category
existed = False
for key, category in obj.aliases.all(return_key_and_category=True):
if key == self.rhs:
obj.aliases.remove(key=self.rhs, category=category)
existed = True
if existed:
caller.msg("Alias '%s' deleted from %s." % (self.rhs, obj.get_display_name(caller)))
else:
caller.msg("%s has no alias '%s'." % (obj.get_display_name(caller), self.rhs))
return
category = None
if "category" in self.switches:
if ":" in self.rhs:
@ -1378,7 +1405,7 @@ class CmdSetHome(CmdLink):
obj.home = new_home
if old_home:
string = (
f"Home location of {obj} was changed from {old_home}({old_home.dbref} to"
f"Home location of {obj} was changed from {old_home}({old_home.dbref}) to"
f" {new_home}({new_home.dbref})."
)
else:
@ -2938,9 +2965,9 @@ class CmdExamine(ObjManipCommand):
):
objdata["Stored Cmdset(s)"] = self.format_stored_cmdsets(obj)
objdata["Merged Cmdset(s)"] = self.format_merged_cmdsets(obj, current_cmdset)
objdata[
f"Commands available to {obj.key} (result of Merged Cmdset(s))"
] = self.format_current_cmds(obj, current_cmdset)
objdata[f"Commands available to {obj.key} (result of Merged Cmdset(s))"] = (
self.format_current_cmds(obj, current_cmdset)
)
if self.object_type == "script":
objdata["Description"] = self.format_script_desc(obj)
objdata["Persistent"] = self.format_script_is_persistent(obj)
@ -3255,9 +3282,15 @@ class CmdFind(COMMAND_DEFAULT_CLASS):
string += f"\n |RNo match found for '{searchstring}' in #dbref interval.|n"
else:
result = result[0]
string += f"\n|g {result.get_display_name(caller)} - {result.path}|n"
string += (
f"\n|g {result.get_display_name(caller)}"
f"{result.get_extra_display_name_info(caller)} - {result.path}|n"
)
if "loc" in self.switches and not is_account and result.location:
string += f" (|wlocation|n: |g{result.location.get_display_name(caller)}|n)"
string += (
f" (|wlocation|n: |g{result.location.get_display_name(caller)}"
f"{result.get_extra_display_name_info(caller)}|n)"
)
else:
# Not an account/dbref search but a wider search; build a queryset.
# Searches for key and aliases
@ -3397,9 +3430,11 @@ class ScriptEvMore(EvMore):
table.add_row(
f"#{script.id}",
f"{script.obj.key}({script.obj.dbref})"
if (hasattr(script, "obj") and script.obj)
else "<Global>",
(
f"{script.obj.key}({script.obj.dbref})"
if (hasattr(script, "obj") and script.obj)
else "<Global>"
),
script.key,
script.interval if script.interval > 0 else "--",
nextrep,

View file

@ -4,6 +4,7 @@ available (i.e. IC commands). Note that some commands, such as
communication-commands are instead put on the account level, in the
Account cmdset. Account commands remain available also to Characters.
"""
from evennia.commands.cmdset import CmdSet
from evennia.commands.default import (
admin,

View file

@ -1,6 +1,7 @@
"""
This module stores session-level commands.
"""
from evennia.commands.cmdset import CmdSet
from evennia.commands.default import account

View file

@ -3,6 +3,7 @@ This module describes the unlogged state of the default game.
The setting STATE_UNLOGGED should be set to the python path
of the state instance in this module.
"""
from evennia.commands.cmdset import CmdSet
from evennia.commands.default import unloggedin

View file

@ -8,6 +8,7 @@ Communication commands:
"""
from django.conf import settings
from django.db.models import Q
from evennia.accounts import bots
from evennia.accounts.models import AccountDB
@ -1338,8 +1339,24 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
# get the messages we've sent (not to channels)
pages_we_sent = Msg.objects.get_messages_by_sender(caller).order_by("-db_date_created")
# get only messages tagged as pages or not tagged at all (legacy pages)
pages_we_sent = pages_we_sent.filter(
Q(db_tags__db_key__iexact="page", db_tags__db_category__iexact="comms")
| Q(db_tags__isnull=True)
)
# we need to default to True to allow for legacy pages
pages_we_sent = [msg for msg in pages_we_sent if msg.access(caller, "read", default=True)]
# get last messages we've got
pages_we_got = Msg.objects.get_messages_by_receiver(caller).order_by("-db_date_created")
pages_we_got = pages_we_got.filter(
Q(db_tags__db_key__iexact="page", db_tags__db_category__iexact="comms")
| Q(db_tags__isnull=True)
)
# we need to default to True to allow for legacy pages
pages_we_got = [msg for msg in pages_we_got if msg.access(caller, "read", default=True)]
# get only messages tagged as pages or not tagged at all (legacy pages)
targets, message, number = [], None, None
if "last" in self.switches:
@ -1360,6 +1377,7 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
targets.append(target_obj)
message = self.rhs.strip()
else:
# no = sign, handler this as well
target, *message = self.args.split(" ", 1)
if target and target.isnumeric():
# a number to specify a historic page
@ -1395,7 +1413,17 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
message = f"{caller.key} {message.strip(':').strip()}"
# create the persistent message object
create.create_message(caller, message, receivers=targets)
create.create_message(
caller,
message,
receivers=targets,
locks=(
f"read:id({caller.id}) or perm(Admin);"
f"delete:id({caller.id}) or perm(Admin);"
f"edit:id({caller.id}) or perm(Admin)"
),
tags=[("page", "comms")],
)
# tell the accounts they got a message.
received = []

View file

@ -1,10 +1,12 @@
"""
General Character commands usually available to all characters
"""
import re
import evennia
from django.conf import settings
import evennia
from evennia.typeclasses.attributes import NickTemplateInvalid
from evennia.utils import utils
@ -378,20 +380,57 @@ class CmdInventory(COMMAND_DEFAULT_CLASS):
self.msg(text=(string, {"type": "inventory"}))
class CmdGet(COMMAND_DEFAULT_CLASS):
class NumberedTargetCommand(COMMAND_DEFAULT_CLASS):
"""
A class that parses out an optional number component from the input string. This
class is intended to be inherited from to provide additional functionality, rather
than used on its own.
"""
def parse(self):
"""
Parser that extracts a `.number` property from the beginning of the input string.
For example, if the input string is "3 apples", this parser will set `self.number = 3` and
`self.args = "apples"`. If the input string is "apples", this parser will set
`self.number = 0` and `self.args = "apples"`.
"""
super().parse()
self.number = 0
if getattr(self, "lhs", None):
# handle self.lhs but don't require it
count, *args = self.lhs.split(maxsplit=1)
# we only use the first word as a count if it's a number and
# there is more text afterwards
if args and count.isdecimal():
self.number = int(count)
self.lhs = args[0]
if self.args:
# check for numbering
count, *args = self.args.split(maxsplit=1)
# we only use the first word as a count if it's a number and
# there is more text afterwards
if args and count.isdecimal():
self.args = args[0]
# we only re-assign self.number if it wasn't already taken from self.lhs
if not self.number:
self.number = int(count)
class CmdGet(NumberedTargetCommand):
"""
pick up something
Usage:
get <obj>
Picks up an object from your location and puts it in
your inventory.
Picks up an object from your location and puts it in your inventory.
"""
key = "get"
aliases = "grab"
locks = "cmd:all();view:perm(Developer);read:perm(Developer)"
locks = "cmd:all()"
arg_regex = r"\s|$"
def func(self):
@ -400,36 +439,49 @@ class CmdGet(COMMAND_DEFAULT_CLASS):
caller = self.caller
if not self.args:
caller.msg("Get what?")
self.msg("Get what?")
return
obj = caller.search(self.args, location=caller.location)
if not obj:
objs = caller.search(self.args, location=caller.location, stacked=self.number)
if not objs:
return
if caller == obj:
caller.msg("You can't get yourself.")
return
if not obj.access(caller, "get"):
if obj.db.get_err_msg:
caller.msg(obj.db.get_err_msg)
else:
caller.msg("You can't get that.")
# the 'stacked' search sometimes returns a list, sometimes not, so we make it always a list
# NOTE: this behavior may be a bug, see issue #3432
objs = utils.make_iter(objs)
if len(objs) == 1 and caller == objs[0]:
self.msg("You can't get yourself.")
return
# calling at_pre_get hook method
if not obj.at_pre_get(caller):
return
# if we aren't allowed to get any of the objects, cancel the get
for obj in objs:
# check the locks
if not obj.access(caller, "get"):
if obj.db.get_err_msg:
self.msg(obj.db.get_err_msg)
else:
self.msg("You can't get that.")
return
# calling at_pre_get hook method
if not obj.at_pre_get(caller):
return
success = obj.move_to(caller, quiet=True, move_type="get")
if not success:
caller.msg("This can't be picked up.")
moved = []
# attempt to move all of the objects
for obj in objs:
if obj.move_to(caller, quiet=True, move_type="get"):
moved.append(obj)
# calling at_get hook method
obj.at_get(caller)
if not moved:
# none of the objects were successfully moved
self.msg("That can't be picked up.")
else:
singular, _ = obj.get_numbered_name(1, caller)
caller.location.msg_contents(f"$You() $conj(pick) up {singular}.", from_obj=caller)
# calling at_get hook method
obj.at_get(caller)
obj_name = moved[0].get_numbered_name(len(moved), caller, return_string=True)
caller.location.msg_contents(f"$You() $conj(pick) up {obj_name}.", from_obj=caller)
class CmdDrop(COMMAND_DEFAULT_CLASS):
class CmdDrop(NumberedTargetCommand):
"""
drop something
@ -454,30 +506,42 @@ class CmdDrop(COMMAND_DEFAULT_CLASS):
# Because the DROP command by definition looks for items
# in inventory, call the search function using location = caller
obj = caller.search(
objs = caller.search(
self.args,
location=caller,
nofound_string=f"You aren't carrying {self.args}.",
multimatch_string=f"You carry more than one {self.args}:",
stacked=self.number,
)
if not obj:
if not objs:
return
# the 'stacked' search sometimes returns a list, sometimes not, so we make it always a list
# NOTE: this behavior may be a bug, see issue #3432
objs = utils.make_iter(objs)
# Call the object script's at_pre_drop() method.
if not obj.at_pre_drop(caller):
return
# if any objects fail the drop permission check, cancel the drop
for obj in objs:
# Call the object's at_pre_drop() method.
if not obj.at_pre_drop(caller):
return
success = obj.move_to(caller.location, quiet=True, move_type="drop")
if not success:
caller.msg("This couldn't be dropped.")
# do the actual dropping
moved = []
for obj in objs:
if obj.move_to(caller.location, quiet=True, move_type="drop"):
moved.append(obj)
# Call the object's at_drop() method.
obj.at_drop(caller)
if not moved:
# none of the objects were successfully moved
self.msg("That can't be dropped.")
else:
singular, _ = obj.get_numbered_name(1, caller)
caller.location.msg_contents(f"$You() $conj(drop) {singular}.", from_obj=caller)
# Call the object script's at_drop() method.
obj.at_drop(caller)
obj_name = moved[0].get_numbered_name(len(moved), caller, return_string=True)
caller.location.msg_contents(f"$You() $conj(drop) {obj_name}.", from_obj=caller)
class CmdGive(COMMAND_DEFAULT_CLASS):
class CmdGive(NumberedTargetCommand):
"""
give away something to someone
@ -500,37 +564,50 @@ class CmdGive(COMMAND_DEFAULT_CLASS):
if not self.args or not self.rhs:
caller.msg("Usage: give <inventory object> = <target>")
return
# find the thing(s) to give away
to_give = caller.search(
self.lhs,
location=caller,
nofound_string=f"You aren't carrying {self.lhs}.",
multimatch_string=f"You carry more than one {self.lhs}:",
stacked=self.number,
)
if not to_give:
return
# find the target to give to
target = caller.search(self.rhs)
if not (to_give and target):
if not target:
return
singular, _ = to_give.get_numbered_name(1, caller)
# the 'stacked' search sometimes returns a list, sometimes not, so we make it always a list
# NOTE: this behavior may be a bug, see issue #3432
to_give = utils.make_iter(to_give)
singular, plural = to_give[0].get_numbered_name(len(to_give), caller)
if target == caller:
caller.msg(f"You keep {singular} to yourself.")
return
if not to_give.location == caller:
caller.msg(f"You are not holding {singular}.")
caller.msg(f"You keep {plural if len(to_give) > 1 else singular} to yourself.")
return
# calling at_pre_give hook method
if not to_give.at_pre_give(caller, target):
return
# if any of the objects aren't allowed to be given, cancel the give
for obj in to_give:
# calling at_pre_give hook method
if not obj.at_pre_give(caller, target):
return
# give object
success = to_give.move_to(target, quiet=True, move_type="give")
if not success:
caller.msg(f"You could not give {singular} to {target.key}.")
# do the actual moving
moved = []
for obj in to_give:
if obj.move_to(target, quiet=True, move_type="give"):
moved.append(obj)
# Call the object's at_give() method.
obj.at_give(caller, target)
if not moved:
caller.msg(f"You could not give that to {target.get_display_name(caller)}.")
else:
caller.msg(f"You give {singular} to {target.key}.")
target.msg(f"{caller.key} gives you {singular}.")
# Call the object script's at_give() method.
to_give.at_give(caller, target)
obj_name = to_give[0].get_numbered_name(len(moved), caller, return_string=True)
caller.msg(f"You give {obj_name} to {target.get_display_name(caller)}.")
target.msg(f"{caller.get_display_name(target)} gives you {obj_name}.")
class CmdSetDesc(COMMAND_DEFAULT_CLASS):

View file

@ -780,13 +780,14 @@ class CmdSetHelp(CmdHelp):
Edit the help database.
Usage:
sethelp[/switches] <topic>[[;alias;alias][,category[,locks]] [= <text>]
sethelp[/switches] <topic>[[;alias;alias][,category[,locks]]
[= <text or new category>]
Switches:
edit - open a line editor to edit the topic's help text.
replace - overwrite existing help topic.
append - add text to the end of existing topic with a newline between.
extend - as append, but don't add a newline.
category - change category of existing help topic.
delete - remove help topic.
Examples:
@ -794,6 +795,7 @@ class CmdSetHelp(CmdHelp):
sethelp/append pickpocketing,Thievery = This steals ...
sethelp/replace pickpocketing, ,attr(is_thief) = This steals ...
sethelp/edit thievery
sethelp/category thievery = classes
If not assigning a category, the `settings.DEFAULT_HELP_CATEGORY` category
will be used. If no lockstring is specified, everyone will be able to read
@ -840,7 +842,7 @@ class CmdSetHelp(CmdHelp):
key = "sethelp"
aliases = []
switch_options = ("edit", "replace", "append", "extend", "delete")
switch_options = ("edit", "replace", "append", "extend", "category", "delete")
locks = "cmd:perm(Helper)"
help_category = "Building"
arg_regex = None
@ -857,7 +859,7 @@ class CmdSetHelp(CmdHelp):
if not self.args:
self.msg(
"Usage: sethelp[/switches] <topic>[;alias;alias][,category[,locks,..] = <text>"
"Usage: sethelp[/switches] <topic>[[;alias;alias][,category[,locks]] [= <text or new category>]"
)
return
@ -953,7 +955,7 @@ class CmdSetHelp(CmdHelp):
else:
helpentry = create.create_help_entry(
topicstr,
self.rhs,
self.rhs if self.rhs is not None else "",
category=category,
locks=lockstring,
aliases=aliases,
@ -986,6 +988,19 @@ class CmdSetHelp(CmdHelp):
self.msg(f"Entry updated:\n{old_entry.entrytext}{aliastxt}")
return
if "category" in switches:
# set the category
if not old_entry:
self.msg(f"Could not find topic '{topicstr}'{aliastxt}.")
return
if not self.rhs:
self.msg("You must supply a category.")
return
category = self.rhs.lower()
old_entry.help_category = category
self.msg(f"Category for entry '{topicstr}'{aliastxt} changed to '{category}'.")
return
if "delete" in switches or "del" in switches:
# delete the help entry
if not old_entry:

View file

@ -4,7 +4,6 @@ System commands
"""
import code
import datetime
import os
@ -112,7 +111,6 @@ class CmdReset(COMMAND_DEFAULT_CLASS):
class CmdShutdown(COMMAND_DEFAULT_CLASS):
"""
stop the server completely
@ -277,7 +275,6 @@ def evennia_local_vars(caller):
class EvenniaPythonConsole(code.InteractiveConsole):
"""Evennia wrapper around a Python interactive console."""
def __init__(self, caller):

View file

@ -14,10 +14,13 @@ main test suite started with
import datetime
from unittest.mock import MagicMock, Mock, patch
import evennia
from anything import Anything
from django.conf import settings
from django.test import override_settings
from parameterized import parameterized
from twisted.internet import task
import evennia
from evennia import (
DefaultCharacter,
DefaultExit,
@ -29,7 +32,14 @@ from evennia import (
from evennia.commands import cmdparser
from evennia.commands.cmdset import CmdSet
from evennia.commands.command import Command, InterruptCommand
from evennia.commands.default import account, admin, batchprocess, building, comms, general
from evennia.commands.default import (
account,
admin,
batchprocess,
building,
comms,
general,
)
from evennia.commands.default import help as help_module
from evennia.commands.default import syscommands, system, unloggedin
from evennia.commands.default.cmdset_character import CharacterCmdSet
@ -38,8 +48,6 @@ from evennia.prototypes import prototypes as protlib
from evennia.utils import create, gametime, utils
from evennia.utils.test_resources import BaseEvenniaCommandTest # noqa
from evennia.utils.test_resources import BaseEvenniaTest, EvenniaCommandTest
from parameterized import parameterized
from twisted.internet import task
# ------------------------------------------------------------
# Command testing
@ -108,8 +116,12 @@ class TestGeneral(BaseEvenniaCommandTest):
self.call(general.CmdNick(), "/list", "Defined Nicks:")
def test_get_and_drop(self):
self.call(general.CmdGet(), "Obj", "You pick up an Obj")
self.call(general.CmdDrop(), "Obj", "You drop an Obj")
self.call(general.CmdGet(), "Obj", "You pick up an Obj.")
self.call(general.CmdDrop(), "Obj", "You drop an Obj.")
# test stacking
self.obj2.key = "Obj"
self.call(general.CmdGet(), "2 Obj", "You pick up two Objs.")
self.call(general.CmdDrop(), "2 Obj", "You drop two Objs.")
def test_give(self):
self.call(general.CmdGive(), "Obj to Char2", "You aren't carrying Obj.")
@ -117,6 +129,21 @@ class TestGeneral(BaseEvenniaCommandTest):
self.call(general.CmdGet(), "Obj", "You pick up an Obj")
self.call(general.CmdGive(), "Obj to Char2", "You give")
self.call(general.CmdGive(), "Obj = Char", "You give", caller=self.char2)
# test stacking
self.obj2.key = "Obj"
self.obj2.location = self.char1
self.call(general.CmdGive(), "2 Obj = Char2", "You give two Objs")
def test_numbered_target_command(self):
class CmdTest(general.NumberedTargetCommand):
key = "test"
def func(self):
self.msg(f"Number: {self.number} Args: {self.args}")
self.call(CmdTest(), "", "Number: 0 Args: ")
self.call(CmdTest(), "obj", "Number: 0 Args: obj")
self.call(CmdTest(), "1 obj", "Number: 1 Args: obj")
def test_mux_command(self):
class CmdTest(MuxCommand):
@ -197,6 +224,12 @@ class TestHelp(BaseEvenniaCommandTest):
cmdset=CharacterCmdSet(),
)
self.call(help_module.CmdHelp(), "testhelp", "Help for testhelp", cmdset=CharacterCmdSet())
self.call(
help_module.CmdSetHelp(),
"/category testhelp = misc",
"Category for entry 'testhelp' changed to 'misc'.",
cmdset=CharacterCmdSet(),
)
@parameterized.expand(
[
@ -784,6 +817,24 @@ class TestBuilding(BaseEvenniaCommandTest):
self.call(building.CmdSetObjAlias(), "Obj2 =", "Cleared aliases from Obj2")
self.call(building.CmdSetObjAlias(), "Obj2 =", "No aliases to clear.")
self.call(building.CmdSetObjAlias(), "Obj =", "Cleared aliases from Obj: testobj1b")
self.call(
building.CmdSetObjAlias(),
"/category Obj = testobj1b:category1",
"Alias(es) for 'Obj' set to 'testobj1b' (category: 'category1').",
)
self.call(
building.CmdSetObjAlias(),
"/category Obj = testobj1b:category2",
"Alias(es) for 'Obj' set to 'testobj1b,testobj1b' (category: 'category2').",
)
self.call(
building.CmdSetObjAlias(), # delete both occurences of alias 'testobj1b'
"/delete Obj = testobj1b",
"Alias 'testobj1b' deleted from Obj.",
)
self.call(building.CmdSetObjAlias(), "Obj =", "No aliases to clear.")
def test_copy(self):
self.call(
building.CmdCopy(),
@ -1754,8 +1805,7 @@ class TestBuilding(BaseEvenniaCommandTest):
self.call(
building.CmdSpawn(),
"{'prototype_key':'GOBLIN', 'typeclass':'evennia.objects.objects.DefaultCharacter', "
"'key':'goblin', 'location':'%s'}"
% spawnLoc.dbref,
"'key':'goblin', 'location':'%s'}" % spawnLoc.dbref,
"Spawned goblin",
)
goblin = get_object(self, "goblin")
@ -1803,8 +1853,7 @@ class TestBuilding(BaseEvenniaCommandTest):
self.call(
building.CmdSpawn(),
"/noloc {'prototype_parent':'TESTBALL', 'key': 'Ball', 'prototype_key': 'foo',"
" 'location':'%s'}"
% spawnLoc.dbref,
" 'location':'%s'}" % spawnLoc.dbref,
"Spawned Ball",
)
ball = get_object(self, "Ball")
@ -2048,6 +2097,11 @@ class TestComms(BaseEvenniaCommandTest):
),
receiver=self.account,
)
from evennia.comms.models import Msg
msgs = Msg.objects.filter(db_tags__db_key="page", db_tags__db_category="comms")
self.assertEqual(msgs[0].senders, [self.account])
self.assertEqual(msgs[0].receivers, [self.account2])
@override_settings(DISCORD_BOT_TOKEN="notarealtoken", DISCORD_ENABLED=True)

View file

@ -2,6 +2,7 @@
Commands that are available from the connect screen.
"""
import datetime
import re
from codecs import lookup as codecs_lookup

View file

@ -2,6 +2,7 @@
Base typeclass for in-game Channels.
"""
import re
from django.contrib.contenttypes.models import ContentType

View file

@ -4,7 +4,6 @@ Comm system components.
"""
from django.conf import settings
from django.db.models import Q

View file

@ -18,6 +18,7 @@ connect to channels by use of a ChannelConnect object (this object is
necessary to easily be able to delete connections on the fly).
"""
from django.conf import settings
from django.db import models
from django.utils import timezone

View file

@ -221,7 +221,6 @@ def get_available_overwrite_name(name, max_length):
@deconstructible
class S3Boto3StorageFile(File):
"""
The default file object used by the S3Boto3Storage backend.
This file implements file streaming using boto's multipart

View file

@ -2,5 +2,6 @@
Build-menu contrib - vincent-lg 2018
"""
from .building_menu import BuildingMenu # noqa
from .building_menu import GenericBuildingCmd # noqa

View file

@ -331,7 +331,6 @@ def menu_edit(caller, choice, obj):
class CmdNoInput(Command):
"""No input has been found."""
key = _CMD_NOINPUT
@ -352,7 +351,6 @@ class CmdNoInput(Command):
class CmdNoMatch(Command):
"""No input has been found."""
key = _CMD_NOMATCH
@ -394,7 +392,6 @@ class CmdNoMatch(Command):
class BuildingMenuCmdSet(CmdSet):
"""Building menu CmdSet."""
key = "building_menu"
@ -421,7 +418,6 @@ class BuildingMenuCmdSet(CmdSet):
class Choice:
"""A choice object, created by `add_choice`."""
def __init__(
@ -557,7 +553,6 @@ class Choice:
class BuildingMenu:
"""
Class allowing to create and set building menus to edit specific objects.
@ -1200,7 +1195,6 @@ class BuildingMenu:
# Generic building menu and command
class GenericBuildingMenu(BuildingMenu):
"""A generic building menu, allowing to edit any object.
This is more a demonstration menu. By default, it allows to edit the
@ -1241,7 +1235,6 @@ class GenericBuildingMenu(BuildingMenu):
class GenericBuildingCmd(Command):
"""
Generic building command.

View file

@ -7,6 +7,7 @@ This helps writing isolated code and reusing it over multiple objects.
See the docs for more information.
"""
from . import exceptions # noqa
from .component import Component # noqa
from .dbfield import DBField, NDBField, TagField # noqa

View file

@ -155,6 +155,22 @@ class Component(metaclass=BaseComponent):
"""
return self.host.attributes
@property
def pk(self):
"""
Shortcut property returning the host's primary key.
Returns:
int: The Host's primary key.
Notes:
This is requried to allow AttributeProperties to correctly update `_SaverMutable` data
(like lists) in-place (since the DBField sits on the Component which doesn't itself
have a primary key, this save operation would otherwise fail).
"""
return self.host.pk
@property
def nattributes(self):
"""

View file

@ -3,9 +3,11 @@ Components - ChrisLR 2022
This file contains the Descriptors used to set Fields in Components
"""
import typing
from evennia.typeclasses.attributes import AttributeProperty, NAttributeProperty
from evennia.typeclasses.attributes import (AttributeProperty,
NAttributeProperty)
if typing.TYPE_CHECKING:
from .components import Component

View file

@ -268,7 +268,7 @@ class TestComponents(EvenniaTest):
def test_mutables_are_not_shared_when_autocreate(self):
self.char1.test_a.my_list.append(1)
self.assertNotEqual(self.char1.test_a.my_list, self.char2.test_a.my_list)
self.assertNotEqual(id(self.char1.test_a.my_list), id(self.char2.test_a.my_list))
def test_replacing_class_component_slot_with_runtime_component(self):
self.char1.components.add_default("replacement_inherited_test_a")

View file

@ -309,7 +309,6 @@ def schedule(callback, repeat=False, **kwargs):
class GametimeScript(DefaultScript):
"""Gametime-sensitive script."""
def at_script_creation(self):

View file

@ -10,6 +10,7 @@ You could also pass extra data to this client for advanced functionality.
See the docs for more information.
"""
from evennia.contrib.base_systems.godotwebsocket.text2bbcode import (
BBCODE_PARSER,
parse_to_bbcode,

View file

@ -3,6 +3,7 @@ Godot Websocket - ChrisLR 2022
This file contains the necessary code and data to convert text with color tags to bbcode (For godot)
"""
from evennia.utils.ansi import *
from evennia.utils.text2html import TextToHTMLparser

View file

@ -4,6 +4,7 @@ Godot Websocket - ChrisLR 2022
This file contains the code necessary to dedicate a port to communicate with Godot via Websockets.
It uses the plugin system and should be plugged via settings as detailed in the readme.
"""
import json
from autobahn.twisted import WebSocketServerFactory

View file

@ -6,7 +6,6 @@ from collections import namedtuple
class CallbackHandler(object):
"""
The callback handler for a specific object.

View file

@ -70,7 +70,6 @@ Use the /del switch to remove callbacks that should not be connected.
class CmdCallback(COMMAND_DEFAULT_CLASS):
"""
Command to edit callbacks.
"""

View file

@ -27,7 +27,6 @@ RE_LINE_ERROR = re.compile(r'^ File "\<string\>", line (\d+)')
class EventHandler(DefaultScript):
"""
The event handler that contains all events in a global script.
@ -600,7 +599,6 @@ class EventHandler(DefaultScript):
# Script to call time-related events
class TimeEventScript(DefaultScript):
"""Gametime-sensitive script."""
def at_script_creation(self):

View file

@ -25,7 +25,6 @@ OLD_EVENTS = {}
class TestEventHandler(BaseEvenniaTest):
"""
Test cases of the event handler to add, edit or delete events.
"""
@ -259,7 +258,6 @@ class TestEventHandler(BaseEvenniaTest):
class TestCmdCallback(BaseEvenniaCommandTest):
"""Test the @callback command."""
def setUp(self):
@ -448,7 +446,6 @@ class TestCmdCallback(BaseEvenniaCommandTest):
class TestDefaultCallbacks(BaseEvenniaCommandTest):
"""Test the default callbacks."""
def setUp(self):

View file

@ -166,7 +166,6 @@ Variables you can use in this event:
@register_events
class EventCharacter(DefaultCharacter):
"""Typeclass to represent a character and call event types."""
_events = {
@ -625,7 +624,6 @@ Variables you can use in this event:
@register_events
class EventExit(DefaultExit):
"""Modified exit including management of events."""
_events = {
@ -721,7 +719,6 @@ Variables you can use in this event:
@register_events
class EventObject(DefaultObject):
"""Default object with management of events."""
_events = {
@ -892,7 +889,6 @@ Variables you can use in this event:
@register_events
class EventRoom(DefaultRoom):
"""Default room with management of events."""
_events = {

View file

@ -251,7 +251,6 @@ def phrase_event(callbacks, parameters):
class InterruptEvent(RuntimeError):
"""
Interrupt the current event.

View file

@ -90,7 +90,7 @@ def node_enter_username(caller, raw_text, **kwargs):
else:
new_user = False
if new_user and not settings.ACCOUNT_REGISTRATION_ENABLED:
if new_user and not settings.NEW_ACCOUNT_REGISTRATION_ENABLED:
caller.msg("Registration is currently disabled.")
return None

View file

@ -41,6 +41,7 @@ class CharacterCmdSet(default_cmds.CharacterCmdSet):
```
"""
from django.conf import settings
from evennia.commands.cmdset import CmdSet

View file

@ -9,7 +9,6 @@ from .unixcommand import UnixCommand
class CmdDummy(UnixCommand):
"""A dummy UnixCommand."""
key = "dummy"

View file

@ -72,14 +72,12 @@ from evennia.utils.ansi import raw
class ParseError(Exception):
"""An error occurred during parsing."""
pass
class UnixCommandParser(argparse.ArgumentParser):
"""A modifier command parser for unix commands.
This parser is used to replace `argparse.ArgumentParser`. It
@ -183,7 +181,6 @@ class UnixCommandParser(argparse.ArgumentParser):
class HelpAction(argparse.Action):
"""Override the -h/--help action in the default parser.
Using the default -h/--help will call the exit function in different

View file

@ -7,6 +7,7 @@ Here player user can set their own description as well as select to create a
new room (to start from scratch) or join an existing room (with other players).
"""
from evennia import EvMenu
from evennia.utils import create, justify, list_to_string, logger
from evennia.utils.evmenu import list_node

View file

@ -43,6 +43,7 @@ Available parents:
- Positionable (supports sit/lie/knee/climb at once)
"""
import inspect
import re

View file

@ -2,6 +2,7 @@
Unit tests for the Evscaperoom
"""
import inspect
import pkgutil
from os import path

View file

@ -72,9 +72,11 @@ with which to test the system:
wear shirt
"""
from collections import defaultdict
from django.conf import settings
from evennia import DefaultCharacter, DefaultObject, default_cmds
from evennia.commands.default.muxcommand import MuxCommand
from evennia.utils import (

View file

@ -30,6 +30,7 @@ or implement the same locks/hooks in your own typeclasses.
at_pre_get_from(getter, target, **kwargs) - called with the pre-get hooks
at_pre_put_in(putter, target, **kwargs) - called with the pre-put hooks
"""
from django.conf import settings
from evennia import AttributeProperty, CmdSet, DefaultObject

View file

@ -237,7 +237,7 @@ class CraftingRecipeBase:
**kwargs: Any optional properties relevant to this send.
"""
self.crafter.msg(message, {"type": "crafting"})
self.crafter.msg(text=(message, {"type": "crafting"}))
def pre_craft(self, **kwargs):
"""
@ -615,9 +615,11 @@ class CraftingRecipe(CraftingRecipeBase):
)
else:
self.output_names = [
prot.get("key", prot.get("typeclass", "unnamed"))
if isinstance(prot, dict)
else str(prot)
(
prot.get("key", prot.get("typeclass", "unnamed"))
if isinstance(prot, dict)
else str(prot)
)
for prot in self.output_prototypes
]

View file

@ -78,7 +78,7 @@ class TestCraftingRecipeBase(BaseEvenniaTestCase):
"""Test messaging to crafter"""
self.recipe.msg("message")
self.crafter.msg.assert_called_with("message", {"type": "crafting"})
self.crafter.msg.assert_called_with(text=("message", {"type": "crafting"}))
def test_pre_craft(self):
"""Test validating hook"""
@ -206,7 +206,7 @@ class TestCraftingRecipe(BaseEvenniaTestCase):
self.assertEqual(result[0].key, "Result1")
self.assertEqual(result[0].tags.all(), ["result1", "resultprot"])
self.crafter.msg.assert_called_with(
recipe.success_message.format(outputs="Result1"), {"type": "crafting"}
text=(recipe.success_message.format(outputs="Result1"), {"type": "crafting"})
)
# make sure consumables are gone
@ -235,7 +235,7 @@ class TestCraftingRecipe(BaseEvenniaTestCase):
self.assertEqual(result[0].key, "Result1")
self.assertEqual(result[0].tags.all(), ["result1", "resultprot"])
self.crafter.msg.assert_called_with(
recipe.success_message.format(outputs="Result1"), {"type": "crafting"}
text=(recipe.success_message.format(outputs="Result1"), {"type": "crafting"})
)
# make sure consumables are gone
@ -251,8 +251,10 @@ class TestCraftingRecipe(BaseEvenniaTestCase):
result = recipe.craft()
self.assertFalse(result)
self.crafter.msg.assert_called_with(
recipe.error_tool_missing_message.format(outputs="Result1", missing="tool2"),
{"type": "crafting"},
text=(
recipe.error_tool_missing_message.format(outputs="Result1", missing="tool2"),
{"type": "crafting"},
)
)
# make sure consumables are still there
@ -269,8 +271,10 @@ class TestCraftingRecipe(BaseEvenniaTestCase):
result = recipe.craft()
self.assertFalse(result)
self.crafter.msg.assert_called_with(
recipe.error_consumable_missing_message.format(outputs="Result1", missing="cons3"),
{"type": "crafting"},
text=(
recipe.error_consumable_missing_message.format(outputs="Result1", missing="cons3"),
{"type": "crafting"},
)
)
# make sure consumables are still there
@ -293,8 +297,10 @@ class TestCraftingRecipe(BaseEvenniaTestCase):
self.assertFalse(result)
self.crafter.msg.assert_called_with(
recipe.error_consumable_missing_message.format(outputs="Result1", missing="cons3"),
{"type": "crafting"},
text=(
recipe.error_consumable_missing_message.format(outputs="Result1", missing="cons3"),
{"type": "crafting"},
)
)
# make sure consumables are deleted even though we failed
@ -317,10 +323,12 @@ class TestCraftingRecipe(BaseEvenniaTestCase):
result = recipe.craft()
self.assertFalse(result)
self.crafter.msg.assert_called_with(
recipe.error_tool_excess_message.format(
outputs="Result1", excess=wrong.get_display_name(looker=self.crafter)
),
{"type": "crafting"},
text=(
recipe.error_tool_excess_message.format(
outputs="Result1", excess=wrong.get_display_name(looker=self.crafter)
),
{"type": "crafting"},
)
)
# make sure consumables are still there
self.assertIsNotNone(self.cons1.pk)
@ -342,10 +350,12 @@ class TestCraftingRecipe(BaseEvenniaTestCase):
result = recipe.craft()
self.assertFalse(result)
self.crafter.msg.assert_called_with(
recipe.error_tool_excess_message.format(
outputs="Result1", excess=tool3.get_display_name(looker=self.crafter)
),
{"type": "crafting"},
text=(
recipe.error_tool_excess_message.format(
outputs="Result1", excess=tool3.get_display_name(looker=self.crafter)
),
{"type": "crafting"},
)
)
# make sure consumables are still there
@ -369,10 +379,12 @@ class TestCraftingRecipe(BaseEvenniaTestCase):
result = recipe.craft()
self.assertFalse(result)
self.crafter.msg.assert_called_with(
recipe.error_consumable_excess_message.format(
outputs="Result1", excess=cons4.get_display_name(looker=self.crafter)
),
{"type": "crafting"},
text=(
recipe.error_consumable_excess_message.format(
outputs="Result1", excess=cons4.get_display_name(looker=self.crafter)
),
{"type": "crafting"},
)
)
# make sure consumables are still there
@ -396,7 +408,7 @@ class TestCraftingRecipe(BaseEvenniaTestCase):
result = recipe.craft()
self.assertTrue(result)
self.crafter.msg.assert_called_with(
recipe.success_message.format(outputs="Result1"), {"type": "crafting"}
text=(recipe.success_message.format(outputs="Result1"), {"type": "crafting"})
)
# make sure consumables are gone
@ -419,7 +431,7 @@ class TestCraftingRecipe(BaseEvenniaTestCase):
result = recipe.craft()
self.assertTrue(result)
self.crafter.msg.assert_called_with(
recipe.success_message.format(outputs="Result1"), {"type": "crafting"}
text=(recipe.success_message.format(outputs="Result1"), {"type": "crafting"})
)
# make sure consumables are gone
@ -439,10 +451,12 @@ class TestCraftingRecipe(BaseEvenniaTestCase):
result = recipe.craft()
self.assertFalse(result)
self.crafter.msg.assert_called_with(
recipe.error_tool_order_message.format(
outputs="Result1", missing=self.tool2.get_display_name(looker=self.crafter)
),
{"type": "crafting"},
text=(
recipe.error_tool_order_message.format(
outputs="Result1", missing=self.tool2.get_display_name(looker=self.crafter)
),
{"type": "crafting"},
)
)
# make sure consumables are still there
@ -462,10 +476,12 @@ class TestCraftingRecipe(BaseEvenniaTestCase):
result = recipe.craft()
self.assertFalse(result)
self.crafter.msg.assert_called_with(
recipe.error_consumable_order_message.format(
outputs="Result1", missing=self.cons3.get_display_name(looker=self.crafter)
),
{"type": "crafting"},
text=(
recipe.error_consumable_order_message.format(
outputs="Result1", missing=self.cons3.get_display_name(looker=self.crafter)
),
{"type": "crafting"},
)
)
# make sure consumables are still there

View file

@ -3,7 +3,6 @@ Test gendersub contrib.
"""
from mock import patch
from evennia.commands.default.tests import BaseEvenniaCommandTest

View file

@ -25,6 +25,7 @@ Reload the server and you should have the +desc command available (it
will replace the default `desc` command).
"""
import re
from evennia import default_cmds

View file

@ -823,7 +823,6 @@ class CmdExtendedRoomDesc(default_cmds.CmdDesc):
class CmdExtendedRoomDetail(default_cmds.MuxCommand):
"""
sets a detail on a room

View file

@ -6,11 +6,12 @@ Testing of ExtendedRoom contrib
import datetime
from django.conf import settings
from evennia import create_object
from evennia.utils.test_resources import BaseEvenniaCommandTest, EvenniaTestCase
from mock import Mock, patch
from parameterized import parameterized
from evennia import create_object
from evennia.utils.test_resources import BaseEvenniaCommandTest, EvenniaTestCase
from . import extended_room

View file

@ -56,6 +56,7 @@ This changes the default map width/height. 2-5 for most clients is sensible.
If you don't want the player to be able to specify the size of the map, ignore any
arguments passed into the Map command.
"""
import time
from django.conf import settings

View file

@ -3,7 +3,6 @@ Tests of ingame_map_display.
"""
from typeclasses import exits, rooms
from evennia.commands.default.tests import BaseEvenniaCommandTest

View file

@ -3,7 +3,6 @@ Tests of simpledoor.
"""
from evennia.commands.default.tests import BaseEvenniaCommandTest
from . import simpledoor

View file

@ -2,6 +2,7 @@
XYZGrid - Griatch 2021
"""
from . import (
example,
launchcmd,

View file

@ -14,6 +14,7 @@ and/or
{'prototype_parent': 'xyz_exit', ...}
"""
from django.conf import settings
try:

View file

@ -3,14 +3,15 @@
Tests for the XYZgrid system.
"""
from random import randint
from unittest import mock
from django.test import TestCase
from evennia.utils.test_resources import (BaseEvenniaCommandTest,
BaseEvenniaTest)
from parameterized import parameterized
from evennia.utils.test_resources import BaseEvenniaCommandTest, BaseEvenniaTest
from . import commands, xymap, xymap_legend, xyzgrid, xyzroom
MAP1 = """

View file

@ -92,6 +92,7 @@ See `./example.py` for a full grid example.
----
"""
import pickle
from collections import defaultdict
from os import mkdir
@ -108,6 +109,7 @@ except ImportError as err:
"the SciPy package. Install with `pip install scipy'."
)
from django.conf import settings
from evennia.prototypes import prototypes as protlib
from evennia.prototypes.spawner import flatten_prototype
from evennia.utils import logger
@ -172,6 +174,7 @@ class XYMap:
but recommended for readability!
"""
mapcorner_symbol = "+"
max_pathfinding_length = 500
empty_symbol = " "
@ -475,10 +478,10 @@ class XYMap:
max_X, max_Y = max(max_X, iX), max(max_Y, iY)
node_index += 1
xygrid[ix][iy] = XYgrid[iX][iY] = node_index_map[
node_index
] = mapnode_or_link_class(
x=ix, y=iy, Z=self.Z, node_index=node_index, symbol=char, xymap=self
xygrid[ix][iy] = XYgrid[iX][iY] = node_index_map[node_index] = (
mapnode_or_link_class(
x=ix, y=iy, Z=self.Z, node_index=node_index, symbol=char, xymap=self
)
)
else:
@ -668,8 +671,7 @@ class XYMap:
"""
global _XYZROOMCLASS
if not _XYZROOMCLASS:
from evennia.contrib.grid.xyzgrid.xyzroom import \
XYZRoom as _XYZROOMCLASS
from evennia.contrib.grid.xyzgrid.xyzroom import XYZRoom as _XYZROOMCLASS
x, y = xy
wildcard = "*"
spawned = []

View file

@ -20,11 +20,11 @@ import uuid
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 (BIGVAL, MAPSCAN, REVERSE_DIRECTIONS, MapError,
MapParserError)
from .utils import BIGVAL, MAPSCAN, REVERSE_DIRECTIONS, MapError, MapParserError
NodeTypeclass = None
ExitTypeclass = None
@ -327,6 +327,13 @@ class MapNode:
nodeobj, err = Typeclass.create(self.prototype.get("key", "An empty room"), xyz=xyz)
if err:
raise RuntimeError(err)
except django_exceptions.MultipleObjectsReturned:
raise MapError(
f"Multiple objects found: {NodeTypeclass.objects.filter_xyz(xyz=xyz)}. "
"This may be due to manual creation of XYZRooms at this position. "
"Delete duplicates.",
self,
)
else:
self.log(f" updating existing room (if changed) at xyz={xyz}")
@ -844,6 +851,7 @@ class SmartRerouterMapLink(MapLink):
/|
"""
multilink = True
def get_direction(self, start_direction):

View file

@ -16,6 +16,7 @@ The grid has three main functions:
"""
from evennia.scripts.scripts import DefaultScript
from evennia.utils import logger
from evennia.utils.utils import variable_from_module

View file

@ -283,7 +283,7 @@ class XYZRoom(DefaultRoom):
def __repr__(self):
x, y, z = self.xyz
return f"<XYZRoom '{self.db_key}', XYZ=({x},{y},{z})>"
return f"<{self.__class__.__name__} '{self.db_key}', XYZ=({x},{y},{z})>"
@property
def xyz(self):

View file

@ -1,4 +1,3 @@
from .buff import (BaseBuff, BuffableProperty, BuffHandler, CmdBuff, # noqa
Mod, cleanup_buffs, tick_buff)
from .samplebuffs import (Exploit, Exploited, Leeching, Poison, Sated, # noqa
StatBuff)
from .buff import CmdBuff # noqa
from .buff import BaseBuff, BuffableProperty, BuffHandler, Mod, cleanup_buffs, tick_buff
from .samplebuffs import Exploit, Exploited, Leeching, Poison, Sated, StatBuff # noqa

View file

@ -98,6 +98,7 @@ You can see all the features of the `BaseBuff` class below, or browse `samplebuf
many attributes and hook methods you can overload to create complex, interrelated buffs.
"""
import time
from random import random

View file

@ -1,6 +1,7 @@
"""
Tests for the buff system contrib
"""
from unittest.mock import Mock, call, patch
from evennia import DefaultObject, create_object

View file

@ -15,6 +15,7 @@ and examples, including how to allow players to choose and confirm
character names from within the menu.
"""
import string
from random import choices

View file

@ -57,6 +57,7 @@ of the roll separately:
"""
import re
from ast import literal_eval
from random import randint

View file

@ -137,6 +137,7 @@ This allows to quickly build a large corpus of translated words
that never change (if this is desired).
"""
import re
from collections import defaultdict
from random import choice, randint

View file

@ -148,19 +148,25 @@ Extra Installation Instructions:
`type/reset/force me = typeclasses.characters.Character`
"""
import re
from collections import defaultdict
from string import punctuation
import inflect
from django.conf import settings
from evennia.commands.cmdset import CmdSet
from evennia.commands.command import Command
from evennia.objects.models import ObjectDB
from evennia.objects.objects import DefaultCharacter, DefaultObject
from evennia.utils import ansi, logger
from evennia.utils.utils import (iter_to_str, lazy_property, make_iter,
variable_from_module)
from evennia.utils.utils import (
iter_to_str,
lazy_property,
make_iter,
variable_from_module,
)
_INFLECT = inflect.engine()
@ -1277,6 +1283,8 @@ class RPSystemCmdSet(CmdSet):
Mix-in for adding rp-commands to default cmdset.
"""
key = "rpsystem_cmdset"
def at_cmdset_creation(self):
self.add(CmdEmote())
self.add(CmdSay())

View file

@ -2,10 +2,13 @@
Tests for RP system
"""
import time
from anything import Anything
from evennia import DefaultObject, create_object, default_cmds
from evennia.commands.default import building
from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.utils.test_resources import BaseEvenniaTest
@ -413,10 +416,9 @@ class TestRPSystemCommands(BaseEvenniaCommandTest):
expected_first_call = [
"More than one match for 'Mushroom' (please narrow target):",
f" Mushroom-1 []",
f" Mushroom-2 []",
f" Mushroom-1",
f" Mushroom-2",
]
self.call(default_cmds.CmdLook(), "Mushroom", "\n".join(expected_first_call)) # PASSES
expected_second_call = f"Mushroom(#{mushroom1.id})\nThe first mushroom is brown."
@ -424,3 +426,13 @@ class TestRPSystemCommands(BaseEvenniaCommandTest):
expected_third_call = f"Mushroom(#{mushroom2.id})\nThe second mushroom is red."
self.call(default_cmds.CmdLook(), "Mushroom-2", expected_third_call) # FAILS
expected_fourth_call = "Alias(es) for 'Mushroom' set to 'fungus'."
self.call(building.CmdSetObjAlias(), "Mushroom-1 = fungus", expected_fourth_call) # PASSES
expected_fifth_call = [
"More than one match for 'Mushroom' (please narrow target):",
f" Mushroom-1 [fungus]",
f" Mushroom-2",
]
self.call(default_cmds.CmdLook(), "Mushroom", "\n".join(expected_fifth_call)) # PASSES

View file

@ -452,7 +452,6 @@ class Character(DefaultCharacter):
"""
from functools import total_ordering
from time import time

View file

@ -13,6 +13,7 @@ The script will only send messages to the object it is stored on, so
make sure to put it on yourself or you won't see any messages!
"""
import random
from evennia import DefaultScript

View file

@ -2,6 +2,7 @@
Tests for the bodyfunctions.
"""
from mock import Mock, patch
from evennia.utils.test_resources import BaseEvenniaTest

View file

@ -49,9 +49,9 @@ class AIHandler:
def __init__(self, obj):
self.obj = obj
self.ai_state = obj.attributes.get(self.attribute_name,
category=self.attribute_category,
default="idle")
self.ai_state = obj.attributes.get(
self.attribute_name, category=self.attribute_category, default="idle"
)
def set_state(self, state):
self.ai_state = state
@ -122,6 +122,7 @@ class AIMixin:
of multiple inheritance. In a real game, you would probably want to use a mixin like this.
"""
@lazy_property
def ai(self):
return AIHandler(self)

View file

@ -2,6 +2,7 @@
EvAdventure character generation.
"""
from django.conf import settings
from evennia.objects.models import ObjectDB

View file

@ -18,7 +18,6 @@ action that takes several vulnerable turns to complete.
"""
import random
from collections import defaultdict
@ -843,5 +842,7 @@ class TurnCombatCmdSet(CmdSet):
CmdSet for the turn-based combat.
"""
key = "turncombat_cmdset"
def at_cmdset_creation(self):
self.add(CmdTurnAttack())

View file

@ -6,6 +6,7 @@ This implements a 'twitch' (aka DIKU or other traditional muds) style of MUD com
----
"""
from evennia import AttributeProperty, CmdSet, default_cmds
from evennia.commands.command import Command, InterruptCommand
from evennia.utils.utils import (
@ -560,6 +561,8 @@ class TwitchCombatCmdSet(CmdSet):
Add to character, to be able to attack others in a twitch-style way.
"""
key = "twitch_combat_cmdset"
def at_cmdset_creation(self):
self.add(CmdAttack())
self.add(CmdHold())
@ -573,5 +576,7 @@ class TwitchLookCmdSet(CmdSet):
This will be added/removed dynamically when in combat.
"""
key = "twitch_look_cmdset"
def at_cmdset_creation(self):
self.add(CmdLook())

View file

@ -105,10 +105,10 @@ class EvAdventureDungeonRoom(EvAdventureRoom):
"""
self.tags.add("not_clear", category="dungeon_room")
def clear_room(self):
self.tags.remove("not_clear", category="dungeon_room")
@property
def is_room_clear(self):
return not bool(self.tags.get("not_clear", category="dungeon_room"))
@ -146,9 +146,7 @@ class EvAdventureDungeonExit(DefaultExit):
dungeon_branch = self.location.db.dungeon_branch
if target_location == self.location:
# destination points back to us - create a new room
self.destination = target_location = dungeon_branch.new_room(
self
)
self.destination = target_location = dungeon_branch.new_room(self)
dungeon_branch.register_exit_traversed(self)
super().at_traverse(traversing_object, target_location, **kwargs)

Some files were not shown because too many files have changed in this diff Show more