mirror of
https://github.com/evennia/evennia.git
synced 2026-04-05 23:47:16 +02:00
Merge branch 'evennia:main' into is_typing
This commit is contained in:
commit
f5a26abf77
98 changed files with 2375 additions and 505 deletions
|
|
@ -33,7 +33,7 @@ jobs:
|
|||
COVERAGE_TEST_SETTINGS: "--settings=settings --timing"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up database (${{ matrix.TESTING_DB }})
|
||||
uses: ./.github/actions/setup-database
|
||||
|
|
|
|||
67
CHANGELOG.md
67
CHANGELOG.md
|
|
@ -2,6 +2,67 @@
|
|||
|
||||
## Main branch
|
||||
|
||||
- Feat: Support `scripts key:typeclass` to create global scripts
|
||||
with dynamic keys (rather than just relying on typeclass' key) (Griatch)
|
||||
- [Feat][pull3595]: Tweak Sqlite3 PRAGMAs for better performance (0xDEADFED5)
|
||||
- Feat: Make Sqlite3 PRAGMAs configurable via settings (Griatch)
|
||||
- [Fix][issue3556]: Better error if trying to treat ObjectDB as a typeclass (Griatch)
|
||||
- [Fix][issue3590]: Make `examine` command properly show `strattr` type
|
||||
Attribute values (Griatch)
|
||||
- [Fix][issue3519]: `GLOBAL_SCRIPTS` container didn't list global scripts not
|
||||
defined explicitly to be restarted/recrated in settings.py (Griatch)
|
||||
- Fix: Passing an already instantiated Script to `obj.scripts.add` (`ScriptHandler.add`)
|
||||
did not add it to the handler's object (Griatch)
|
||||
[Docs][issue3591]: Fix of NPC reaction tutorial code (Griatch)
|
||||
- Docs: Tutorial fixes (Griatch)
|
||||
|
||||
[issue3591]: https://github.com/evennia/evennia/issues/3591
|
||||
[issue3590]: https://github.com/evennia/evennia/issues/3590
|
||||
[issue3556]: https://github.com/evennia/evennia/issues/3556
|
||||
[issue3519]: https://github.com/evennia/evennia/issues/3519
|
||||
[pull3595]: https://github.com/evennia/evennia/pull/3595
|
||||
|
||||
|
||||
## Evennia 4.3.0
|
||||
|
||||
Aug 11, 2024
|
||||
|
||||
- [Feat][pull3531]: New contrib; `in-game reports` for handling user reports,
|
||||
bugs etc in-game (InspectorCaracal)
|
||||
- [Feat][pull3586]: Add ANSI color support `|U`, `|I`, `|i`, `|s`, `|S` for
|
||||
underline reset, italic/reset and strikethrough/reset (0xDEADFED5)
|
||||
- Feat: Add `Trait.traithandler` back-reference so custom Traits from the Traits
|
||||
contrib can find and reference other Traits. (Griatch)
|
||||
- [Feat][pull3582]: Add true-color parsing/fallback for ANSIString (0xDEADFED5)
|
||||
- [Fix][pull3571]: Better visual display of partial multimatch search results
|
||||
(InspectorCaracal)
|
||||
- [Fix][issue3378]: Prototype 'alias' key was not properly homogenized to a list
|
||||
(Griatch)
|
||||
- [Fix][pull3550]: Issue where rpsystem contrib search would do a global instead
|
||||
of local search on multimatch (InspectorCaracal)
|
||||
- [Fix][pull3585]: `TagCmd.switch_options` was misnamed (erratic-pattern)
|
||||
- [Fix][pull3580]: Fix typo that made `find/loc` show the wrong dbref in result (erratic-pattern)
|
||||
- [Fix][pull3571]: Issue disambiguating between certain partial multimatches
|
||||
(InspectorCaracal)
|
||||
- [Fix][pull3589]: Fix regex escaping in utils.py for future Python versions (hhsiao)
|
||||
- [Docs]: Add True-color description for Colors documentation (0xDEADFED5)
|
||||
- [Docs]: Doc fixes (Griatch, InspectorCaracal, 0xDEADFED5)
|
||||
|
||||
[pull3585]: https://github.com/evennia/evennia/pull/3585
|
||||
[pull3580]: https://github.com/evennia/evennia/pull/3580
|
||||
[pull3571]: https://github.com/evennia/evennia/pull/3571
|
||||
[pull3586]: https://github.com/evennia/evennia/pull/3586
|
||||
[pull3550]: https://github.com/evennia/evennia/pull/3550
|
||||
[pull3531]: https://github.com/evennia/evennia/pull/3531
|
||||
[pull3571]: https://github.com/evennia/evennia/pull/3571
|
||||
[pull3582]: https://github.com/evennia/evennia/pull/3582
|
||||
[pull3589]: https://github.com/evennia/evennia/pull/3589
|
||||
[issue3378]: https://github.com/evennia/evennia/issues/3578
|
||||
|
||||
## Evennia 4.2.0
|
||||
|
||||
June 27, 2024
|
||||
|
||||
- [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)
|
||||
|
|
@ -48,6 +109,10 @@
|
|||
template properly (InspectorCaracal)
|
||||
- [Fix][pull3545]: Fix fallback issue in cmdhandler for local-object cmdsets (InspectorCaracal)
|
||||
- [Fix][pull3554]: Fix/readd custom `ic` command to the `character_creator` contrib (InspectorCaracal)
|
||||
- [Fix][pull3566]: Make sure the `website/base.html` website base is targeted
|
||||
explicitly so it doesn't get overridden by same file name elsewhere in app (InspectorCaracal)
|
||||
- [fix][issue3387]: Update all game template doc strings to be more up-to-date
|
||||
(Griatch)
|
||||
- [Docs]: Doc fixes (Griatch, chiizujin, InspectorCaracal, iLPDev)
|
||||
|
||||
[pull3470]: https://github.com/evennia/evennia/pull/3470
|
||||
|
|
@ -79,7 +144,9 @@
|
|||
[pull3549]: https://github.com/evennia/evennia/pull/3549
|
||||
[pull3554]: https://github.com/evennia/evennia/pull/3554
|
||||
[pull3523]: https://github.com/evennia/evennia/pull/3523
|
||||
[pull3566]: https://github.com/evennia/evennia/pull/3566
|
||||
[issue3522]: https://github.com/evennia/evennia/issue/3522
|
||||
[issue3387]: https://github.com/evennia/evennia/issue/3387
|
||||
|
||||
|
||||
## Evennia 4.1.1
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ Update dynamically generated doc pages based on github sources.
|
|||
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from os.path import abspath, dirname
|
||||
from os.path import join as pathjoin
|
||||
from re import sub as re_sub
|
||||
|
||||
ROOTDIR = dirname(dirname(dirname(abspath(__file__))))
|
||||
DOCDIR = pathjoin(ROOTDIR, "docs")
|
||||
|
|
@ -86,12 +88,45 @@ if settings.SERVERNAME == "Evennia":
|
|||
|
||||
print(" -- Updated Settings-Default.md")
|
||||
|
||||
def update_index():
|
||||
"""
|
||||
Read the index.md file and inject the latest version number and updated time.
|
||||
|
||||
"""
|
||||
indexfile = pathjoin(DOCSRCDIR, "index.md")
|
||||
versionfile = pathjoin(EVENNIADIR, "VERSION.txt")
|
||||
|
||||
with open(indexfile) as f:
|
||||
srcdata = f.read()
|
||||
|
||||
# replace the version number
|
||||
with open(versionfile) as f:
|
||||
version = f.read().strip()
|
||||
|
||||
pattern = r"Evennia version is \d+\.\d+\.\d+\."
|
||||
replacement = f"Evennia version is {version}."
|
||||
|
||||
srcdata = re_sub(pattern, replacement, srcdata)
|
||||
|
||||
# replace the last-updated time
|
||||
now = datetime.now().strftime("%B %d, %Y")
|
||||
|
||||
pattern = r"This manual was last updated [A-Z][a-z]+ \d{1,2}, \d{4}"
|
||||
replacement = f"This manual was last updated {now}"
|
||||
|
||||
srcdata = re_sub(pattern, replacement, srcdata)
|
||||
|
||||
with open(indexfile, "w") as f:
|
||||
f.write(srcdata)
|
||||
|
||||
print(" -- Updated index.md")
|
||||
|
||||
def update_dynamic_pages():
|
||||
"""
|
||||
Run the various updaters
|
||||
|
||||
"""
|
||||
update_index()
|
||||
update_changelog()
|
||||
update_default_settings()
|
||||
update_code_style()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,64 @@
|
|||
|
||||
## Main branch
|
||||
|
||||
Feat: Support `scripts key:typeclass` form to create global scripts
|
||||
with dynamic keys (rather than just relying on typeclass' key). Support
|
||||
searching using the same syntax (Griatch)
|
||||
[Fix][issue3556]: Better error if trying to treat ObjectDB as a typeclass (Griatch)
|
||||
[Fix][issue3590]: Make `examine` command properly show `strattr` type
|
||||
Attribute values (Griatch)
|
||||
[Fix][issue3519]: `GLOBAL_SCRIPTS` container didn't list global scripts not
|
||||
defined explicitly to be restarted/recrated in settings.py (Griatch)
|
||||
Fix: Passing an already instantiated Script to `obj.scripts.add` (`ScriptHandler.add`)
|
||||
did not add it to the handler's object (Griatch)
|
||||
[Docs][issue3591]: Fix of NPC reaction tutorial code (Griatch)
|
||||
|
||||
[issue3591]: https://github.com/evennia/evennia/issues/3591
|
||||
[issue3590]: https://github.com/evennia/evennia/issues/3590
|
||||
[issue3556]: https://github.com/evennia/evennia/issues/3556
|
||||
[issue3519]: https://github.com/evennia/evennia/issues/3519
|
||||
|
||||
|
||||
## Evennia 4.3.0
|
||||
|
||||
Aug 11, 2024
|
||||
|
||||
- [Feat][pull3531]: New contrib; `in-game reports` for handling user reports,
|
||||
bugs etc in-game (InspectorCaracal)
|
||||
- [Feat][pull3586]: Add ANSI color support `|U`, `|I`, `|i`, `|s`, `|S` for
|
||||
underline reset, italic/reset and strikethrough/reset (0xDEADFED5)
|
||||
- Feat: Add `Trait.traithandler` back-reference so custom Traits from the Traits
|
||||
contrib can find and reference other Traits. (Griatch)
|
||||
- [Feat][pull3582]: Add true-color parsing/fallback for ANSIString (0xDEADFED5)
|
||||
- [Fix][pull3571]: Better visual display of partial multimatch search results
|
||||
(InspectorCaracal)
|
||||
- [Fix][issue3378]: Prototype 'alias' key was not properly homogenized to a list
|
||||
(Griatch)
|
||||
- [Fix][pull3550]: Issue where rpsystem contrib search would do a global instead
|
||||
of local search on multimatch (InspectorCaracal)
|
||||
- [Fix][pull3585]: `TagCmd.switch_options` was misnamed (erratic-pattern)
|
||||
- [Fix][pull3580]: Fix typo that made `find/loc` show the wrong dbref in result (erratic-pattern)
|
||||
- [Fix][pull3571]: Issue disambiguating between certain partial multimatches
|
||||
(InspectorCaracal)
|
||||
- [Fix][pull3589]: Fix regex escaping in utils.py for future Python versions (hhsiao)
|
||||
- [Docs]: Add True-color description for Colors documentation (0xDEADFED5)
|
||||
- [Docs]: Doc fixes (Griatch, InspectorCaracal, 0xDEADFED5)
|
||||
|
||||
[pull3585]: https://github.com/evennia/evennia/pull/3585
|
||||
[pull3580]: https://github.com/evennia/evennia/pull/3580
|
||||
[pull3571]: https://github.com/evennia/evennia/pull/3571
|
||||
[pull3586]: https://github.com/evennia/evennia/pull/3586
|
||||
[pull3550]: https://github.com/evennia/evennia/pull/3550
|
||||
[pull3531]: https://github.com/evennia/evennia/pull/3531
|
||||
[pull3571]: https://github.com/evennia/evennia/pull/3571
|
||||
[pull3582]: https://github.com/evennia/evennia/pull/3582
|
||||
[pull3589]: https://github.com/evennia/evennia/pull/3589
|
||||
[issue3378]: https://github.com/evennia/evennia/issues/3578
|
||||
|
||||
## Evennia 4.2.0
|
||||
|
||||
June 27, 2024
|
||||
|
||||
- [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)
|
||||
|
|
@ -48,6 +106,10 @@
|
|||
template properly (InspectorCaracal)
|
||||
- [Fix][pull3545]: Fix fallback issue in cmdhandler for local-object cmdsets (InspectorCaracal)
|
||||
- [Fix][pull3554]: Fix/readd custom `ic` command to the `character_creator` contrib (InspectorCaracal)
|
||||
- [Fix][pull3566]: Make sure the `website/base.html` website base is targeted
|
||||
explicitly so it doesn't get overridden by same file name elsewhere in app (InspectorCaracal)
|
||||
- [fix][issue3387]: Update all game template doc strings to be more up-to-date
|
||||
(Griatch)
|
||||
- [Docs]: Doc fixes (Griatch, chiizujin, InspectorCaracal, iLPDev)
|
||||
|
||||
[pull3470]: https://github.com/evennia/evennia/pull/3470
|
||||
|
|
@ -79,7 +141,9 @@
|
|||
[pull3549]: https://github.com/evennia/evennia/pull/3549
|
||||
[pull3554]: https://github.com/evennia/evennia/pull/3554
|
||||
[pull3523]: https://github.com/evennia/evennia/pull/3523
|
||||
[pull3566]: https://github.com/evennia/evennia/pull/3566
|
||||
[issue3522]: https://github.com/evennia/evennia/issue/3522
|
||||
[issue3387]: https://github.com/evennia/evennia/issue/3387
|
||||
|
||||
|
||||
## Evennia 4.1.1
|
||||
|
|
|
|||
|
|
@ -99,7 +99,8 @@ Below are the access_types checked by the default commandset.
|
|||
- `send` - who may send to the channel.
|
||||
- `listen` - who may subscribe and listen to the channel.
|
||||
- [HelpEntry](./Help-System.md):
|
||||
- `examine` - who may view this help entry (usually everyone)
|
||||
- `view` - if the help entry header should show up in the help index
|
||||
- `read` - who may view this help entry (usually everyone)
|
||||
- `edit` - who may edit this help entry.
|
||||
|
||||
So to take an example, whenever an exit is to be traversed, a lock of the type *traverse* will be checked. Defining a suitable lock type for an exit object would thus involve a lockstring `traverse: <lock functions>`.
|
||||
|
|
|
|||
|
|
@ -56,8 +56,14 @@ For the webclient, Evennia will translate the codes to CSS tags.
|
|||
|\|X | normal black foreground color |
|
||||
| \|\[# | background colours, e.g. \|\[c for bright cyan background and \|\[C a normal cyan background. |
|
||||
| \|!# | foreground color that inherits brightness from previous tags. Always uppcase, like \|!R |
|
||||
| \|h | make any following foreground ANSI colors bright (no effect on Xterm colors). Use with \|!#. Technically, \|h\|G == \|g. |
|
||||
| \|H | negates the effects of \|h, return foreground to normal (no effect on Xterm colors) |
|
||||
| \|h | make any following foreground ANSI colors bright (for Xterm256/true color makes the font bold if client supports it). Use with \|!#. Technically, \|h\|G == \|g. |
|
||||
| \|H | negates the effects of \|h |
|
||||
| \|u | underline font if client supports it |
|
||||
| \|U | negates the effects of \|u |
|
||||
| \|i | italic font if client supports it |
|
||||
| \|I | negates the effects of \|i |
|
||||
| \|s | strikethrough font if client supports it |
|
||||
| \|S | negates the effects of \|s |
|
||||
| \|/ | line break. Use instead of Python \\n when adding strings from in-game. |
|
||||
| \|- | tab character when adding strings in-game. Can vay per client, so usually better with spaces. |
|
||||
| \|_ | a space. Only needed to avoid auto-cropping at the end of a in-game input |
|
||||
|
|
@ -126,14 +132,14 @@ actually change the background color instead of the foreground:
|
|||
```
|
||||
|*reversed text |!R now BG is red.
|
||||
```
|
||||
For a detailed explanation of these caveats, see the [Understanding Color Tags](Understanding-Color-
|
||||
Tags) tutorial. But most of the time you might be better off to simply avoid `|*` and mark your text
|
||||
For a detailed explanation of these caveats, see the [Understanding Color Tags](../Howtos/Tutorial\-Understanding\-Color\-Tags.md)
|
||||
tutorial. But most of the time you might be better off to simply avoid `|*` and mark your text
|
||||
manually instead.
|
||||
|
||||
## Xterm256 Colours
|
||||
|
||||
```{sidebar}
|
||||
See the [Understanding Color Tags](../Howtos/Tutorial-Understanding-Color-Tags.md) tutorial, for more on the use of ANSI color tags and the pitfalls of mixing ANSI and Xterms256 color tags in the same context.
|
||||
See the [Understanding Color Tags](../Howtos/Tutorial\-Understanding\-Color\-Tags.md) tutorial, for more on the use of ANSI color tags and the pitfalls of mixing ANSI and Xterms256 color tags in the same context.
|
||||
```
|
||||
The _Xterm256_ standard is a colour scheme that supports 256 colours for text and/or background. It can be combined freely with ANSI colors (above), but some ANSI tags don't affect Xterm256 tags.
|
||||
|
||||
|
|
@ -173,3 +179,40 @@ If you have a client that supports Xterm256, you can use
|
|||
|
||||
to get a table of all the 256 colours and the codes that produce them. If the table looks broken up
|
||||
into a few blocks of colors, it means Xterm256 is not supported and ANSI are used as a replacement. You can use the `options` command to see if xterm256 is active for you. This depends on if your client told Evennia what it supports - if not, and you know what your client supports, you may have to activate some features manually.
|
||||
|
||||
## 24-bit Colors (True color)
|
||||
|
||||
```{sidebar}
|
||||
See the [Wikipedia entry on web colors](https://en.wikipedia.org/wiki/Web_colors) for more detailed information on this color format.
|
||||
```
|
||||
|
||||
Some clients support 24-bit colors. This is also called [true color](https://en.wikipedia.org/wiki/Color_depth#True_color_(24-bit)).
|
||||
Not all clients support true color, they will instead see the closest equivalent. It's important to bear in mind that things may look quite
|
||||
different from what you intended if you use subtle gradations in true color and it's viewed with a client that doesn't support true color.
|
||||
The hexadecimal color codes used here are the same ones used in web design.
|
||||
|
||||
|
||||
| Tag | Effect |
|
||||
| -------- | ---- |
|
||||
| \|#$$$$$$ | foreground RGB (red/green/blue), 6-digit hexadecimal format, where $ = 0-F |
|
||||
| \|\[#$$$$$$ | background RGB |
|
||||
| \|#$$$ | foreground RGB (red/green/blue), 3-digit hexadecimal format. |
|
||||
| \|\[#$$$ | background RGB |
|
||||
|
||||
Some 6-digit examples:
|
||||
|
||||
| Tag | Effect |
|
||||
| -------- | ---- |
|
||||
| \|#ff0000 | bright red foreground|
|
||||
| \|#00ff00 | bright green foreground|
|
||||
| \|#0000ff | bright blue foreground|
|
||||
| \|#\[ff0000 | bright red background|
|
||||
|
||||
Some 3-digit examples:
|
||||
|
||||
| Tag | Effect |
|
||||
| ---- | ---- |
|
||||
| \|#f00 | bright red foreground|
|
||||
| \|#0f0 | bright green foreground|
|
||||
| \|#00f | bright blue foreground|
|
||||
| \|\[#f00 | bright red background|
|
||||
|
|
@ -31,7 +31,7 @@ class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
|||
self.add(CmdAchieve)
|
||||
```
|
||||
|
||||
**Optional** - The achievements contrib stores individual progress data on the `achievements` attribute by default, visible via `obj.db.attributes`. You can change this by assigning an attribute (key, category) tuple to the setting `ACHIEVEMENT_CONTRIB_ATTRIBUTE`
|
||||
**Optional** - The achievements contrib stores individual progress data on the `achievements` attribute by default, visible via `obj.db.achievements`. You can change this by assigning an attribute (key, category) tuple to the setting `ACHIEVEMENT_CONTRIB_ATTRIBUTE`
|
||||
|
||||
Example:
|
||||
```py
|
||||
|
|
|
|||
136
docs/source/Contribs/Contrib-Ingame-Reports.md
Normal file
136
docs/source/Contribs/Contrib-Ingame-Reports.md
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
# In-Game Reporting System
|
||||
|
||||
Contrib by InspectorCaracal, 2024
|
||||
|
||||
This contrib provides an in-game reports system, handling bug reports, player reports, and idea submissions by default. It also supports adding your own types of reports, or removing any of the default report types.
|
||||
|
||||
Each type of report has its own command for submitting new reports, and an admin command is also provided for managing the reports through a menu.
|
||||
|
||||
## Installation
|
||||
|
||||
To install the reports contrib, just add the provided cmdset to your default AccountCmdSet:
|
||||
|
||||
```python
|
||||
# in commands/default_cmdset.py
|
||||
|
||||
from evennia.contrib.base_systems.ingame_reports import ReportsCmdSet
|
||||
|
||||
class AccountCmdSet(default_cmds.AccountCmdSet):
|
||||
# ...
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
# ...
|
||||
self.add(ReportsCmdSet)
|
||||
```
|
||||
|
||||
The contrib also has two optional settings: `INGAME_REPORT_TYPES` and `INGAME_REPORT_STATUS_TAGS`.
|
||||
|
||||
The `INGAME_REPORT_TYPES` setting is covered in detail in the section "Adding new types of reports".
|
||||
|
||||
The `INGAME_REPORT_STATUS_TAGS` setting is covered in the section "Managing reports".
|
||||
|
||||
## Usage
|
||||
|
||||
By default, the following report types are available:
|
||||
|
||||
* Bugs: Report bugs encountered during gameplay.
|
||||
* Ideas: Submit suggestions for game improvement.
|
||||
* Players: Report inappropriate player behavior.
|
||||
|
||||
Players can submit new reports through the command for each report type, and staff are given access to a report-management command and menu.
|
||||
|
||||
### Submitting reports
|
||||
|
||||
Players can submit reports using the following commands:
|
||||
|
||||
* `bug <text>` - Files a bug report. An optional target can be included - `bug <target> = <text>` - making it easier for devs/builders to track down issues.
|
||||
* `report <player> = <text>` - Reports a player for inappropriate or rule-breaking behavior. *Requires* a target to be provided - it searches among accounts by default.
|
||||
* `idea <text>` - Submits a general suggestion, with no target. It also has an alias of `ideas` which allows you to view all of your submitted ideas.
|
||||
|
||||
### Managing reports
|
||||
|
||||
The `manage reports` command allows staff to review and manage the various types of reports by launching a management menu.
|
||||
|
||||
This command will dynamically add aliases to itself based on the types of reports available, with each command string launching a menu for that particular report type. The aliases are built on the pattern `manage <report type>s` - by default, this means it makes `manage bugs`, `manage players`, and `manage ideas` available along with the default `manage reports`, and that e.g. `manage bugs` will launch the management menu for `bug`-type reports.
|
||||
|
||||
Aside from reading over existing reports, the menu allows you to change the status of any given report. By default, the contrib includes two different status tags: `in progress` and `closed`.
|
||||
|
||||
> Note: A report is created with no status tags, which is considered "open"
|
||||
|
||||
If you want a different set of statuses for your reports, you can define the `INGAME_REPORT_STATUS_TAGS` to your list of statuses.
|
||||
|
||||
**Example**
|
||||
|
||||
```python
|
||||
# in server/conf/settings.py
|
||||
|
||||
# this will allow for the statuses of 'in progress', 'rejected', and 'completed', without the contrib-default of 'closed'
|
||||
INGAME_REPORT_STATUS_TAGS = ('in progress', 'rejected', 'completed')
|
||||
```
|
||||
|
||||
### Adding new types of reports
|
||||
|
||||
The contrib is designed to make adding new types of reports to the system as simple as possible, requiring only two steps:
|
||||
|
||||
1. Update your settings file to include an `INGAME_REPORT_TYPES` setting.
|
||||
2. Create and add a new `ReportCmd` to your command set.
|
||||
|
||||
#### Update your settings
|
||||
|
||||
The contrib optionally references `INGAME_REPORT_TYPES` in your settings.py to see which types of reports can be managed. If you want to change the available report types, you'll need to define this setting.
|
||||
|
||||
```python
|
||||
# in server/conf/settings.py
|
||||
|
||||
# this will include the contrib's report types as well as a custom 'complaint' report type
|
||||
INGAME_REPORT_TYPES = ('bugs', 'ideas', 'players', 'complaints')
|
||||
```
|
||||
|
||||
You can also use this setting to remove any of the contrib's report types - the contrib will respect this setting when building its cmdset with no additional steps.
|
||||
|
||||
```python
|
||||
# in server/conf/settings.py
|
||||
|
||||
# this redefines the setting to not include 'ideas', so the ideas command and reports won't be available
|
||||
INGAME_REPORT_TYPES = ('bugs', 'players')
|
||||
```
|
||||
|
||||
#### Create a new ReportCmd
|
||||
|
||||
`ReportCmdBase` is a parent command class which comes with the main functionality for submitting reports. Creating a new reporting command is as simple as inheriting from this class and defining a couple of class attributes.
|
||||
|
||||
* `key` - This is the same as for any other command, setting the command's usable key. It also acts as the report type if that isn't explicitly set.
|
||||
* `report_type` - The type of report this command is for (e.g. `player`). You only need to set it if you want a different string from the key.
|
||||
* `report_locks` - The locks you want applied to the created reports. Defaults to `"read:pperm(Admin)"`
|
||||
* `success_msg` - The string which is sent to players after submitting a report of this type. Defaults to `"Your report has been filed."`
|
||||
* `require_target`: Set to `True` if your report type requires a target (e.g. player reports).
|
||||
|
||||
> Note: The contrib's own commands - `CmdBug`, `CmdIdea`, and `CmdReport` - are implemented the same way, so you can review them as examples.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
from evennia.contrib.base_systems.ingame_reports.reports import ReportCmdBase
|
||||
|
||||
class CmdCustomReport(ReportCmdBase):
|
||||
"""
|
||||
file a custom report
|
||||
|
||||
Usage:
|
||||
customreport <message>
|
||||
|
||||
This is a custom report type.
|
||||
"""
|
||||
|
||||
key = "customreport"
|
||||
report_type = "custom"
|
||||
success_message = "You have successfully filed a custom report."
|
||||
```
|
||||
|
||||
Add this new command to your default cmdset to enable filing your new report type.
|
||||
|
||||
|
||||
----
|
||||
|
||||
<small>This document page is generated from `evennia/contrib/base_systems/ingame_reports/README.md`. Changes to this
|
||||
file will be overwritten, so edit that file rather than this one.</small>
|
||||
|
|
@ -73,8 +73,14 @@ class Room(ContribRPRoom):
|
|||
# ...
|
||||
|
||||
```
|
||||
You need to set up Evennia to use the RPsystem's form to separate
|
||||
between sdescs (`3-tall`) to make it compatible with how the rest of Evennia
|
||||
separates between other multi-matches of searches/commands:
|
||||
|
||||
You will then need to reload the server and potentially force-reload
|
||||
SEARCH_MULTIMATCH_REGEX = r"(?P<number>[0-9]+)-(?P<name>[^-]*)(?P<args>.*)"
|
||||
SEARCH_MULTIMATCH_TEMPLATE = " {number}-{name}{aliases}{info}\n"
|
||||
|
||||
Finally, you will then need to reload the server and potentially force-reload
|
||||
your objects, if you originally created them without this.
|
||||
|
||||
Example for your character:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ Contribution by Griatch 2020, based on code by Whitenoise and Ainneve contribs,
|
|||
|
||||
A `Trait` represents a modifiable property on (usually) a Character. They can
|
||||
be used to represent everything from attributes (str, agi etc) to skills
|
||||
(hunting 10, swords 14 etc) and dynamically changing things like HP, XP etc.
|
||||
(hunting 10, swords 14 etc) and dynamically changing things like HP, XP etc.
|
||||
Traits differ from normal Attributes in that they track their changes and limit
|
||||
themselves to particular value-ranges. One can add/subtract from them easily and
|
||||
they can even change dynamically at a particular rate (like you being poisoned or
|
||||
|
|
@ -50,8 +50,6 @@ class Character(DefaultCharacter):
|
|||
self.traits.add("hp", "Health", trait_type="gauge", min=0, max=100)
|
||||
self.traits.add("hunting", "Hunting Skill", trait_type="counter",
|
||||
base=10, mod=1, min=0, max=100)
|
||||
|
||||
|
||||
```
|
||||
When adding the trait, you supply the name of the property (`hunting`) along
|
||||
with a more human-friendly name ("Hunting Skill"). The latter will show if you
|
||||
|
|
@ -78,7 +76,6 @@ class Object(DefaultObject):
|
|||
strength = TraitProperty("Strength", trait_type="static", base=10, mod=2)
|
||||
health = TraitProperty("Health", trait_type="gauge", min=0, base=100, mod=2)
|
||||
hunting = TraitProperty("Hunting Skill", trait_type="counter", base=10, mod=1, min=0, max=100)
|
||||
|
||||
```
|
||||
|
||||
> Note that the property-name will become the name of the trait and you don't supply `trait_key`
|
||||
|
|
@ -92,7 +89,7 @@ class Object(DefaultObject):
|
|||
|
||||
## Using traits
|
||||
|
||||
A trait is added to the traithandler (if you use `TraitProperty` the handler is just created under
|
||||
A trait are entities added to the traithandler (if you use `TraitProperty` the handler is just created under
|
||||
the hood) after which one can access it as a property on the handler (similarly to how you can do
|
||||
.db.attrname for Attributes in Evennia).
|
||||
|
||||
|
|
@ -137,9 +134,30 @@ obj.traits.strength.value
|
|||
> obj.strength.value += 5
|
||||
> obj.strength.value
|
||||
17
|
||||
|
||||
```
|
||||
|
||||
### Relating traits to one another
|
||||
|
||||
From a trait you can access its own Traithandler as `.traithandler`. You can
|
||||
also find another trait on the same handler by using the
|
||||
`Trait.get_trait("traitname")` method.
|
||||
|
||||
```python
|
||||
> obj.strength.get_trait("hp").value
|
||||
100
|
||||
```
|
||||
|
||||
This is not too useful for the default trait types - they are all operating
|
||||
independently from one another. But if you create your own trait classes, you
|
||||
can use this to make traits that depend on each other.
|
||||
|
||||
For example, you could picture making a Trait that is the sum of the values of
|
||||
two other traits and capped by the value of a third trait. Such complex
|
||||
interactions are common in RPG rule systems but are by definition game specific.
|
||||
|
||||
See an example in the section about [making your own Trait classes](#expanding-with-your-own-traits).
|
||||
|
||||
|
||||
## Trait types
|
||||
|
||||
All default traits have a read-only `.value` property that shows the relevant or
|
||||
|
|
@ -158,7 +176,6 @@ compatible type.
|
|||
> trait1 + 2
|
||||
> trait1.value
|
||||
5
|
||||
|
||||
```
|
||||
|
||||
Two numerical traits can also be compared (bigger-than etc), which is useful in
|
||||
|
|
@ -168,7 +185,6 @@ all sorts of rule-resolution.
|
|||
|
||||
if trait1 > trait2:
|
||||
# do stuff
|
||||
|
||||
```
|
||||
|
||||
### Trait
|
||||
|
|
@ -193,7 +209,6 @@ like a glorified Attribute.
|
|||
> obj.traits.mytrait.value = "stringvalue"
|
||||
> obj.traits.mytrait.value
|
||||
"stringvalue"
|
||||
|
||||
```
|
||||
|
||||
### Static trait
|
||||
|
|
@ -217,7 +232,6 @@ that varies slowly or not at all, and which may be modified in-place.
|
|||
> obj.traits.mytrait.mod = 0
|
||||
> obj.traits.mytrait.value
|
||||
12
|
||||
|
||||
```
|
||||
|
||||
### Counter
|
||||
|
|
@ -253,8 +267,6 @@ remove it. A suggested use for a Counter Trait would be to track skill values.
|
|||
|
||||
# for TraitProperties, pass the args/kwargs of traits.add() to the
|
||||
# TraitProperty constructor instead.
|
||||
|
||||
|
||||
```
|
||||
|
||||
Counters have some extra properties:
|
||||
|
|
@ -286,7 +298,6 @@ By calling `.desc()` on the Counter, you will get the text matching the current
|
|||
|
||||
> obj.traits.hunting.desc()
|
||||
"expert"
|
||||
|
||||
```
|
||||
|
||||
#### .rate
|
||||
|
|
@ -327,12 +338,10 @@ a previous value.
|
|||
71 # we have stopped at the ratetarget
|
||||
|
||||
> obj.traits.hunting.rate = 0 # disable auto-change
|
||||
|
||||
|
||||
```
|
||||
Note that when retrieving the `current`, the result will always be of the same
|
||||
type as the `.base` even `rate` is a non-integer value. So if `base` is an `int`
|
||||
(default)`, the `current` value will also be rounded the closest full integer.
|
||||
(default), the `current` value will also be rounded the closest full integer.
|
||||
If you want to see the exact `current` value, set `base` to a float - you
|
||||
will then need to use `round()` yourself on the result if you want integers.
|
||||
|
||||
|
|
@ -347,7 +356,6 @@ return the value as a percentage.
|
|||
|
||||
> obj.traits.hunting.percent(formatting=None)
|
||||
71.0
|
||||
|
||||
```
|
||||
|
||||
### Gauge
|
||||
|
|
@ -379,7 +387,6 @@ stamina and the like.
|
|||
> obj.traits.hp.current -= 30
|
||||
> obj.traits.hp.value
|
||||
80
|
||||
|
||||
```
|
||||
|
||||
The Gauge trait is subclass of the Counter, so you have access to the same
|
||||
|
|
@ -412,8 +419,6 @@ class RageTrait(StaticTrait):
|
|||
|
||||
def sedate(self):
|
||||
self.mod = 0
|
||||
|
||||
|
||||
```
|
||||
|
||||
Above is an example custom-trait-class "rage" that stores a property "rage" on
|
||||
|
|
@ -432,12 +437,24 @@ Reload the server and you should now be able to use your trait:
|
|||
> obj.traits.add("mood", "A dark mood", rage=30, trait_type='rage')
|
||||
> obj.traits.mood.rage
|
||||
30
|
||||
```
|
||||
|
||||
Remember that you can use `.get_trait("name")` to access other traits on the
|
||||
same handler. Let's say that the rage modifier is actually limited by
|
||||
the characters's current STR value times 3, with a max of 100:
|
||||
|
||||
```python
|
||||
class RageTrait(StaticTrait):
|
||||
#...
|
||||
def berserk(self):
|
||||
self.mod = min(100, self.get_trait("STR").value * 3)
|
||||
```
|
||||
|
||||
# as TraitProperty
|
||||
|
||||
```
|
||||
class Character(DefaultCharacter):
|
||||
rage = TraitProperty("A dark mood", rage=30, trait_type='rage')
|
||||
|
||||
```
|
||||
|
||||
## Adding additional TraitHandlers
|
||||
|
|
@ -459,7 +476,7 @@ class Character(DefaultCharacter):
|
|||
def traits(self):
|
||||
# this adds the handler as .traits
|
||||
return TraitHandler(self)
|
||||
|
||||
|
||||
@lazy_property
|
||||
def stats(self):
|
||||
# this adds the handler as .stats
|
||||
|
|
@ -479,6 +496,9 @@ class Character(DefaultCharacter):
|
|||
base=10, mod=1, min=0, max=100)
|
||||
```
|
||||
|
||||
> Rememebr that the `.get_traits()` method only works for accessing Traits within the
|
||||
_same_ TraitHandler.
|
||||
|
||||
|
||||
----
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ in the [Community Contribs & Snippets][forum] forum.
|
|||
_Contribs_ are optional code snippets and systems contributed by
|
||||
the Evennia community. They vary in size and complexity and
|
||||
may be more specific about game types and styles than 'core' Evennia.
|
||||
This page is auto-generated and summarizes all **50** contribs currently included
|
||||
This page is auto-generated and summarizes all **51** contribs currently included
|
||||
with the Evennia distribution.
|
||||
|
||||
All contrib categories are imported from `evennia.contrib`, such as
|
||||
|
|
@ -34,11 +34,12 @@ If you want to add a contrib, see [the contrib guidelines](./Contribs-Guidelines
|
|||
| [color_markups](#color_markups) | [components](#components) | [containers](#containers) | [cooldowns](#cooldowns) | [crafting](#crafting) |
|
||||
| [custom_gametime](#custom_gametime) | [dice](#dice) | [email_login](#email_login) | [evadventure](#evadventure) | [evscaperoom](#evscaperoom) |
|
||||
| [extended_room](#extended_room) | [fieldfill](#fieldfill) | [gendersub](#gendersub) | [git_integration](#git_integration) | [godotwebsocket](#godotwebsocket) |
|
||||
| [health_bar](#health_bar) | [ingame_map_display](#ingame_map_display) | [ingame_python](#ingame_python) | [llm](#llm) | [mail](#mail) |
|
||||
| [mapbuilder](#mapbuilder) | [menu_login](#menu_login) | [mirror](#mirror) | [multidescer](#multidescer) | [mux_comms_cmds](#mux_comms_cmds) |
|
||||
| [name_generator](#name_generator) | [puzzles](#puzzles) | [random_string_generator](#random_string_generator) | [red_button](#red_button) | [rpsystem](#rpsystem) |
|
||||
| [simpledoor](#simpledoor) | [slow_exit](#slow_exit) | [talking_npc](#talking_npc) | [traits](#traits) | [tree_select](#tree_select) |
|
||||
| [turnbattle](#turnbattle) | [tutorial_world](#tutorial_world) | [unixcommand](#unixcommand) | [wilderness](#wilderness) | [xyzgrid](#xyzgrid) |
|
||||
| [health_bar](#health_bar) | [ingame_map_display](#ingame_map_display) | [ingame_python](#ingame_python) | [ingame_reports](#ingame_reports) | [llm](#llm) |
|
||||
| [mail](#mail) | [mapbuilder](#mapbuilder) | [menu_login](#menu_login) | [mirror](#mirror) | [multidescer](#multidescer) |
|
||||
| [mux_comms_cmds](#mux_comms_cmds) | [name_generator](#name_generator) | [puzzles](#puzzles) | [random_string_generator](#random_string_generator) | [red_button](#red_button) |
|
||||
| [rpsystem](#rpsystem) | [simpledoor](#simpledoor) | [slow_exit](#slow_exit) | [talking_npc](#talking_npc) | [traits](#traits) |
|
||||
| [tree_select](#tree_select) | [turnbattle](#turnbattle) | [tutorial_world](#tutorial_world) | [unixcommand](#unixcommand) | [wilderness](#wilderness) |
|
||||
| [xyzgrid](#xyzgrid) |
|
||||
|
||||
|
||||
|
||||
|
|
@ -64,6 +65,7 @@ Contrib-Custom-Gametime.md
|
|||
Contrib-Email-Login.md
|
||||
Contrib-Godotwebsocket.md
|
||||
Contrib-Ingame-Python.md
|
||||
Contrib-Ingame-Reports.md
|
||||
Contrib-Menu-Login.md
|
||||
Contrib-Mux-Comms-Cmds.md
|
||||
Contrib-Unixcommand.md
|
||||
|
|
@ -173,6 +175,16 @@ this module carefully before continuing.
|
|||
|
||||
|
||||
|
||||
### `ingame_reports`
|
||||
|
||||
_Contrib by InspectorCaracal, 2024_
|
||||
|
||||
This contrib provides an in-game reports system, handling bug reports, player reports, and idea submissions by default. It also supports adding your own types of reports, or removing any of the default report types.
|
||||
|
||||
[Read the documentation](./Contrib-Ingame-Reports.md) - [Browse the Code](evennia.contrib.base_systems.ingame_reports)
|
||||
|
||||
|
||||
|
||||
### `menu_login`
|
||||
|
||||
_Contribution by Vincent-lg 2016. Reworked for modern EvMenu by Griatch, 2019._
|
||||
|
|
@ -642,7 +654,7 @@ _Contribution by Griatch 2020, based on code by Whitenoise and Ainneve contribs,
|
|||
|
||||
A `Trait` represents a modifiable property on (usually) a Character. They can
|
||||
be used to represent everything from attributes (str, agi etc) to skills
|
||||
(hunting 10, swords 14 etc) and dynamically changing things like HP, XP etc.
|
||||
(hunting 10, swords 14 etc) and dynamically changing things like HP, XP etc.
|
||||
Traits differ from normal Attributes in that they track their changes and limit
|
||||
themselves to particular value-ranges. One can add/subtract from them easily and
|
||||
they can even change dynamically at a particular rate (like you being poisoned or
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@
|
|||
In this lesson we'll learn how to create our own Evennia [Commands](../../../Components/Commands.md) If you are new to Python you'll also learn some more basics about how to manipulate strings and get information out of Evennia.
|
||||
|
||||
A Command is something that handles the input from a user and causes a result to happen.
|
||||
An example is `look`, which examines your current location and tells you what it looks like and
|
||||
what is in it.
|
||||
An example is `look`, which examines your current location and tells you what it looks like and what is in it.
|
||||
|
||||
```{sidebar} Commands are not typeclassed
|
||||
|
||||
|
|
@ -20,7 +19,7 @@ Command-Sets are then associated with objects, for example with your Character.
|
|||
|
||||
## Creating a custom command
|
||||
|
||||
Open `mygame/commands/command.py`:
|
||||
Open `mygame/commands/command.py`. This file already has stuff filled in for you.
|
||||
|
||||
```python
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -23,12 +23,15 @@ On the `DefaultObject` is a `.search` method which we have already tried out whe
|
|||
- It will always return exactly one match. If it found zero or more than one match, the return is `None`. This is different from `evennia.search` (see below), which always returns a list.
|
||||
- On a no-match or multimatch, `.search` will automatically send an error message to `obj`. So you don't have to worry about reporting messages if the result is `None`.
|
||||
|
||||
In other words, this method handles error messaging for you. A very common way to use it is in commands:
|
||||
In other words, this method handles error messaging for you. A very common way to use it is in commands. You can put your command anywhere, but let's try the pre-filled-in `mygame/commands/command.py`.
|
||||
|
||||
```python
|
||||
# in for example mygame/commands/command.py
|
||||
|
||||
from evennia import Command
|
||||
from evennia import Command as BaseCommand
|
||||
|
||||
class Command(BaseCommand):
|
||||
# ...
|
||||
|
||||
class CmdQuickFind(Command):
|
||||
"""
|
||||
|
|
@ -132,9 +135,9 @@ Above we find first the rose and then an Account. You can try both using `py`:
|
|||
> py evennia.search_account("YourName")[0]
|
||||
<Player: YourName>
|
||||
|
||||
In the example above we used `[0]` to only get the first match of the queryset, which in this case gives us the rose and your Account respectively. Note that if you don't find any matches, using `[0]` like this leads to an error, so it's mostly useful for debugging.
|
||||
The `search_object/account` returns all matches. We use `[0]` to only get the first match of the queryset, which in this case gives us the rose and your Account respectively. Note that if you don't find any matches, using `[0]` like this leads to an error, so it's mostly useful for debugging.
|
||||
|
||||
If you you really want all matches to the search parameters you specify. In other situations, having zero or more than one match is a sign of a problem and you need to handle this case yourself. This is too detailed for testing out just with `py`, but good to know if you want to make your own search methods:
|
||||
In other situations, having zero or more than one match is a sign of a problem and you need to handle this case yourself. This is too detailed for testing out just with `py`, but good to know if you want to make your own search methods:
|
||||
|
||||
```python
|
||||
the_one_ring = evennia.search_object("The one Ring")
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ Here is a simple example of the prompt sent/updated from a command class:
|
|||
if not self.args:
|
||||
target = self.caller
|
||||
else:
|
||||
target = self.search(self.args)
|
||||
target = self.caller.search(self.args)
|
||||
if not target:
|
||||
return
|
||||
# try to get health, mana and stamina
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class NPC(Character):
|
|||
"""
|
||||
A NPC typeclass which extends the character class.
|
||||
"""
|
||||
def at_char_entered(self, character):
|
||||
def at_char_entered(self, character, **kwargs):
|
||||
"""
|
||||
A simple is_aggressive check.
|
||||
Can be expanded upon later.
|
||||
|
|
@ -38,7 +38,13 @@ class NPC(Character):
|
|||
self.execute_cmd(f"say Greetings, {character}!")
|
||||
```
|
||||
|
||||
Here we make a simple method on the `NPC`˙. We expect it to be called when a (player-)character enters the room. We don't actually set the `is_aggressive` [Attribute](../Components/Attributes.md) beforehand; if it's not set, the NPC is simply non-hostile.
|
||||
```{sidebar} Passing extra information
|
||||
Note that we don't use the `**kwargs` property here. This can be used to pass extra information into hooks in your game and would be used when you make custom move commands. For example, if you `run` into the room, you could inform all hooks by doing `obj.move_to(..., running=True)`. Maybe your librarian NPC should have a separate reaction for people running into their library!
|
||||
|
||||
We make sure to pass the `**kwargs` from the standard `at_object_receive` hook below.
|
||||
```
|
||||
|
||||
Here we make a simple method on the `NPC`˙ called `at_char_entered`. We expect it to be called when a (player-)character enters the room. We don't actually set the `is_aggressive` [Attribute](../Components/Attributes.md) beforehand; we leave this up for the admin to activate in-game. if it's not set, the NPC is simply non-hostile.
|
||||
|
||||
Whenever _something_ enters the `Room`, its [at_object_receive](DefaultObject.at_object_receive) hook will be called. So we should override it.
|
||||
|
||||
|
|
@ -54,18 +60,18 @@ class Room(ObjectParent, DefaultRoom):
|
|||
|
||||
# ...
|
||||
|
||||
def at_object_receive(self, arriving_obj, source_location):
|
||||
def at_object_receive(self, arriving_obj, source_location, **kwargs):
|
||||
if arriving_obj.account:
|
||||
# this has an active acccount - a player character
|
||||
for item in self.contents:
|
||||
# get all npcs in the room and inform them
|
||||
if utils.inherits_from(item, "typeclasses.npcs.NPC"):
|
||||
self.at_char_entered(arriving_obj)
|
||||
if utils.inherits_from(item, "typeclasses.npcs.NPC"):
|
||||
item.at_char_entered(arriving_obj, **kwargs)
|
||||
|
||||
```
|
||||
|
||||
```{sidebar} Universal Object methods
|
||||
Remember that Rooms are `Objects`. So the same `at_object_receive` hook will fire for you when you pick something up (making you 'receive' it). Or for a box when putting something inside it.
|
||||
Remember that Rooms are `Objects`, and other Objects have these same hooks. So an `at_object_receive` hook will fire for you when you pick something up (making you 'receive' it). Or for a box when putting something inside it, for example.
|
||||
```
|
||||
A currently puppeted Character will have an `.account` attached to it. We use that to know that the thing arriving is a Character. We then use Evennia's [utils.inherits_from](evennia.utils.utils.inherits_from) helper utility to get every NPC in the room can each of their newly created `at_char_entered` method.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
```{eval-rst}
|
||||
evennia.contrib.base\_systems.ingame\_reports
|
||||
=====================================================
|
||||
|
||||
.. automodule:: evennia.contrib.base_systems.ingame_reports
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 6
|
||||
|
||||
evennia.contrib.base_systems.ingame_reports.menu
|
||||
evennia.contrib.base_systems.ingame_reports.reports
|
||||
evennia.contrib.base_systems.ingame_reports.tests
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
```{eval-rst}
|
||||
evennia.contrib.base\_systems.ingame\_reports.menu
|
||||
=========================================================
|
||||
|
||||
.. automodule:: evennia.contrib.base_systems.ingame_reports.menu
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
```{eval-rst}
|
||||
evennia.contrib.base\_systems.ingame\_reports.reports
|
||||
============================================================
|
||||
|
||||
.. automodule:: evennia.contrib.base_systems.ingame_reports.reports
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
```{eval-rst}
|
||||
evennia.contrib.base\_systems.ingame\_reports.tests
|
||||
==========================================================
|
||||
|
||||
.. automodule:: evennia.contrib.base_systems.ingame_reports.tests
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
```
|
||||
|
|
@ -19,6 +19,7 @@ evennia.contrib.base\_systems
|
|||
evennia.contrib.base_systems.email_login
|
||||
evennia.contrib.base_systems.godotwebsocket
|
||||
evennia.contrib.base_systems.ingame_python
|
||||
evennia.contrib.base_systems.ingame_reports
|
||||
evennia.contrib.base_systems.menu_login
|
||||
evennia.contrib.base_systems.mux_comms_cmds
|
||||
evennia.contrib.base_systems.unixcommand
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# Evennia Documentation
|
||||
|
||||
This is the manual of [Evennia](https://www.evennia.com), the open source Python `MU*` creation system. Use the Search bar on the left to find or discover interesting articles.
|
||||
This is the manual of [Evennia](https://www.evennia.com), the open source Python `MU*` creation system. Use the Search bar on the left to find or discover interesting articles. This manual was last updated August 11, 2024, see the [Evennia Changelog](Coding/Changelog.md). Latest released Evennia version is 4.3.0.
|
||||
|
||||
- [Introduction](./Evennia-Introduction.md) - what is this Evennia thing?
|
||||
- [Introduction](./Evennia-Introduction.md) - what is this Evennia thing?
|
||||
- [Evennia in Pictures](./Evennia-In-Pictures.md) - a visual overview of Evennia
|
||||
- [Contributing and Getting help](./Contributing.md) - when you get stuck or want to chip in
|
||||
|
||||
|
|
@ -10,7 +10,7 @@ This is the manual of [Evennia](https://www.evennia.com), the open source Python
|
|||
|
||||
- [Installation](Setup/Setup-Overview.md#installation-and-running) - getting started
|
||||
- [Running the Game](Setup/Running-Evennia.md) - how to start, stop and reload Evennia
|
||||
- [Updating the Server](Setup/Updating-Evennia.md) - how to update Evennia
|
||||
- [Updating the Server](Setup/Updating-Evennia.md) - how to update Evennia
|
||||
- [Configuration](Setup/Setup-Overview.md#configuration) - how to set up Evennia the way you like it
|
||||
- [Going Online](Setup/Setup-Overview.md#going-online) - bringing your game online
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
4.1.1
|
||||
4.3.0
|
||||
|
|
|
|||
|
|
@ -226,7 +226,6 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
- user (User, read-only) - django User authorization object
|
||||
- obj (Object) - game object controlled by account. 'character' can also
|
||||
be used.
|
||||
- sessions (list of Sessions) - sessions connected to this account
|
||||
- is_superuser (bool, read-only) - if the connected user is a superuser
|
||||
|
||||
* Handlers
|
||||
|
|
@ -239,18 +238,47 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
- scripts - script-handler. Add new scripts to object with scripts.add()
|
||||
- cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object
|
||||
- nicks - nick-handler. New nicks with nicks.add().
|
||||
- sessions - session-handler. Use session.get() to see all sessions connected, if any
|
||||
- options - option-handler. Defaults are taken from settings.OPTIONS_ACCOUNT_DEFAULT
|
||||
- characters - handler for listing the account's playable characters
|
||||
|
||||
* Helper methods
|
||||
* Helper methods (check autodocs for full updated listing)
|
||||
|
||||
- msg(text=None, from_obj=None, session=None, options=None, **kwargs)
|
||||
- execute_cmd(raw_string)
|
||||
- search(ostring, global_search=False, attribute_name=None,
|
||||
use_nicks=False, location=None,
|
||||
ignore_errors=False, account=False)
|
||||
- search(searchdata, return_puppet=False, search_object=False, typeclass=None,
|
||||
nofound_string=None, multimatch_string=None, use_nicks=True,
|
||||
quiet=False, **kwargs)
|
||||
- is_typeclass(typeclass, exact=False)
|
||||
- swap_typeclass(new_typeclass, clean_attributes=False, no_default=True)
|
||||
- access(accessing_obj, access_type='read', default=False, no_superuser_bypass=False)
|
||||
- access(accessing_obj, access_type='read', default=False, no_superuser_bypass=False, **kwargs)
|
||||
- check_permstring(permstring)
|
||||
- get_cmdsets(caller, current, **kwargs)
|
||||
- get_cmdset_providers()
|
||||
- uses_screenreader(session=None)
|
||||
- get_display_name(looker, **kwargs)
|
||||
- get_extra_display_name_info(looker, **kwargs)
|
||||
- disconnect_session_from_account()
|
||||
- puppet_object(session, obj)
|
||||
- unpuppet_object(session)
|
||||
- unpuppet_all()
|
||||
- get_puppet(session)
|
||||
- get_all_puppets()
|
||||
- is_banned(**kwargs)
|
||||
- get_username_validators(validator_config=settings.AUTH_USERNAME_VALIDATORS)
|
||||
- authenticate(username, password, ip="", **kwargs)
|
||||
- normalize_username(username)
|
||||
- validate_username(username)
|
||||
- validate_password(password, account=None)
|
||||
- set_password(password, **kwargs)
|
||||
- get_character_slots()
|
||||
- get_available_character_slots()
|
||||
- create_character(*args, **kwargs)
|
||||
- create(*args, **kwargs)
|
||||
- delete(*args, **kwargs)
|
||||
- channel_msg(message, channel, senders=None, **kwargs)
|
||||
- idle_time()
|
||||
- connection_time()
|
||||
|
||||
* Hook methods
|
||||
|
||||
|
|
@ -261,15 +289,26 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
usually handled on the character level:
|
||||
|
||||
- at_init()
|
||||
- at_first_save()
|
||||
- at_access()
|
||||
- at_cmdset_get(**kwargs)
|
||||
- at_password_change(**kwargs)
|
||||
- at_first_login()
|
||||
- at_pre_login()
|
||||
- at_post_login(session=None)
|
||||
- at_disconnect()
|
||||
- at_failed_login(session, **kwargs)
|
||||
- at_disconnect(reason=None, **kwargs)
|
||||
- at_post_disconnect(**kwargs)
|
||||
- at_message_receive()
|
||||
- at_message_send()
|
||||
- at_server_reload()
|
||||
- at_server_shutdown()
|
||||
- at_look(target=None, session=None, **kwargs)
|
||||
- at_post_create_character(character, **kwargs)
|
||||
- at_post_add_character(char)
|
||||
- at_post_remove_character(char)
|
||||
- at_pre_channel_msg(message, channel, senders=None, **kwargs)
|
||||
- at_post_chnnel_msg(message, channel, senders=None, **kwargs)
|
||||
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@
|
|||
|
||||
|
||||
import django.core.validators
|
||||
import evennia.accounts.manager
|
||||
from django.db import migrations, models
|
||||
|
||||
import evennia.accounts.manager
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
|
|
|
|||
|
|
@ -3,14 +3,18 @@
|
|||
from random import randint
|
||||
from unittest import TestCase
|
||||
|
||||
import evennia
|
||||
from django.test import override_settings
|
||||
from evennia.accounts.accounts import (AccountSessionHandler, DefaultAccount,
|
||||
DefaultGuest)
|
||||
from mock import MagicMock, Mock, patch
|
||||
|
||||
import evennia
|
||||
from evennia.accounts.accounts import (
|
||||
AccountSessionHandler,
|
||||
DefaultAccount,
|
||||
DefaultGuest,
|
||||
)
|
||||
from evennia.utils import create
|
||||
from evennia.utils.test_resources import BaseEvenniaTest
|
||||
from evennia.utils.utils import uses_database
|
||||
from mock import MagicMock, Mock, patch
|
||||
|
||||
|
||||
class TestAccountSessionHandler(TestCase):
|
||||
|
|
|
|||
|
|
@ -5,11 +5,10 @@ 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.locks.lockhandler import LockException
|
||||
|
|
@ -2831,8 +2830,12 @@ class CmdExamine(ObjManipCommand):
|
|||
_FUNCPARSER = funcparser.FuncParser(settings.FUNCPARSER_OUTGOING_MESSAGES_MODULES)
|
||||
|
||||
key, category, value = attr.db_key, attr.db_category, attr.value
|
||||
valuetype = ""
|
||||
if value is None and attr.strvalue is not None:
|
||||
value = attr.strvalue
|
||||
valuetype = " |B[strvalue]|n"
|
||||
typ = self._get_attribute_value_type(value)
|
||||
typ = f" |B[type: {typ}]|n" if typ else ""
|
||||
typ = f" |B[type:{typ}]|n{valuetype}" if typ else f"{valuetype}"
|
||||
value = utils.to_str(value)
|
||||
value = _FUNCPARSER.parse(ansi_raw(value), escape=True)
|
||||
return (
|
||||
|
|
@ -2846,8 +2849,12 @@ class CmdExamine(ObjManipCommand):
|
|||
_FUNCPARSER = funcparser.FuncParser(settings.FUNCPARSER_OUTGOING_MESSAGES_MODULES)
|
||||
|
||||
key, category, value = attr.db_key, attr.db_category, attr.value
|
||||
valuetype = ""
|
||||
if value is None and attr.strvalue is not None:
|
||||
value = attr.strvalue
|
||||
valuetype = " |B[strvalue]|n"
|
||||
typ = self._get_attribute_value_type(value)
|
||||
typ = f" |B[type: {typ}]|n" if typ else ""
|
||||
typ = f" |B[type: {typ}]|n{valuetype}" if typ else f"{valuetype}"
|
||||
value = utils.to_str(value)
|
||||
value = _FUNCPARSER.parse(ansi_raw(value), escape=True)
|
||||
value = utils.crop(value)
|
||||
|
|
@ -3293,7 +3300,7 @@ class CmdFind(COMMAND_DEFAULT_CLASS):
|
|||
if "loc" in self.switches and not is_account and result.location:
|
||||
string += (
|
||||
f" (|wlocation|n: |g{result.location.get_display_name(caller)}"
|
||||
f"{result.get_extra_display_name_info(caller)}|n)"
|
||||
f"{result.location.get_extra_display_name_info(caller)}|n)"
|
||||
)
|
||||
else:
|
||||
# Not an account/dbref search but a wider search; build a queryset.
|
||||
|
|
@ -3439,7 +3446,7 @@ class ScriptEvMore(EvMore):
|
|||
if (hasattr(script, "obj") and script.obj)
|
||||
else "<Global>"
|
||||
),
|
||||
script.key,
|
||||
script.db_key,
|
||||
script.interval if script.interval > 0 else "--",
|
||||
nextrep,
|
||||
rept,
|
||||
|
|
@ -3460,17 +3467,20 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
|
|||
script[/start||stop] <obj> = [<script.path or script-key>]
|
||||
|
||||
Switches:
|
||||
start - start/unpause an existing script's timer.
|
||||
stop - stops an existing script's timer
|
||||
pause - pause a script's timer
|
||||
start - start/unpause an existing script's timer.
|
||||
stop - stops an existing script's timer
|
||||
pause - pause a script's timer
|
||||
delete - deletes script. This will also stop the timer as needed
|
||||
|
||||
Examples:
|
||||
script - list all scripts
|
||||
script foo.bar.Script - create a new global Script
|
||||
script/pause foo.bar.Script - pause global script
|
||||
script scriptname|#dbref - examine named existing global script
|
||||
script/delete #dbref[-#dbref] - delete script or range by #dbref
|
||||
script - list all scripts
|
||||
script key:foo.bar.Script - create a new global Script with typeclass
|
||||
and key 'key'
|
||||
script foo.bar.Script - create a new global Script with typeclass
|
||||
(key taken from typeclass or auto-generated)
|
||||
script/pause foo.bar.Script - pause global script
|
||||
script typeclass|name|#dbref - examine named existing global script
|
||||
script/delete #dbref[-#dbref] - delete script or range by #dbref
|
||||
|
||||
script myobj = - list all scripts on object
|
||||
script myobj = foo.bar.Script - create and assign script to object
|
||||
|
|
@ -3495,14 +3505,13 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
key = "@scripts"
|
||||
aliases = ["@script"]
|
||||
switch_options = ("create", "start", "stop", "pause", "delete")
|
||||
switch_options = ("start", "stop", "pause", "delete")
|
||||
locks = "cmd:perm(scripts) or perm(Builder)"
|
||||
help_category = "System"
|
||||
|
||||
excluded_typeclass_paths = ["evennia.prototypes.prototypes.DbPrototype"]
|
||||
|
||||
switch_mapping = {
|
||||
"create": "|gCreated|n",
|
||||
"start": "|gStarted|n",
|
||||
"stop": "|RStopped|n",
|
||||
"pause": "|Paused|n",
|
||||
|
|
@ -3511,21 +3520,32 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
|
|||
# never show these script types
|
||||
hide_script_paths = ("evennia.prototypes.prototypes.DbPrototype",)
|
||||
|
||||
def _search_script(self, args):
|
||||
# test first if this is a script match
|
||||
scripts = ScriptDB.objects.get_all_scripts(key=args).exclude(
|
||||
db_typeclass_path__in=self.hide_script_paths
|
||||
)
|
||||
if scripts:
|
||||
return scripts
|
||||
# try typeclass path
|
||||
def _search_script(self):
|
||||
|
||||
# see if a dbref was provided
|
||||
if dbref(self.typeclass_query):
|
||||
scripts = ScriptDB.objects.get_all_scripts(self.typeclass_query)
|
||||
if scripts:
|
||||
return scripts
|
||||
self.caller.msg(f"No script found with dbref {self.typeclass_query}")
|
||||
raise InterruptCommand
|
||||
|
||||
# if we provided a key, we must find an exact match, otherwise we're creating that anew
|
||||
if self.key_query:
|
||||
return ScriptDB.objects.filter(
|
||||
db_key__iexact=self.key_query, db_typeclass_path__iendswith=self.typeclass_query
|
||||
).exclude(db_typeclass_path__in=self.hide_script_paths)
|
||||
|
||||
# the more general case - try typeclass path
|
||||
scripts = (
|
||||
ScriptDB.objects.filter(db_typeclass_path__iendswith=args)
|
||||
ScriptDB.objects.filter(db_typeclass_path__iendswith=self.typeclass_query)
|
||||
.exclude(db_typeclass_path__in=self.hide_script_paths)
|
||||
.order_by("id")
|
||||
)
|
||||
if scripts:
|
||||
return scripts
|
||||
|
||||
args = self.typeclass_query
|
||||
if "-" in args:
|
||||
# may be a dbref-range
|
||||
val1, val2 = (dbref(part.strip()) for part in args.split("-", 1))
|
||||
|
|
@ -3538,6 +3558,29 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
|
|||
if scripts:
|
||||
return scripts
|
||||
|
||||
def parse(self):
|
||||
super().parse()
|
||||
|
||||
if not self.args:
|
||||
return
|
||||
|
||||
def _separate_key_typeclass(part):
|
||||
part1, *part2 = part.split(":", 1)
|
||||
return (part1, part2[0]) if part2 else (None, part1)
|
||||
|
||||
if self.rhs:
|
||||
# arg with "="
|
||||
self.obj_query = self.lhs
|
||||
self.key_query, self.typeclass_query = _separate_key_typeclass(self.rhs)
|
||||
elif self.rhs is not None:
|
||||
# an empty "="
|
||||
self.obj_query = self.lhs
|
||||
self.key_query, self.typeclass_query = None, None
|
||||
else:
|
||||
# arg without "="
|
||||
self.obj_query = None
|
||||
self.key_query, self.typeclass_query = _separate_key_typeclass(self.args)
|
||||
|
||||
def func(self):
|
||||
"""implement method"""
|
||||
|
||||
|
|
@ -3553,20 +3596,8 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
|
|||
return
|
||||
|
||||
# find script or object to operate on
|
||||
scripts, obj = None, None
|
||||
if self.rhs:
|
||||
obj_query = self.lhs
|
||||
script_query = self.rhs
|
||||
elif self.rhs is not None:
|
||||
# an empty "="
|
||||
obj_query = self.lhs
|
||||
script_query = None
|
||||
else:
|
||||
obj_query = None
|
||||
script_query = self.args
|
||||
|
||||
scripts = self._search_script(script_query) if script_query else None
|
||||
objects = caller.search(obj_query, quiet=True) if obj_query else None
|
||||
scripts = self._search_script() if self.typeclass_query else None
|
||||
objects = caller.search(self.obj_query, quiet=True) if self.obj_query else None
|
||||
obj = objects[0] if objects else None
|
||||
|
||||
if not self.switches:
|
||||
|
|
@ -3575,7 +3606,7 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
|
|||
# we have an object
|
||||
if self.rhs:
|
||||
# creation mode
|
||||
if obj.scripts.add(self.rhs, autostart=True):
|
||||
if obj.scripts.add(self.typeclass_query, key=self.key_query, autostart=True):
|
||||
caller.msg(
|
||||
f"Script |w{self.rhs}|n successfully added and "
|
||||
f"started on {obj.get_display_name(caller)}."
|
||||
|
|
@ -3603,7 +3634,9 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
|
|||
else:
|
||||
# create global script
|
||||
try:
|
||||
new_script = create.create_script(self.args)
|
||||
new_script = create.create_script(
|
||||
typeclass=self.typeclass_query, key=self.key_query
|
||||
)
|
||||
except ImportError:
|
||||
logger.log_trace()
|
||||
new_script = None
|
||||
|
|
@ -3922,7 +3955,7 @@ class CmdTag(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
key = "@tag"
|
||||
aliases = ["@tags"]
|
||||
options = ("search", "del")
|
||||
switch_options = ("search", "del")
|
||||
locks = "cmd:perm(tag) or perm(Builder)"
|
||||
help_category = "Building"
|
||||
arg_regex = r"(/\w+?(\s|$))|\s|$"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,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
|
||||
from evennia.comms.comms import DefaultChannel
|
||||
|
|
@ -1412,9 +1413,7 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
|
|||
message = f"{caller.key} {message.strip(':').strip()}"
|
||||
|
||||
# create the persistent message object
|
||||
target_perms = " or ".join(
|
||||
[f"id({target.id})" for target in targets + [caller]]
|
||||
)
|
||||
target_perms = " or ".join([f"id({target.id})" for target in targets + [caller]])
|
||||
create.create_message(
|
||||
caller,
|
||||
message,
|
||||
|
|
|
|||
|
|
@ -14,13 +14,10 @@ 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,
|
||||
|
|
@ -32,14 +29,7 @@ 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
|
||||
|
|
@ -48,6 +38,8 @@ 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
|
||||
|
|
@ -446,7 +438,7 @@ class TestCmdTasks(BaseEvenniaCommandTest):
|
|||
args = f"/pause {self.task.get_id()}"
|
||||
wanted_msg = "Pause task 1 with completion date"
|
||||
cmd_result = self.call(system.CmdTasks(), args, wanted_msg)
|
||||
self.assertRegex(cmd_result, " \(func_test_cmd_tasks\) ")
|
||||
self.assertRegex(cmd_result, r" \(func_test_cmd_tasks\) ")
|
||||
self.char1.execute_cmd("y")
|
||||
self.assertTrue(self.task.paused)
|
||||
self.task_handler.clock.advance(self.timedelay + 1)
|
||||
|
|
@ -455,7 +447,7 @@ class TestCmdTasks(BaseEvenniaCommandTest):
|
|||
self.assertTrue(self.task.exists())
|
||||
wanted_msg = "Unpause task 1 with completion date"
|
||||
cmd_result = self.call(system.CmdTasks(), args, wanted_msg)
|
||||
self.assertRegex(cmd_result, " \(func_test_cmd_tasks\) ")
|
||||
self.assertRegex(cmd_result, r" \(func_test_cmd_tasks\) ")
|
||||
self.char1.execute_cmd("y")
|
||||
# verify task continues after unpause
|
||||
self.task_handler.clock.advance(1)
|
||||
|
|
@ -465,7 +457,7 @@ class TestCmdTasks(BaseEvenniaCommandTest):
|
|||
args = f"/do_task {self.task.get_id()}"
|
||||
wanted_msg = "Do_task task 1 with completion date"
|
||||
cmd_result = self.call(system.CmdTasks(), args, wanted_msg)
|
||||
self.assertRegex(cmd_result, " \(func_test_cmd_tasks\) ")
|
||||
self.assertRegex(cmd_result, r" \(func_test_cmd_tasks\) ")
|
||||
self.char1.execute_cmd("y")
|
||||
self.assertFalse(self.task.exists())
|
||||
|
||||
|
|
@ -473,7 +465,7 @@ class TestCmdTasks(BaseEvenniaCommandTest):
|
|||
args = f"/remove {self.task.get_id()}"
|
||||
wanted_msg = "Remove task 1 with completion date"
|
||||
cmd_result = self.call(system.CmdTasks(), args, wanted_msg)
|
||||
self.assertRegex(cmd_result, " \(func_test_cmd_tasks\) ")
|
||||
self.assertRegex(cmd_result, r" \(func_test_cmd_tasks\) ")
|
||||
self.char1.execute_cmd("y")
|
||||
self.assertFalse(self.task.exists())
|
||||
|
||||
|
|
@ -481,7 +473,7 @@ class TestCmdTasks(BaseEvenniaCommandTest):
|
|||
args = f"/call {self.task.get_id()}"
|
||||
wanted_msg = "Call task 1 with completion date"
|
||||
cmd_result = self.call(system.CmdTasks(), args, wanted_msg)
|
||||
self.assertRegex(cmd_result, " \(func_test_cmd_tasks\) ")
|
||||
self.assertRegex(cmd_result, r" \(func_test_cmd_tasks\) ")
|
||||
self.char1.execute_cmd("y")
|
||||
# make certain the task is still active
|
||||
self.assertTrue(self.task.active())
|
||||
|
|
@ -493,7 +485,7 @@ class TestCmdTasks(BaseEvenniaCommandTest):
|
|||
args = f"/cancel {self.task.get_id()}"
|
||||
wanted_msg = "Cancel task 1 with completion date"
|
||||
cmd_result = self.call(system.CmdTasks(), args, wanted_msg)
|
||||
self.assertRegex(cmd_result, " \(func_test_cmd_tasks\) ")
|
||||
self.assertRegex(cmd_result, r" \(func_test_cmd_tasks\) ")
|
||||
self.char1.execute_cmd("y")
|
||||
self.assertTrue(self.task.exists())
|
||||
self.assertFalse(self.task.active())
|
||||
|
|
@ -797,7 +789,7 @@ class TestBuilding(BaseEvenniaCommandTest):
|
|||
self.call(
|
||||
building.CmdExamine(),
|
||||
"self/test2",
|
||||
"Attribute Char/test2 [category=None]:\n\nthis is a \$random() value.",
|
||||
"Attribute Char/test2 [category=None]:\n\nthis is a \\$random() value.",
|
||||
)
|
||||
|
||||
self.room1.scripts.add(self.script.__class__)
|
||||
|
|
@ -805,6 +797,13 @@ class TestBuilding(BaseEvenniaCommandTest):
|
|||
self.account.scripts.add(self.script.__class__)
|
||||
self.call(building.CmdExamine(), "*TestAccount")
|
||||
|
||||
self.char1.attributes.add("strattr", "testval", strattr=True)
|
||||
self.call(
|
||||
building.CmdExamine(),
|
||||
"self/strattr",
|
||||
"Attribute Char/strattr [category=None] [strvalue]:\n\ntestval",
|
||||
)
|
||||
|
||||
def test_set_obj_alias(self):
|
||||
self.call(building.CmdSetObjAlias(), "Obj =", "Cleared aliases from Obj")
|
||||
self.call(
|
||||
|
|
@ -1654,17 +1653,17 @@ class TestBuilding(BaseEvenniaCommandTest):
|
|||
)
|
||||
|
||||
def test_script_multi_delete(self):
|
||||
script1 = create.create_script()
|
||||
script2 = create.create_script()
|
||||
script3 = create.create_script()
|
||||
script1 = create.create_script(key="script1")
|
||||
script2 = create.create_script(key="script2")
|
||||
script3 = create.create_script(key="script3")
|
||||
|
||||
self.call(
|
||||
building.CmdScripts(),
|
||||
"/delete #{}-#{}".format(script1.id, script3.id),
|
||||
(
|
||||
f"Global Script Deleted - #{script1.id} (evennia.scripts.scripts.DefaultScript)|"
|
||||
f"Global Script Deleted - #{script2.id} (evennia.scripts.scripts.DefaultScript)|"
|
||||
f"Global Script Deleted - #{script3.id} (evennia.scripts.scripts.DefaultScript)"
|
||||
f"Global Script Deleted - script1 (evennia.scripts.scripts.DefaultScript)|"
|
||||
f"Global Script Deleted - script2 (evennia.scripts.scripts.DefaultScript)|"
|
||||
f"Global Script Deleted - script3 (evennia.scripts.scripts.DefaultScript)"
|
||||
),
|
||||
inputs=["y"],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,10 +5,11 @@ Base typeclass for in-game Channels.
|
|||
|
||||
import re
|
||||
|
||||
import evennia
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.urls import reverse
|
||||
from django.utils.text import slugify
|
||||
|
||||
import evennia
|
||||
from evennia.comms.managers import ChannelManager
|
||||
from evennia.comms.models import ChannelDB
|
||||
from evennia.typeclasses.models import TypeclassBase
|
||||
|
|
@ -50,6 +51,70 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
|
|||
the account-level `channel` command is used. If you were to rename that command you must
|
||||
tweak the output to something like `yourchannelcommandname {channelname} = $1`.
|
||||
|
||||
* Properties:
|
||||
mutelist
|
||||
banlist
|
||||
wholist
|
||||
|
||||
* Working methods:
|
||||
get_log_filename()
|
||||
set_log_filename(filename)
|
||||
has_connection(account) - check if the given account listens to this channel
|
||||
connect(account) - connect account to this channel
|
||||
disconnect(account) - disconnect account from channel
|
||||
access(access_obj, access_type='listen', default=False) - check the
|
||||
access on this channel (default access_type is listen)
|
||||
create(key, creator=None, *args, **kwargs)
|
||||
delete() - delete this channel
|
||||
message_transform(msg, emit=False, prefix=True,
|
||||
sender_strings=None, external=False) - called by
|
||||
the comm system and triggers the hooks below
|
||||
msg(msgobj, header=None, senders=None, sender_strings=None,
|
||||
persistent=None, online=False, emit=False, external=False) - main
|
||||
send method, builds and sends a new message to channel.
|
||||
tempmsg(msg, header=None, senders=None) - wrapper for sending non-persistent
|
||||
messages.
|
||||
distribute_message(msg, online=False) - send a message to all
|
||||
connected accounts on channel, optionally sending only
|
||||
to accounts that are currently online (optimized for very large sends)
|
||||
mute(subscriber, **kwargs)
|
||||
unmute(subscriber, **kwargs)
|
||||
ban(target, **kwargs)
|
||||
unban(target, **kwargs)
|
||||
add_user_channel_alias(user, alias, **kwargs)
|
||||
remove_user_channel_alias(user, alias, **kwargs)
|
||||
|
||||
|
||||
Useful hooks:
|
||||
at_channel_creation() - called once, when the channel is created
|
||||
basetype_setup()
|
||||
at_init()
|
||||
at_first_save()
|
||||
channel_prefix() - how the channel should be
|
||||
prefixed when returning to user. Returns a string
|
||||
format_senders(senders) - should return how to display multiple
|
||||
senders to a channel
|
||||
pose_transform(msg, sender_string) - should detect if the
|
||||
sender is posing, and if so, modify the string
|
||||
format_external(msg, senders, emit=False) - format messages sent
|
||||
from outside the game, like from IRC
|
||||
format_message(msg, emit=False) - format the message body before
|
||||
displaying it to the user. 'emit' generally means that the
|
||||
message should not be displayed with the sender's name.
|
||||
channel_prefix()
|
||||
|
||||
pre_join_channel(joiner) - if returning False, abort join
|
||||
post_join_channel(joiner) - called right after successful join
|
||||
pre_leave_channel(leaver) - if returning False, abort leave
|
||||
post_leave_channel(leaver) - called right after successful leave
|
||||
at_pre_msg(message, **kwargs)
|
||||
at_post_msg(message, **kwargs)
|
||||
web_get_admin_url()
|
||||
web_get_create_url()
|
||||
web_get_detail_url()
|
||||
web_get_update_url()
|
||||
web_get_delete_url()
|
||||
|
||||
"""
|
||||
|
||||
objects = ChannelManager()
|
||||
|
|
@ -856,7 +921,7 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
|
|||
# Used by Django Sites/Admin
|
||||
get_absolute_url = web_get_detail_url
|
||||
|
||||
# TODO Evennia 1.0+ removed hooks. Remove in 1.1.
|
||||
# TODO Evennia 1.0+ removed hooks. Remove in 5.0
|
||||
def message_transform(self, *args, **kwargs):
|
||||
raise RuntimeError(
|
||||
"Channel.message_transform is no longer used in 1.0+. "
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ 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
|
||||
|
||||
from evennia.comms import managers
|
||||
from evennia.locks.lockhandler import LockHandler
|
||||
from evennia.typeclasses.models import TypedObject
|
||||
|
|
|
|||
|
|
@ -15,16 +15,17 @@ class BaseComponent(type):
|
|||
This is the metaclass for components,
|
||||
responsible for registering components to the listing.
|
||||
"""
|
||||
|
||||
def __new__(cls, name, parents, attrs):
|
||||
"""
|
||||
Every class that uses this metaclass will be registered
|
||||
as a component in the Component Listing using its name.
|
||||
All of them require a unique name.
|
||||
"""
|
||||
attrs_name = attrs.get('name')
|
||||
attrs_name = attrs.get("name")
|
||||
if attrs_name and not COMPONENT_LISTING.get(attrs_name):
|
||||
new_fields = {}
|
||||
attrs['_fields'] = new_fields
|
||||
attrs["_fields"] = new_fields
|
||||
for parent in parents:
|
||||
_parent_fields = getattr(parent, "_fields")
|
||||
if _parent_fields:
|
||||
|
|
|
|||
|
|
@ -87,23 +87,23 @@ class TestComponents(EvenniaTest):
|
|||
|
||||
def test_character_components_set_fields_properly(self):
|
||||
test_a_fields = self.char1.test_a._fields
|
||||
self.assertIn('my_int', test_a_fields)
|
||||
self.assertIn('my_list', test_a_fields)
|
||||
self.assertIn("my_int", test_a_fields)
|
||||
self.assertIn("my_list", test_a_fields)
|
||||
self.assertEqual(len(test_a_fields), 2)
|
||||
|
||||
test_b_fields = self.char1.test_b._fields
|
||||
self.assertIn('my_int', test_b_fields)
|
||||
self.assertIn('my_list', test_b_fields)
|
||||
self.assertIn('default_tag', test_b_fields)
|
||||
self.assertIn('single_tag', test_b_fields)
|
||||
self.assertIn('multiple_tags', test_b_fields)
|
||||
self.assertIn('default_single_tag', test_b_fields)
|
||||
self.assertIn("my_int", test_b_fields)
|
||||
self.assertIn("my_list", test_b_fields)
|
||||
self.assertIn("default_tag", test_b_fields)
|
||||
self.assertIn("single_tag", test_b_fields)
|
||||
self.assertIn("multiple_tags", test_b_fields)
|
||||
self.assertIn("default_single_tag", test_b_fields)
|
||||
self.assertEqual(len(test_b_fields), 6)
|
||||
|
||||
test_ic_a_fields = self.char1.ic_a._fields
|
||||
self.assertIn('my_int', test_ic_a_fields)
|
||||
self.assertIn('my_list', test_ic_a_fields)
|
||||
self.assertIn('my_other_int', test_ic_a_fields)
|
||||
self.assertIn("my_int", test_ic_a_fields)
|
||||
self.assertIn("my_list", test_ic_a_fields)
|
||||
self.assertIn("my_other_int", test_ic_a_fields)
|
||||
self.assertEqual(len(test_ic_a_fields), 3)
|
||||
|
||||
def test_inherited_typeclass_does_not_include_child_class_components(self):
|
||||
|
|
|
|||
130
evennia/contrib/base_systems/ingame_reports/README.md
Normal file
130
evennia/contrib/base_systems/ingame_reports/README.md
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
# In-Game Reporting System
|
||||
|
||||
Contrib by InspectorCaracal, 2024
|
||||
|
||||
This contrib provides an in-game reports system, handling bug reports, player reports, and idea submissions by default. It also supports adding your own types of reports, or removing any of the default report types.
|
||||
|
||||
Each type of report has its own command for submitting new reports, and an admin command is also provided for managing the reports through a menu.
|
||||
|
||||
## Installation
|
||||
|
||||
To install the reports contrib, just add the provided cmdset to your default AccountCmdSet:
|
||||
|
||||
```python
|
||||
# in commands/default_cmdset.py
|
||||
|
||||
from evennia.contrib.base_systems.ingame_reports import ReportsCmdSet
|
||||
|
||||
class AccountCmdSet(default_cmds.AccountCmdSet):
|
||||
# ...
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
# ...
|
||||
self.add(ReportsCmdSet)
|
||||
```
|
||||
|
||||
The contrib also has two optional settings: `INGAME_REPORT_TYPES` and `INGAME_REPORT_STATUS_TAGS`.
|
||||
|
||||
The `INGAME_REPORT_TYPES` setting is covered in detail in the section "Adding new types of reports".
|
||||
|
||||
The `INGAME_REPORT_STATUS_TAGS` setting is covered in the section "Managing reports".
|
||||
|
||||
## Usage
|
||||
|
||||
By default, the following report types are available:
|
||||
|
||||
* Bugs: Report bugs encountered during gameplay.
|
||||
* Ideas: Submit suggestions for game improvement.
|
||||
* Players: Report inappropriate player behavior.
|
||||
|
||||
Players can submit new reports through the command for each report type, and staff are given access to a report-management command and menu.
|
||||
|
||||
### Submitting reports
|
||||
|
||||
Players can submit reports using the following commands:
|
||||
|
||||
* `bug <text>` - Files a bug report. An optional target can be included - `bug <target> = <text>` - making it easier for devs/builders to track down issues.
|
||||
* `report <player> = <text>` - Reports a player for inappropriate or rule-breaking behavior. *Requires* a target to be provided - it searches among accounts by default.
|
||||
* `idea <text>` - Submits a general suggestion, with no target. It also has an alias of `ideas` which allows you to view all of your submitted ideas.
|
||||
|
||||
### Managing reports
|
||||
|
||||
The `manage reports` command allows staff to review and manage the various types of reports by launching a management menu.
|
||||
|
||||
This command will dynamically add aliases to itself based on the types of reports available, with each command string launching a menu for that particular report type. The aliases are built on the pattern `manage <report type>s` - by default, this means it makes `manage bugs`, `manage players`, and `manage ideas` available along with the default `manage reports`, and that e.g. `manage bugs` will launch the management menu for `bug`-type reports.
|
||||
|
||||
Aside from reading over existing reports, the menu allows you to change the status of any given report. By default, the contrib includes two different status tags: `in progress` and `closed`.
|
||||
|
||||
> Note: A report is created with no status tags, which is considered "open"
|
||||
|
||||
If you want a different set of statuses for your reports, you can define the `INGAME_REPORT_STATUS_TAGS` to your list of statuses.
|
||||
|
||||
**Example**
|
||||
|
||||
```python
|
||||
# in server/conf/settings.py
|
||||
|
||||
# this will allow for the statuses of 'in progress', 'rejected', and 'completed', without the contrib-default of 'closed'
|
||||
INGAME_REPORT_STATUS_TAGS = ('in progress', 'rejected', 'completed')
|
||||
```
|
||||
|
||||
### Adding new types of reports
|
||||
|
||||
The contrib is designed to make adding new types of reports to the system as simple as possible, requiring only two steps:
|
||||
|
||||
1. Update your settings file to include an `INGAME_REPORT_TYPES` setting.
|
||||
2. Create and add a new `ReportCmd` to your command set.
|
||||
|
||||
#### Update your settings
|
||||
|
||||
The contrib optionally references `INGAME_REPORT_TYPES` in your settings.py to see which types of reports can be managed. If you want to change the available report types, you'll need to define this setting.
|
||||
|
||||
```python
|
||||
# in server/conf/settings.py
|
||||
|
||||
# this will include the contrib's report types as well as a custom 'complaint' report type
|
||||
INGAME_REPORT_TYPES = ('bugs', 'ideas', 'players', 'complaints')
|
||||
```
|
||||
|
||||
You can also use this setting to remove any of the contrib's report types - the contrib will respect this setting when building its cmdset with no additional steps.
|
||||
|
||||
```python
|
||||
# in server/conf/settings.py
|
||||
|
||||
# this redefines the setting to not include 'ideas', so the ideas command and reports won't be available
|
||||
INGAME_REPORT_TYPES = ('bugs', 'players')
|
||||
```
|
||||
|
||||
#### Create a new ReportCmd
|
||||
|
||||
`ReportCmdBase` is a parent command class which comes with the main functionality for submitting reports. Creating a new reporting command is as simple as inheriting from this class and defining a couple of class attributes.
|
||||
|
||||
* `key` - This is the same as for any other command, setting the command's usable key. It also acts as the report type if that isn't explicitly set.
|
||||
* `report_type` - The type of report this command is for (e.g. `player`). You only need to set it if you want a different string from the key.
|
||||
* `report_locks` - The locks you want applied to the created reports. Defaults to `"read:pperm(Admin)"`
|
||||
* `success_msg` - The string which is sent to players after submitting a report of this type. Defaults to `"Your report has been filed."`
|
||||
* `require_target`: Set to `True` if your report type requires a target (e.g. player reports).
|
||||
|
||||
> Note: The contrib's own commands - `CmdBug`, `CmdIdea`, and `CmdReport` - are implemented the same way, so you can review them as examples.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
from evennia.contrib.base_systems.ingame_reports.reports import ReportCmdBase
|
||||
|
||||
class CmdCustomReport(ReportCmdBase):
|
||||
"""
|
||||
file a custom report
|
||||
|
||||
Usage:
|
||||
customreport <message>
|
||||
|
||||
This is a custom report type.
|
||||
"""
|
||||
|
||||
key = "customreport"
|
||||
report_type = "custom"
|
||||
success_message = "You have successfully filed a custom report."
|
||||
```
|
||||
|
||||
Add this new command to your default cmdset to enable filing your new report type.
|
||||
1
evennia/contrib/base_systems/ingame_reports/__init__.py
Normal file
1
evennia/contrib/base_systems/ingame_reports/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from .reports import ReportsCmdSet
|
||||
134
evennia/contrib/base_systems/ingame_reports/menu.py
Normal file
134
evennia/contrib/base_systems/ingame_reports/menu.py
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
"""
|
||||
The report-management menu module.
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from evennia.comms.models import Msg
|
||||
from evennia.utils import logger
|
||||
from evennia.utils.utils import crop, datetime_format, is_iter, iter_to_str
|
||||
|
||||
# the number of reports displayed on each page
|
||||
_REPORTS_PER_PAGE = 10
|
||||
|
||||
_REPORT_STATUS_TAGS = ("closed", "in progress")
|
||||
if hasattr(settings, "INGAME_REPORT_STATUS_TAGS"):
|
||||
if is_iter(settings.INGAME_REPORT_STATUS_TAGS):
|
||||
_REPORT_STATUS_TAGS = settings.INGAME_REPORT_STATUS_TAGS
|
||||
else:
|
||||
logger.log_warn(
|
||||
"The 'INGAME_REPORT_STATUS_TAGS' setting must be an iterable of strings; falling back to defaults."
|
||||
)
|
||||
|
||||
|
||||
def menunode_list_reports(caller, raw_string, **kwargs):
|
||||
"""Paginates and lists out reports for the provided hub"""
|
||||
hub = caller.ndb._evmenu.hub
|
||||
|
||||
page = kwargs.get("page", 0)
|
||||
start = page * _REPORTS_PER_PAGE
|
||||
end = start + _REPORTS_PER_PAGE
|
||||
report_slice = report_list[start:end]
|
||||
hub_name = " ".join(hub.key.split("_")).title()
|
||||
text = f"Managing {hub_name}"
|
||||
|
||||
if not (report_list := getattr(caller.ndb._evmenu, "report_list", None)):
|
||||
report_list = Msg.objects.search_message(receiver=hub).order_by("db_date_created")
|
||||
caller.ndb._evmenu.report_list = report_list
|
||||
# allow the menu to filter print-outs by status
|
||||
if kwargs.get("status"):
|
||||
new_report_list = report_list.filter(db_tags__db_key=kwargs["status"])
|
||||
# we don't filter reports if there are no reports under that filter
|
||||
if not new_report_list:
|
||||
text = f"(No {kwargs['status']} reports)\n{text}"
|
||||
else:
|
||||
report_list = new_report_list
|
||||
text = f"Managing {kwargs['status']} {hub_name}"
|
||||
else:
|
||||
report_list = report_list.exclude(db_tags__db_key="closed")
|
||||
|
||||
# filter by lock access
|
||||
report_list = [msg for msg in report_list if msg.access(caller, "read")]
|
||||
|
||||
# this will catch both no reports filed and no permissions
|
||||
if not report_list:
|
||||
return "There is nothing there for you to manage.", {}
|
||||
|
||||
options = [
|
||||
{
|
||||
"desc": f"{datetime_format(report.date_created)} - {crop(report.message, 50)}",
|
||||
"goto": ("menunode_manage_report", {"report": report}),
|
||||
}
|
||||
for report in report_slice
|
||||
]
|
||||
options.append(
|
||||
{
|
||||
"key": ("|uF|nilter by status", "filter", "status", "f"),
|
||||
"goto": "menunode_choose_filter",
|
||||
}
|
||||
)
|
||||
if start > 0:
|
||||
options.append(
|
||||
{
|
||||
"key": (f"|uP|nrevious {_REPORTS_PER_PAGE}", "previous", "prev", "p"),
|
||||
"goto": (
|
||||
"menunode_list_reports",
|
||||
{"page": max(start - _REPORTS_PER_PAGE, 0) // _REPORTS_PER_PAGE},
|
||||
),
|
||||
}
|
||||
)
|
||||
if end < len(report_list):
|
||||
options.append(
|
||||
{
|
||||
"key": (f"|uN|next {_REPORTS_PER_PAGE}", "next", "n"),
|
||||
"goto": (
|
||||
"menunode_list_reports",
|
||||
{"page": (start + _REPORTS_PER_PAGE) // _REPORTS_PER_PAGE},
|
||||
),
|
||||
}
|
||||
)
|
||||
return text, options
|
||||
|
||||
|
||||
def menunode_choose_filter(caller, raw_string, **kwargs):
|
||||
"""apply or clear a status filter to the main report view"""
|
||||
text = "View which reports?"
|
||||
# options for all the possible statuses
|
||||
options = [
|
||||
{"desc": status, "goto": ("menunode_list_reports", {"status": status})}
|
||||
for status in _REPORT_STATUS_TAGS
|
||||
]
|
||||
# no filter
|
||||
options.append({"desc": "All open reports", "goto": "menunode_list_reports"})
|
||||
return text, options
|
||||
|
||||
|
||||
def _report_toggle_tag(caller, raw_string, report, tag, **kwargs):
|
||||
"""goto callable to toggle a status tag on or off"""
|
||||
if tag in report.tags.all():
|
||||
report.tags.remove(tag)
|
||||
else:
|
||||
report.tags.add(tag)
|
||||
return ("menunode_manage_report", {"report": report})
|
||||
|
||||
|
||||
def menunode_manage_report(caller, raw_string, report, **kwargs):
|
||||
"""
|
||||
Read out the full report text and targets, and allow for changing the report's status.
|
||||
"""
|
||||
receivers = [r for r in report.receivers if r != caller.ndb._evmenu.hub]
|
||||
text = f"""\
|
||||
{report.message}
|
||||
{datetime_format(report.date_created)} by {iter_to_str(report.senders)}{' about '+iter_to_str(r.get_display_name(caller) for r in receivers) if receivers else ''}
|
||||
{iter_to_str(report.tags.all())}"""
|
||||
|
||||
options = []
|
||||
for tag in _REPORT_STATUS_TAGS:
|
||||
options.append(
|
||||
{
|
||||
"desc": f"{'Unmark' if tag in report.tags.all() else 'Mark' } as {tag}",
|
||||
"goto": (_report_toggle_tag, {"report": report, "tag": tag}),
|
||||
}
|
||||
)
|
||||
options.append({"desc": f"Manage another report", "goto": "menunode_list_reports"})
|
||||
return text, options
|
||||
315
evennia/contrib/base_systems/ingame_reports/reports.py
Normal file
315
evennia/contrib/base_systems/ingame_reports/reports.py
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
"""
|
||||
In-Game Reporting System
|
||||
|
||||
This contrib provides an in-game reporting system, with player-facing commands and a staff
|
||||
management interface.
|
||||
|
||||
# Installation
|
||||
|
||||
To install, just add the provided cmdset to your default AccountCmdSet:
|
||||
|
||||
# in commands/default_cmdset.py
|
||||
|
||||
from evennia.contrib.base_systems.ingame_reports import ReportsCmdSet
|
||||
|
||||
class AccountCmdSet(default_cmds.AccountCmdSet):
|
||||
# ...
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
# ...
|
||||
self.add(ReportsCmdSet)
|
||||
|
||||
# Features
|
||||
|
||||
The contrib provides three commands by default and their associated report types: `CmdBug`, `CmdIdea`,
|
||||
and `CmdReport` (which is for reporting other players).
|
||||
|
||||
The `ReportCmdBase` class holds most of the functionality for creating new reports, providing a
|
||||
convenient parent class for adding your own categories of reports.
|
||||
|
||||
The contrib can be further configured through two settings, `INGAME_REPORT_TYPES` and `INGAME_REPORT_STATUS_TAGS`
|
||||
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from evennia import CmdSet
|
||||
from evennia.utils import create, evmenu, logger, search
|
||||
from evennia.utils.utils import class_from_module, datetime_format, is_iter, iter_to_str
|
||||
from evennia.commands.default.muxcommand import MuxCommand
|
||||
from evennia.comms.models import Msg
|
||||
|
||||
from . import menu
|
||||
|
||||
_DEFAULT_COMMAND_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
||||
|
||||
# the default report types
|
||||
_REPORT_TYPES = ("bugs", "ideas", "players")
|
||||
if hasattr(settings, "INGAME_REPORT_TYPES"):
|
||||
if is_iter(settings.INGAME_REPORT_TYPES):
|
||||
_REPORT_TYPES = settings.INGAME_REPORT_TYPES
|
||||
else:
|
||||
logger.log_warn(
|
||||
"The 'INGAME_REPORT_TYPES' setting must be an iterable of strings; falling back to defaults."
|
||||
)
|
||||
|
||||
|
||||
def _get_report_hub(report_type):
|
||||
"""
|
||||
A helper function to retrieve the global script which acts as the hub for a given report type.
|
||||
|
||||
Args:
|
||||
report_type (str): The category of reports to retrieve the script for.
|
||||
|
||||
Returns:
|
||||
Script or None: The global script, or None if it couldn't be retrieved or created
|
||||
|
||||
Note: If no matching valid script exists, this function will attempt to create it.
|
||||
"""
|
||||
hub_key = f"{report_type}_reports"
|
||||
# NOTE: due to a regression in GLOBAL_SCRIPTS, we use search_script instead of the container
|
||||
if not (hub := search.search_script(hub_key)):
|
||||
hub = create.create_script(key=hub_key)
|
||||
return hub or None
|
||||
|
||||
|
||||
class CmdManageReports(_DEFAULT_COMMAND_CLASS):
|
||||
"""
|
||||
manage the various reports
|
||||
|
||||
Usage:
|
||||
manage [report type]
|
||||
|
||||
Available report types:
|
||||
bugs
|
||||
ideas
|
||||
players
|
||||
|
||||
Initializes a menu for reviewing and changing the status of current reports.
|
||||
"""
|
||||
|
||||
key = "manage reports"
|
||||
aliases = tuple(f"manage {report_type}" for report_type in _REPORT_TYPES)
|
||||
locks = "cmd:pperm(Admin)"
|
||||
|
||||
def get_help(self):
|
||||
"""Returns a help string containing the configured available report types"""
|
||||
|
||||
report_types = iter_to_str("\n ".join(_REPORT_TYPES))
|
||||
|
||||
helptext = f"""\
|
||||
manage the various reports
|
||||
|
||||
Usage:
|
||||
manage [report type]
|
||||
|
||||
Available report types:
|
||||
{report_types}
|
||||
|
||||
Initializes a menu for reviewing and changing the status of current reports.
|
||||
"""
|
||||
|
||||
return helptext
|
||||
|
||||
def func(self):
|
||||
report_type = self.cmdstring.split()[-1]
|
||||
if report_type == "reports":
|
||||
report_type = "players"
|
||||
if report_type not in _REPORT_TYPES:
|
||||
self.msg(f"'{report_type}' is not a valid report category.")
|
||||
return
|
||||
# remove the trailing s, just so everything reads nicer
|
||||
report_type = report_type[:-1]
|
||||
hub = _get_report_hub(report_type)
|
||||
if not hub:
|
||||
self.msg("You cannot manage that.")
|
||||
|
||||
evmenu.EvMenu(
|
||||
self.account, menu, startnode="menunode_list_reports", hub=hub, persistent=True
|
||||
)
|
||||
|
||||
|
||||
class ReportCmdBase(_DEFAULT_COMMAND_CLASS):
|
||||
"""
|
||||
A parent class for creating report commands. This help text may be displayed if
|
||||
your command's help text is not properly configured.
|
||||
"""
|
||||
|
||||
help_category = "reports"
|
||||
# defines what locks the reports generated by this command will have set
|
||||
report_locks = "read:pperm(Admin)"
|
||||
# determines if the report can be filed without a target
|
||||
require_target = False
|
||||
# the message sent to the reporter after the report has been created
|
||||
success_msg = "Your report has been filed."
|
||||
# the report type for this command, if different from the key
|
||||
report_type = None
|
||||
|
||||
def at_pre_cmd(self):
|
||||
"""validate that the needed hub script exists - if not, cancel the command"""
|
||||
hub = _get_report_hub(self.report_type or self.key)
|
||||
if not hub:
|
||||
# a return value of True from `at_pre_cmd` cancels the command
|
||||
return True
|
||||
self.hub = hub
|
||||
return super().at_pre_cmd()
|
||||
|
||||
def parse(self):
|
||||
"""
|
||||
Parse the target and message out of the arguments.
|
||||
|
||||
Override if you want different syntax, but make sure to assign `report_message` and `target_str`.
|
||||
"""
|
||||
# do the base MuxCommand parsing first
|
||||
super().parse()
|
||||
# split out the report message and target strings
|
||||
if self.rhs:
|
||||
self.report_message = self.rhs
|
||||
self.target_str = self.lhs
|
||||
else:
|
||||
self.report_message = self.lhs
|
||||
self.target_str = ""
|
||||
|
||||
def target_search(self, searchterm, **kwargs):
|
||||
"""
|
||||
Search for a target that matches the given search term. By default, does a normal search via the
|
||||
caller - a local object search for a Character, or an account search for an Account.
|
||||
|
||||
Args:
|
||||
searchterm (str) - The string to search for
|
||||
|
||||
Returns:
|
||||
result (Object, Account, or None) - the result of the search
|
||||
"""
|
||||
return self.caller.search(searchterm)
|
||||
|
||||
def create_report(self, *args, **kwargs):
|
||||
"""
|
||||
Creates the report. By default, this creates a Msg with any provided args and kwargs.
|
||||
|
||||
Returns:
|
||||
success (bool) - True if the report was created successfully, or False if there was an issue.
|
||||
"""
|
||||
return create.create_message(*args, **kwargs)
|
||||
|
||||
def func(self):
|
||||
hub = self.hub
|
||||
if not self.args:
|
||||
self.msg("You must provide a message.")
|
||||
return
|
||||
|
||||
target = None
|
||||
if self.target_str:
|
||||
target = self.target_search(self.target_str)
|
||||
if not target:
|
||||
return
|
||||
elif self.require_target:
|
||||
self.msg("You must include a target.")
|
||||
return
|
||||
|
||||
receivers = [hub]
|
||||
if target:
|
||||
receivers.append(target)
|
||||
|
||||
if self.create_report(
|
||||
self.account, self.report_message, receivers=receivers, locks=self.report_locks, tags=["report"]
|
||||
):
|
||||
# the report Msg was successfully created
|
||||
self.msg(self.success_msg)
|
||||
else:
|
||||
# something went wrong
|
||||
self.msg(
|
||||
"Something went wrong creating your report. Please try again later or contact staff directly."
|
||||
)
|
||||
|
||||
|
||||
# The commands below are the usable reporting commands
|
||||
|
||||
|
||||
class CmdBug(ReportCmdBase):
|
||||
"""
|
||||
file a bug
|
||||
|
||||
Usage:
|
||||
bug [<target> =] <message>
|
||||
|
||||
Note: If a specific object, location or character is bugged, please target it for the report.
|
||||
|
||||
Examples:
|
||||
bug hammer = This doesn't work as a crafting tool but it should
|
||||
bug every time I go through a door I get the message twice
|
||||
"""
|
||||
|
||||
key = "bug"
|
||||
report_locks = "read:pperm(Developer)"
|
||||
|
||||
|
||||
class CmdReport(ReportCmdBase):
|
||||
"""
|
||||
report a player
|
||||
|
||||
Usage:
|
||||
report <player> = <message>
|
||||
|
||||
All player reports will be reviewed.
|
||||
"""
|
||||
|
||||
key = "report"
|
||||
report_type = "player"
|
||||
require_target = True
|
||||
account_caller = True
|
||||
|
||||
|
||||
class CmdIdea(ReportCmdBase):
|
||||
"""
|
||||
submit a suggestion
|
||||
|
||||
Usage:
|
||||
ideas
|
||||
idea <message>
|
||||
|
||||
Example:
|
||||
idea wouldn't it be cool if we had horses we could ride
|
||||
"""
|
||||
|
||||
key = "idea"
|
||||
aliases = ("ideas",)
|
||||
report_locks = "read:pperm(Builder)"
|
||||
success_msg = "Thank you for your suggestion!"
|
||||
|
||||
def func(self):
|
||||
# we add an extra feature to this command, allowing you to see all your submitted ideas
|
||||
if self.cmdstring == "ideas":
|
||||
# list your ideas
|
||||
if (
|
||||
ideas := Msg.objects.search_message(sender=self.account, receiver=self.hub)
|
||||
.order_by("-db_date_created")
|
||||
.exclude(db_tags__db_key="closed")
|
||||
):
|
||||
# todo: use a paginated menu
|
||||
self.msg(
|
||||
"Ideas you've submitted:\n "
|
||||
+ "\n ".join(
|
||||
f"|w{item.message}|n (submitted {datetime_format(item.date_created)})"
|
||||
for item in ideas
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.msg("You have no open suggestions.")
|
||||
return
|
||||
# proceed to do the normal report-command functionality
|
||||
super().func()
|
||||
|
||||
|
||||
class ReportsCmdSet(CmdSet):
|
||||
key = "Reports CmdSet"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
super().at_cmdset_creation()
|
||||
if "bugs" in _REPORT_TYPES:
|
||||
self.add(CmdBug)
|
||||
if "ideas" in _REPORT_TYPES:
|
||||
self.add(CmdIdea)
|
||||
if "players" in _REPORT_TYPES:
|
||||
self.add(CmdReport)
|
||||
self.add(CmdManageReports)
|
||||
86
evennia/contrib/base_systems/ingame_reports/tests.py
Normal file
86
evennia/contrib/base_systems/ingame_reports/tests.py
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
from unittest.mock import Mock, patch, MagicMock
|
||||
from evennia.utils import create
|
||||
from evennia.comms.models import TempMsg
|
||||
from evennia.utils.test_resources import EvenniaCommandTest
|
||||
|
||||
from . import menu, reports
|
||||
|
||||
|
||||
class _MockQuerySet(list):
|
||||
def order_by(self, *args, **kwargs):
|
||||
return self
|
||||
|
||||
def exclude(self, *args, **kwargs):
|
||||
return self
|
||||
|
||||
def filter(self, *args, **kwargs):
|
||||
return self
|
||||
|
||||
|
||||
def _mock_pre(cmdobj):
|
||||
"""helper to mock at_pre_cmd"""
|
||||
cmdobj.hub = Mock()
|
||||
|
||||
|
||||
class TestReportCommands(EvenniaCommandTest):
|
||||
@patch.object(create, "create_message", new=MagicMock())
|
||||
def test_report_cmd_base(self):
|
||||
"""verify that the base command functionality works"""
|
||||
cmd = reports.ReportCmdBase
|
||||
|
||||
# avoid test side-effects
|
||||
with patch.object(cmd, "at_pre_cmd", new=_mock_pre) as _:
|
||||
# no arguments
|
||||
self.call(cmd(), "", "You must provide a message.")
|
||||
# arguments, no target, no target required
|
||||
self.call(cmd(), "test", "Your report has been filed.")
|
||||
# arguments, custom success message
|
||||
custom_success = "custom success message"
|
||||
cmd.success_msg = custom_success
|
||||
self.call(cmd(), "test", custom_success)
|
||||
# arguments, no target, target required
|
||||
cmd.require_target = True
|
||||
self.call(cmd(), "test", "You must include a target.")
|
||||
|
||||
@patch.object(create, "create_message", new=MagicMock())
|
||||
@patch.object(reports, "datetime_format", return_value="now")
|
||||
def test_ideas_list(self, mock_datetime_format):
|
||||
cmd = reports.CmdIdea
|
||||
|
||||
fake_ideas = _MockQuerySet([TempMsg(message=f"idea {i+1}") for i in range(3)])
|
||||
expected = """\
|
||||
Ideas you've submitted:
|
||||
idea 1 (submitted now)
|
||||
idea 2 (submitted now)
|
||||
idea 3 (submitted now)
|
||||
"""
|
||||
|
||||
with patch.object(cmd, "at_pre_cmd", new=_mock_pre) as _:
|
||||
# submitting an idea
|
||||
self.call(cmd(), "", "You must provide a message.")
|
||||
# arguments, no target, no target required
|
||||
self.call(cmd(), "test", "Thank you for your suggestion!")
|
||||
|
||||
# viewing your submitted ideas
|
||||
with patch.object(reports.Msg.objects, "search_message", return_value=fake_ideas):
|
||||
self.call(cmd(), "", cmdstring="ideas", msg=expected)
|
||||
|
||||
@patch.object(reports.evmenu, "EvMenu")
|
||||
def test_cmd_manage_reports(self, evmenu_mock):
|
||||
cmd = reports.CmdManageReports
|
||||
hub = Mock()
|
||||
|
||||
with patch.object(reports, "_get_report_hub", return_value=hub) as _:
|
||||
# invalid report type fails
|
||||
self.call(
|
||||
cmd(), "", cmdstring="manage custom", msg="'custom' is not a valid report category."
|
||||
)
|
||||
# verify valid type triggers evmenu
|
||||
self.call(cmd(), "", cmdstring="manage bugs")
|
||||
evmenu_mock.assert_called_once_with(
|
||||
self.account,
|
||||
menu,
|
||||
startnode="menunode_list_reports",
|
||||
hub=hub,
|
||||
persistent=True,
|
||||
)
|
||||
|
|
@ -31,7 +31,7 @@ class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
|||
self.add(CmdAchieve)
|
||||
```
|
||||
|
||||
**Optional** - The achievements contrib stores individual progress data on the `achievements` attribute by default, visible via `obj.db.attributes`. You can change this by assigning an attribute (key, category) tuple to the setting `ACHIEVEMENT_CONTRIB_ATTRIBUTE`
|
||||
**Optional** - The achievements contrib stores individual progress data on the `achievements` attribute by default, visible via `obj.db.achievements`. You can change this by assigning an attribute (key, category) tuple to the setting `ACHIEVEMENT_CONTRIB_ATTRIBUTE`
|
||||
|
||||
Example:
|
||||
```py
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
from .achievements import (
|
||||
get_achievement,
|
||||
search_achievement,
|
||||
all_achievements,
|
||||
track_achievements,
|
||||
get_achievement_progress,
|
||||
CmdAchieve,
|
||||
all_achievements,
|
||||
get_achievement,
|
||||
get_achievement_progress,
|
||||
search_achievement,
|
||||
track_achievements,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -50,11 +50,18 @@ Example:
|
|||
"""
|
||||
|
||||
from collections import Counter
|
||||
|
||||
from django.conf import settings
|
||||
from evennia.utils import logger
|
||||
from evennia.utils.utils import all_from_module, is_iter, make_iter, string_partial_matching
|
||||
from evennia.utils.evmore import EvMore
|
||||
|
||||
from evennia.commands.default.muxcommand import MuxCommand
|
||||
from evennia.utils import logger
|
||||
from evennia.utils.evmore import EvMore
|
||||
from evennia.utils.utils import (
|
||||
all_from_module,
|
||||
is_iter,
|
||||
make_iter,
|
||||
string_partial_matching,
|
||||
)
|
||||
|
||||
# this is either a string of the attribute name, or a tuple of strings of the attribute name and category
|
||||
_ACHIEVEMENT_ATTR = make_iter(getattr(settings, "ACHIEVEMENT_CONTRIB_ATTRIBUTE", "achievements"))
|
||||
|
|
@ -322,12 +329,12 @@ class CmdAchieve(MuxCommand):
|
|||
elif not achievement_data.get("progress"):
|
||||
status = "|yNot Started|n"
|
||||
else:
|
||||
count = achievement_data.get("count",1)
|
||||
count = achievement_data.get("count", 1)
|
||||
# is this achievement tracking items separately?
|
||||
if is_iter(achievement_data["progress"]):
|
||||
# we'll display progress as how many items have been completed
|
||||
completed = Counter(val >= count for val in achievement_data["progress"])[True]
|
||||
pct = (completed * 100) // len(achievement_data['progress'])
|
||||
pct = (completed * 100) // len(achievement_data["progress"])
|
||||
else:
|
||||
# we display progress as the percent of the total count
|
||||
pct = (achievement_data["progress"] * 100) // count
|
||||
|
|
@ -379,8 +386,7 @@ class CmdAchieve(MuxCommand):
|
|||
elif "all" in self.switches:
|
||||
# we merge our progress data into the full dict of achievements
|
||||
achievement_data = {
|
||||
key: data | progress_data.get(key, {})
|
||||
for key, data in achievements.items()
|
||||
key: data | progress_data.get(key, {}) for key, data in achievements.items()
|
||||
}
|
||||
|
||||
# we show all of the currently available achievements regardless of progress status
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
from evennia.utils.test_resources import BaseEvenniaTest, BaseEvenniaCommandTest
|
||||
from mock import patch
|
||||
|
||||
from evennia.utils.test_resources import BaseEvenniaCommandTest, BaseEvenniaTest
|
||||
|
||||
from . import achievements
|
||||
|
||||
_dummy_achievements = {
|
||||
|
|
|
|||
|
|
@ -677,7 +677,7 @@ class CmdExtendedRoomDesc(default_cmds.CmdDesc):
|
|||
edit - Open up a line editor for more advanced editing.
|
||||
del - Delete the description of an object. If another state is given, its description
|
||||
will be deleted.
|
||||
spring|summer|autumn|winter - room description to use in respective in-game season
|
||||
spring||summer||autumn||winter - room description to use in respective in-game season
|
||||
<other> - room description to use with an arbitrary room state.
|
||||
|
||||
Sets the description an object. If an object is not given,
|
||||
|
|
|
|||
|
|
@ -22,9 +22,9 @@ from random import choices
|
|||
from django.conf import settings
|
||||
|
||||
from evennia import DefaultAccount
|
||||
from evennia.commands.default.muxcommand import MuxAccountCommand
|
||||
from evennia.commands.default.account import CmdIC
|
||||
from evennia.commands.cmdset import CmdSet
|
||||
from evennia.commands.default.account import CmdIC
|
||||
from evennia.commands.default.muxcommand import MuxAccountCommand
|
||||
from evennia.objects.models import ObjectDB
|
||||
from evennia.utils.evmenu import EvMenu
|
||||
from evennia.utils.utils import is_iter, string_partial_matching
|
||||
|
|
|
|||
|
|
@ -73,8 +73,14 @@ class Room(ContribRPRoom):
|
|||
# ...
|
||||
|
||||
```
|
||||
You need to set up Evennia to use the RPsystem's form to separate
|
||||
between sdescs (`3-tall`) to make it compatible with how the rest of Evennia
|
||||
separates between other multi-matches of searches/commands:
|
||||
|
||||
You will then need to reload the server and potentially force-reload
|
||||
SEARCH_MULTIMATCH_REGEX = r"(?P<number>[0-9]+)-(?P<name>[^-]*)(?P<args>.*)"
|
||||
SEARCH_MULTIMATCH_TEMPLATE = " {number}-{name}{aliases}{info}\n"
|
||||
|
||||
Finally, you will then need to reload the server and potentially force-reload
|
||||
your objects, if you originally created them without this.
|
||||
|
||||
Example for your character:
|
||||
|
|
|
|||
|
|
@ -392,7 +392,7 @@ def parse_sdescs_and_recogs(
|
|||
# if no sdesc, include key plus aliases instead
|
||||
else:
|
||||
candidate_map.append((obj, obj.key))
|
||||
candidate_map.extend([(obj, alias) for alias in obj.aliases.all()])
|
||||
candidate_map.extend([(obj, alias) for alias in obj.aliases.all()])
|
||||
|
||||
# escape mapping syntax on the form {#id} if it exists already in emote,
|
||||
# if so it is replaced with just "id".
|
||||
|
|
@ -422,7 +422,10 @@ def parse_sdescs_and_recogs(
|
|||
|
||||
# first see if there is a number given (e.g. 1-tall)
|
||||
num_identifier, _ = marker_match.groups("") # return "" if no match, rather than None
|
||||
# get the beginning of the actual text, minus the numeric identifier
|
||||
match_index = marker_match.start()
|
||||
if num_identifier:
|
||||
match_index += len(num_identifier) + 1
|
||||
# split the emote string at the reference marker, to process everything after it
|
||||
head = string[:match_index]
|
||||
tail = string[match_index + 1 :]
|
||||
|
|
@ -439,7 +442,7 @@ def parse_sdescs_and_recogs(
|
|||
(re.search(rquery, text, _RE_FLAGS), obj, text) for obj, text in candidate_map
|
||||
)
|
||||
# filter out any non-matching candidates
|
||||
bestmatches = [(obj, match.group()) for match, obj, text in matches if match]
|
||||
bestmatches = [(obj, mtch.group()) for mtch, obj, text in matches if mtch]
|
||||
|
||||
else:
|
||||
# to find the longest match, we start from the marker and lengthen the
|
||||
|
|
@ -1333,30 +1336,30 @@ class ContribRPObject(DefaultObject):
|
|||
"""
|
||||
# we also want to use the default search method
|
||||
search_obj = super().get_search_result
|
||||
is_builder = self.locks.check_lockstring(self, "perm(Builder)")
|
||||
is_builder = self.permissions.check("Builder")
|
||||
results = []
|
||||
|
||||
if candidates:
|
||||
candidates = parse_sdescs_and_recogs(
|
||||
if candidates is not None:
|
||||
searched_results = parse_sdescs_and_recogs(
|
||||
self, candidates, _PREFIX + searchdata, search_mode=True
|
||||
)
|
||||
results = []
|
||||
for candidate in candidates:
|
||||
# we search by candidate keys here; this allows full error
|
||||
# management and use of all kwargs - we will use searchdata
|
||||
# in eventual error reporting later (not their keys). Doing
|
||||
# it like this e.g. allows for use of the typeclass kwarg
|
||||
# limiter.
|
||||
results.extend(
|
||||
[obj for obj in search_obj(candidate.key, **kwargs) if obj not in results]
|
||||
)
|
||||
|
||||
if not results and is_builder:
|
||||
# builders get to do a global search by key+alias
|
||||
results = search_obj(searchdata, **kwargs)
|
||||
if not searched_results and is_builder:
|
||||
# builders get to do a search by key
|
||||
results = search_obj(searchdata, candidates=candidates, **kwargs)
|
||||
else:
|
||||
# we do a default search on each result by key, here, to apply extra filtering kwargs
|
||||
for searched_obj in searched_results:
|
||||
results.extend(
|
||||
[
|
||||
obj
|
||||
for obj in search_obj(
|
||||
searched_obj.key, candidates=[searched_obj], **kwargs
|
||||
)
|
||||
if obj not in results
|
||||
]
|
||||
)
|
||||
else:
|
||||
# global searches with #drefs end up here. Global searches are
|
||||
# only done in code, so is controlled, #dbrefs are turned off
|
||||
# for non-Builders.
|
||||
# no candidates means it's a global search, so we pass it back to the default
|
||||
results = search_obj(searchdata, **kwargs)
|
||||
return results
|
||||
|
||||
|
|
|
|||
|
|
@ -346,6 +346,44 @@ class TestRPSystem(BaseEvenniaTest):
|
|||
self.assertEqual(self.speaker.search("receiver of emotes"), self.receiver1)
|
||||
self.assertEqual(self.speaker.search("colliding"), self.receiver2)
|
||||
|
||||
def test_get_search_result(self):
|
||||
self.obj1 = create_object(rpsystem.ContribRPObject, key="Obj1", location=self.room)
|
||||
self.obj1.sdesc.add("something")
|
||||
self.obj2 = create_object(rpsystem.ContribRPCharacter, key="Obj2", location=self.room)
|
||||
self.obj2.sdesc.add("something")
|
||||
candidates = [self.obj1, self.obj2]
|
||||
|
||||
# search candidates by sdesc: both objects should be found
|
||||
result = self.speaker.get_search_result("something", candidates)
|
||||
self.assertEqual(list(result), candidates)
|
||||
|
||||
# search by sdesc with 2-disambiguator: only second object should be found
|
||||
result = self.speaker.get_search_result("2-something", candidates)
|
||||
self.assertEqual(list(result), [self.obj2])
|
||||
|
||||
# search empty candidates: no objects should be found
|
||||
result = self.speaker.get_search_result("something", candidates=[])
|
||||
self.assertEqual(list(result), [])
|
||||
|
||||
# typeclass was given: only matching object should be found
|
||||
result = self.speaker.get_search_result(
|
||||
"something", candidates=candidates, typeclass=rpsystem.ContribRPCharacter
|
||||
)
|
||||
self.assertEqual(list(result), [self.obj2])
|
||||
|
||||
# search by key with player permissions: no objects should be found
|
||||
result = self.speaker.get_search_result("obj1", candidates)
|
||||
self.assertEqual(list(result), [])
|
||||
|
||||
# search by key with builder permissions: object should be found
|
||||
self.speaker.permissions.add("builder")
|
||||
result = self.speaker.get_search_result("obj1", candidates)
|
||||
self.assertEqual(list(result), [self.obj1])
|
||||
|
||||
# search by key with builder permissions when NOT IN candidates: object should NOT be found
|
||||
result = self.speaker.get_search_result("obj1", [self.obj2])
|
||||
self.assertEqual(list(result), [])
|
||||
|
||||
|
||||
class TestRPSystemCommands(BaseEvenniaCommandTest):
|
||||
def setUp(self):
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ Contribution by Griatch 2020, based on code by Whitenoise and Ainneve contribs,
|
|||
|
||||
A `Trait` represents a modifiable property on (usually) a Character. They can
|
||||
be used to represent everything from attributes (str, agi etc) to skills
|
||||
(hunting 10, swords 14 etc) and dynamically changing things like HP, XP etc.
|
||||
(hunting 10, swords 14 etc) and dynamically changing things like HP, XP etc.
|
||||
Traits differ from normal Attributes in that they track their changes and limit
|
||||
themselves to particular value-ranges. One can add/subtract from them easily and
|
||||
they can even change dynamically at a particular rate (like you being poisoned or
|
||||
|
|
@ -50,8 +50,6 @@ class Character(DefaultCharacter):
|
|||
self.traits.add("hp", "Health", trait_type="gauge", min=0, max=100)
|
||||
self.traits.add("hunting", "Hunting Skill", trait_type="counter",
|
||||
base=10, mod=1, min=0, max=100)
|
||||
|
||||
|
||||
```
|
||||
When adding the trait, you supply the name of the property (`hunting`) along
|
||||
with a more human-friendly name ("Hunting Skill"). The latter will show if you
|
||||
|
|
@ -78,7 +76,6 @@ class Object(DefaultObject):
|
|||
strength = TraitProperty("Strength", trait_type="static", base=10, mod=2)
|
||||
health = TraitProperty("Health", trait_type="gauge", min=0, base=100, mod=2)
|
||||
hunting = TraitProperty("Hunting Skill", trait_type="counter", base=10, mod=1, min=0, max=100)
|
||||
|
||||
```
|
||||
|
||||
> Note that the property-name will become the name of the trait and you don't supply `trait_key`
|
||||
|
|
@ -92,7 +89,7 @@ class Object(DefaultObject):
|
|||
|
||||
## Using traits
|
||||
|
||||
A trait is added to the traithandler (if you use `TraitProperty` the handler is just created under
|
||||
A trait are entities added to the traithandler (if you use `TraitProperty` the handler is just created under
|
||||
the hood) after which one can access it as a property on the handler (similarly to how you can do
|
||||
.db.attrname for Attributes in Evennia).
|
||||
|
||||
|
|
@ -137,9 +134,30 @@ obj.traits.strength.value
|
|||
> obj.strength.value += 5
|
||||
> obj.strength.value
|
||||
17
|
||||
|
||||
```
|
||||
|
||||
### Relating traits to one another
|
||||
|
||||
From a trait you can access its own Traithandler as `.traithandler`. You can
|
||||
also find another trait on the same handler by using the
|
||||
`Trait.get_trait("traitname")` method.
|
||||
|
||||
```python
|
||||
> obj.strength.get_trait("hp").value
|
||||
100
|
||||
```
|
||||
|
||||
This is not too useful for the default trait types - they are all operating
|
||||
independently from one another. But if you create your own trait classes, you
|
||||
can use this to make traits that depend on each other.
|
||||
|
||||
For example, you could picture making a Trait that is the sum of the values of
|
||||
two other traits and capped by the value of a third trait. Such complex
|
||||
interactions are common in RPG rule systems but are by definition game specific.
|
||||
|
||||
See an example in the section about [making your own Trait classes](#expanding-with-your-own-traits).
|
||||
|
||||
|
||||
## Trait types
|
||||
|
||||
All default traits have a read-only `.value` property that shows the relevant or
|
||||
|
|
@ -158,7 +176,6 @@ compatible type.
|
|||
> trait1 + 2
|
||||
> trait1.value
|
||||
5
|
||||
|
||||
```
|
||||
|
||||
Two numerical traits can also be compared (bigger-than etc), which is useful in
|
||||
|
|
@ -168,7 +185,6 @@ all sorts of rule-resolution.
|
|||
|
||||
if trait1 > trait2:
|
||||
# do stuff
|
||||
|
||||
```
|
||||
|
||||
### Trait
|
||||
|
|
@ -193,7 +209,6 @@ like a glorified Attribute.
|
|||
> obj.traits.mytrait.value = "stringvalue"
|
||||
> obj.traits.mytrait.value
|
||||
"stringvalue"
|
||||
|
||||
```
|
||||
|
||||
### Static trait
|
||||
|
|
@ -217,7 +232,6 @@ that varies slowly or not at all, and which may be modified in-place.
|
|||
> obj.traits.mytrait.mod = 0
|
||||
> obj.traits.mytrait.value
|
||||
12
|
||||
|
||||
```
|
||||
|
||||
### Counter
|
||||
|
|
@ -253,8 +267,6 @@ remove it. A suggested use for a Counter Trait would be to track skill values.
|
|||
|
||||
# for TraitProperties, pass the args/kwargs of traits.add() to the
|
||||
# TraitProperty constructor instead.
|
||||
|
||||
|
||||
```
|
||||
|
||||
Counters have some extra properties:
|
||||
|
|
@ -286,7 +298,6 @@ By calling `.desc()` on the Counter, you will get the text matching the current
|
|||
|
||||
> obj.traits.hunting.desc()
|
||||
"expert"
|
||||
|
||||
```
|
||||
|
||||
#### .rate
|
||||
|
|
@ -327,12 +338,10 @@ a previous value.
|
|||
71 # we have stopped at the ratetarget
|
||||
|
||||
> obj.traits.hunting.rate = 0 # disable auto-change
|
||||
|
||||
|
||||
```
|
||||
Note that when retrieving the `current`, the result will always be of the same
|
||||
type as the `.base` even `rate` is a non-integer value. So if `base` is an `int`
|
||||
(default)`, the `current` value will also be rounded the closest full integer.
|
||||
(default), the `current` value will also be rounded the closest full integer.
|
||||
If you want to see the exact `current` value, set `base` to a float - you
|
||||
will then need to use `round()` yourself on the result if you want integers.
|
||||
|
||||
|
|
@ -347,7 +356,6 @@ return the value as a percentage.
|
|||
|
||||
> obj.traits.hunting.percent(formatting=None)
|
||||
71.0
|
||||
|
||||
```
|
||||
|
||||
### Gauge
|
||||
|
|
@ -379,7 +387,6 @@ stamina and the like.
|
|||
> obj.traits.hp.current -= 30
|
||||
> obj.traits.hp.value
|
||||
80
|
||||
|
||||
```
|
||||
|
||||
The Gauge trait is subclass of the Counter, so you have access to the same
|
||||
|
|
@ -412,8 +419,6 @@ class RageTrait(StaticTrait):
|
|||
|
||||
def sedate(self):
|
||||
self.mod = 0
|
||||
|
||||
|
||||
```
|
||||
|
||||
Above is an example custom-trait-class "rage" that stores a property "rage" on
|
||||
|
|
@ -432,12 +437,24 @@ Reload the server and you should now be able to use your trait:
|
|||
> obj.traits.add("mood", "A dark mood", rage=30, trait_type='rage')
|
||||
> obj.traits.mood.rage
|
||||
30
|
||||
```
|
||||
|
||||
Remember that you can use `.get_trait("name")` to access other traits on the
|
||||
same handler. Let's say that the rage modifier is actually limited by
|
||||
the characters's current STR value times 3, with a max of 100:
|
||||
|
||||
```python
|
||||
class RageTrait(StaticTrait):
|
||||
#...
|
||||
def berserk(self):
|
||||
self.mod = min(100, self.get_trait("STR").value * 3)
|
||||
```
|
||||
|
||||
# as TraitProperty
|
||||
|
||||
```
|
||||
class Character(DefaultCharacter):
|
||||
rage = TraitProperty("A dark mood", rage=30, trait_type='rage')
|
||||
|
||||
```
|
||||
|
||||
## Adding additional TraitHandlers
|
||||
|
|
@ -459,7 +476,7 @@ class Character(DefaultCharacter):
|
|||
def traits(self):
|
||||
# this adds the handler as .traits
|
||||
return TraitHandler(self)
|
||||
|
||||
|
||||
@lazy_property
|
||||
def stats(self):
|
||||
# this adds the handler as .stats
|
||||
|
|
@ -478,3 +495,6 @@ class Character(DefaultCharacter):
|
|||
self.skills.add("hunting", "Hunting Skill", trait_type="counter",
|
||||
base=10, mod=1, min=0, max=100)
|
||||
```
|
||||
|
||||
> Rememebr that the `.get_traits()` method only works for accessing Traits within the
|
||||
_same_ TraitHandler.
|
||||
|
|
|
|||
|
|
@ -9,10 +9,9 @@ Unit test module for Trait classes.
|
|||
from copy import copy
|
||||
|
||||
from anything import Something
|
||||
from mock import MagicMock, patch
|
||||
|
||||
from evennia.objects.objects import DefaultCharacter
|
||||
from evennia.utils.test_resources import BaseEvenniaTestCase, EvenniaTest
|
||||
from mock import MagicMock, patch
|
||||
|
||||
from . import traits
|
||||
|
||||
|
|
@ -156,6 +155,16 @@ class TraitHandlerTest(_TraitHandlerBase):
|
|||
self.obj.attributes.get("traits", category="traits")["test1"]["value"], None
|
||||
)
|
||||
|
||||
def test_related_traits(self):
|
||||
"""Test traits related to each other via Trait.get_trait()"""
|
||||
|
||||
trait1 = self.traithandler.test1
|
||||
trait2 = self.traithandler.test2
|
||||
|
||||
self.assertEqual(trait1.traithandler, self.traithandler)
|
||||
self.assertEqual(trait1.get_trait("test1"), trait1)
|
||||
self.assertEqual(trait1.get_trait("test2"), trait2)
|
||||
|
||||
|
||||
class TestTrait(_TraitHandlerBase):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -456,15 +456,9 @@ from functools import total_ordering
|
|||
from time import time
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from evennia.utils import logger
|
||||
from evennia.utils.dbserialize import _SaverDict
|
||||
from evennia.utils.utils import (
|
||||
class_from_module,
|
||||
inherits_from,
|
||||
list_to_string,
|
||||
percent,
|
||||
)
|
||||
from evennia.utils.utils import class_from_module, inherits_from, list_to_string, percent
|
||||
|
||||
# Available Trait classes.
|
||||
# This way the user can easily supply their own. Each
|
||||
|
|
@ -657,7 +651,9 @@ class TraitHandler:
|
|||
if trait is None and trait_key in self.trait_data:
|
||||
trait_type = self.trait_data[trait_key]["trait_type"]
|
||||
trait_cls = self._get_trait_class(trait_type)
|
||||
trait = self._cache[trait_key] = trait_cls(_GA(self, "trait_data")[trait_key])
|
||||
trait = self._cache[trait_key] = trait_cls(
|
||||
_GA(self, "trait_data")[trait_key], handler=self
|
||||
)
|
||||
return trait
|
||||
|
||||
def add(
|
||||
|
|
@ -856,7 +852,7 @@ class Trait:
|
|||
# and have them treated like data to store.
|
||||
allow_extra_properties = True
|
||||
|
||||
def __init__(self, trait_data):
|
||||
def __init__(self, trait_data, handler=None):
|
||||
"""
|
||||
This both initializes and validates the Trait on creation. It must
|
||||
raise exception if validation fails. The TraitHandler will call this
|
||||
|
|
@ -869,12 +865,15 @@ class Trait:
|
|||
value in cls.data_default_values. Any extra kwargs will be made
|
||||
available as extra properties on the Trait, assuming the class
|
||||
variable `allow_extra_properties` is set.
|
||||
handler (TraitHandler): The handler that this Trait is connected to.
|
||||
This is for referencing other traits.
|
||||
|
||||
Raises:
|
||||
TraitException: If input-validation failed.
|
||||
|
||||
"""
|
||||
self._data = self.__class__.validate_input(self.__class__, trait_data)
|
||||
self.traithandler = handler
|
||||
|
||||
if not isinstance(trait_data, _SaverDict):
|
||||
logger.log_warn(
|
||||
|
|
@ -955,6 +954,7 @@ class Trait:
|
|||
"data_default",
|
||||
"trait_type",
|
||||
"allow_extra_properties",
|
||||
"traithandler",
|
||||
):
|
||||
return _GA(self, key)
|
||||
try:
|
||||
|
|
@ -970,10 +970,9 @@ class Trait:
|
|||
"""Set extra parameters as attributes.
|
||||
|
||||
Arbitrary attributes set on a Trait object will be
|
||||
stored in the 'extra' key of the `_data` attribute.
|
||||
stored as extra keys in the Trait's data.
|
||||
|
||||
This behavior is enabled by setting the instance
|
||||
variable `_locked` to True.
|
||||
This behavior is enabled by setting the instance variable `allow_extra_properties`.
|
||||
|
||||
"""
|
||||
propobj = getattr(self.__class__, key, None)
|
||||
|
|
@ -984,7 +983,7 @@ class Trait:
|
|||
return
|
||||
else:
|
||||
# this is some other value
|
||||
if key in ("_data",):
|
||||
if key in ("_data", "traithandler"):
|
||||
_SA(self, key, value)
|
||||
return
|
||||
if _GA(self, "allow_extra_properties"):
|
||||
|
|
@ -1053,6 +1052,11 @@ class Trait:
|
|||
"""Display name for the trait."""
|
||||
return self._data["name"]
|
||||
|
||||
def get_trait(self, trait_key):
|
||||
"""Get another Trait from the handler. Not used by default, but can be used
|
||||
for custom traits that are affected by other traits on the same handler."""
|
||||
return self.traithandler.get(trait_key)
|
||||
|
||||
key = name
|
||||
|
||||
# Numeric operations
|
||||
|
|
|
|||
|
|
@ -27,68 +27,112 @@ from evennia.accounts.accounts import DefaultAccount, DefaultGuest
|
|||
|
||||
class Account(DefaultAccount):
|
||||
"""
|
||||
This class describes the actual OOC account (i.e. the user connecting
|
||||
to the MUD). It does NOT have visual appearance in the game world (that
|
||||
is handled by the character which is connected to this). Comm channels
|
||||
are attended/joined using this object.
|
||||
An Account is the actual OOC player entity. It doesn't exist in the game,
|
||||
but puppets characters.
|
||||
|
||||
It can be useful e.g. for storing configuration options for your game, but
|
||||
should generally not hold any character-related info (that's best handled
|
||||
on the character level).
|
||||
This is the base Typeclass for all Accounts. Accounts represent
|
||||
the person playing the game and tracks account info, password
|
||||
etc. They are OOC entities without presence in-game. An Account
|
||||
can connect to a Character Object in order to "enter" the
|
||||
game.
|
||||
|
||||
Can be set using BASE_ACCOUNT_TYPECLASS.
|
||||
Account Typeclass API:
|
||||
|
||||
* Available properties (only available on initiated typeclass objects)
|
||||
|
||||
* available properties
|
||||
|
||||
key (string) - name of account
|
||||
name (string)- wrapper for user.username
|
||||
aliases (list of strings) - aliases to the object. Will be saved to database as AliasDB entries but returned as strings.
|
||||
dbref (int, read-only) - unique #id-number. Also "id" can be used.
|
||||
date_created (string) - time stamp of object creation
|
||||
permissions (list of strings) - list of permission strings
|
||||
|
||||
user (User, read-only) - django User authorization object
|
||||
obj (Object) - game object controlled by account. 'character' can also be used.
|
||||
sessions (list of Sessions) - sessions connected to this account
|
||||
is_superuser (bool, read-only) - if the connected user is a superuser
|
||||
- key (string) - name of account
|
||||
- name (string)- wrapper for user.username
|
||||
- aliases (list of strings) - aliases to the object. Will be saved to
|
||||
database as AliasDB entries but returned as strings.
|
||||
- dbref (int, read-only) - unique #id-number. Also "id" can be used.
|
||||
- date_created (string) - time stamp of object creation
|
||||
- permissions (list of strings) - list of permission strings
|
||||
- user (User, read-only) - django User authorization object
|
||||
- obj (Object) - game object controlled by account. 'character' can also
|
||||
be used.
|
||||
- is_superuser (bool, read-only) - if the connected user is a superuser
|
||||
|
||||
* Handlers
|
||||
|
||||
locks - lock-handler: use locks.add() to add new lock strings
|
||||
db - attribute-handler: store/retrieve database attributes on this self.db.myattr=val, val=self.db.myattr
|
||||
ndb - non-persistent attribute handler: same as db but does not create a database entry when storing data
|
||||
scripts - script-handler. Add new scripts to object with scripts.add()
|
||||
cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object
|
||||
nicks - nick-handler. New nicks with nicks.add().
|
||||
- locks - lock-handler: use locks.add() to add new lock strings
|
||||
- db - attribute-handler: store/retrieve database attributes on this
|
||||
self.db.myattr=val, val=self.db.myattr
|
||||
- ndb - non-persistent attribute handler: same as db but does not
|
||||
create a database entry when storing data
|
||||
- scripts - script-handler. Add new scripts to object with scripts.add()
|
||||
- cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object
|
||||
- nicks - nick-handler. New nicks with nicks.add().
|
||||
- sessions - session-handler. Use session.get() to see all sessions connected, if any
|
||||
- options - option-handler. Defaults are taken from settings.OPTIONS_ACCOUNT_DEFAULT
|
||||
- characters - handler for listing the account's playable characters
|
||||
|
||||
* Helper methods
|
||||
* Helper methods (check autodocs for full updated listing)
|
||||
|
||||
msg(text=None, **kwargs)
|
||||
execute_cmd(raw_string, session=None)
|
||||
search(ostring, global_search=False, attribute_name=None, use_nicks=False, location=None, ignore_errors=False, account=False)
|
||||
is_typeclass(typeclass, exact=False)
|
||||
swap_typeclass(new_typeclass, clean_attributes=False, no_default=True)
|
||||
access(accessing_obj, access_type='read', default=False)
|
||||
check_permstring(permstring)
|
||||
- msg(text=None, from_obj=None, session=None, options=None, **kwargs)
|
||||
- execute_cmd(raw_string)
|
||||
- search(searchdata, return_puppet=False, search_object=False, typeclass=None,
|
||||
nofound_string=None, multimatch_string=None, use_nicks=True,
|
||||
quiet=False, **kwargs)
|
||||
- is_typeclass(typeclass, exact=False)
|
||||
- swap_typeclass(new_typeclass, clean_attributes=False, no_default=True)
|
||||
- access(accessing_obj, access_type='read', default=False, no_superuser_bypass=False, **kwargs)
|
||||
- check_permstring(permstring)
|
||||
- get_cmdsets(caller, current, **kwargs)
|
||||
- get_cmdset_providers()
|
||||
- uses_screenreader(session=None)
|
||||
- get_display_name(looker, **kwargs)
|
||||
- get_extra_display_name_info(looker, **kwargs)
|
||||
- disconnect_session_from_account()
|
||||
- puppet_object(session, obj)
|
||||
- unpuppet_object(session)
|
||||
- unpuppet_all()
|
||||
- get_puppet(session)
|
||||
- get_all_puppets()
|
||||
- is_banned(**kwargs)
|
||||
- get_username_validators(validator_config=settings.AUTH_USERNAME_VALIDATORS)
|
||||
- authenticate(username, password, ip="", **kwargs)
|
||||
- normalize_username(username)
|
||||
- validate_username(username)
|
||||
- validate_password(password, account=None)
|
||||
- set_password(password, **kwargs)
|
||||
- get_character_slots()
|
||||
- get_available_character_slots()
|
||||
- create_character(*args, **kwargs)
|
||||
- create(*args, **kwargs)
|
||||
- delete(*args, **kwargs)
|
||||
- channel_msg(message, channel, senders=None, **kwargs)
|
||||
- idle_time()
|
||||
- connection_time()
|
||||
|
||||
* Hook methods (when re-implementation, remember methods need to have self as first arg)
|
||||
* Hook methods
|
||||
|
||||
basetype_setup()
|
||||
at_account_creation()
|
||||
|
||||
- note that the following hooks are also found on Objects and are
|
||||
> note that the following hooks are also found on Objects and are
|
||||
usually handled on the character level:
|
||||
|
||||
at_init()
|
||||
at_cmdset_get(**kwargs)
|
||||
at_first_login()
|
||||
at_post_login(session=None)
|
||||
at_disconnect()
|
||||
at_message_receive()
|
||||
at_message_send()
|
||||
at_server_reload()
|
||||
at_server_shutdown()
|
||||
- at_init()
|
||||
- at_first_save()
|
||||
- at_access()
|
||||
- at_cmdset_get(**kwargs)
|
||||
- at_password_change(**kwargs)
|
||||
- at_first_login()
|
||||
- at_pre_login()
|
||||
- at_post_login(session=None)
|
||||
- at_failed_login(session, **kwargs)
|
||||
- at_disconnect(reason=None, **kwargs)
|
||||
- at_post_disconnect(**kwargs)
|
||||
- at_message_receive()
|
||||
- at_message_send()
|
||||
- at_server_reload()
|
||||
- at_server_shutdown()
|
||||
- at_look(target=None, session=None, **kwargs)
|
||||
- at_post_create_character(character, **kwargs)
|
||||
- at_post_add_character(char)
|
||||
- at_post_remove_character(char)
|
||||
- at_pre_channel_msg(message, channel, senders=None, **kwargs)
|
||||
- at_post_chnnel_msg(message, channel, senders=None, **kwargs)
|
||||
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -16,14 +16,53 @@ from evennia.comms.comms import DefaultChannel
|
|||
|
||||
|
||||
class Channel(DefaultChannel):
|
||||
"""
|
||||
Working methods:
|
||||
at_channel_creation() - called once, when the channel is created
|
||||
r"""
|
||||
This is the base class for all Channel Comms. Inherit from this to
|
||||
create different types of communication channels.
|
||||
|
||||
Class-level variables:
|
||||
- `send_to_online_only` (bool, default True) - if set, will only try to
|
||||
send to subscribers that are actually active. This is a useful optimization.
|
||||
- `log_file` (str, default `"channel_{channelname}.log"`). This is the
|
||||
log file to which the channel history will be saved. The `{channelname}` tag
|
||||
will be replaced by the key of the Channel. If an Attribute 'log_file'
|
||||
is set, this will be used instead. If this is None and no Attribute is found,
|
||||
no history will be saved.
|
||||
- `channel_prefix_string` (str, default `"[{channelname} ]"`) - this is used
|
||||
as a simple template to get the channel prefix with `.channel_prefix()`. It is used
|
||||
in front of every channel message; use `{channelmessage}` token to insert the
|
||||
name of the current channel. Set to `None` if you want no prefix (or want to
|
||||
handle it in a hook during message generation instead.
|
||||
- `channel_msg_nick_pattern`(str, default `"{alias}\s*?|{alias}\s+?(?P<arg1>.+?)") -
|
||||
this is what used when a channel subscriber gets a channel nick assigned to this
|
||||
channel. The nickhandler uses the pattern to pick out this channel's name from user
|
||||
input. The `{alias}` token will get both the channel's key and any set/custom aliases
|
||||
per subscriber. You need to allow for an `<arg1>` regex group to catch any message
|
||||
that should be send to the channel. You usually don't need to change this pattern
|
||||
unless you are changing channel command-style entirely.
|
||||
- `channel_msg_nick_replacement` (str, default `"channel {channelname} = $1"` - this
|
||||
is used by the nickhandler to generate a replacement string once the nickhandler (using
|
||||
the `channel_msg_nick_pattern`) identifies that the channel should be addressed
|
||||
to send a message to it. The `<arg1>` regex pattern match from `channel_msg_nick_pattern`
|
||||
will end up at the `$1` position in the replacement. Together, this allows you do e.g.
|
||||
'public Hello' and have that become a mapping to `channel public = Hello`. By default,
|
||||
the account-level `channel` command is used. If you were to rename that command you must
|
||||
tweak the output to something like `yourchannelcommandname {channelname} = $1`.
|
||||
|
||||
* Properties:
|
||||
mutelist
|
||||
banlist
|
||||
wholist
|
||||
|
||||
* Working methods:
|
||||
get_log_filename()
|
||||
set_log_filename(filename)
|
||||
has_connection(account) - check if the given account listens to this channel
|
||||
connect(account) - connect account to this channel
|
||||
disconnect(account) - disconnect account from channel
|
||||
access(access_obj, access_type='listen', default=False) - check the
|
||||
access on this channel (default access_type is listen)
|
||||
create(key, creator=None, *args, **kwargs)
|
||||
delete() - delete this channel
|
||||
message_transform(msg, emit=False, prefix=True,
|
||||
sender_strings=None, external=False) - called by
|
||||
|
|
@ -36,8 +75,19 @@ class Channel(DefaultChannel):
|
|||
distribute_message(msg, online=False) - send a message to all
|
||||
connected accounts on channel, optionally sending only
|
||||
to accounts that are currently online (optimized for very large sends)
|
||||
mute(subscriber, **kwargs)
|
||||
unmute(subscriber, **kwargs)
|
||||
ban(target, **kwargs)
|
||||
unban(target, **kwargs)
|
||||
add_user_channel_alias(user, alias, **kwargs)
|
||||
remove_user_channel_alias(user, alias, **kwargs)
|
||||
|
||||
|
||||
Useful hooks:
|
||||
at_channel_creation() - called once, when the channel is created
|
||||
basetype_setup()
|
||||
at_init()
|
||||
at_first_save()
|
||||
channel_prefix() - how the channel should be
|
||||
prefixed when returning to user. Returns a string
|
||||
format_senders(senders) - should return how to display multiple
|
||||
|
|
@ -49,13 +99,19 @@ class Channel(DefaultChannel):
|
|||
format_message(msg, emit=False) - format the message body before
|
||||
displaying it to the user. 'emit' generally means that the
|
||||
message should not be displayed with the sender's name.
|
||||
channel_prefix()
|
||||
|
||||
pre_join_channel(joiner) - if returning False, abort join
|
||||
post_join_channel(joiner) - called right after successful join
|
||||
pre_leave_channel(leaver) - if returning False, abort leave
|
||||
post_leave_channel(leaver) - called right after successful leave
|
||||
pre_send_message(msg) - runs just before a message is sent to channel
|
||||
post_send_message(msg) - called just after message was sent to channel
|
||||
at_pre_msg(message, **kwargs)
|
||||
at_post_msg(message, **kwargs)
|
||||
web_get_admin_url()
|
||||
web_get_create_url()
|
||||
web_get_detail_url()
|
||||
web_get_update_url()
|
||||
web_get_delete_url()
|
||||
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -15,22 +15,11 @@ from .objects import ObjectParent
|
|||
|
||||
class Character(ObjectParent, DefaultCharacter):
|
||||
"""
|
||||
The Character defaults to reimplementing some of base Object's hook methods with the
|
||||
following functionality:
|
||||
The Character just re-implements some of the Object's methods and hooks
|
||||
to represent a Character entity in-game.
|
||||
|
||||
at_basetype_setup - always assigns the DefaultCmdSet to this object type
|
||||
(important!)sets locks so character cannot be picked up
|
||||
and its commands only be called by itself, not anyone else.
|
||||
(to change things, use at_object_creation() instead).
|
||||
at_post_move(source_location) - Launches the "look" command after every move.
|
||||
at_post_unpuppet(account) - when Account disconnects from the Character, we
|
||||
store the current location in the prelogout_location Attribute and
|
||||
move it to a None-location so the "unpuppeted" character
|
||||
object does not need to stay on grid. Echoes "Account has disconnected"
|
||||
to the room.
|
||||
at_pre_puppet - Just before Account re-connects, retrieves the character's
|
||||
prelogout_location Attribute and move it back on the grid.
|
||||
at_post_puppet - Echoes "AccountName has entered the game" to the room.
|
||||
See mygame/typeclasses/objects.py for a list of
|
||||
properties and methods available on all Object child classes like this.
|
||||
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -15,27 +15,12 @@ from .objects import ObjectParent
|
|||
class Exit(ObjectParent, DefaultExit):
|
||||
"""
|
||||
Exits are connectors between rooms. Exits are normal Objects except
|
||||
they defines the `destination` property. It also does work in the
|
||||
following methods:
|
||||
they defines the `destination` property and overrides some hooks
|
||||
and methods to represent the exits.
|
||||
|
||||
basetype_setup() - sets default exit locks (to change, use `at_object_creation` instead).
|
||||
at_cmdset_get(**kwargs) - this is called when the cmdset is accessed and should
|
||||
rebuild the Exit cmdset along with a command matching the name
|
||||
of the Exit object. Conventionally, a kwarg `force_init`
|
||||
should force a rebuild of the cmdset, this is triggered
|
||||
by the `@alias` command when aliases are changed.
|
||||
at_failed_traverse() - gives a default error message ("You cannot
|
||||
go there") if exit traversal fails and an
|
||||
attribute `err_traverse` is not defined.
|
||||
See mygame/typeclasses/objects.py for a list of
|
||||
properties and methods available on all Objects child classes like this.
|
||||
|
||||
Relevant hooks to overload (compared to other types of Objects):
|
||||
at_traverse(traveller, target_loc) - called to do the actual traversal and calling of the other hooks.
|
||||
If overloading this, consider using super() to use the default
|
||||
movement implementation (and hook-calling).
|
||||
at_post_traverse(traveller, source_loc) - called by at_traverse just after traversing.
|
||||
at_failed_traverse(traveller) - called by at_traverse if traversal failed for some reason. Will
|
||||
not be called if the attribute `err_traverse` is
|
||||
defined, in which case that will simply be echoed.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
"""
|
||||
Object
|
||||
|
||||
The Object is the "naked" base class for things in the game world.
|
||||
The Object is the class for general items in the game world.
|
||||
|
||||
Note that the default Character, Room and Exit does not inherit from
|
||||
this Object, but from their respective default implementations in the
|
||||
evennia library. If you want to use this class as a parent to change
|
||||
the other types, you can do so by adding this as a multiple
|
||||
inheritance.
|
||||
Use the ObjectParent class to implement common features for *all* entities
|
||||
with a location in the game world (like Characters, Rooms, Exits).
|
||||
|
||||
"""
|
||||
|
||||
|
|
@ -28,20 +25,18 @@ class ObjectParent:
|
|||
|
||||
class Object(ObjectParent, DefaultObject):
|
||||
"""
|
||||
This is the root typeclass object, implementing an in-game Evennia
|
||||
game object, such as having a location, being able to be
|
||||
manipulated or looked at, etc. If you create a new typeclass, it
|
||||
must always inherit from this object (or any of the other objects
|
||||
in this file, since they all actually inherit from BaseObject, as
|
||||
seen in src.object.objects).
|
||||
This is the root Object typeclass, representing all entities that
|
||||
have an actual presence in-game. DefaultObjects generally have a
|
||||
location. They can also be manipulated and looked at. Game
|
||||
entities you define should inherit from DefaultObject at some distance.
|
||||
|
||||
The BaseObject class implements several hooks tying into the game
|
||||
engine. By re-implementing these hooks you can control the
|
||||
system. You should never need to re-implement special Python
|
||||
methods, such as __init__ and especially never __getattribute__ and
|
||||
__setattr__ since these are used heavily by the typeclass system
|
||||
of Evennia and messing with them might well break things for you.
|
||||
It is recommended to create children of this class using the
|
||||
`evennia.create_object()` function rather than to initialize the class
|
||||
directly - this will both set things up and efficiently save the object
|
||||
without `obj.save()` having to be called explicitly.
|
||||
|
||||
Note: Check the autodocs for complete class members, this may not always
|
||||
be up-to date.
|
||||
|
||||
* Base properties defined/available on all Objects
|
||||
|
||||
|
|
@ -58,12 +53,16 @@ class Object(ObjectParent, DefaultObject):
|
|||
location (Object) - current location. Is None if this is a room
|
||||
home (Object) - safety start-location
|
||||
has_account (bool, read-only)- will only return *connected* accounts
|
||||
contents (list of Objects, read-only) - returns all objects inside this
|
||||
object (including exits)
|
||||
contents (list, read only) - returns all objects inside this object
|
||||
exits (list of Objects, read-only) - returns all exits from this
|
||||
object, if any
|
||||
destination (Object) - only set if this object is an exit.
|
||||
is_superuser (bool, read-only) - True/False if this user is a superuser
|
||||
is_connected (bool, read-only) - True if this object is associated with
|
||||
an Account with any connected sessions.
|
||||
has_account (bool, read-only) - True is this object has an associated account.
|
||||
is_superuser (bool, read-only): True if this object has an account and that
|
||||
account is a superuser.
|
||||
|
||||
* Handlers available
|
||||
|
||||
|
|
@ -84,18 +83,49 @@ class Object(ObjectParent, DefaultObject):
|
|||
|
||||
* Helper methods (see src.objects.objects.py for full headers)
|
||||
|
||||
search(ostring, global_search=False, attribute_name=None,
|
||||
use_nicks=False, location=None, ignore_errors=False, account=False)
|
||||
execute_cmd(raw_string)
|
||||
msg(text=None, **kwargs)
|
||||
msg_contents(message, exclude=None, from_obj=None, **kwargs)
|
||||
get_search_query_replacement(searchdata, **kwargs)
|
||||
get_search_direct_match(searchdata, **kwargs)
|
||||
get_search_candidates(searchdata, **kwargs)
|
||||
get_search_result(searchdata, attribute_name=None, typeclass=None,
|
||||
candidates=None, exact=False, use_dbref=None, tags=None, **kwargs)
|
||||
get_stacked_result(results, **kwargs)
|
||||
handle_search_results(searchdata, results, **kwargs)
|
||||
search(searchdata, global_search=False, use_nicks=True, typeclass=None,
|
||||
location=None, attribute_name=None, quiet=False, exact=False,
|
||||
candidates=None, use_locks=True, nofound_string=None,
|
||||
multimatch_string=None, use_dbref=None, tags=None, stacked=0)
|
||||
search_account(searchdata, quiet=False)
|
||||
execute_cmd(raw_string, session=None, **kwargs))
|
||||
msg(text=None, from_obj=None, session=None, options=None, **kwargs)
|
||||
for_contents(func, exclude=None, **kwargs)
|
||||
msg_contents(message, exclude=None, from_obj=None, mapping=None,
|
||||
raise_funcparse_errors=False, **kwargs)
|
||||
move_to(destination, quiet=False, emit_to_obj=None, use_destination=True)
|
||||
clear_contents()
|
||||
create(key, account, caller, method, **kwargs)
|
||||
copy(new_key=None)
|
||||
at_object_post_copy(new_obj, **kwargs)
|
||||
delete()
|
||||
is_typeclass(typeclass, exact=False)
|
||||
swap_typeclass(new_typeclass, clean_attributes=False, no_default=True)
|
||||
access(accessing_obj, access_type='read', default=False)
|
||||
access(accessing_obj, access_type='read', default=False,
|
||||
no_superuser_bypass=False, **kwargs)
|
||||
filter_visible(obj_list, looker, **kwargs)
|
||||
get_default_lockstring()
|
||||
get_cmdsets(caller, current, **kwargs)
|
||||
check_permstring(permstring)
|
||||
get_cmdset_providers()
|
||||
get_display_name(looker=None, **kwargs)
|
||||
get_extra_display_name_info(looker=None, **kwargs)
|
||||
get_numbered_name(count, looker, **kwargs)
|
||||
get_display_header(looker, **kwargs)
|
||||
get_display_desc(looker, **kwargs)
|
||||
get_display_exits(looker, **kwargs)
|
||||
get_display_characters(looker, **kwargs)
|
||||
get_display_things(looker, **kwargs)
|
||||
get_display_footer(looker, **kwargs)
|
||||
format_appearance(appearance, looker, **kwargs)
|
||||
return_apperance(looker, **kwargs)
|
||||
|
||||
* Hooks (these are class methods, so args should start with self):
|
||||
|
||||
|
|
@ -113,6 +143,7 @@ class Object(ObjectParent, DefaultObject):
|
|||
|
||||
at_init() - called whenever typeclass is cached from memory,
|
||||
at least once every server restart/reload
|
||||
at_first_save()
|
||||
at_cmdset_get(**kwargs) - this is called just before the command handler
|
||||
requests a cmdset from this object. The kwargs are
|
||||
not normally used unless the cmdset is created
|
||||
|
|
@ -140,12 +171,16 @@ class Object(ObjectParent, DefaultObject):
|
|||
after move, if obj.move_to() has quiet=False
|
||||
at_post_move(source_location) - always called after a move has
|
||||
been successfully performed.
|
||||
at_pre_object_leave(leaving_object, destination, **kwargs)
|
||||
at_object_leave(obj, target_location, move_type="move", **kwargs)
|
||||
at_object_leave(obj, target_location) - called when an object leaves
|
||||
this object in any fashion
|
||||
at_object_receive(obj, source_location) - called when this object receives
|
||||
at_pre_object_receive(obj, source_location)
|
||||
at_object_receive(obj, source_location, move_type="move", **kwargs) - called when this object receives
|
||||
another object
|
||||
at_post_move(source_location, move_type="move", **kwargs)
|
||||
|
||||
at_traverse(traversing_object, source_loc) - (exit-objects only)
|
||||
at_traverse(traversing_object, target_location, **kwargs) - (exit-objects only)
|
||||
handles all moving across the exit, including
|
||||
calling the other exit hooks. Use super() to retain
|
||||
the default functionality.
|
||||
|
|
@ -164,11 +199,18 @@ class Object(ObjectParent, DefaultObject):
|
|||
command by default
|
||||
at_desc(looker=None) - called by 'look' whenever the
|
||||
appearance is requested.
|
||||
at_pre_get(getter, **kwargs)
|
||||
at_get(getter) - called after object has been picked up.
|
||||
Does not stop pickup.
|
||||
at_drop(dropper) - called when this object has been dropped.
|
||||
at_say(speaker, message) - by default, called if an object inside this
|
||||
object speaks
|
||||
at_pre_give(giver, getter, **kwargs)
|
||||
at_give(giver, getter, **kwargs)
|
||||
at_pre_drop(dropper, **kwargs)
|
||||
at_drop(dropper, **kwargs) - called when this object has been dropped.
|
||||
at_pre_say(speaker, message, **kwargs)
|
||||
at_say(message, msg_self=None, msg_location=None, receivers=None, msg_receivers=None, **kwargs)
|
||||
|
||||
at_look(target, **kwargs)
|
||||
at_desc(looker=None)
|
||||
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -17,10 +17,17 @@ from evennia.scripts.scripts import DefaultScript
|
|||
|
||||
class Script(DefaultScript):
|
||||
"""
|
||||
This is the base TypeClass for all Scripts. Scripts describe
|
||||
all entities/systems without a physical existence in the game world
|
||||
that require database storage (like an economic system or
|
||||
combat tracker). They
|
||||
can also have a timer/ticker component.
|
||||
|
||||
A script type is customized by redefining some or all of its hook
|
||||
methods and variables.
|
||||
|
||||
* available properties
|
||||
* available properties (check docs for full listing, this could be
|
||||
outdated).
|
||||
|
||||
key (string) - name of object
|
||||
name (string)- same as key
|
||||
|
|
@ -52,6 +59,7 @@ class Script(DefaultScript):
|
|||
|
||||
* Helper methods
|
||||
|
||||
create(key, **kwargs)
|
||||
start() - start script (this usually happens automatically at creation
|
||||
and obj.script.add() etc)
|
||||
stop() - stop script, and delete it
|
||||
|
|
@ -81,11 +89,14 @@ class Script(DefaultScript):
|
|||
will delay the first call of this method by self.interval
|
||||
seconds. If self.interval==0, this method will never
|
||||
be called.
|
||||
at_pause()
|
||||
at_stop() - Called as the script object is stopped and is about to be
|
||||
removed from the game, e.g. because is_valid() returned False.
|
||||
at_script_delete()
|
||||
at_server_reload() - Called when server reloads. Can be used to
|
||||
save temporary variables you want should survive a reload.
|
||||
at_server_shutdown() - called at a full server shutdown.
|
||||
at_server_start()
|
||||
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ from django.db import models
|
|||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.text import slugify
|
||||
|
||||
from evennia.help.manager import HelpEntryManager
|
||||
from evennia.locks.lockhandler import LockHandler
|
||||
from evennia.typeclasses.models import AliasHandler, Tag, TagHandler
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ from django.conf import settings
|
|||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.validators import validate_comma_separated_integer_list
|
||||
from django.db import models
|
||||
|
||||
from evennia.objects.manager import ObjectDBManager
|
||||
from evennia.typeclasses.models import TypedObject
|
||||
from evennia.utils import logger
|
||||
|
|
@ -71,8 +70,18 @@ class ContentsHandler:
|
|||
objects = self.load()
|
||||
self._pkcache = {obj.pk: True for obj in objects}
|
||||
for obj in objects:
|
||||
for ctype in obj._content_types:
|
||||
self._typecache[ctype][obj.pk] = True
|
||||
try:
|
||||
ctypes = obj._content_types
|
||||
except AttributeError:
|
||||
logger.log_err(
|
||||
f"Object {obj} has no `_content_types` property. Skipping content-cache setup. "
|
||||
"This error suggests it is not a valid Evennia Typeclass but maybe a root model "
|
||||
"like `ObjectDB`. Investigate the `db_typeclass_path` of the object and make sure "
|
||||
"it points to a proper, existing Typeclass."
|
||||
)
|
||||
else:
|
||||
for ctype in obj._content_types:
|
||||
self._typecache[ctype][obj.pk] = True
|
||||
|
||||
def get(self, exclude=None, content_type=None):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -10,11 +10,10 @@ import time
|
|||
import typing
|
||||
from collections import defaultdict
|
||||
|
||||
import evennia
|
||||
import inflect
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
import evennia
|
||||
from evennia.commands import cmdset
|
||||
from evennia.commands.cmdsethandler import CmdSetHandler
|
||||
from evennia.objects.manager import ObjectManager
|
||||
|
|
@ -204,6 +203,184 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
directly - this will both set things up and efficiently save the object
|
||||
without `obj.save()` having to be called explicitly.
|
||||
|
||||
Note: Check the autodocs for complete class members, this may not always
|
||||
be up-to date.
|
||||
|
||||
* Base properties defined/available on all Objects
|
||||
|
||||
key (string) - name of object
|
||||
name (string)- same as key
|
||||
dbref (int, read-only) - unique #id-number. Also "id" can be used.
|
||||
date_created (string) - time stamp of object creation
|
||||
|
||||
account (Account) - controlling account (if any, only set together with
|
||||
sessid below)
|
||||
sessid (int, read-only) - session id (if any, only set together with
|
||||
account above). Use `sessions` handler to get the
|
||||
Sessions directly.
|
||||
location (Object) - current location. Is None if this is a room
|
||||
home (Object) - safety start-location
|
||||
has_account (bool, read-only)- will only return *connected* accounts
|
||||
contents (list, read only) - returns all objects inside this object
|
||||
exits (list of Objects, read-only) - returns all exits from this
|
||||
object, if any
|
||||
destination (Object) - only set if this object is an exit.
|
||||
is_superuser (bool, read-only) - True/False if this user is a superuser
|
||||
is_connected (bool, read-only) - True if this object is associated with
|
||||
an Account with any connected sessions.
|
||||
has_account (bool, read-only) - True is this object has an associated account.
|
||||
is_superuser (bool, read-only): True if this object has an account and that
|
||||
account is a superuser.
|
||||
|
||||
* Handlers available
|
||||
|
||||
aliases - alias-handler: use aliases.add/remove/get() to use.
|
||||
permissions - permission-handler: use permissions.add/remove() to
|
||||
add/remove new perms.
|
||||
locks - lock-handler: use locks.add() to add new lock strings
|
||||
scripts - script-handler. Add new scripts to object with scripts.add()
|
||||
cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object
|
||||
nicks - nick-handler. New nicks with nicks.add().
|
||||
sessions - sessions-handler. Get Sessions connected to this
|
||||
object with sessions.get()
|
||||
attributes - attribute-handler. Use attributes.add/remove/get.
|
||||
db - attribute-handler: Shortcut for attribute-handler. Store/retrieve
|
||||
database attributes using self.db.myattr=val, val=self.db.myattr
|
||||
ndb - non-persistent attribute handler: same as db but does not create
|
||||
a database entry when storing data
|
||||
|
||||
* Helper methods (see src.objects.objects.py for full headers)
|
||||
|
||||
get_search_query_replacement(searchdata, **kwargs)
|
||||
get_search_direct_match(searchdata, **kwargs)
|
||||
get_search_candidates(searchdata, **kwargs)
|
||||
get_search_result(searchdata, attribute_name=None, typeclass=None,
|
||||
candidates=None, exact=False, use_dbref=None, tags=None, **kwargs)
|
||||
get_stacked_result(results, **kwargs)
|
||||
handle_search_results(searchdata, results, **kwargs)
|
||||
search(searchdata, global_search=False, use_nicks=True, typeclass=None,
|
||||
location=None, attribute_name=None, quiet=False, exact=False,
|
||||
candidates=None, use_locks=True, nofound_string=None,
|
||||
multimatch_string=None, use_dbref=None, tags=None, stacked=0)
|
||||
search_account(searchdata, quiet=False)
|
||||
execute_cmd(raw_string, session=None, **kwargs))
|
||||
msg(text=None, from_obj=None, session=None, options=None, **kwargs)
|
||||
for_contents(func, exclude=None, **kwargs)
|
||||
msg_contents(message, exclude=None, from_obj=None, mapping=None,
|
||||
raise_funcparse_errors=False, **kwargs)
|
||||
move_to(destination, quiet=False, emit_to_obj=None, use_destination=True)
|
||||
clear_contents()
|
||||
create(key, account, caller, method, **kwargs)
|
||||
copy(new_key=None)
|
||||
at_object_post_copy(new_obj, **kwargs)
|
||||
delete()
|
||||
is_typeclass(typeclass, exact=False)
|
||||
swap_typeclass(new_typeclass, clean_attributes=False, no_default=True)
|
||||
access(accessing_obj, access_type='read', default=False,
|
||||
no_superuser_bypass=False, **kwargs)
|
||||
filter_visible(obj_list, looker, **kwargs)
|
||||
get_default_lockstring()
|
||||
get_cmdsets(caller, current, **kwargs)
|
||||
check_permstring(permstring)
|
||||
get_cmdset_providers()
|
||||
get_display_name(looker=None, **kwargs)
|
||||
get_extra_display_name_info(looker=None, **kwargs)
|
||||
get_numbered_name(count, looker, **kwargs)
|
||||
get_display_header(looker, **kwargs)
|
||||
get_display_desc(looker, **kwargs)
|
||||
get_display_exits(looker, **kwargs)
|
||||
get_display_characters(looker, **kwargs)
|
||||
get_display_things(looker, **kwargs)
|
||||
get_display_footer(looker, **kwargs)
|
||||
format_appearance(appearance, looker, **kwargs)
|
||||
return_apperance(looker, **kwargs)
|
||||
|
||||
* Hooks (these are class methods, so args should start with self):
|
||||
|
||||
basetype_setup() - only called once, used for behind-the-scenes
|
||||
setup. Normally not modified.
|
||||
basetype_posthook_setup() - customization in basetype, after the object
|
||||
has been created; Normally not modified.
|
||||
|
||||
at_object_creation() - only called once, when object is first created.
|
||||
Object customizations go here.
|
||||
at_object_delete() - called just before deleting an object. If returning
|
||||
False, deletion is aborted. Note that all objects
|
||||
inside a deleted object are automatically moved
|
||||
to their <home>, they don't need to be removed here.
|
||||
|
||||
at_init() - called whenever typeclass is cached from memory,
|
||||
at least once every server restart/reload
|
||||
at_first_save()
|
||||
at_cmdset_get(**kwargs) - this is called just before the command handler
|
||||
requests a cmdset from this object. The kwargs are
|
||||
not normally used unless the cmdset is created
|
||||
dynamically (see e.g. Exits).
|
||||
at_pre_puppet(account)- (account-controlled objects only) called just
|
||||
before puppeting
|
||||
at_post_puppet() - (account-controlled objects only) called just
|
||||
after completing connection account<->object
|
||||
at_pre_unpuppet() - (account-controlled objects only) called just
|
||||
before un-puppeting
|
||||
at_post_unpuppet(account) - (account-controlled objects only) called just
|
||||
after disconnecting account<->object link
|
||||
at_server_reload() - called before server is reloaded
|
||||
at_server_shutdown() - called just before server is fully shut down
|
||||
|
||||
at_access(result, accessing_obj, access_type) - called with the result
|
||||
of a lock access check on this object. Return value
|
||||
does not affect check result.
|
||||
|
||||
at_pre_move(destination) - called just before moving object
|
||||
to the destination. If returns False, move is cancelled.
|
||||
announce_move_from(destination) - called in old location, just
|
||||
before move, if obj.move_to() has quiet=False
|
||||
announce_move_to(source_location) - called in new location, just
|
||||
after move, if obj.move_to() has quiet=False
|
||||
at_post_move(source_location) - always called after a move has
|
||||
been successfully performed.
|
||||
at_pre_object_leave(leaving_object, destination, **kwargs)
|
||||
at_object_leave(obj, target_location, move_type="move", **kwargs)
|
||||
at_object_leave(obj, target_location) - called when an object leaves
|
||||
this object in any fashion
|
||||
at_pre_object_receive(obj, source_location)
|
||||
at_object_receive(obj, source_location, move_type="move", **kwargs) - called when this object
|
||||
receives another object
|
||||
at_post_move(source_location, move_type="move", **kwargs)
|
||||
|
||||
at_traverse(traversing_object, target_location, **kwargs) - (exit-objects only)
|
||||
handles all moving across the exit, including
|
||||
calling the other exit hooks. Use super() to retain
|
||||
the default functionality.
|
||||
at_post_traverse(traversing_object, source_location) - (exit-objects only)
|
||||
called just after a traversal has happened.
|
||||
at_failed_traverse(traversing_object) - (exit-objects only) called if
|
||||
traversal fails and property err_traverse is not defined.
|
||||
|
||||
at_msg_receive(self, msg, from_obj=None, **kwargs) - called when a message
|
||||
(via self.msg()) is sent to this obj.
|
||||
If returns false, aborts send.
|
||||
at_msg_send(self, msg, to_obj=None, **kwargs) - called when this objects
|
||||
sends a message to someone via self.msg().
|
||||
|
||||
return_appearance(looker) - describes this object. Used by "look"
|
||||
command by default
|
||||
at_desc(looker=None) - called by 'look' whenever the
|
||||
appearance is requested.
|
||||
at_pre_get(getter, **kwargs)
|
||||
at_get(getter) - called after object has been picked up.
|
||||
Does not stop pickup.
|
||||
at_pre_give(giver, getter, **kwargs)
|
||||
at_give(giver, getter, **kwargs)
|
||||
at_pre_drop(dropper, **kwargs)
|
||||
at_drop(dropper, **kwargs) - called when this object has been dropped.
|
||||
at_pre_say(speaker, message, **kwargs)
|
||||
at_say(message, msg_self=None, msg_location=None, receivers=None, msg_receivers=None, **kwargs)
|
||||
|
||||
at_look(target, **kwargs)
|
||||
at_desc(looker=None)
|
||||
|
||||
|
||||
"""
|
||||
|
||||
# Determines which order command sets begin to be assembled from.
|
||||
|
|
@ -316,7 +493,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
"obj.location to move an object here.".format(self.__class__)
|
||||
)
|
||||
|
||||
contents = property(contents_get, contents_set, contents_set, contents_set)
|
||||
contents = property(contents_get, contents_set, contents_set)
|
||||
|
||||
@property
|
||||
def exits(self):
|
||||
|
|
@ -378,7 +555,8 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
provided to the search function is included, and could be modified in-place here.
|
||||
|
||||
Args:
|
||||
searchdata (str): The search criterion (could be modified by `get_search_query_replacement`).
|
||||
searchdata (str): The search criterion (could be modified by
|
||||
`get_search_query_replacement`).
|
||||
**kwargs (any): These are the same as passed to the `search` method.
|
||||
|
||||
Returns:
|
||||
|
|
@ -1011,8 +1189,9 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
the text message generated by announce_move_to and announce_move_from by defining
|
||||
their {"type": move_type} for outgoing text. This can be used for altering
|
||||
messages and/or overloaded hook behaviors.
|
||||
**kwargs: Passed on to announce_move_to and announce_move_from hooks. Exits will set
|
||||
the "exit_obj" kwarg to themselves.
|
||||
**kwargs: Passed on to all movement- and announcement hooks. Use in your game to let
|
||||
hooks know about any special condition of the move (such as running or sneaking).
|
||||
Exits will pass an "exit_obj" kwarg.
|
||||
|
||||
Returns:
|
||||
bool: True/False depending on if there were problems with the move. This method may also
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ from django.conf import settings
|
|||
from django.core.paginator import Paginator
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from evennia.locks.lockhandler import check_lockstring, validate_lockstring
|
||||
from evennia.objects.models import ObjectDB
|
||||
from evennia.scripts.scripts import DefaultScript
|
||||
|
|
@ -104,6 +103,7 @@ def homogenize_prototype(prototype, custom_keys=None):
|
|||
prototype[protkey] = ""
|
||||
|
||||
homogenized = {}
|
||||
homogenized_aliases = []
|
||||
homogenized_tags = []
|
||||
homogenized_attrs = []
|
||||
homogenized_parents = []
|
||||
|
|
@ -111,7 +111,10 @@ def homogenize_prototype(prototype, custom_keys=None):
|
|||
for key, val in prototype.items():
|
||||
if key in reserved:
|
||||
# check all reserved keys
|
||||
if key == "tags":
|
||||
if key == "aliases":
|
||||
# make sure aliases are always in a list even if given as a single string
|
||||
homogenized_aliases = make_iter(val)
|
||||
elif key == "tags":
|
||||
# tags must be on form [(tag, category, data), ...]
|
||||
tags = make_iter(prototype.get("tags", []))
|
||||
for tag in tags:
|
||||
|
|
@ -160,13 +163,14 @@ def homogenize_prototype(prototype, custom_keys=None):
|
|||
else:
|
||||
# normal prototype-parent names are added as-is
|
||||
homogenized_parents.append(parent)
|
||||
|
||||
else:
|
||||
# another reserved key
|
||||
homogenized[key] = val
|
||||
else:
|
||||
# unreserved keys -> attrs
|
||||
homogenized_attrs.append((key, val, None, ""))
|
||||
if homogenized_aliases:
|
||||
homogenized["aliases"] = homogenized_aliases
|
||||
if homogenized_attrs:
|
||||
homogenized["attrs"] = homogenized_attrs
|
||||
if homogenized_tags:
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ added to all game objects. You access it through the property
|
|||
"""
|
||||
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from evennia.scripts.models import ScriptDB
|
||||
from evennia.utils import create, logger
|
||||
|
||||
|
|
@ -73,18 +72,27 @@ class ScriptHandler(object):
|
|||
Script: The newly created Script.
|
||||
|
||||
"""
|
||||
if self.obj.__dbclass__.__name__ == "AccountDB":
|
||||
# we add to an Account, not an Object
|
||||
script = create.create_script(
|
||||
scriptclass, key=key, account=self.obj, autostart=autostart
|
||||
)
|
||||
elif isinstance(scriptclass, str) or callable(scriptclass):
|
||||
if isinstance(scriptclass, str) or callable(scriptclass):
|
||||
# a str or class to use create before adding to an Object. We wait to autostart
|
||||
# so we can differentiate a failing creation from a script that immediately starts/stops.
|
||||
script = create.create_script(scriptclass, key=key, obj=self.obj, autostart=False)
|
||||
if self.obj.__dbclass__.__name__ == "AccountDB":
|
||||
# we add to an Account, not an Object
|
||||
script = create.create_script(
|
||||
scriptclass, key=key, account=self.obj, autostart=False
|
||||
)
|
||||
else:
|
||||
script = create.create_script(scriptclass, key=key, obj=self.obj, autostart=False)
|
||||
else:
|
||||
# already an instantiated class
|
||||
script = scriptclass
|
||||
if script.db_obj and script.db_obj != self.obj:
|
||||
logger.log_err(
|
||||
f"Script instance {script} already belongs to "
|
||||
f"another object: {script.db_obj}."
|
||||
)
|
||||
return None
|
||||
script.db_obj = self.obj
|
||||
script.save()
|
||||
|
||||
if not script:
|
||||
logger.log_err(f"Script {scriptclass} failed to be created.")
|
||||
|
|
|
|||
|
|
@ -6,13 +6,12 @@ ability to run timers.
|
|||
"""
|
||||
|
||||
from django.utils.translation import gettext as _
|
||||
from twisted.internet.defer import Deferred, maybeDeferred
|
||||
from twisted.internet.task import LoopingCall
|
||||
|
||||
from evennia.scripts.manager import ScriptManager
|
||||
from evennia.scripts.models import ScriptDB
|
||||
from evennia.typeclasses.models import TypeclassBase
|
||||
from evennia.utils import create, logger
|
||||
from twisted.internet.defer import Deferred, maybeDeferred
|
||||
from twisted.internet.task import LoopingCall
|
||||
|
||||
__all__ = ["DefaultScript", "DoNothing", "Store"]
|
||||
|
||||
|
|
@ -423,7 +422,12 @@ class ScriptBase(ScriptDB, metaclass=TypeclassBase):
|
|||
updates = []
|
||||
if not cdict.get("key"):
|
||||
if not self.db_key:
|
||||
self.db_key = "#%i" % self.dbid
|
||||
if hasattr(self, "key"):
|
||||
# take key from the object typeclass
|
||||
self.db_key = self.key
|
||||
else:
|
||||
# no key set anywhere, use class+dbid as key
|
||||
self.db_key = f"{self.__class__.__name__}(#{self.dbid})"
|
||||
updates.append("db_key")
|
||||
elif self.db_key != cdict["key"]:
|
||||
self.db_key = cdict["key"]
|
||||
|
|
@ -672,8 +676,85 @@ class ScriptBase(ScriptDB, metaclass=TypeclassBase):
|
|||
class DefaultScript(ScriptBase):
|
||||
"""
|
||||
This is the base TypeClass for all Scripts. Scripts describe
|
||||
events, timers and states in game, they can have a time component
|
||||
or describe a state that changes under certain conditions.
|
||||
all entities/systems without a physical existence in the game world
|
||||
that require database storage (like an economic system or
|
||||
combat tracker). They
|
||||
can also have a timer/ticker component.
|
||||
|
||||
A script type is customized by redefining some or all of its hook
|
||||
methods and variables.
|
||||
|
||||
* available properties (check docs for full listing, this could be
|
||||
outdated).
|
||||
|
||||
key (string) - name of object
|
||||
name (string)- same as key
|
||||
aliases (list of strings) - aliases to the object. Will be saved
|
||||
to database as AliasDB entries but returned as strings.
|
||||
dbref (int, read-only) - unique #id-number. Also "id" can be used.
|
||||
date_created (string) - time stamp of object creation
|
||||
permissions (list of strings) - list of permission strings
|
||||
|
||||
desc (string) - optional description of script, shown in listings
|
||||
obj (Object) - optional object that this script is connected to
|
||||
and acts on (set automatically by obj.scripts.add())
|
||||
interval (int) - how often script should run, in seconds. <0 turns
|
||||
off ticker
|
||||
start_delay (bool) - if the script should start repeating right away or
|
||||
wait self.interval seconds
|
||||
repeats (int) - how many times the script should repeat before
|
||||
stopping. 0 means infinite repeats
|
||||
persistent (bool) - if script should survive a server shutdown or not
|
||||
is_active (bool) - if script is currently running
|
||||
|
||||
* Handlers
|
||||
|
||||
locks - lock-handler: use locks.add() to add new lock strings
|
||||
db - attribute-handler: store/retrieve database attributes on this
|
||||
self.db.myattr=val, val=self.db.myattr
|
||||
ndb - non-persistent attribute handler: same as db but does not
|
||||
create a database entry when storing data
|
||||
|
||||
* Helper methods
|
||||
|
||||
create(key, **kwargs)
|
||||
start() - start script (this usually happens automatically at creation
|
||||
and obj.script.add() etc)
|
||||
stop() - stop script, and delete it
|
||||
pause() - put the script on hold, until unpause() is called. If script
|
||||
is persistent, the pause state will survive a shutdown.
|
||||
unpause() - restart a previously paused script. The script will continue
|
||||
from the paused timer (but at_start() will be called).
|
||||
time_until_next_repeat() - if a timed script (interval>0), returns time
|
||||
until next tick
|
||||
|
||||
* Hook methods (should also include self as the first argument):
|
||||
|
||||
at_script_creation() - called only once, when an object of this
|
||||
class is first created.
|
||||
is_valid() - is called to check if the script is valid to be running
|
||||
at the current time. If is_valid() returns False, the running
|
||||
script is stopped and removed from the game. You can use this
|
||||
to check state changes (i.e. an script tracking some combat
|
||||
stats at regular intervals is only valid to run while there is
|
||||
actual combat going on).
|
||||
at_start() - Called every time the script is started, which for persistent
|
||||
scripts is at least once every server start. Note that this is
|
||||
unaffected by self.delay_start, which only delays the first
|
||||
call to at_repeat().
|
||||
at_repeat() - Called every self.interval seconds. It will be called
|
||||
immediately upon launch unless self.delay_start is True, which
|
||||
will delay the first call of this method by self.interval
|
||||
seconds. If self.interval==0, this method will never
|
||||
be called.
|
||||
at_pause()
|
||||
at_stop() - Called as the script object is stopped and is about to be
|
||||
removed from the game, e.g. because is_valid() returned False.
|
||||
at_script_delete()
|
||||
at_server_reload() - Called when server reloads. Can be used to
|
||||
save temporary variables you want should survive a reload.
|
||||
at_server_shutdown() - called at a full server shutdown.
|
||||
at_server_start()
|
||||
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ Unit tests for the scripts package
|
|||
from collections import defaultdict
|
||||
from unittest import TestCase, mock
|
||||
|
||||
from parameterized import parameterized
|
||||
|
||||
from evennia import DefaultScript
|
||||
from evennia.objects.objects import DefaultObject
|
||||
from evennia.scripts.manager import ScriptDBManager
|
||||
|
|
@ -19,6 +17,7 @@ from evennia.scripts.tickerhandler import TickerHandler
|
|||
from evennia.utils.create import create_script
|
||||
from evennia.utils.dbserialize import dbserialize
|
||||
from evennia.utils.test_resources import BaseEvenniaTest, EvenniaTest
|
||||
from parameterized import parameterized
|
||||
|
||||
|
||||
class TestScript(BaseEvenniaTest):
|
||||
|
|
@ -105,6 +104,15 @@ class TestScriptHandler(BaseEvenniaTest):
|
|||
script = self.obj.scripts.get("interval_test")
|
||||
self.assertTrue(bool(script))
|
||||
|
||||
def test_add_already_existing_script(self):
|
||||
"Checks that Scripthandler add function adds script correctly"
|
||||
|
||||
# make a new script with no obj connection
|
||||
script = create_script(TestingListIntervalScript, key="interval_test2")
|
||||
self.obj.scripts.add(script)
|
||||
self.assertEqual([script], list(self.obj.scripts.get("interval_test2")))
|
||||
self.assertTrue(bool(self.obj.scripts.get("interval_test")))
|
||||
|
||||
|
||||
class TestScriptDB(TestCase):
|
||||
"Check the singleton/static ScriptDB object works correctly"
|
||||
|
|
|
|||
|
|
@ -474,7 +474,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS):
|
|||
_RE_N.sub("", prompt) + ("||n" if prompt.endswith("|") else "|n"),
|
||||
strip_ansi=nocolor,
|
||||
xterm256=xterm256,
|
||||
truecolor=truecolor
|
||||
truecolor=truecolor,
|
||||
)
|
||||
if mxp:
|
||||
prompt = mxp_parse(prompt)
|
||||
|
|
@ -511,7 +511,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS):
|
|||
strip_ansi=nocolor,
|
||||
xterm256=xterm256,
|
||||
mxp=mxp,
|
||||
truecolor=truecolor
|
||||
truecolor=truecolor,
|
||||
)
|
||||
if mxp:
|
||||
linetosend = mxp_parse(linetosend)
|
||||
|
|
|
|||
|
|
@ -130,10 +130,10 @@ class Ttype:
|
|||
self.protocol.protocol_flags["NOPROMPTGOAHEAD"] = False
|
||||
|
||||
if (
|
||||
clientname.startswith("XTERM")
|
||||
or clientname.endswith("-256COLOR")
|
||||
or clientname
|
||||
in (
|
||||
clientname.startswith("XTERM")
|
||||
or clientname.endswith("-256COLOR")
|
||||
or clientname
|
||||
in (
|
||||
"ATLANTIS", # > 0.9.9.0 (aug 2009)
|
||||
"CMUD", # > 3.04 (mar 2009)
|
||||
"KILDCLIENT", # > 2.2.0 (sep 2005)
|
||||
|
|
@ -143,17 +143,13 @@ class Ttype:
|
|||
"BEIP", # > 2.00.206 (late 2009) (BeipMu)
|
||||
"POTATO", # > 2.00 (maybe earlier)
|
||||
"TINYFUGUE", # > 4.x (maybe earlier)
|
||||
)
|
||||
)
|
||||
):
|
||||
xterm256 = True
|
||||
|
||||
# use name to identify support for xterm truecolor
|
||||
truecolor = False
|
||||
if (clientname.endswith("-TRUECOLOR") or
|
||||
clientname in (
|
||||
"AXMUD",
|
||||
"TINTIN"
|
||||
)):
|
||||
if clientname.endswith("-TRUECOLOR") or clientname in ("AXMUD", "TINTIN"):
|
||||
truecolor = True
|
||||
|
||||
# all clients supporting TTYPE at all seem to support ANSI
|
||||
|
|
@ -169,9 +165,9 @@ class Ttype:
|
|||
tupper = term.upper()
|
||||
# identify xterm256 based on flag
|
||||
xterm256 = (
|
||||
tupper.endswith("-256COLOR")
|
||||
or tupper.endswith("XTERM") # Apple Terminal, old Tintin
|
||||
and not tupper.endswith("-COLOR") # old Tintin, Putty
|
||||
tupper.endswith("-256COLOR")
|
||||
or tupper.endswith("XTERM") # Apple Terminal, old Tintin
|
||||
and not tupper.endswith("-COLOR") # old Tintin, Putty
|
||||
)
|
||||
if xterm256:
|
||||
self.protocol.protocol_flags["ANSI"] = True
|
||||
|
|
|
|||
|
|
@ -8,20 +8,19 @@ import time
|
|||
import traceback
|
||||
|
||||
import django
|
||||
import evennia
|
||||
from django.conf import settings
|
||||
from django.db import connection
|
||||
from django.db.utils import OperationalError
|
||||
from django.utils.translation import gettext as _
|
||||
from evennia.utils import logger
|
||||
from evennia.utils.utils import get_evennia_version, make_iter, mod_import
|
||||
from twisted.application import internet
|
||||
from twisted.application.service import MultiService
|
||||
from twisted.internet import defer, reactor
|
||||
from twisted.internet.defer import Deferred
|
||||
from twisted.internet.task import LoopingCall
|
||||
|
||||
import evennia
|
||||
from evennia.utils import logger
|
||||
from evennia.utils.utils import get_evennia_version, make_iter, mod_import
|
||||
|
||||
_SA = object.__setattr__
|
||||
|
||||
|
||||
|
|
@ -282,11 +281,10 @@ class EvenniaServerService(MultiService):
|
|||
and settings.DATABASES.get("default", {}).get("ENGINE", None)
|
||||
== "django.db.backends.sqlite3"
|
||||
):
|
||||
# sqlite3 database pragmas (directives)
|
||||
cursor = connection.cursor()
|
||||
cursor.execute("PRAGMA cache_size=10000")
|
||||
cursor.execute("PRAGMA synchronous=OFF")
|
||||
cursor.execute("PRAGMA count_changes=OFF")
|
||||
cursor.execute("PRAGMA temp_store=2")
|
||||
for pragma in settings.SQLITE3_PRAGMAS:
|
||||
cursor.execute(pragma)
|
||||
|
||||
def update_defaults(self):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -300,6 +300,16 @@ DATABASES = {
|
|||
"PORT": "",
|
||||
}
|
||||
}
|
||||
# PRAGMA (directives) for the default Sqlite3 database operations. This can be used to tweak
|
||||
# performance for your setup. Don't change this unless you know what # you are doing.
|
||||
SQLITE3_PRAGMAS = (
|
||||
"PRAGMA cache_size=10000",
|
||||
"PRAGMA synchronous=1",
|
||||
"PRAGMA count_changes=OFF",
|
||||
"PRAGMA temp_store=2",
|
||||
"PRAGMA journal_mode=WAL",
|
||||
)
|
||||
|
||||
# How long the django-database connection should be kept open, in seconds.
|
||||
# If you get errors about the database having gone away after long idle
|
||||
# periods, shorten this value (e.g. MySQL defaults to a timeout of 8 hrs)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ from copy import copy
|
|||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils.encoding import smart_str
|
||||
|
||||
from evennia.locks.lockhandler import LockHandler
|
||||
from evennia.utils.dbserialize import from_pickle, to_pickle
|
||||
from evennia.utils.idmapper.models import SharedMemoryModel
|
||||
|
|
@ -142,6 +141,8 @@ class InMemoryAttribute(IAttribute):
|
|||
# Value and locks are special. We must call the wrappers.
|
||||
if key == "value":
|
||||
self.value = value
|
||||
elif key == "strvalue":
|
||||
self.db_strvalue = value
|
||||
elif key == "lock_storage":
|
||||
self.lock_storage = value
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ these to create custom managers.
|
|||
|
||||
"""
|
||||
|
||||
import evennia
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
|
@ -37,21 +36,30 @@ from django.urls import reverse
|
|||
from django.utils import timezone
|
||||
from django.utils.encoding import smart_str
|
||||
from django.utils.text import slugify
|
||||
|
||||
import evennia
|
||||
from evennia.locks.lockhandler import LockHandler
|
||||
from evennia.server.signals import SIGNAL_TYPED_OBJECT_POST_RENAME
|
||||
from evennia.typeclasses import managers
|
||||
from evennia.typeclasses.attributes import (Attribute, AttributeHandler,
|
||||
AttributeProperty, DbHolder,
|
||||
InMemoryAttributeBackend,
|
||||
ModelAttributeBackend)
|
||||
from evennia.typeclasses.tags import (AliasHandler, PermissionHandler, Tag,
|
||||
TagCategoryProperty, TagHandler,
|
||||
TagProperty)
|
||||
from evennia.utils.idmapper.models import (SharedMemoryModel,
|
||||
SharedMemoryModelBase)
|
||||
from evennia.typeclasses.attributes import (
|
||||
Attribute,
|
||||
AttributeHandler,
|
||||
AttributeProperty,
|
||||
DbHolder,
|
||||
InMemoryAttributeBackend,
|
||||
ModelAttributeBackend,
|
||||
)
|
||||
from evennia.typeclasses.tags import (
|
||||
AliasHandler,
|
||||
PermissionHandler,
|
||||
Tag,
|
||||
TagCategoryProperty,
|
||||
TagHandler,
|
||||
TagProperty,
|
||||
)
|
||||
from evennia.utils.idmapper.models import SharedMemoryModel, SharedMemoryModelBase
|
||||
from evennia.utils.logger import log_trace
|
||||
from evennia.utils.utils import (class_from_module, inherits_from, is_iter,
|
||||
lazy_property)
|
||||
from evennia.utils.utils import class_from_module, inherits_from, is_iter, lazy_property
|
||||
|
||||
__all__ = ("TypedObject",)
|
||||
|
||||
|
|
|
|||
|
|
@ -67,10 +67,9 @@ import re
|
|||
from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from evennia.utils import logger, utils
|
||||
from evennia.utils.utils import to_str
|
||||
from evennia.utils.hex_colors import HexColors
|
||||
from evennia.utils.utils import to_str
|
||||
|
||||
hex2truecolor = HexColors()
|
||||
hex_sub = HexColors.hex_sub
|
||||
|
|
@ -84,6 +83,11 @@ ANSI_ESCAPE = "\033"
|
|||
ANSI_NORMAL = "\033[0m"
|
||||
|
||||
ANSI_UNDERLINE = "\033[4m"
|
||||
ANSI_UNDERLINE_RESET = "\033[24m"
|
||||
ANSI_ITALIC = "\033[3m"
|
||||
ANSI_ITALIC_RESET = "\033[23m"
|
||||
ANSI_STRIKE = "\033[9m"
|
||||
ANSI_STRIKE_RESET = "\033[29m"
|
||||
ANSI_HILITE = "\033[1m"
|
||||
ANSI_UNHILITE = "\033[22m"
|
||||
ANSI_BLINK = "\033[5m"
|
||||
|
|
@ -119,7 +123,7 @@ ANSI_TAB = "\t"
|
|||
ANSI_SPACE = " "
|
||||
|
||||
# Escapes
|
||||
ANSI_ESCAPES = ("{{", "\\\\", "\|\|")
|
||||
ANSI_ESCAPES = ("{{", r"\\", r"\|\|")
|
||||
|
||||
_PARSE_CACHE = OrderedDict()
|
||||
_PARSE_CACHE_SIZE = 10000
|
||||
|
|
@ -149,6 +153,11 @@ class ANSIParser(object):
|
|||
(r"|*", ANSI_INVERSE), # invert
|
||||
(r"|^", ANSI_BLINK), # blinking text (very annoying and not supported by all clients)
|
||||
(r"|u", ANSI_UNDERLINE), # underline
|
||||
(r"|U", ANSI_UNDERLINE_RESET), # underline reset
|
||||
(r"|i", ANSI_ITALIC), # italic
|
||||
(r"|I", ANSI_ITALIC_RESET), # italic reset
|
||||
(r"|s", ANSI_STRIKE), # strikethrough
|
||||
(r"|S", ANSI_STRIKE_RESET), # strikethrough reset
|
||||
(r"|r", ANSI_HILITE + ANSI_RED),
|
||||
(r"|g", ANSI_HILITE + ANSI_GREEN),
|
||||
(r"|y", ANSI_HILITE + ANSI_YELLOW),
|
||||
|
|
@ -812,7 +821,7 @@ class ANSIString(str, metaclass=ANSIMeta):
|
|||
if not decoded:
|
||||
# Completely new ANSI String
|
||||
clean_string = parser.parse_ansi(string, strip_ansi=True, mxp=MXP_ENABLED)
|
||||
string = parser.parse_ansi(string, xterm256=True, mxp=MXP_ENABLED)
|
||||
string = parser.parse_ansi(string, xterm256=True, mxp=MXP_ENABLED, truecolor=True)
|
||||
elif clean_string is not None:
|
||||
# We have an explicit clean string.
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ from pickle import dumps
|
|||
|
||||
from django.conf import settings
|
||||
from django.db.utils import OperationalError, ProgrammingError
|
||||
|
||||
from evennia.scripts.models import ScriptDB
|
||||
from evennia.utils import logger
|
||||
from evennia.utils.utils import callables_from_module, class_from_module
|
||||
|
||||
|
|
@ -217,7 +217,7 @@ class GlobalScriptContainer(Container):
|
|||
"""
|
||||
if not self.loaded:
|
||||
self.load_data()
|
||||
out_value = default
|
||||
script_found = None
|
||||
if key in self.loaded_data:
|
||||
if key not in self.typeclass_storage:
|
||||
# this means we are trying to load in a loop
|
||||
|
|
@ -230,8 +230,12 @@ class GlobalScriptContainer(Container):
|
|||
script_found = self._load_script(key)
|
||||
if script_found:
|
||||
out_value = script_found
|
||||
else:
|
||||
# script not found in settings, see if one exists in database (not
|
||||
# auto-started/recreated)
|
||||
script_found = ScriptDB.objects.filter(db_key__iexact=key, db_obj__isnull=True).first()
|
||||
|
||||
return out_value
|
||||
return script_found if script_found is not None else default
|
||||
|
||||
def all(self):
|
||||
"""
|
||||
|
|
@ -239,12 +243,19 @@ class GlobalScriptContainer(Container):
|
|||
scripts defined in settings.
|
||||
|
||||
Returns:
|
||||
scripts (list): All global script objects stored on the container.
|
||||
list: All global script objects in game (both managed and unmanaged),
|
||||
sorted alphabetically.
|
||||
|
||||
"""
|
||||
if not self.loaded:
|
||||
self.load_data()
|
||||
return list(self.loaded_data.values())
|
||||
managed_scripts = list(self.loaded_data.values())
|
||||
unmanaged_scripts = list(
|
||||
ScriptDB.objects.filter(db_obj__isnull=True).exclude(
|
||||
id__in=[scr.id for scr in managed_scripts]
|
||||
)
|
||||
)
|
||||
return list(sorted(managed_scripts + unmanaged_scripts, key=lambda scr: scr.db_key))
|
||||
|
||||
|
||||
# Create all singletons
|
||||
|
|
|
|||
|
|
@ -722,7 +722,7 @@ class CmdEditorGroup(CmdEditorBase):
|
|||
}
|
||||
align_name = {"f": "Full", "c": "Center", "l": "Left", "r": "Right"}
|
||||
# shift width arg right if no alignment specified
|
||||
if self.arg1.startswith('='):
|
||||
if self.arg1.startswith("="):
|
||||
self.arg2 = self.arg1
|
||||
self.arg1 = None
|
||||
if self.arg1 and self.arg1.lower() not in align_map:
|
||||
|
|
|
|||
|
|
@ -520,13 +520,15 @@ class EvMenu:
|
|||
startnode (str, optional): The starting node name in the menufile.
|
||||
cmdset_mergetype (str, optional): 'Replace' (default) means the menu
|
||||
commands will be exclusive - no other normal commands will
|
||||
be usable while the user is in the menu. 'Union' means the
|
||||
menu commands will be integrated with the existing commands
|
||||
(it will merge with `merge_priority`), if so, make sure that
|
||||
the menu's command names don't collide with existing commands
|
||||
in an unexpected way. Also the CMD_NOMATCH and CMD_NOINPUT will
|
||||
be overloaded by the menu cmdset. Other cmdser mergetypes
|
||||
has little purpose for the menu.
|
||||
be usable while the user is in the menu. 'Union' does merge the menu
|
||||
command, but note that the only command used in EvMenu has key/alias
|
||||
of NOINPUT/NOMATCH. So if you merge with 'Union' and a high `cmdset_prio`
|
||||
(below), you won't replace individual normal commands as you may
|
||||
expect. Instead commands will work normally and you'll only always fall
|
||||
back to menu commands when no other command is found. There is no way
|
||||
to partially replace normal commands with EvMenu actions - to do this,
|
||||
remove the normal command from the caller's cmdset - if not found
|
||||
the menu's version will kick in instead.
|
||||
cmdset_priority (int, optional): The merge priority for the
|
||||
menu command set. The default (1) is usually enough for most
|
||||
types of menus.
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ class HexColors:
|
|||
Based on code from @InspectorCaracal
|
||||
"""
|
||||
|
||||
_RE_FG = "\|#"
|
||||
_RE_BG = "\|\[#"
|
||||
_RE_FG_OR_BG = "\|\[?#"
|
||||
_RE_FG = r"\|#"
|
||||
_RE_BG = r"\|\[#"
|
||||
_RE_FG_OR_BG = r"\|\[?#"
|
||||
_RE_HEX_LONG = "[0-9a-fA-F]{6}"
|
||||
_RE_HEX_SHORT = "[0-9a-fA-F]{3}"
|
||||
_RE_BYTE = "[0-2]?[0-9]?[0-9]"
|
||||
|
|
@ -23,8 +23,8 @@ class HexColors:
|
|||
# Used for greyscale
|
||||
_GREYS = "abcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
TRUECOLOR_FG = f"\x1b\[38;2;{_RE_BYTE};{_RE_BYTE};{_RE_BYTE}m"
|
||||
TRUECOLOR_BG = f"\x1b\[48;2;{_RE_BYTE};{_RE_BYTE};{_RE_BYTE}m"
|
||||
TRUECOLOR_FG = rf"\x1b\[38;2;{_RE_BYTE};{_RE_BYTE};{_RE_BYTE}m"
|
||||
TRUECOLOR_BG = rf"\x1b\[48;2;{_RE_BYTE};{_RE_BYTE};{_RE_BYTE}m"
|
||||
|
||||
# Our matchers for use with ANSIParser and ANSIString
|
||||
hex_sub = re.compile(rf"{_RE_HEX_PATTERN}", re.DOTALL)
|
||||
|
|
@ -121,27 +121,22 @@ class HexColors:
|
|||
|
||||
r, g, b = self._hex_to_rgb_24_bit(tag)
|
||||
|
||||
# Is it greyscale?
|
||||
if r == g and g == b:
|
||||
return f"{indicator}=" + self._GREYS[self._grey_int(r)]
|
||||
if not truecolor:
|
||||
# Fallback to xterm256 syntax
|
||||
r, g, b = self._rgb_24_bit_to_256(r, g, b)
|
||||
return f"{indicator}{r}{g}{b}"
|
||||
|
||||
else:
|
||||
if not truecolor:
|
||||
# Fallback to xterm256 syntax
|
||||
r, g, b = self._rgb_24_bit_to_256(r, g, b)
|
||||
return f"{indicator}{r}{g}{b}"
|
||||
xtag = f"\033["
|
||||
if "[" in indicator:
|
||||
# Background Color
|
||||
xtag += "4"
|
||||
|
||||
else:
|
||||
xtag = f"\033["
|
||||
if "[" in indicator:
|
||||
# Background Color
|
||||
xtag += "4"
|
||||
xtag += "3"
|
||||
|
||||
else:
|
||||
xtag += "3"
|
||||
|
||||
xtag += f"8;2;{r};{g};{b}m"
|
||||
return xtag
|
||||
xtag += f"8;2;{r};{g};{b}m"
|
||||
return xtag
|
||||
|
||||
def xterm_truecolor_to_html_style(self, fg="", bg="") -> str:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -10,10 +10,11 @@ from ast import literal_eval
|
|||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from django.test import TestCase, override_settings
|
||||
from evennia.utils import funcparser, test_resources
|
||||
from parameterized import parameterized
|
||||
from simpleeval import simple_eval
|
||||
|
||||
from evennia.utils import funcparser, test_resources
|
||||
|
||||
|
||||
def _test_callable(*args, **kwargs):
|
||||
kwargs.pop("funcparser", None)
|
||||
|
|
|
|||
|
|
@ -49,9 +49,7 @@ class TestText2Html(TestCase):
|
|||
# True Color
|
||||
self.assertEqual(
|
||||
'<span class="" style="color: #ff0000;">red</span>foo',
|
||||
parser.format_styles(
|
||||
f'\x1b[38;2;255;0;0m' + "red" + ansi.ANSI_NORMAL + "foo"
|
||||
),
|
||||
parser.format_styles(f"\x1b[38;2;255;0;0m" + "red" + ansi.ANSI_NORMAL + "foo"),
|
||||
)
|
||||
|
||||
def test_remove_bells(self):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from django.test import TestCase
|
||||
|
||||
from evennia.utils.ansi import ANSIString as AN, ANSIParser
|
||||
from evennia.utils.ansi import ANSIParser
|
||||
from evennia.utils.ansi import ANSIString as AN
|
||||
|
||||
parser = ANSIParser().parse_ansi
|
||||
|
||||
|
|
@ -14,57 +15,77 @@ class TestANSIStringHex(TestCase):
|
|||
def setUp(self):
|
||||
self.str = "test "
|
||||
self.output1 = "\x1b[38;5;16mtest \x1b[0m"
|
||||
self.output1_truecolor = "\x1b[38;2;0;0;0mtest \x1b[0m"
|
||||
self.output2 = "\x1b[48;5;16mtest \x1b[0m"
|
||||
self.output2_truecolor = "\x1b[48;2;0;0;0mtest \x1b[0m"
|
||||
self.output3 = "\x1b[38;5;46mtest \x1b[0m"
|
||||
self.output3_truecolor = "\x1b[38;2;0;255;0mtest \x1b[0m"
|
||||
self.output4 = "\x1b[48;5;46mtest \x1b[0m"
|
||||
self.output4_truecolor = "\x1b[48;2;0;255;0mtest \x1b[0m"
|
||||
|
||||
def test_long_grayscale_fg(self):
|
||||
raw = f"|#000000{self.str}|n"
|
||||
ansi = AN(raw)
|
||||
ansi_256 = parser(raw, xterm256=True)
|
||||
self.assertEqual(ansi.clean(), self.str, "Cleaned")
|
||||
self.assertEqual(ansi.raw(), self.output1, "Output")
|
||||
self.assertEqual(ansi.raw(), self.output1_truecolor, "Output truecolor")
|
||||
self.assertEqual(ansi_256, self.output1, "Output xterm256")
|
||||
|
||||
def test_long_grayscale_bg(self):
|
||||
raw = f"|[#000000{self.str}|n"
|
||||
ansi = AN(raw)
|
||||
ansi_256 = parser(raw, xterm256=True)
|
||||
self.assertEqual(ansi.clean(), self.str, "Cleaned")
|
||||
self.assertEqual(ansi.raw(), self.output2, "Output")
|
||||
self.assertEqual(ansi.raw(), self.output2_truecolor, "Output truecolor")
|
||||
self.assertEqual(ansi_256, self.output2, "Output xterm256")
|
||||
|
||||
def test_short_grayscale_fg(self):
|
||||
raw = f"|#000{self.str}|n"
|
||||
ansi = AN(raw)
|
||||
ansi_256 = parser(raw, xterm256=True)
|
||||
self.assertEqual(ansi.clean(), self.str, "Cleaned")
|
||||
self.assertEqual(ansi.raw(), self.output1, "Output")
|
||||
self.assertEqual(ansi.raw(), self.output1_truecolor, "Output truecolor")
|
||||
self.assertEqual(ansi_256, self.output1, "Output xterm256")
|
||||
|
||||
def test_short_grayscale_bg(self):
|
||||
raw = f"|[#000{self.str}|n"
|
||||
ansi = AN(raw)
|
||||
ansi_256 = parser(raw, xterm256=True)
|
||||
self.assertEqual(ansi.clean(), self.str, "Cleaned")
|
||||
self.assertEqual(ansi.raw(), self.output2, "Output")
|
||||
self.assertEqual(ansi.raw(), self.output2_truecolor, "Output truecolor")
|
||||
self.assertEqual(ansi_256, self.output2, "Output xterm256")
|
||||
|
||||
def test_short_color_fg(self):
|
||||
raw = f"|#0F0{self.str}|n"
|
||||
ansi = AN(raw)
|
||||
ansi_256 = parser(raw, xterm256=True)
|
||||
self.assertEqual(ansi.clean(), self.str, "Cleaned")
|
||||
self.assertEqual(ansi.raw(), self.output3, "Output")
|
||||
self.assertEqual(ansi.raw(), self.output3_truecolor, "Output truecolor")
|
||||
self.assertEqual(ansi_256, self.output3, "Output xterm256")
|
||||
|
||||
def test_short_color_bg(self):
|
||||
raw = f"|[#0f0{self.str}|n"
|
||||
ansi = AN(raw)
|
||||
ansi_256 = parser(raw, xterm256=True)
|
||||
self.assertEqual(ansi.clean(), self.str, "Cleaned")
|
||||
self.assertEqual(ansi.raw(), self.output4, "Output")
|
||||
self.assertEqual(ansi.raw(), self.output4_truecolor, "Output truecolor")
|
||||
self.assertEqual(ansi_256, self.output4, "Output xterm256")
|
||||
|
||||
def test_long_color_fg(self):
|
||||
raw = f"|#00ff00{self.str}|n"
|
||||
ansi = AN(raw)
|
||||
ansi_256 = parser(raw, xterm256=True)
|
||||
self.assertEqual(ansi.clean(), self.str, "Cleaned")
|
||||
self.assertEqual(ansi.raw(), self.output3, "Output")
|
||||
self.assertEqual(ansi.raw(), self.output3_truecolor, "Output truecolor")
|
||||
self.assertEqual(ansi_256, self.output3, "Output xterm256")
|
||||
|
||||
def test_long_color_bg(self):
|
||||
raw = f"|[#00FF00{self.str}|n"
|
||||
ansi = AN(raw)
|
||||
ansi_256 = parser(raw, xterm256=True)
|
||||
self.assertEqual(ansi.clean(), self.str, "Cleaned")
|
||||
self.assertEqual(ansi.raw(), self.output4, "Output")
|
||||
self.assertEqual(ansi.raw(), self.output4_truecolor, "Output truecolor")
|
||||
self.assertEqual(ansi_256, self.output4, "Output xterm256")
|
||||
|
||||
|
||||
class TestANSIParser(TestCase):
|
||||
|
|
|
|||
|
|
@ -812,6 +812,65 @@ class TestJustify(TestCase):
|
|||
self.assertIn(ANSI_RED, str(result))
|
||||
|
||||
|
||||
class TestAtSearchResult(TestCase):
|
||||
"""
|
||||
Test the utils.at_search_result function.
|
||||
|
||||
"""
|
||||
|
||||
class MockObject:
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
self.aliases = ''
|
||||
|
||||
def get_display_name(self, looker, **kwargs):
|
||||
return self.key
|
||||
|
||||
def get_extra_info(self, looker, **kwargs):
|
||||
return ''
|
||||
|
||||
def __repr__(self):
|
||||
return f"MockObject({self.key})"
|
||||
|
||||
def test_single_match(self):
|
||||
"""if there is only one match, it should return the matched object"""
|
||||
obj1 = self.MockObject("obj1")
|
||||
caller = mock.MagicMock()
|
||||
self.assertEqual(obj1, utils.at_search_result([obj1], caller, "obj1"))
|
||||
|
||||
def test_no_match(self):
|
||||
"""if there are no provided matches, the caller should receive the correct error message"""
|
||||
caller = mock.MagicMock()
|
||||
self.assertIsNone(utils.at_search_result([], caller, "obj1"))
|
||||
caller.msg.assert_called_once_with("Could not find 'obj1'.")
|
||||
|
||||
def test_basic_multimatch(self):
|
||||
"""multiple matches with the same name should return a message with incrementing indices"""
|
||||
matches = [ self.MockObject("obj1") for _ in range(3) ]
|
||||
caller = mock.MagicMock()
|
||||
self.assertIsNone(utils.at_search_result(matches, caller, "obj1"))
|
||||
multimatch_msg = """\
|
||||
More than one match for 'obj1' (please narrow target):
|
||||
obj1-1
|
||||
obj1-2
|
||||
obj1-3"""
|
||||
caller.msg.assert_called_once_with(multimatch_msg)
|
||||
|
||||
def test_partial_multimatch(self):
|
||||
"""multiple partial matches with different names should increment index by unique name"""
|
||||
matches = [ self.MockObject("obj1") for _ in range(3) ] + [ self.MockObject("obj2") for _ in range(2) ]
|
||||
caller = mock.MagicMock()
|
||||
self.assertIsNone(utils.at_search_result(matches, caller, "obj"))
|
||||
multimatch_msg = """\
|
||||
More than one match for 'obj' (please narrow target):
|
||||
obj1-1
|
||||
obj1-2
|
||||
obj1-3
|
||||
obj2-1
|
||||
obj2-2"""
|
||||
caller.msg.assert_called_once_with(multimatch_msg)
|
||||
|
||||
|
||||
class TestGroupObjectsByKeyAndDesc(TestCase):
|
||||
"""
|
||||
Test the utils.group_objects_by_key_and_desc function.
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import re
|
|||
from html import escape as html_escape
|
||||
|
||||
from .ansi import *
|
||||
|
||||
from .hex_colors import HexColors
|
||||
|
||||
# All xterm256 RGB equivalents
|
||||
|
|
|
|||
|
|
@ -473,7 +473,7 @@ def iter_to_str(iterable, sep=",", endsep=", and", addquote=False):
|
|||
list_to_string = iter_to_str
|
||||
iter_to_string = iter_to_str
|
||||
|
||||
re_empty = re.compile("\n\s*\n")
|
||||
re_empty = re.compile("\n\\s*\n")
|
||||
|
||||
|
||||
def compress_whitespace(text, max_linebreaks=1, max_spacing=2):
|
||||
|
|
@ -494,7 +494,7 @@ def compress_whitespace(text, max_linebreaks=1, max_spacing=2):
|
|||
# this allows the blank-line compression to eliminate them if needed
|
||||
text = re_empty.sub("\n\n", text)
|
||||
# replace groups of extra spaces with the maximum number of spaces
|
||||
text = re.sub(f"(?<=\S) {{{max_spacing},}}", " " * max_spacing, text)
|
||||
text = re.sub(fr"(?<=\S) {{{max_spacing},}}", " " * max_spacing, text)
|
||||
# replace groups of extra newlines with the maximum number of newlines
|
||||
text = re.sub(f"\n{{{max_linebreaks},}}", "\n" * max_linebreaks, text)
|
||||
return text
|
||||
|
|
@ -2397,28 +2397,35 @@ def at_search_result(matches, caller, query="", quiet=False, **kwargs):
|
|||
query=query
|
||||
)
|
||||
|
||||
for num, result in enumerate(matches):
|
||||
# we need to consider that result could be a Command, where .aliases
|
||||
# is a list of strings
|
||||
if hasattr(result.aliases, "all"):
|
||||
# result is a typeclassed entity where `.aliases` is an AliasHandler.
|
||||
aliases = result.aliases.all(return_objs=True)
|
||||
# remove pluralization aliases
|
||||
aliases = [alias.db_key for alias in aliases if alias.db_category != "plural_key"]
|
||||
else:
|
||||
# result is likely a Command, where `.aliases` is a list of strings.
|
||||
aliases = result.aliases
|
||||
|
||||
error += _MULTIMATCH_TEMPLATE.format(
|
||||
number=num + 1,
|
||||
name=(
|
||||
result.get_display_name(caller)
|
||||
if hasattr(result, "get_display_name")
|
||||
# group results by display name to properly disambiguate
|
||||
grouped_matches = defaultdict(list)
|
||||
for item in matches:
|
||||
group_key = (
|
||||
item.get_display_name(caller)
|
||||
if hasattr(item, "get_display_name")
|
||||
else query
|
||||
),
|
||||
aliases=" [{alias}]".format(alias=";".join(aliases)) if aliases else "",
|
||||
info=result.get_extra_info(caller),
|
||||
)
|
||||
)
|
||||
grouped_matches[group_key].append(item)
|
||||
|
||||
for key, match_list in grouped_matches.items():
|
||||
for num, result in enumerate(match_list):
|
||||
# we need to consider that result could be a Command, where .aliases
|
||||
# is a list of strings
|
||||
if hasattr(result.aliases, "all"):
|
||||
# result is a typeclassed entity where `.aliases` is an AliasHandler.
|
||||
aliases = result.aliases.all(return_objs=True)
|
||||
# remove pluralization aliases
|
||||
aliases = [alias.db_key for alias in aliases if alias.db_category != "plural_key"]
|
||||
else:
|
||||
# result is likely a Command, where `.aliases` is a list of strings.
|
||||
aliases = result.aliases
|
||||
|
||||
error += _MULTIMATCH_TEMPLATE.format(
|
||||
number=num + 1,
|
||||
name=key,
|
||||
aliases=" [{alias}]".format(alias=";".join(aliases)) if aliases else "",
|
||||
info=result.get_extra_info(caller),
|
||||
)
|
||||
matches = None
|
||||
else:
|
||||
# exactly one match
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "website/base.html" %}
|
||||
|
||||
{% block titleblock %}
|
||||
{{ view.page_title }} ({{ object }})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "website/base.html" %}
|
||||
|
||||
{% block titleblock %}
|
||||
{{ view.page_title }}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "website/base.html" %}
|
||||
|
||||
{% block titleblock %}
|
||||
{{ view.page_title }}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "website/base.html" %}
|
||||
|
||||
{% block titleblock %}
|
||||
{{ view.page_title }}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "website/base.html" %}
|
||||
|
||||
{% block titleblock %}
|
||||
{{ view.page_title }}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "website/base.html" %}
|
||||
|
||||
{% block titleblock %}
|
||||
Form
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "website/base.html" %}
|
||||
|
||||
{% block titleblock %}
|
||||
{{ view.page_title }} ({{ object|title }})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "website/base.html" %}
|
||||
|
||||
{% block titleblock %}
|
||||
{{ view.page_title }}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "website/base.html" %}
|
||||
|
||||
{% block titleblock %}
|
||||
{{ view.page_title }}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "website/base.html" %}
|
||||
|
||||
{% block titleblock %}
|
||||
{{ view.page_title }} ({{ object }})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "website/base.html" %}
|
||||
|
||||
{% block titleblock %}
|
||||
{{ view.page_title }}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "website/base.html" %}
|
||||
|
||||
{% block titleblock %}
|
||||
Password Changed
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "website/base.html" %}
|
||||
|
||||
{% block titleblock %}
|
||||
Password Change
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "website/base.html" %}
|
||||
|
||||
{% block titleblock %}
|
||||
Forgot Password - Reset Successful
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "website/base.html" %}
|
||||
|
||||
{% block titleblock %}
|
||||
Forgot Password - Reset
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "website/base.html" %}
|
||||
|
||||
{% block titleblock %}
|
||||
Forgot Password - Reset Link Sent
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "website/base.html" %}
|
||||
|
||||
{% block titleblock %}
|
||||
Forgot Password
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "website/base.html" %}
|
||||
|
||||
{% block titleblock %}
|
||||
Register
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||
|
||||
[project]
|
||||
name = "evennia"
|
||||
version = "4.1.1"
|
||||
version = "4.3.0"
|
||||
maintainers = [{ name = "Griatch", email = "griatch@gmail.com" }]
|
||||
description = "A full-featured toolkit and server for text-based multiplayer games (MUDs, MU*, etc)."
|
||||
requires-python = ">=3.10"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue