Merge branch 'evennia:main' into is_typing

This commit is contained in:
Michael Faith 2025-05-02 16:33:39 -07:00 committed by GitHub
commit 35c8f56a0b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
174 changed files with 4170 additions and 2354 deletions

View file

@ -23,7 +23,7 @@ runs:
if: ${{ inputs.database == 'postgresql' }}
uses: harmon758/postgresql-action@v1
with:
postgresql version: "12"
postgresql version: "13"
postgresql db: "evennia"
postgresql user: "evennia"
postgresql password: "password"

View file

@ -20,7 +20,7 @@ jobs:
strategy:
matrix:
python-version: ['3.10']
python-version: ['3.11']
steps:
- name: Checkout ${{ github.ref }} branch

View file

@ -0,0 +1,19 @@
name: Automatically add issue to project view
on:
issues:
types:
- opened
- reopened
jobs:
add-to-project:
runs-on: ubuntu-latest
steps:
- name: Add To GitHub projects
uses: actions/add-to-project@v1.0.2
with:
# URL of the project to add issues to
project-url: https://github.com/orgs/evennia/projects/1
# A GitHub personal access token with write access to the project
github-token: ${{ secrets.EVENNIA_TICKET_TO_PROJECT }}

View file

@ -19,10 +19,10 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12"]
TESTING_DB: ["sqlite3", "postgresql", "mysql"]
python-version: ["3.11", "3.12", "3.13"]
TESTING_DB: ["sqlite3", "mysql"]
include:
- python-version: "3.10"
- python-version: "3.11"
TESTING_DB: "sqlite3"
coverage-test: true

View file

@ -2,30 +2,239 @@
## Main branch
Updated dependencies: Django >5.1 (<5,2), Twisted >24 (<25).
Python versions: 3.11, 3.12, 3.13.
This upgrade requires running `evennia migrate` on your existing database
(ignore any prompts to run `evennia makemigrations`).
- Feat (backwards incompatible): RUN MIGRATIONS (`evennia migrate`): Now requiring Django 5.1 (Griatch)
- Feat (backwards incompatible): Drop support and testing for Python 3.10 (Griatch)
- [Feat][pull3719]: Support Python 3.13. (0xDEADFED5)
- [Feat][pull3633]: Default object's default descs are now taken from a `default_description`
class variable instead of the `desc` Attribute always being set (count-infinity)
- [Feat][pull3718]: Remove twistd.bat creation for Windows, should not be needed anymore (0xDEADFED5)
- [Feat][pull3756]: Updated German translation (JohnFi)
- [Feat][pull3757]: Add more i18n strings to `DefaultObject` for easier translation (JohnFi)
- [Feat][pull3783]: Support users of `ruff` linter by adding compatible config in `pyproject.toml` (jaborsh)
- [Fix][pull3677]: Make sure that `DefaultAccount.create` normalizes to empty
strings instead of `None` if no name is provided, also enforce string type (InspectorCaracal)
- [Fix][pull3682]: Allow in-game help searching for commands natively starting
with `*` (which is the Lunr search wildcard) (count-infinity)
- [Fix][pull3684]: Web client stopped auto-focusing the input box after opening settings (count-infinity)
- [Fix][pull3689]: Partial matching fix in default search, makes sure e.g. `b sw` uniquely
finds `big sword` even if another type of sword is around (InspectorCaracal)
- [Fix][pull3690]: In searches, allow special 'here' and 'me' keywords only be valid queries
unless current location and/or caller is in valid search candidates respectively (InspectorCaracal)
- [Fix][pull3694]: Funcparser swallowing rest of line after a `\`-escape (count-infinity)
- [Fix][pull3705]: Properly serialize `IntFlag` enum types (0xDEADFED5)
- [Fix][pull3707]: Correct links in `about` command (0xDEADFED5)
- [Fix][pull3710]: Clean reduntant session clearin in `at_server_cold_start` (InspectorCaracal)
- [Fix][pull3711]: Usability improvements in the Discord integration (InspectorCaracal)
- [Fix][pull3721]: Avoid loading cmdsets that don't need to be checked, avoiding
a performance hit for loading cmdsets in rooms with a lot of objects (InspectorCaracal)
- [Fix][issue3688]: Made TutorialWorld possible to build cleanly without being a superuser (Griatch)
- [Fix][issue3687]: Fixed batchcommand/interactive with developer perms (Griatch)
- [Fix][pull3723]: Bug in `ingame-map-display` contrib when using ordinal alises (aMiss-aWry)
- [Fix][pull3726]: Fix Twisted v25 issue with returnValue()
- [Fix][pull3729]: Godot client text2bbcode mxp link conversion error (ChrisLR)
- [Fix][pull3737]: The `evennia --gamedir` command didn't properly set the alt gamedir (Russel-Jones)
- [Fix][pull3739]: Fixing f-string in account.py for i18n (JohnFi)
- [Fix][pull3744]: Fix for format strings not getting picked up in i18n (JohnFi)
- [Fix][pull3743]: Log full stack trace on failed object creation (aMiss-aWry)
- [Fix][pull3747]: TutorialWorld bridge-room didn't correctly randomize weather effects (SpyrosRoum)
- [Fix][pull3765]: Storing TickerHandler `store_key` in a db attribute would not
work correctly (0xDEADFED5)
- [Fix][pull3753]: Make sure `AttributeProperty`s are initialized with default values also in parent class (JohnFi)
- [Fix][pull3751]: The `access` and `inventory` commands would traceback if run on a character without an Account (EliasWatson)
- [Fix][pull3768]: Make sure the `CmdCopy` command copies object categories,
since otherwise plurals were lost (jaborsh)
- [Fix][issue3788]: `GLOBAL_SCRIPTS.all()` raised error (Griatch)
- Fix: `options` setting `NOPROMPTGOAHEAD` was not possible to set (Griatch)
- Fix: Make `\\` properly preserve one backlash in funcparser (Griatch)
- Fix: The testing 'echo' inputfunc didn't work correctly; now returns both args/kwargs (Griatch)
- Fix: When an object was used as an On-Demand Task's category, and that object was then deleted,
it caused an OnDemandHandler save error on reload. Will now clean up on save. (Griatch)
used as the task's category (Griatch)
- Fix: Correct aws contrib's use of legacy django string utils (Griatch)
- [Docs]: Fixes from InspectorCaracal, Griatch, ChrisLR, JohnFi, 0xDEADFED5, jaborsh, Problematic, BlaneWins
[pull3633]: https://github.com/evennia/evennia/pull/3633
[pull3677]: https://github.com/evennia/evennia/pull/3677
[pull3682]: https://github.com/evennia/evennia/pull/3682
[pull3684]: https://github.com/evennia/evennia/pull/3684
[pull3689]: https://github.com/evennia/evennia/pull/3689
[pull3690]: https://github.com/evennia/evennia/pull/3690
[pull3705]: https://github.com/evennia/evennia/pull/3705
[pull3707]: https://github.com/evennia/evennia/pull/3707
[pull3710]: https://github.com/evennia/evennia/pull/3710
[pull3711]: https://github.com/evennia/evennia/pull/3711
[pull3718]: https://github.com/evennia/evennia/pull/3718
[pull3719]: https://github.com/evennia/evennia/pull/3719
[pull3721]: https://github.com/evennia/evennia/pull/3721
[pull3723]: https://github.com/evennia/evennia/pull/3723
[pull3726]: https://github.com/evennia/evennia/pull/3726
[pull3729]: https://github.com/evennia/evennia/pull/3729
[pull3737]: https://github.com/evennia/evennia/pull/3737
[pull3739]: https://github.com/evennia/evennia/pull/3739
[pull3743]: https://github.com/evennia/evennia/pull/3743
[pull3744]: https://github.com/evennia/evennia/pull/3744
[pull3747]: https://github.com/evennia/evennia/pull/3747
[pull3765]: https://github.com/evennia/evennia/pull/3765
[pull3753]: https://github.com/evennia/evennia/pull/3753
[pull3751]: https://github.com/evennia/evennia/pull/3751
[pull3756]: https://github.com/evennia/evennia/pull/3756
[pull3757]: https://github.com/evennia/evennia/pull/3757
[pull3768]: https://github.com/evennia/evennia/pull/3768
[pull3783]: https://github.com/evennia/evennia/pull/3783
[issue3688]: https://github.com/evennia/evennia/issues/3688
[issue3687]: https://github.com/evennia/evennia/issues/3687
[issue3788]: https://github.com/evennia/evennia/issues/3788
## Evennia 4.5.0
Nov 12, 2024
- [Feat][pull3634]: New contrib for in-game `storage` of items in rooms (aMiss-aWry)
- [Feat][pull3636]: Make `cpattr` command also support Attribute categories (aMiss-aWry)
- [Feat][pull3653]: Updated Chinese translation (Pridell).
- [Fix][pull3635]: Fix memory leak in Portal Telnet connections, force weak
references to Telnet negotiations, stop LoopingCall on disconnect (a-rodian-jedi)
- [Fix][pull3626]: Typo in `defense_type` in evadventure tutorial (feyrkh)
- [Fix][pull3632]: Made fallback permissions on be set correctly (InspectorCaracal)
- [Fix][pull3639]: Fix `system` command when environment uses a language with
commas for decimal points (aMiss-aWry)
- [Fix][pull3645]: Correct `character_creator` contrib's error return (InspectorCaracal)
- [Fix][pull3640]: Typo fixes for conjugate verbs (aMiss-aWry)
- [Fix][pull3647]: Contents cache didn't reset internal typecache on use of `init` hook (InspectorCaracal)
- [Fix][issue3627]: Traceback from contrib `in-game reports` `help manage` command (Griatch)
- [Fix][issue3643]: Fix for Commands metaclass interpreting e.g. `usercmd:false()` locks as
a `cmd:` type lock for the purposes of default access fallbacks (Griatch).
- [Fix][pull3651]: EvEditor `:j` defaulted to 'full' justify instead of 'left' as
was documented (willmofield)
- [Fix][pull3657]: Fix error in `do_search` that caused `FileHelpEntries` to
traceback (a-rodian-jedi)
- [Fix][pull3660]: Numbered aliases didn't refresh after a object rename unless
the endpoint hook was re-called; now triggers the call autiomatically (count-infinity)
- [Fix][pull3664]: The `Account.last_login` field was updated also when user
disconnected, which is not useful (InspectorCaracal)
- [Fix][pull3665]: Remove faulty verb conjugation exceptions for 'offer',
'hinder' and 'alter' in automatic verb-conjugation engine (aMiss-aWry)
- [Fix][pull3669]: The `page` command tracebacked for some input combinations (InspectorCaracal)
- [Fix][pull3642]: Give friendlier error if EvMore object is not available
neither on Object, nor on account fallback. (InspectorCaracal)
- [Docs][pull3655]: Fixed many erroneously created links on `file.py` names in
the docs (marado)
- [Docs][pull3576]: Rework doc for [Pycharm howto][doc-pycharm]
- Docs updates: feykrh, Griatch, marado, jaborsh
[pull3626]: https://github.com/evennia/evennia/pull/3626
[pull3676]: https://github.com/evennia/evennia/pull/3676
[pull3634]: https://github.com/evennia/evennia/pull/3634
[pull3632]: https://github.com/evennia/evennia/pull/3632
[pull3636]: https://github.com/evennia/evennia/pull/3636
[pull3639]: https://github.com/evennia/evennia/pull/3639
[pull3645]: https://github.com/evennia/evennia/pull/3645
[pull3640]: https://github.com/evennia/evennia/pull/3640
[pull3647]: https://github.com/evennia/evennia/pull/3647
[pull3635]: https://github.com/evennia/evennia/pull/3635
[pull3651]: https://github.com/evennia/evennia/pull/3651
[pull3655]: https://github.com/evennia/evennia/pull/3655
[pull3657]: https://github.com/evennia/evennia/pull/3657
[pull3653]: https://github.com/evennia/evennia/pull/3653
[pull3660]: https://github.com/evennia/evennia/pull/3660
[pull3664]: https://github.com/evennia/evennia/pull/3664
[pull3665]: https://github.com/evennia/evennia/pull/3665
[pull3669]: https://github.com/evennia/evennia/pull/3669
[pull3642]: https://github.com/evennia/evennia/pull/3642
[pull3576]: https://github.com/evennia/evennia/pull/3576
[issue3627]: https://github.com/evennia/evennia/issues/3627
[issue3643]: https://github.com/evennia/evennia/issues/3643
[doc-pycharm]: https://www.evennia.com/docs/latest/Coding/Setting-up-PyCharm.html
## Evennia 4.4.1
Oct 1, 2024
- [Fix][issue3629]: Reverting change of default Sqlite3 PRAGMA settings, changing them for
existing database caused corruption of db. For empty db, can still change in
`SQLITE3_PRAGMAS` settings. (Griatch)
[issue3629]: https://github.com/evennia/evennia/issues/3629
## Evennia 4.4.0
Sep 29, 2024
> WARNING: Due to a bug in the default Sqlite3 PRAGMA settings, it is
> recommended to not upgrade to this version if you are using Sqlite3.
> Use `4.4.1` or higher instead.
- 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][pull3494]: Update/clean some Evennia dependencies (0xDEADFED5)
- [Feat][pull3592]: Revised German locationlization ('Du' instead of 'Sie',
cleanup) (Drakon72)
- [Feat][pull3541]: Rework main Object searching to respect partial matches, empty
search now partial matching all candidates, overall cleanup (InspectorCaracal)
- [Feat][pull3588]: New `DefaultObject` hooks: `at_object_post_creation`, called once after
first creation but after any prototypes have been applied, and
`at_object_post_spawn(prototype)`, called only after creation/update with a prototype (InspectorCaracal)
- [Fix][pull3594]: Update/clean some Evennia dependencies (0xDEADFED5)
- [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)
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)
- [Fix](pull3533): Fix Lunr search issues preventing finding help entries with similar
- [Fix][pull3533]: Fix Lunr search issues preventing finding help entries with similar
names (chiizyjin)
[Docs][issue3591]: Fix of NPC reaction tutorial code (Griatch)
- Docs: Tutorial fixes (Griatch)
- [Fix][pull3603]: Fix of client header for LLM contrib for remote APIs (InspectorCaracal)
- [Fix][pull3605]: Correctly pass node kwargs through `@list_node` decorated evmenu nodes
(InspectorCaracal)
- [Fix][pull3597]: Address timing issue for testing `new_task_waiting_input `on
Windows (0xDEADFED5)
- [Fix][pull3611]: Fix and update for Reports contrib (InspectorCaracal)
- [Fix][pull3625]: Lycanthropy tutorial page had some issues (feyrkh)
- [Fix][pull3622]: Fix for examine command tracebacking with strvalue error
(aMiss-aWry)
- [Fix][issue3612]: Make sure help entries' `subtopic_separator_char` is
respected (Griatch)
- [Fix][issue3624]: Setting tags with integer names caused errors on postgres (Griatch)
- [Fix][issue3615]: Using `print()` in `py` caused an infinite loop (Griatch)
- [Fix][issue3620]: Better handle TaskHandler running against an attribute that
was removed since last reload (Griatch)
- [Fix][issue3616]: The `color ansi` command output was broken (Griatch)
- Fix: Extended the `color truecolor` display with usage examples. Also updated docs (Griatch)
- [Docs][issue3591]: Fix of NPC reaction tutorial code (Griatch)
- Docs: Tutorial fixes (Griatch, aMiss-aWry, feyrkh)
[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
[issue3612]: https://github.com/evennia/evennia/issues/3612
[issue3624]: https://github.com/evennia/evennia/issues/3624
[issue3615]: https://github.com/evennia/evennia/issues/3615
[issue3620]: https://github.com/evennia/evennia/issues/3620
[issue3616]: https://github.com/evennia/evennia/issues/3616
[pull3595]: https://github.com/evennia/evennia/pull/3595
[pull3533]: https://github.com/evennia/evennia/pull/3533
[pull3594]: https://github.com/evennia/evennia/pull/3594
[pull3592]: https://github.com/evennia/evennia/pull/3592
[pull3603]: https://github.com/evennia/evennia/pull/3603
[pull3605]: https://github.com/evennia/evennia/pull/3605
[pull3597]: https://github.com/evennia/evennia/pull/3597
[pull3611]: https://github.com/evennia/evennia/pull/3611
[pull3541]: https://github.com/evennia/evennia/pull/3541
[pull3588]: https://github.com/evennia/evennia/pull/3588
[pull3625]: https://github.com/evennia/evennia/pull/3625
[pull3622]: https://github.com/evennia/evennia/pull/3622
## Evennia 4.3.0
@ -47,15 +256,12 @@ underline reset, italic/reset and strikethrough/reset (0xDEADFED5)
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)
- [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
@ -1152,7 +1358,7 @@ without arguments starts a full interactive Python console.
- `VALIDATOR_FUNC_MODULES` - (general) text validator functions, for verifying an input
is on a specific form.
### Utils
### Utilities
- `evennia` launcher now fully handles all django-admin commands, like running tests in parallel.
- `evennia.utils.create.account` now also takes `tags` and `attrs` keywords.
@ -1175,7 +1381,7 @@ without arguments starts a full interactive Python console.
- Option Classes added to make storing user-options easier and smoother.
- `evennia.VALIDATOR_CONTAINER` and `evennia.OPTION_CONTAINER` added to load these.
### Contribs
### New Contribs
- Evscaperoom - a full puzzle engine for making multiplayer escape rooms in Evennia. Used to make
the entry for the MUD-Coder's Guild's 2019 Game Jam with the theme "One Room", where it ranked #1.
@ -1299,7 +1505,7 @@ without arguments starts a full interactive Python console.
- Removed the enforcing of `MAX_NR_CHARACTERS=1` for `MULTISESSION_MODE` `0` and `1` by default.
- Add `evennia.utils.logger.log_sec` for logging security-related messages (marked SS in log).
### Contribs
### More Contribs
- `Auditing` (Johnny): Log and filter server input/output for security purposes
- `Build Menu` (vincent-lg): New @edit command to edit object properties in a menu.
@ -1403,7 +1609,7 @@ base-modules where removed from game/gamesrc. Instead admins are
encouraged to explicitly create new modules under game/gamesrc/ when
they want to implement their game - gamesrc/ is empty by default
except for the example folders that contain template files to use for
this purpose. We also added the ev.py file, implementing a new, flat
this purpose. We also added the `ev.py` file, implementing a new, flat
API. Work is ongoing to add support for mud-specific telnet
extensions, notably the MSDP and GMCP out-of-band extensions. On the
community side, evennia's dev blog was started and linked on planet

View file

@ -5,7 +5,7 @@ myst-parser==0.15.2
myst-parser[linkify]==0.15.2
Jinja2 < 3.1
# pinned to allow for sphinx 3.x to still work, latest requried 5+
# pinned to allow for sphinx 3.x to still work, latest required 5+
alabaster==0.7.13
sphinxcontrib-applehelp<1.0.7
sphinxcontrib-devhelp<1.0.6

View file

@ -2,28 +2,206 @@
## Main branch
Updated dependencies: Twisted >24 (<25). Python 3.10, 3.11, 3.12, 3.13. Will
drop 3.10 support as part of next major release.
- [Feat][pull3719]: Support Python 3.13. (0xDEADFED5)
- [Feat][pull3633]: Default object's default descs are now taken from a `default_description`
class variable instead of the `desc` Attribute always being set (count-infinity)
- [Feat][pull3718]: Remove twistd.bat creation for Windows, should not be needed anymore (0xDEADFED5)
- [Fix][pull3677]: Make sure that `DefaultAccount.create` normalizes to empty
strings instead of `None` if no name is provided, also enforce string type (InspectorCaracal)
- [Fix][pull3682]: Allow in-game help searching for commands natively starting
with `*` (which is the Lunr search wildcard) (count-infinity)
- [Fix][pull3684]: Web client stopped auto-focusing the input box after opening
settings (count-infinity)
- [Fix][pull3689]: Partial matching fix in default search, makes sure e.g. `b sw` uniquely
finds `big sword` even if another type of sword is around (InspectorCaracal)
- [Fix][pull3690]: In searches, allow special 'here' and 'me' keywords only be valid queries
unless current location and/or caller is in valid search candidates respectively (InspectorCaracal)
- [Fix][pull3694]: Funcparser swallowing rest of line after a `\`-escape (count-infinity)
- [Fix][pull3705]: Properly serialize `IntFlag` enum types (0xDEADFED5)
- [Fix][pull3707]: Correct links in `about` command (0xDEADFED5)
- [Fix][pull3710]: Clean reduntant session clearin in `at_server_cold_start` (InspectorCaracal)
- [Fix][pull3711]: Usability improvements in the Discord integration (InspectorCaracal)
- [Fix][pull3721]: Avoid loading cmdsets that don't need to be checked, avoiding
a performance hit for loading cmdsets in rooms with a lot of objects (InspectorCaracal)
- [Fix][issue3688]: Made TutorialWorld possible to build cleanly without being a superuser (Griatch)
- [Fix][issue3687]: Fixed batchcommand/interactive with developer perms (Griatch)
- [Fix][issue3723]: Bug in `ingame-map-display` contrib when using ordinal alises (aMiss-aWry)
- [Fix][issue3726]: Fix Twisted v25 issue with returnValue()
- [Fix][issue3729]: Godot client text2bbcode mxp link conversion error (ChrisLR)
- Fix: Make `\\` properly preserve one backlash in funcparser (Griatch)
- Fix: When an object was used as an On-Demand Task's category, and that object was then deleted,
it caused an OnDemandHandler save error on reload. Will now clean up on save. (Griatch)
used as the task's category (Griatch)
- [Docs]: Fixes from InspectorCaracal, Griatch, ChrisLR
[pull3633]: https://github.com/evennia/evennia/pull/3633
[pull3677]: https://github.com/evennia/evennia/pull/3677
[pull3682]: https://github.com/evennia/evennia/pull/3682
[pull3684]: https://github.com/evennia/evennia/pull/3684
[pull3689]: https://github.com/evennia/evennia/pull/3689
[pull3690]: https://github.com/evennia/evennia/pull/3690
[pull3705]: https://github.com/evennia/evennia/pull/3705
[pull3707]: https://github.com/evennia/evennia/pull/3707
[pull3710]: https://github.com/evennia/evennia/pull/3710
[pull3711]: https://github.com/evennia/evennia/pull/3711
[pull3718]: https://github.com/evennia/evennia/pull/3718
[pull3719]: https://github.com/evennia/evennia/pull/3719
[pull3721]: https://github.com/evennia/evennia/pull/3721
[pull3723]: https://github.com/evennia/evennia/pull/3723
[pull3726]: https://github.com/evennia/evennia/pull/3726
[pull3729]: https://github.com/evennia/evennia/pull/3729
[issue3688]: https://github.com/evennia/evennia/issues/3688
[issue3688]: https://github.com/evennia/evennia/issues/3687
## Evennia 4.5.0
Nov 12, 2024
- [Feat][pull3634]: New contrib for in-game `storage` of items in rooms (aMiss-aWry)
- [Feat][pull3636]: Make `cpattr` command also support Attribute categories (aMiss-aWry)
- [Feat][pull3653]: Updated Chinese translation (Pridell).
- [Fix][pull3635]: Fix memory leak in Portal Telnet connections, force weak
references to Telnet negotiations, stop LoopingCall on disconnect (a-rodian-jedi)
- [Fix][pull3626]: Typo in `defense_type` in evadventure tutorial (feyrkh)
- [Fix][pull3632]: Made fallback permissions on be set correctly (InspectorCaracal)
- [Fix][pull3639]: Fix `system` command when environment uses a language with
commas for decimal points (aMiss-aWry)
- [Fix][pull3645]: Correct `character_creator` contrib's error return (InspectorCaracal)
- [Fix][pull3640]: Typo fixes for conjugate verbs (aMiss-aWry)
- [Fix][pull3647]: Contents cache didn't reset internal typecache on use of `init` hook (InspectorCaracal)
- [Fix][issue3627]: Traceback from contrib `in-game reports` `help manage` command (Griatch)
- [Fix][issue3643]: Fix for Commands metaclass interpreting e.g. `usercmd:false()` locks as
a `cmd:` type lock for the purposes of default access fallbacks (Griatch).
- [Fix][pull3651]: EvEditor `:j` defaulted to 'full' justify instead of 'left' as
was documented (willmofield)
- [Fix][pull3657]: Fix error in `do_search` that caused `FileHelpEntries` to
traceback (a-rodian-jedi)
- [Fix][pull3660]: Numbered aliases didn't refresh after a object rename unless
the endpoint hook was re-called; now triggers the call autiomatically (count-infinity)
- [Fix][pull3664]: The `Account.last_login` field was updated also when user
disconnected, which is not useful (InspectorCaracal)
- [Fix][pull3665]: Remove faulty verb conjugation exceptions for 'offer',
'hinder' and 'alter' in automatic verb-conjugation engine (aMiss-aWry)
- [Fix][pull3669]: The `page` command tracebacked for some input combinations (InspectorCaracal)
- [Fix][pull3642]: Give friendlier error if EvMore object is not available
neither on Object, nor on account fallback. (InspectorCaracal)
- [Docs][pull3655]: Fixed many erroneously created links on `file.py` names in
the docs (marado)
- [Docs][pull3576]: Rework doc for [Pycharm howto][doc-pycharm]
- Docs updates: feykrh, Griatch, marado, jaborsh
[pull3626]: https://github.com/evennia/evennia/pull/3626
[pull3676]: https://github.com/evennia/evennia/pull/3676
[pull3634]: https://github.com/evennia/evennia/pull/3634
[pull3632]: https://github.com/evennia/evennia/pull/3632
[pull3636]: https://github.com/evennia/evennia/pull/3636
[pull3639]: https://github.com/evennia/evennia/pull/3639
[pull3645]: https://github.com/evennia/evennia/pull/3645
[pull3640]: https://github.com/evennia/evennia/pull/3640
[pull3647]: https://github.com/evennia/evennia/pull/3647
[pull3635]: https://github.com/evennia/evennia/pull/3635
[pull3651]: https://github.com/evennia/evennia/pull/3651
[pull3655]: https://github.com/evennia/evennia/pull/3655
[pull3657]: https://github.com/evennia/evennia/pull/3657
[pull3653]: https://github.com/evennia/evennia/pull/3653
[pull3660]: https://github.com/evennia/evennia/pull/3660
[pull3664]: https://github.com/evennia/evennia/pull/3664
[pull3665]: https://github.com/evennia/evennia/pull/3665
[pull3669]: https://github.com/evennia/evennia/pull/3669
[pull3642]: https://github.com/evennia/evennia/pull/3642
[pull3576]: https://github.com/evennia/evennia/pull/3576
[issue3627]: https://github.com/evennia/evennia/issues/3627
[issue3643]: https://github.com/evennia/evennia/issues/3643
[doc-pycharm]: https://www.evennia.com/docs/latest/Coding/Setting-up-PyCharm.html
## Evennia 4.4.1
Oct 1, 2024
- [Fix][issue3629]: Reverting change of default Sqlite3 PRAGMA settings, changing them for
existing database caused corruption of db. For empty db, can still change in
`SQLITE3_PRAGMAS` settings. (Griatch)
[issue3629]: https://github.com/evennia/evennia/issues/3629
## Evennia 4.4.0
Sep 29, 2024
> WARNING: Due to a bug in the default Sqlite3 PRAGMA settings, it is
> recommended to not upgrade to this version if you are using Sqlite3.
> Use `4.4.1` or higher instead.
- 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)
- [Feat][pull3592]: Revised German locationlization ('Du' instead of 'Sie',
cleanup) (Drakon72)
- [Feat][pull3541]: Rework main Object searching to respect partial matches, empty
search now partial matching all candidates, overall cleanup (InspectorCaracal)
- [Feat][pull3588]: New `DefaultObject` hooks: `at_object_post_creation`, called once after
first creation but after any prototypes have been applied, and
`at_object_post_spawn(prototype)`, called only after creation/update with a prototype (InspectorCaracal)
- [Fix][pull3594]: Update/clean some Evennia dependencies (0xDEADFED5)
- [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)
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)
- [Fix](pull3533): Fix Lunr search issues preventing finding help entries with similar
- [Fix][pull3533]: Fix Lunr search issues preventing finding help entries with similar
names (chiizyjin)
[Docs][issue3591]: Fix of NPC reaction tutorial code (Griatch)
- Docs: Tutorial fixes (Griatch)
- [Fix][pull3603]: Fix of client header for LLM contrib for remote APIs (InspectorCaracal)
- [Fix][pull3605]: Correctly pass node kwargs through `@list_node` decorated evmenu nodes
(InspectorCaracal)
- [Fix][pull3597]: Address timing issue for testing `new_task_waiting_input `on
Windows (0xDEADFED5)
- [Fix][pull3611]: Fix and update for Reports contrib (InspectorCaracal)
- [Fix][pull3625]: Lycanthropy tutorial page had some issues (feyrkh)
- [Fix][pull3622]: Fix for examine command tracebacking with strvalue error
(aMiss-aWry)
- [Fix][issue3612]: Make sure help entries' `subtopic_separator_char` is
respected (Griatch)
- [Fix][issue3624]: Setting tags with integer names caused errors on postgres (Griatch)
- [Fix][issue3615]: Using `print()` in `py` caused an infinite loop (Griatch)
- [Fix][issue3620]: Better handle TaskHandler running against an attribute that
was removed since last reload (Griatch)
- [Fix][issue3616]: The `color ansi` command output was broken (Griatch)
- Fix: Extended the `color truecolor` display with usage examples. Also updated docs (Griatch)
- [Docs][issue3591]: Fix of NPC reaction tutorial code (Griatch)
- Docs: Tutorial fixes (Griatch, aMiss-aWry, feyrkh)
[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
[issue3612]: https://github.com/evennia/evennia/issues/3612
[issue3624]: https://github.com/evennia/evennia/issues/3624
[issue3615]: https://github.com/evennia/evennia/issues/3615
[issue3620]: https://github.com/evennia/evennia/issues/3620
[issue3616]: https://github.com/evennia/evennia/issues/3616
[pull3595]: https://github.com/evennia/evennia/pull/3595
[pull3595]: https://github.com/evennia/evennia/pull/3595
[pull3533]: https://github.com/evennia/evennia/pull/3533
[pull3594]: https://github.com/evennia/evennia/pull/3594
[pull3592]: https://github.com/evennia/evennia/pull/3592
[pull3603]: https://github.com/evennia/evennia/pull/3603
[pull3605]: https://github.com/evennia/evennia/pull/3605
[pull3597]: https://github.com/evennia/evennia/pull/3597
[pull3611]: https://github.com/evennia/evennia/pull/3611
[pull3541]: https://github.com/evennia/evennia/pull/3541
[pull3588]: https://github.com/evennia/evennia/pull/3588
[pull3625]: https://github.com/evennia/evennia/pull/3625
[pull3622]: https://github.com/evennia/evennia/pull/3622
## Evennia 4.3.0
@ -47,7 +225,7 @@ underline reset, italic/reset and strikethrough/reset (0xDEADFED5)
- [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)
- [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)
@ -1401,7 +1579,7 @@ base-modules where removed from game/gamesrc. Instead admins are
encouraged to explicitly create new modules under game/gamesrc/ when
they want to implement their game - gamesrc/ is empty by default
except for the example folders that contain template files to use for
this purpose. We also added the ev.py file, implementing a new, flat
this purpose. We also added the `ev.py` file, implementing a new, flat
API. Work is ongoing to add support for mud-specific telnet
extensions, notably the MSDP and GMCP out-of-band extensions. On the
community side, evennia's dev blog was started and linked on planet

View file

@ -1,84 +1,125 @@
# Setting up PyCharm with Evennia
[PyCharm](https://www.jetbrains.com/pycharm/) is a Python developer's IDE from Jetbrains available for Windows, Mac and Linux. It is a commercial product but offer free trials, a scaled-down community edition and also generous licenses for OSS projects like Evennia.
[PyCharm](https://www.jetbrains.com/pycharm/) is a Python developer's IDE from Jetbrains available for Windows, Mac and Linux.
It is a commercial product but offer free trials, a scaled-down community edition and also generous licenses for OSS projects like Evennia.
> This page was originally tested on Windows (so use Windows-style path examples), but should work the same for all platforms.
First, download and install the IDE edition of your choosing.
The community edition should have everything you need,
but the professional edition has integrated support for Django which can help.
First, install Evennia on your local machine with [[Getting Started]]. If you're new to PyCharm, loading your project is as easy as selecting the `Open` option when PyCharm starts, and browsing to your game folder (the one created with `evennia --init`). We refer to it as `mygame` here.
## From an existing project
If you want to be able to examine evennia's core code or the scripts inside your virtualenv, you'll
need to add them to your project too:
Use this if you want to use PyCharm with an already existing Evennia game.
First, ensure you have completed the steps outlined [here](https://www.evennia.com/docs/latest/Setup/Installation.html#requirements).
Especially the virtualenv part, this will make setting the IDE up much easier.
1. Go to `File > Open...`
1. Select the folder (i.e. the `evennia` root)
1. Select "Open in current window" and "Add to currently opened projects"
1. Open Pycharm and click on the open button, open your root folder corresponding to `mygame/`.
2. Click on File -> Settings -> Project -> Python Interpreter -> Add Interpreter -> Add Local Interpreter
![Example](https://imgur.com/QRo8O1C.png)
3. Click on VirtualEnv -> Existing Interpreter -> Select your existing virtualenv folder,
should be `evenv` if you followed the default installation.
It's a good idea to set up the interpreter this before attempting anything further. The rest of this page assumes your project is already configured in PyCharm.
![Example](https://imgur.com/XDmgjTw.png)
1. Go to `File > Settings... > Project: \<mygame\> > Project Interpreter`
1. Click the Gear symbol `> Add local`
1. Navigate to your `evenv/scripts directory`, and select Python.exe
## From a new project
Use this if you are starting from scratch or want to make a new Evennia game.
1. Click on the new project button.
2. Select the location for your project.
You should create two new folders, one for the root of your project and one
for the evennia game directly. It should look like `/location/projectfolder/gamefolder`
3. Select the `Custom environment` interpreter type, using `Generate New` of type `Virtual env` using a
compatible base python version as recommended in https://www.evennia.com/docs/latest/Setup/Installation.html#requirements
Then choose a folder for your virtual environment as a sub folder of your project folder.
![Example new project configuration](https://imgur.com/R5Yr9I4.png)
Click on the create button and it will take you inside your new project with a bare bones virtual environment.
To install Evennia, you can then either clone evennia in your project folder or install it via pip.
The simplest way is to use pip.
Click on the `terminal` button
![Terminal Button](https://i.imgur.com/fDr4nhv.png)
1. Type in `pip install evennia`
2. Close the IDE and navigate to the project folder
3. Rename the game folder to a temporary name and create a new empty folder with the previous name
4. Open your OS terminal, navigate to your project folder and activate your virtualenv.
On linux, `source .evenv/bin/activate`
On windows, `evenv\Scripts\activate`
5. Type in `evennia --init mygame`
6. Move the files from your temporary folder, which should contain the `.idea/` folder into
the folder you have created at step 3 and delete the now empty temporary folder.
7. In the terminal, Move into the folder and type in `evennia migrate`
8. Start evennia to ensure that it works with `evennia start` and stop it with `evennia stop`
At this point, you can reopen your IDE and it should be functional.
[Look here for additional information](https://www.evennia.com/docs/latest/Setup/Installation.html)
Enjoy seeing all your imports checked properly, setting breakpoints, and live variable watching!
## Debug Evennia from inside PyCharm
1. Launch Evennia in your preferred way (usually from a console/terminal)
1. Open your project in PyCharm
1. In the PyCharm menu, select `Run > Attach to Local Process...`
1. From the list, pick the `twistd` process with the `server.py` parameter (Example: `twistd.exe --nodaemon --logfile=\<mygame\>\server\logs\server.log --python=\<evennia repo\>\evennia\server\server.py`)
### Attaching to the process
1. Launch Evennia in the pycharm terminal
2. Attempt to start it twice, this will give you the process ID of the server
3. In the PyCharm menu, select `Run > Attach to Process...`
4. From the list, pick the corresponding process id, it should be the `twistd` process with the `server.py` parameter (Example: `twistd.exe --nodaemon --logfile=\<mygame\>\server\logs\server.log --python=\<evennia repo\>\evennia\server\server.py`)
Of course you can attach to the `portal` process as well. If you want to debug the Evennia launcher
You can attach to the `portal` process as well, if you want to debug the Evennia launcher
or runner for some reason (or just learn how they work!), see Run Configuration below.
> NOTE: Whenever you reload Evennia, the old Server process will die and a new one start. So when you restart you have to detach from the old and then reattach to the new process that was created.
> To make the process less tedious you can apply a filter in settings to show only the server.py process in the list. To do that navigate to: `Settings/Preferences | Build, Execution, Deployment | Python Debugger` and then in `Attach to process` field put in: `twistd.exe" --nodaemon`. This is an example for windows, I don't have a working mac/linux box.
![Example process filter configuration](https://i.imgur.com/vkSheR8.png)
### Run Evennia with a Run/Debug Configuration
## Run Evennia from inside PyCharm
This configuration allows you to launch Evennia from inside PyCharm.
Besides convenience, it also allows suspending and debugging the evennia_launcher or evennia_runner
at points earlier than you could by running them externally and attaching.
In fact by the time the server and/or portal are running the launcher will have exited already.
This configuration allows you to launch Evennia from inside PyCharm. Besides convenience, it also allows suspending and debugging the evennia_launcher or evennia_runner at points earlier than you could by running them externally and attaching. In fact by the time the server and/or portal are running the launcher will have exited already.
#### On Windows
1. Go to `Run > Edit Configutations...`
2. Click the plus-symbol to add a new configuration and choose Python
3. Add the script: `\<yourprojectfolder>\.evenv\Scripts\evennia_launcher.py` (substitute your virtualenv if it's not named `evenv`)
4. Set script parameters to: `start -l` (-l enables console logging)
5. Ensure the chosen interpreter is your virtualenv
6. Set Working directory to your `mygame` folder (not your project folder nor evennia)
7. You can refer to the PyCharm documentation for general info, but you'll want to set at least a config name (like "MyMUD start" or similar).
A dropdown box holding your new configurations should appear next to your PyCharm run button.
Select it start and press the debug icon to begin debugging.
#### On Linux
1. Go to `Run > Edit Configutations...`
2. Click the plus-symbol to add a new configuration and choose Python
3. Add the script: `/<yourprojectfolder>/.evenv/bin/twistd` (substitute your virtualenv if it's not named `evenv`)
4. Set script parameters to: `--python=/<yourprojectfolder>/.evenv/lib/python3.11/site-packages/evennia/server/server.py --logger=evennia.utils.logger.GetServerLogObserver --pidfile=/<yourprojectfolder>/<yourgamefolder>/server/server.pid --nodaemon`
5. Add an environment variable `DJANGO_SETTINGS_MODULE=server.conf.settings`
6. Ensure the chosen interpreter is your virtualenv
7. Set Working directory to your game folder (not your project folder nor evennia)
8. You can refer to the PyCharm documentation for general info, but you'll want to set at least a config name (like "MyMUD Server" or similar).
A dropdown box holding your new configurations should appear next to your PyCharm run button.
Select it start and press the debug icon to begin debugging.
Note that this only starts the server process, you can either start the portal manually or set up
the configuration for the portal. The steps are very similar to the ones above.
1. Go to `Run > Edit Configutations...`
1. Click the plus-symbol to add a new configuration and choose Python
1. Add the script: `\<yourrepo\>\evenv\Scripts\evennia_launcher.py` (substitute your virtualenv if it's not named `evenv`)
1. Set script parameters to: `start -l` (-l enables console logging)
1. Ensure the chosen interpreter is from your virtualenv
1. Set Working directory to your `mygame` folder (not evenv nor evennia)
1. You can refer to the PyCharm documentation for general info, but you'll want to set at least a config name (like "MyMUD start" or similar).
2. Click the plus-symbol to add a new configuration and choose Python
3. Add the script: `/<yourprojectfolder>/.evenv/bin/twistd` (substitute your virtualenv if it's not named `evenv`)
4. Set script parameters to: `--python=/<yourprojectfolder>/.evenv/lib/python3.11/site-packages/evennia/server/portal/portal.py --logger=evennia.utils.logger.GetServerLogObserver --pidfile=/<yourprojectfolder>/<yourgamefolder>/server/portal.pid --nodaemon`
5. Add an environment variable `DJANGO_SETTINGS_MODULE=server.conf.settings`
6. Ensure the chosen interpreter is your virtualenv
7. Set Working directory to your game folder (not your project folder nor evennia)
8. You can refer to the PyCharm documentation for general info, but you'll want to set at least a config name (like "MyMUD Portal" or similar).
Now set up a "stop" configuration by following the same steps as above, but set your Script parameters to: stop (and name the configuration appropriately).
You should now be able to start both modes and get full debugging.
If you want to go one step further, you can add another config to automatically start both.
A dropdown box holding your new configurations should appear next to your PyCharm run button. Select MyMUD start and press the debug icon to begin debugging. Depending on how far you let the program run, you may need to run your "MyMUD stop" config to actually stop the server, before you'll be able start it again.
1. Go to `Run > Edit Configutations...`
2. Click the plus-symbol to add a new configuration and choose Compound
3. Add your two previous configurations, name it appropriately and press Ok.
### Alternative config - utilizing logfiles as source of data
This configuration takes a bit different approach as instead of focusing on getting the data back through logfiles. Reason for that is this way you can easily separate data streams, for example you rarely want to follow both server and portal at the same time, and this will allow it. This will also make sure to stop the evennia before starting it, essentially working as reload command (it will also include instructions how to disable that part of functionality). We will start by defining a configuration that will stop evennia. This assumes that `upfire` is your pycharm project name, and also the game name, hence the `upfire/upfire` path.
1. Go to `Run > Edit Configutations...`\
1. Click the plus-symbol to add a new configuration and choose the python interpreter to use (should be project default)
1. Name the configuration as "stop evennia" and fill rest of the fields accordingly to the image:
![Stop run configuration](https://i.imgur.com/gbkXhlG.png)
1. Press `Apply`
Now we will define the start/reload command that will make sure that evennia is not running already, and then start the server in one go.
1. Go to `Run > Edit Configutations...`\
1. Click the plus-symbol to add a new configuration and choose the python interpreter to use (should be project default)
1. Name the configuration as "start evennia" and fill rest of the fields accordingly to the image:
![Start run configuration](https://i.imgur.com/5YEjeHq.png)
1. Navigate to the `Logs` tab and add the log files you would like to follow. The picture shows
adding `portal.log` which will show itself in `portal` tab when running:
![Configuring logs following](https://i.imgur.com/gWYuOWl.png)
1. Skip the following steps if you don't want the launcher to stop evennia before starting.
1. Head back to `Configuration` tab and press the `+` sign at the bottom, under `Before launch....`
and select `Run another configuration` from the submenu that will pop up.
1. Click `stop evennia` and make sure that it's added to the list like on the image above.
1. Click `Apply` and close the run configuration window.
You are now ready to go, and if you will fire up `start evennia` configuration you should see
following in the bottom panel:
![Example of running alternative configuration](https://i.imgur.com/nTfpC04.png)
and you can click through the tabs to check appropriate logs, or even the console output as it is
still running in interactive mode.
You can now start your game with one click with full debugging active.

View file

@ -5,63 +5,45 @@
A typical unit test set calls some function or method with a given input, looks at the result and makes sure that this result looks as expected. Rather than having lots of stand-alone test programs, Evennia makes use of a central *test runner*. This is a program that gathers all available tests all over the Evennia source code (called *test suites*) and runs them all in one go. Errors and tracebacks are reported.
By default Evennia only tests itself. But you can also add your own tests to your game code and have Evennia run those for you.
## Running the Evennia test suite
To run the full Evennia test suite, go to your game folder and issue the command
evennia test evennia
This will run all the evennia tests using the default settings. You could also run only a subset of
all tests by specifying a subpackage of the library:
This will run all the evennia tests using the default settings. You could also run only a subset of all tests by specifying a subpackage of the library:
evennia test evennia.commands.default
A temporary database will be instantiated to manage the tests. If everything works out you will see
how many tests were run and how long it took. If something went wrong you will get error messages.
If you contribute to Evennia, this is a useful sanity check to see you haven't introduced an
unexpected bug.
A temporary database will be instantiated to manage the tests. If everything works out you will see how many tests were run and how long it took. If something went wrong you will get error messages. If you contribute to Evennia, this is a useful sanity check to see you haven't introduced an unexpected bug.
## Running custom game-dir unit tests
If you have implemented your own tests for your game you can run them from your game dir
with
If you have implemented your own tests for your game you can run them from your game dir with
evennia test --settings settings.py .
The period (`.`) means to run all tests found in the current directory and all subdirectories. You
could also specify, say, `typeclasses` or `world` if you wanted to just run tests in those subdirs.
The period (`.`) means to run all tests found in the current directory and all subdirectories. You could also specify, say, `typeclasses` or `world` if you wanted to just run tests in those subdirs.
An important thing to note is that those tests will all be run using the _default Evennia settings_.
To run the tests with your own settings file you must use the `--settings` option:
An important thing to note is that those tests will all be run using the _default Evennia settings_. To run the tests with your own settings file you must use the `--settings` option:
evennia test --settings settings.py .
The `--settings` option of Evennia takes a file name in the `mygame/server/conf` folder. It is
normally used to swap settings files for testing and development. In combination with `test`, it
forces Evennia to use this settings file over the default one.
The `--settings` option of Evennia takes a file name in the `mygame/server/conf` folder. It is normally used to swap settings files for testing and development. In combination with `test`, it forces Evennia to use this settings file over the default one.
You can also test specific things by giving their path
evennia test --settings settings.py world.tests.YourTest
## Writing new unit tests
Evennia's test suite makes use of Django unit test system, which in turn relies on Python's
*unittest* module.
Evennia's test suite makes use of Django unit test system, which in turn relies on Python's *unittest* module.
To make the test runner find the tests, they must be put in a module named `test*.py` (so `test.py`,
`tests.py` etc). Such a test module will be found wherever it is in the package. It can be a good
idea to look at some of Evennia's `tests.py` modules to see how they look.
To make the test runner find the tests, they must be put in a module named `test*.py` (so `test.py`, `tests.py` etc). Such a test module will be found wherever it is in the package. It can be a good idea to look at some of Evennia's `tests.py` modules to see how they look.
Inside the module you need to put a class inheriting (at any distance) from `unittest.TestCase`. Each
method on that class that starts with `test_` will be run separately as a unit test. There
are two special, optional methods `setUp` and `tearDown` that will (if you define them) run before
_every_ test. This can be useful for setting up and deleting things.
Inside the module you need to put a class inheriting (at any distance) from `unittest.TestCase`. Each method on that class that starts with `test_` will be run separately as a unit test. There are two special, optional methods `setUp` and `tearDown` that will (if you define them) respectively run before and after _every_ test. This can be useful for creating, configuring and cleaning up things that every test in the class needs.
To actually test things, you use special `assert...` methods on the class. Most common on is
`assertEqual`, which makes sure a result is what you expect it to be.
To actually test things, you use special `assert...` methods on the class. Most common on is `assertEqual`, which makes sure a result is what you expect it to be.
Here's an example of the principle. Let's assume you put this in `mygame/world/tests.py`
and want to test a function in `mygame/world/myfunctions.py`
@ -76,7 +58,7 @@ and want to test a function in `mygame/world/myfunctions.py`
class TestObj(unittest.TestCase):
"This tests a function myfunc."
"""This tests a function myfunc."""
def setUp(self):
"""done before every of the test_ * methods below"""
@ -120,11 +102,11 @@ You might also want to read the [Python documentation for the unittest module](h
### Using the Evennia testing classes
Evennia offers many custom testing classes that helps with testing Evennia features.
They are all found in [evennia.utils.test_resources](evennia.utils.test_resources). Note that
these classes implement the `setUp` and `tearDown` already, so if you want to add stuff in them
yourself you should remember to use e.g. `super().setUp()` in your code.
Evennia offers many custom testing classes that helps with testing Evennia features. They are all found in [evennia.utils.test_resources](evennia.utils.test_resources).
```{important}
Note that these base classes implement the `setUp` and `tearDown` already, so if you want to add stuff in them yourself you should remember to use e.g. `super().setUp()` in your code.
```
#### Classes for testing your game dir
These all use whatever setting you pass to them and works well for testing code in your game dir.
@ -147,10 +129,7 @@ These all use whatever setting you pass to them and works well for testing code
without a timing component.
- `.session` - A fake [Session](evennia.server.serversession.ServerSession) that mimics a player
connecting to the game. It is used by `.account1` and has a sessid of 1.
- `EvenniaCommandTest` - has the same environment like `EvenniaTest` but also adds a special
[.call()](evennia.utils.test_resources.EvenniaCommandTestMixin.call) method specifically for
testing Evennia [Commands](../Components/Commands.md). It allows you to compare what the command _actually_
returns to the player with what you expect. Read the `call` api doc for more info.
- `EvenniaCommandTest` - has the same environment like `EvenniaTest` but also adds a special [.call()](evennia.utils.test_resources.EvenniaCommandTestMixin.call) method specifically for testing Evennia [Commands](../Components/Commands.md). It allows you to compare what the command _actually_ returns to the player with what you expect. Read the `call` api doc for more info.
- `EvenniaTestCase` - This is identical to the regular Python `TestCase` class, it's
just there for naming symmetry with `BaseEvenniaTestCase` below.
@ -181,29 +160,24 @@ from commands import command as mycommand
class TestSet(EvenniaCommandTest):
"tests the look command by simple call, using Char2 as a target"
"""Tests the look command by simple call, using Char2 as a target"""
def test_mycmd_char(self):
self.call(mycommand.CmdMyLook(), "Char2", "Char2(#7)")
def test_mycmd_room(self):
"tests the look command by simple call, with target as room"
"""Tests the look command by simple call, with target as room"""
self.call(mycommand.CmdMyLook(), "Room",
"Room(#1)\nroom_desc\nExits: out(#3)\n"
"You see: Obj(#4), Obj2(#5), Char2(#7)")
```
When using `.call`, you don't need to specify the entire string; you can just give the beginning
of it and if it matches, that's enough. Use `\n` to denote line breaks and (this is a special for
the `.call` helper), `||` to indicate multiple uses of `.msg()` in the Command. The `.call` helper
has a lot of arguments for mimicing different ways of calling a Command, so make sure to
[read the API docs for .call()](evennia.utils.test_resources.EvenniaCommandTestMixin.call).
When using `.call`, you don't need to specify the entire string; you can just give the beginning of it and if it matches, that's enough. Use `\n` to denote line breaks and (this is a special for the `.call` helper), `||` to indicate multiple uses of `.msg()` in the Command. The `.call` helper has a lot of arguments for mimicing different ways of calling a Command, so make sure to [read the API docs for .call()](evennia.utils.test_resources.EvenniaCommandTestMixin.call).
#### Classes for testing Evennia core
These are used for testing Evennia itself. They provide the same resources as the classes
above but enforce Evennias default settings found in `evennia/settings_default.py`, ignoring
any settings changes in your game dir.
above but enforce Evennias default settings found in `evennia/settings_default.py`, ignoring any settings changes in your game dir.
- `BaseEvenniaTest` - all the default objects above but with enforced default settings
- `BaseEvenniaCommandTest` - for testing Commands, but with enforced default settings
@ -216,19 +190,12 @@ be useful if you want to mix your own testing classes:
- `EvenniaCommandMixin` - A class mixin that adds the `.call()` Command-tester helper.
If you want to help out writing unittests for Evennia, take a look at Evennia's [coveralls.io
page](https://coveralls.io/github/evennia/evennia). There you see which modules have any form of
test coverage and which does not. All help is appreciated!
page](https://coveralls.io/github/evennia/evennia). There you see which modules have any form of test coverage and which does not. All help is appreciated!
### Unit testing contribs with custom models
A special case is if you were to create a contribution to go to the `evennia/contrib` folder that
uses its [own database models](../Concepts/Models.md). The problem with this is that Evennia (and Django) will
only recognize models in `settings.INSTALLED_APPS`. If a user wants to use your contrib, they will
be required to add your models to their settings file. But since contribs are optional you cannot
add the model to Evennia's central `settings_default.py` file - this would always create your
optional models regardless of if the user wants them. But at the same time a contribution is a part
of the Evennia distribution and its unit tests should be run with all other Evennia tests using
`evennia test evennia`.
A special case is if you were to create a contribution to go to the `evennia/contrib` folder that uses its [own database models](../Concepts/Models.md). The problem with this is that Evennia (and Django) will
only recognize models in `settings.INSTALLED_APPS`. If a user wants to use your contrib, they will be required to add your models to their settings file. But since contribs are optional you cannot add the model to Evennia's central `settings_default.py` file - this would always create your optional models regardless of if the user wants them. But at the same time a contribution is a part of the Evennia distribution and its unit tests should be run with all other Evennia tests using `evennia test evennia`.
The way to do this is to only temporarily add your models to the `INSTALLED_APPS` directory when the test runs. here is an example of how to do it.
@ -284,8 +251,7 @@ class TestMyModel(BaseEvenniaTest):
### A note on making the test runner faster
If you have custom models with a large number of migrations, creating the test database can take a very long time. If you don't require migrations to run for your tests, you can disable them with the
django-test-without-migrations package. To install it, simply:
If you have custom models with a large number of migrations, creating the test database can take a very long time. If you don't require migrations to run for your tests, you can disable them with the django-test-without-migrations package. To install it, simply:
```
$ pip install django-test-without-migrations

View file

@ -111,18 +111,25 @@ Use `nn` and `bb` (next and back) to step through the file; e.g. `nn 12` will ju
## Limitations and Caveats
The batch-command processor is great for automating smaller builds or for testing new commands and objects repeatedly without having to write so much. There are several caveats you have to be aware of when using the batch-command processor for building larger, complex worlds though.
The main issue with batch-command builds is that when you run a batch-command script you (*you*, as in your character) are actually moving around in the game creating and building rooms in sequence, just as if you had been entering those commands manually, one by one.
The main issue is that when you run a batch-command script you (*you*, as in your superuser
character) are actually moving around in the game creating and building rooms in sequence, just as if you had been entering those commands manually, one by one. You have to take this into account when creating the file, so that you can 'walk' (or teleport) to the right places in order.
You have to take this into account when creating the file, so that you can 'walk' (or teleport) to the right places in order. It also means that you may be affected by the things you create, such as mobs attacking you or traps immediately hurting you.
This also means there are several pitfalls when designing and adding certain types of objects. Here are some examples:
If you know that your rooms and objects are going to be deployed via a batch-command script, you can plan for this beforehand. To help with this, you can use the fact that the non-persistent Attribute `batch_batchmode` is _only_ set while the batch-processor is running. Here's an example of how to use it:
- *Rooms that change your [Command Set](./Command-Sets.md)*: Imagine that you build a 'dark' room, which severely limits the cmdsets of those entering it (maybe you have to find the light switch to proceed). In your batch script you would create this room, then teleport to it - and promptly be shifted into the dark state where none of your normal build commands work ...
- *Auto-teleportation*: Rooms that automatically teleport those that enter them to another place (like a trap room, for example). You would be teleported away too.
- *Mobiles*: If you add aggressive mobs, they might attack you, drawing you into combat. If they have AI they might even follow you around when building - or they might move away from you before you've had time to finish describing and equipping them!
```python
class HorribleTrapRoom(Room):
# ...
def at_object_receive(self, received_obj, source_location, **kwargs):
"""Apply the horrible traps the moment the room is entered!"""
if received_obj.ndb.batch_batchmode:
# skip if we are currently building the room
return
# commence horrible trap code
```
So we skip the hook if we are currently building the room. This can work for anything, including making sure mobs don't start attacking you while you are creating them.
The solution to all these is to plan ahead. Make sure that superusers are never affected by whatever effects are in play. Add an on/off switch to objects and make sure it's always set to *off* upon creation. It's all doable, one just needs to keep it in mind.
There are other strategies, such as adding an on/off switch to actiive objects and make sure it's always set to *off* upon creation.
## Editor highlighting for .ev files

View file

@ -501,7 +501,7 @@ See `evennia/utils/evmenu.py` for the details of their default implementations.
## EvMenu templating language
In evmenu.py are two helper functions `parse_menu_template` and `template2menu` that is used to parse a _menu template_ string into an EvMenu:
In `evmenu.py` are two helper functions `parse_menu_template` and `template2menu` that is used to parse a _menu template_ string into an EvMenu:
evmenu.template2menu(caller, menu_template, goto_callables)

View file

@ -20,7 +20,7 @@ To escape the inlinefunc (e.g. to explain to someone how it works, use `$$`)
You say "To get a random value from 1 to 5, use $randint(1,5)."
```
While `randint` may look and work just like `random.randint` from the standard Python library, it is _not_. Instead it's a `inlinefunc` named `randint` made available to Evennia (which in turn uses the standard library function). For security reasons, only functions explicitly assigned to be used as inlinefuncs are viable.
While `randint` may look and work just like `random.randint` from the standard Python library, it is _not_. Instead it's an `inlinefunc` named `randint` made available to Evennia (which in turn uses the standard library function). For security reasons, only functions explicitly assigned to be used as inlinefuncs are viable.
You can apply the `FuncParser` manually. The parser is initialized with the inlinefunc(s) it's supposed to recognize in that string. Below is an example of a parser only understanding a single `$pow` inlinefunc:
@ -84,7 +84,7 @@ parser = FuncParser(["game.myfuncparser_callables", "game.more_funcparser_callab
Here, `callables` points to a collection of normal Python functions (see next section) for you to make
available to the parser as you parse strings with it. It can either be
- A `dict` of `{"functionname": callable, ...}`. This allows you do pick and choose exactly which callables
- A `dict` of `{"functionname": callable, ...}`. This allows you to pick and choose exactly which callables
to include and how they should be named. Do you want a callable to be available under more than one name?
Just add it multiple times to the dict, with a different key.
- A `module` or (more commonly) a `python-path` to a module. This module can define a dict
@ -235,7 +235,7 @@ everything but the outermost double quotes.
Since you don't know in which order users may use your callables, they should
always check the types of its inputs and convert to the type the callable needs.
Note also that when converting from strings, there are limits what inputs you
Note also that when converting from strings, there are limits on what inputs you
can support. This is because FunctionParser strings can be used by
non-developer players/builders and some things (such as complex
classes/callables etc) are just not safe/possible to convert from string

View file

@ -182,6 +182,9 @@ Some useful default lockfuncs (see `src/locks/lockfuncs.py` for more):
- `attr(attrname, value)` - checks so an attribute exists on accessing_object *and* has the given value.
- `attr_gt(attrname, value)` - checks so accessing_object has a value larger (`>`) than the given value.
- `attr_ge, attr_lt, attr_le, attr_ne` - corresponding for `>=`, `<`, `<=` and `!=`.
- `tag(tagkey[, category])` - checks if the accessing_object has the specified tag and optional category.
- `objtag(tagkey[, category])` - checks if the *accessed_object* has the specified tag and optional category.
- `objloctag(tagkey[, category])` - checks if the *accessed_obj*'s location has the specified tag and optional category.
- `holds(objid)` - checks so the accessing objects contains an object of given name or dbref.
- `inside()` - checks so the accessing object is inside the accessed object (the inverse of `holds()`).
- `pperm(perm)`, `pid(num)/pdbref(num)` - same as `perm`, `id/dbref` but always looks for permissions and dbrefs of *Accounts*, not on Characters.

View file

@ -39,7 +39,7 @@ In dictionary form, a prototype can look something like this:
```
If you wanted to load it into the spawner in-game you could just put all on one line:
spawn {"prototype_key="house", "key": "Large house", ...}
spawn {"prototype_key"="house", "key": "Large house", ...}
> Note that the prototype dict as given on the command line must be a valid Python structure - so you need to put quotes around strings etc. For security reasons, a dict inserted from-in game cannot have any other advanced Python functionality, such as executable code, `lambda` etc. If builders are supposed to be able to use such features, you need to offer them through [$protfuncs](Spawner-and- Prototypes#protfuncs), embedded runnable functions that you have full control to check and vet before running.

View file

@ -1,5 +1,7 @@
# Tags
_Tags_ are short text lables one can 'attach' to objects in order to organize, group and quickly find out their properties, similarly to how you attach labels to your luggage.
```{code-block}
:caption: In game
> tag obj = tagname
@ -28,7 +30,7 @@ class Sword(DefaultObject):
```
In-game, tags are controlled `tag` command:
In-game, tags are controlled by the default `tag` command:
> tag Chair = furniture
> tag Chair = furniture
@ -37,12 +39,13 @@ In-game, tags are controlled `tag` command:
> tag/search furniture
Chair, Sofa, Table
_Tags_ are short text lables one can 'hang' on objects in order to organize, group and quickly find out their properties. An Evennia entity can be tagged by any number of tags. They are more efficient than [Attributes](./Attributes.md) since on the database-side, Tags are _shared_ between all objects with that particular tag. A tag does not carry a value in itself; it either sits on the entity
You manage Tags using the `TagHandler` (`.tags`) on typeclassed entities. You can also assign Tags on the class level through the `TagProperty` (one tag, one category per line) or the `TagCategoryProperty` (one category, multiple tags per line). Both of these use the `TagHandler` under the hood, they are just convenient ways to add tags already when you define your class.
An Evennia entity can be tagged by any number of tags. Tags are more efficient than [Attributes](./Attributes.md) since on the database-side, Tags are _shared_ between all objects with that particular tag. A tag does not carry a value in itself; rather the existence of the tag itself is what is checked - a given object either has a given tag or not.
In code, you manage Tags using the `TagHandler` (`.tags`) on typeclassed entities. You can also assign Tags on the class level through the `TagProperty` (one tag, one category per line) or the `TagCategoryProperty` (one category, multiple tags per line). Both of these use the `TagHandler` under the hood, they are just convenient ways to add tags already when you define your class.
Above, the tags inform us that the `Sword` is both sharp and can be wielded. If that's all they do, they could just be a normal Python flag. When tags become important is if there are a lot of objects with different combinations of tags. Maybe you have a magical spell that dulls _all_ sharp-edged objects in the castle - whether sword, dagger, spear or kitchen knife! You can then just grab all objects with the `has_sharp_edge` tag.
Another example would be a weather script affecting all rooms tagged as `outdoors` or finding all characters tagged with `belongs_to_fighter_guild`.
Another example would be a weather script affecting all rooms tagged as `outdoors` or finding all characters tagged with the `belongs_to_fighter_guild` tag.
In Evennia, Tags are technically also used to implement `Aliases` (alternative names for objects) and `Permissions` (simple strings for [Locks](./Locks.md) to check for).

View file

@ -244,18 +244,18 @@ The arguments to this method are described [in the API docs here](github:evennia
Technically, typeclasses are [Django proxy models](https://docs.djangoproject.com/en/4.1/topics/db/models/#proxy-models). The only database models that are "real" in the typeclass system (that is, are represented by actual tables in the database) are `AccountDB`, `ObjectDB`, `ScriptDB` and `ChannelDB` (there are also [Attributes](./Attributes.md) and [Tags](./Tags.md) but they are not typeclasses themselves). All the subclasses of them are "proxies", extending them with Python code without actually modifying the database layout.
Evennia modifies Django's proxy model in various ways to allow them to work without any boiler plate (for example you don't need to set the Django "proxy" property in the model `Meta` subclass, Evennia handles this for you using metaclasses). Evennia also makes sure you can query subclasses as well as patches django to allow multiple inheritance from the same base class.
Evennia modifies Django's proxy model in various ways to allow them to work without any boiler plate (for example you don't need to set the Django "proxy" property in the model `Meta` subclass, Evennia handles this for you using metaclasses). Evennia also makes sure you can query subclasses as well as patches Django to allow multiple inheritance from the same base class.
### Caveats
Evennia uses the *idmapper* to cache its typeclasses (Django proxy models) in memory. The idmapper allows things like on-object handlers and properties to be stored on typeclass instances and to not get lost as long as the server is running (they will only be cleared on a Server reload). Django does not work like this by default; by default every time you search for an object in the database you'll get a *different* instance of that object back and anything you stored on it that was not in the database would be lost. The bottom line is that Evennia's Typeclass instances subside in memory a lot longer than vanilla Django model instance do.
Evennia uses the *idmapper* to cache its typeclasses (Django proxy models) in memory. The idmapper allows things like on-object handlers and properties to be stored on typeclass instances and to not get lost as long as the server is running (they will only be cleared on a Server reload). Django does not work like this by default; by default every time you search for an object in the database you'll get a *different* instance of that object back and anything you stored on it that was not in the database would be lost. The bottom line is that Evennia's Typeclass instances subsist in memory a lot longer than vanilla Django model instances do.
There is one caveat to consider with this, and that relates to [making your own models](New-
Models): Foreign relationships to typeclasses are cached by Django and that means that if you were to change an object in a foreign relationship via some other means than via that relationship, the object seeing the relationship may not reliably update but will still see its old cached version. Due to typeclasses staying so long in memory, stale caches of such relationships could be more visible than common in Django. See the [closed issue #1098 and its comments](https://github.com/evennia/evennia/issues/1098) for examples and solutions.
## Will I run out of dbrefs?
Evennia does not re-use its `#dbrefs`. This means new objects get an ever-increasing `#dbref`, also if you delete older objects. There are technical and safety reasons for this. But you may wonder if this means you have to worry about a big game 'running out' of dbref integers eventually.
Evennia does not re-use its `#dbrefs`. This means new objects get an ever-increasing `#dbref`, even if you delete older objects. There are technical and safety reasons for this. But you may wonder if this means you have to worry about a big game 'running out' of dbref integers eventually.
The answer is simply **no**.

View file

@ -102,7 +102,7 @@ and static files.
- The api code is located in `evennia/web/api/` - the `url.py` file here is responsible for
collecting all view-classes.
Contrary to other web components, there is no pre-made urls.py set up for
Contrary to other web components, there is no pre-made `urls.py` set up for
`mygame/web/api/`. This is because the registration of models with the api is
strongly integrated with the REST api functionality. Easiest is probably to
copy over `evennia/web/api/urls.py` and modify it in place.

View file

@ -104,7 +104,7 @@ This is the layout of the `mygame/web/` folder relevant for the website:
Game folders created with older versions of Evennia will lack most of this
convenient `mygame/web/` layout. If you use a game dir from an older version,
you should copy over the missing `evennia/game_template/web/` folders from
there, as well as the main urls.py file.
there, as well as the main `urls.py` file.
```

View file

@ -5,27 +5,21 @@
Color can be a very useful tool for your game. It can be used to increase readability and make your
game more appealing visually.
Remember however that, with the exception of the webclient, you generally don't control the client
used to connect to the game. There is, for example, one special tag meaning "yellow". But exactly
*which* hue of yellow is actually displayed on the user's screen depends on the settings of their
particular mud client. They could even swap the colours around or turn them off altogether if so
desired. Some clients don't even support color - text games are also played with special reading
equipment by people who are blind or have otherwise diminished eyesight.
Remember however that, with the exception of the webclient, you generally don't control the client used to connect to the game. There is, for example, one special tag meaning "yellow". But exactly *which* hue of yellow is actually displayed on the user's screen depends on the settings of their particular mud client. They could even swap the colours around or turn them off altogether if so desired. Some clients don't even support color - text games are also played with special reading equipment by people who are blind or have otherwise diminished eyesight.
So a good rule of thumb is to use colour to enhance your game but don't *rely* on it to display
critical information. If you are coding the game, you can add functionality to let users disable
colours as they please, as described [here](../Howtos/Manually-Configuring-Color.md).
critical information. The default `screenreader` command will automatically turn off all color for a user (as well as clean up many line decorations etc). Make sure your game is still playable and understandable with this active.
Evennia supports two color standards:
- `ANSI` - 16 foreground colors + 8 background colors. Widely supported.
- `Xterm256` - 128 RGB colors, 32 greyscales. Not always supported in old clients.
- `Xterm256` - 128 RGB colors, 32 greyscales. Not always supported in old clients. Falls back to ANSI.
- `Truecolor` - 24B RGB colors using hex notation. Not supported by many clients. Falls back to `XTerm256`.
To see which colours your client support, use the default `color` command. This will list all
available colours for ANSI and Xterm256 along with the codes you use for them. The
central ansi/xterm256 parser is located in [evennia/utils/ansi.py](evennia.utils.ansi).
available colours for ANSI and Xterm256, as well as a selection of True color codes along with the codes you use for them. The central ansi/xterm256 parser is located in [evennia/utils/ansi.py](evennia.utils.ansi), the true color one in [evennia/utils/true/hex_colors.py](evennia.utils.hex_colors).
## ANSI colours
## ANSI colours and symbols
Evennia supports the `ANSI` standard for text. This is by far the most supported MUD-color standard, available in all but the most ancient mud clients.
@ -35,39 +29,39 @@ will see the text in the specified colour, otherwise the tags will be stripped (
For the webclient, Evennia will translate the codes to CSS tags.
| Tag | Effect |
| ---- | ----- |
| \|n | end all color formatting, including background colors. |
|\|r | bright red foreground color |
|\|g | bright green foreground color |
|\|y | bright yellow foreground color |
|\|b | bright blue foreground color |
|\|m | bright magentaforeground color |
|\|c | bright cyan foreground color |
|\|w | bright white foreground color |
|\|x | bright black (dark grey) foreground color |
|\|R | normal red foreground color |
|\|G | normal green foreground color |
|\|Y | normal yellow foreground color |
|\|B | normal blue foreground color |
|\|M | normal magentaforeground color |
|\|C | normal cyan foreground color |
|\|W | normal white (light grey) foreground color |
|\|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 (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 |
| \|* | invert the current text/background colours, like a marker. See note below. |
| Tag | Effect | |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- |
| \|n | end all color formatting, including background colors. | |
| \|r | bright red foreground color | |
| \|g | bright green foreground color | |
| \|y | bright yellow foreground color | |
| \|b | bright blue foreground color | |
| \|m | bright magentaforeground color | |
| \|c | bright cyan foreground color | |
| \|w | bright white foreground color | |
| \|x | bright black (dark grey) foreground color | |
| \|R | normal red foreground color | |
| \|G | normal green foreground color | |
| \|Y | normal yellow foreground color | |
| \|B | normal blue foreground color | |
| \|M | normal magentaforeground color | |
| \|C | normal cyan foreground color | |
| \|W | normal white (light grey) foreground color | |
| \|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 (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 (not supported in Evennia webclient) | |
| \|U | negates the effects of \|u | |
| \|i | italic font (not supported in Evennia webclient) | |
| \|I | negates the effects of \|i | |
| \|s | strikethrough font (not supported in Evennia webclient) | |
| \|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 | |
| \|* | invert the current text/background colours, like a marker. See note below. | |
Here is an example of the tags in action:
@ -187,11 +181,9 @@ See the [Wikipedia entry on web colors](https://en.wikipedia.org/wiki/Web_colors
```
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.
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 |

View file

@ -15,7 +15,7 @@ updated after Sept 2022 will be missing some translations.
+---------------+----------------------+--------------+
| Language Code | Language | Last updated |
+===============+======================+==============+
| de | German | Dec 2022 |
| de | German | Aug 2024 |
+---------------+----------------------+--------------+
| es | Spanish | Aug 2019 |
+---------------+----------------------+--------------+
@ -35,7 +35,7 @@ updated after Sept 2022 will be missing some translations.
+---------------+----------------------+--------------+
| sv | Swedish | Sep 2022 |
+---------------+----------------------+--------------+
| zh | Chinese (simplified) | May 2019 |
| zh | Chinese (simplified) | Oct 2024 |
+---------------+----------------------+--------------+
```
@ -117,21 +117,21 @@ find there. You can edit this with a normal text editor but it is easiest if
you use a special po-file editor from the web (search the web for "po editor"
for many free alternatives), for example:
- [gtranslator](https://wiki.gnome.org/Apps/Gtranslator)
- [poeditor](https://poeditor.com/)
- [gtranslator](https://wiki.gnome.org/Apps/Gtranslator)
- [poeditor](https://poeditor.com/)
The concept of translating is simple, it's just a matter of taking the english
strings you find in the `**.po` file and add your language's translation best
strings you find in the `django.po` file and add your language's translation best
you can. Once you are done, run
`evennia compilemessages`
evennia compilemessages
This will compile all languages. Check your language and also check back to your
`.po` file in case the process updated it - you may need to fill in some missing
header fields and should usually note who did the translation.
When you are done, make sure that everyone can benefit from your translation!
Make a PR against Evennia with the updated `**.po` file. Less ideally (if git is
Make a PR against Evennia with the updated `django.po` file. Less ideally (if git is
not your thing) you can also attach it to a new post in our forums.
### Hints on translation
@ -161,3 +161,31 @@ English anyway.
\n(Traceback was logged {timestamp})"
Swedish: "Fel medan cmdset laddades: Ingen cmdset-klass med namn '{classname}' i {path}.
\n(Traceback loggades {timestamp})"
## Marking Strings in Code for Translation
If you modify the Python module code, you can mark strings for translation by passing them to the `gettext()` method. In Evennia, this is usually imported as `_()` for convenience:
```python
from django.utils.translation import gettext as _
string = _("Text to translate")
```
### Formatting Considerations
When using formatted strings, ensure that you pass the "raw" string to `gettext` for translation first and then format the output. Otherwise, placeholders will be replaced before translation occurs, preventing the correct string from being found in the `.po` file. It's also recommended to use named placeholders (e.g., `{char}`) instead of positional ones (e.g., `{}`) for better readability and maintainability.
```python
# incorrect:
string2 = _("Hello {char}!".format(char=caller.name))
# correct:
string2 = _("Hello {char}!").format(char=caller.name)
```
This is also why f-strings don't work with `gettext`:
```python
# will not work
string = _(f"Hello {char}!")
```

View file

@ -43,7 +43,7 @@ For the `look`-command (and anything else written by the player), the `text` `co
### Inputfuncs
On the Evennia server side, a list of [inputfucs](Inputuncs) are registered. You can add your own by extending `settings.INPUT_FUNC_MODULES`.
On the Evennia server side, a list of [inputfuncs](Inputfuncs) are registered. You can add your own by extending `settings.INPUT_FUNC_MODULES`.
```python
inputfunc_commandname(session, *args, **kwargs)

View file

@ -21,7 +21,7 @@ Would result in this added description:
## Installation
To install, import this module and have your default character
inherit from ClothedCharacter in your game's characters.py file:
inherit from ClothedCharacter in your game's `characters.py` file:
```python

View file

@ -9,7 +9,7 @@ You can use Godot to provide advanced functionality with proper Evennia support.
## Installation
You need to add the following settings in your settings.py and restart evennia.
You need to add the following settings in your `settings.py` and restart evennia.
```python
PORTAL_SERVICES_PLUGIN_MODULES.append('evennia.contrib.base_systems.godotwebsocket.webclient')
@ -64,7 +64,7 @@ This will connect when the Scene is ready, poll and print the data when we recei
extends Node
# The URL we will connect to.
var websocket_url = "ws://localhost:4008"
var websocket_url = "ws://127.0.0.1:4008"
var socket := WebSocketPeer.new()
func _ready():
@ -144,7 +144,7 @@ func _on_button_pressed():
extends Node
# The URL we will connect to.
var websocket_url = "ws://localhost:4008"
var websocket_url = "ws://127.0.0.1:4008"
var socket := WebSocketPeer.new()
@onready var output_label = $"../Panel/VBoxContainer/RichTextLabel"
@ -193,6 +193,7 @@ func _exit_tree():
```
----
<small>This document page is generated from `evennia/contrib/base_systems/godotwebsocket/README.md`. Changes to this

View file

@ -77,7 +77,7 @@ The contrib is designed to make adding new types of reports to the system as sim
#### 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.
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

View file

@ -66,7 +66,8 @@ LLM_PATH = "/api/v1/generate"
# if you wanted to authenticated to some external service, you could
# add an Authenticate header here with a token
LLM_HEADERS = {"Content-Type": "application/json"}
# note that the content of each header must be an iterable
LLM_HEADERS = {"Content-Type": ["application/json"]}
# this key will be inserted in the request, with your user-input
LLM_PROMPT_KEYNAME = "prompt"
@ -77,7 +78,7 @@ LLM_REQUEST_BODY = {
"temperature": 0.7, # 0-2. higher=more random, lower=predictable
}
# helps guide the NPC AI. See the LLNPC section.
LLM_PROMPT_PREFIx = (
LLM_PROMPT_PREFIX = (
"You are roleplaying as {name}, a {desc} existing in {location}. "
"Answer with short sentences. Only respond as {name} would. "
"From here on, the conversation between {name} and {character} begins."
@ -148,8 +149,8 @@ Here is an untested example of the Evennia setting for calling [OpenAI's v1/comp
```python
LLM_HOST = "https://api.openai.com"
LLM_PATH = "/v1/completions"
LLM_HEADERS = {"Content-Type": "application/json",
"Authorization": "Bearer YOUR_OPENAI_API_KEY"}
LLM_HEADERS = {"Content-Type": ["application/json"],
"Authorization": ["Bearer YOUR_OPENAI_API_KEY"]}
LLM_PROMPT_KEYNAME = "prompt"
LLM_REQUEST_BODY = {
"model": "gpt-3.5-turbo",

View file

@ -86,7 +86,7 @@ For example:
Below are two examples showcasing the use of automatic exit generation and
custom exit generation. Whilst located, and can be used, from this module for
convenience The below example code should be in mymap.py in mygame/world.
convenience The below example code should be in `mymap.py` in mygame/world.
### Example One

View file

@ -0,0 +1,42 @@
# Item Storage
Contribution by helpme (2024)
This module allows certain rooms to be marked as storage locations.
In those rooms, players can `list`, `store`, and `retrieve` items. Storages can be shared or individual.
## Installation
This utility adds the storage-related commands. Import the module into your commands and add it to your command set to make it available.
Specifically, in `mygame/commands/default_cmdsets.py`:
```python
...
from evennia.contrib.game_systems.storage import StorageCmdSet # <---
class CharacterCmdset(default_cmds.Character_CmdSet):
...
def at_cmdset_creation(self):
...
self.add(StorageCmdSet) # <---
```
Then `reload` to make the `list`, `retrieve`, `store`, and `storage` commands available.
## Usage
To mark a location as having item storage, use the `storage` command. By default this is a builder-level command. Storage can be shared, which means everyone using the storage can access all items stored there, or individual, which means only the person who stores an item can retrieve it. See `help storage` for further details.
## Technical info
This is a tag-based system. Rooms set as storage rooms are tagged with an identifier marking them as shared or not. Items stored in those rooms are tagged with the storage room identifier and, if the storage room is not shared, the character identifier, and then they are removed from the grid i.e. their location is set to `None`. Upon retrieval, items are untagged and moved back to character inventories.
When a room is unmarked as storage with the `storage` command, all stored objects are untagged and dropped to the room. You should use the `storage` command to create and remove storages, as otherwise stored objects may become lost.
----
<small>This document page is generated from `evennia/contrib/game_systems/storage/README.md`. Changes to this
file will be overwritten, so edit that file rather than this one.</small>

View file

@ -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 **51** contribs currently included
This page is auto-generated and summarizes all **52** contribs currently included
with the Evennia distribution.
All contrib categories are imported from `evennia.contrib`, such as
@ -37,9 +37,9 @@ If you want to add a contrib, see [the contrib guidelines](./Contribs-Guidelines
| [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) |
| [rpsystem](#rpsystem) | [simpledoor](#simpledoor) | [slow_exit](#slow_exit) | [storage](#storage) | [talking_npc](#talking_npc) |
| [traits](#traits) | [tree_select](#tree_select) | [turnbattle](#turnbattle) | [tutorial_world](#tutorial_world) | [unixcommand](#unixcommand) |
| [wilderness](#wilderness) | [xyzgrid](#xyzgrid) |
@ -288,6 +288,7 @@ Contrib-Gendersub.md
Contrib-Mail.md
Contrib-Multidescer.md
Contrib-Puzzles.md
Contrib-Storage.md
Contrib-Turnbattle.md
```
@ -420,6 +421,16 @@ the puzzle entirely from in-game.
### `storage`
_Contribution by helpme (2024)_
This module allows certain rooms to be marked as storage locations.
[Read the documentation](./Contrib-Storage.md) - [Browse the Code](evennia.contrib.game_systems.storage)
### `turnbattle`
_Contribution by Tim Ashley Jenkins, 2017_

View file

@ -80,7 +80,7 @@ The `east` exit has a `key` of `east`, a `location` of `Meadow` and a `destinati
Meadow -> east -> Forest
Forest -> west -> Meadow
In-game you do this with `tunnel` and `dig` commands, bit if you want to ever set up these links in code, you can do it like this:
In-game you do this with `tunnel` and `dig` commands, but if you want to set up these links in code, you can do it like this:
```python
from evennia import create_object

View file

@ -14,7 +14,7 @@ But sometimes you need to be more specific:
- You want to find all `Characters` ...
- ... who are in Rooms tagged as `moonlit` ...
- ... _and_ who has the Attribute `lycantrophy` with a level higher than 2 ...
- ... _and_ who has the Attribute `lycanthropy` with a level equal to 2 ...
- ... because they should immediately transform to werewolves!
In principle you could achieve this with the existing search functions combined with a lot of loops
@ -159,7 +159,7 @@ of this lesson.
Firstly, we make ourselves and our current location match the criteria, so we can test:
> py here.tags.add("moonlit")
> py me.db.lycantrophy = 3
> py me.db.lycanthropy = 2
This is an example of a more complex query. We'll consider it an example of what is
possible.
@ -174,14 +174,16 @@ will_transform = (
Character.objects
.filter(
db_location__db_tags__db_key__iexact="moonlit",
db_attributes__db_key="lycantrophy",
db_attributes__db_value__eq=2
db_attributes__db_key__iexact="lycanthropy",
db_attributes__db_value=2
)
)
```
```{sidebar} Attributes vs database fields
Don't confuse database fields with [Attributes](../../../Components/Attributes.md) you set via `obj.db.attr = 'foo'` or `obj.attributes.add()`. Attributes are custom database entities *linked* to an object. They are not separate fields *on* that object like `db_key` or `db_location` are.
Don't confuse database fields with [Attributes](../../../Components/Attributes.md) you set via `obj.db.attr = 'foo'` or `obj.attributes.add()`. Attributes are custom database entities *linked* to an object. They are not separate fields *on* that object like `db_key` or `db_location` are.
While an Attribute's `db_key` is just a normal string, theor `db_value` is in fact a serialized piece of data. This means that cannot query this with additional operators. So if you use e.g. `db_attributes__db_value__iexact=2`, you'll get an error. While Attributes are very flexible, this is their drawback - their stored value is not possible to directly query with advanced query methods beyond finding the exact match.
```
- **Line 4** We want to find `Character`s, so we access `.objects` on the `Character` typeclass.
- We start to filter ...
@ -189,19 +191,19 @@ Don't confuse database fields with [Attributes](../../../Components/Attributes.m
- ... and on that location, we get the value of `db_tags` (this is a _many-to-many_ database field
that we can treat like an object for this purpose; it references all Tags on the location)
- ... and from those `Tags`, we looking for `Tags` whose `db_key` is "monlit" (non-case sensitive).
- **Line 7**: ... We also want only Characters with `Attributes` whose `db_key` is exactly `"lycantrophy"`
- **Line 8** :... at the same time as the `Attribute`'s `db_value` is exactly 2.
- **Line 7**: ... We also want only Characters with `Attributes` whose `db_key` is exactly `"lycanthropy"`
- **Line 8** :... at the same time as the `Attribute`'s `db_value` is 2.
Running this query makes our newly lycantrophic Character appear in `will_transform` so we know to transform it. Success!
Running this query makes our newly lycanthropic Character appear in `will_transform` so we know to transform it. Success!
```{important}
You can't query for an Attribute `db_value` quite as freely as other data-types. This is because Attributes can store any Python entity and is actually stored as _strings_ on the database side. So while you can use `__eq=2` in the above example, you will not be able to `__gt=2` or `__lt=2` because these operations don't make sense for strings. See [Attributes](../../../Components/Attributes.md#querying-by-attribute) for more information on dealing with Attributes.
You can't query for an Attribute `db_value` quite as freely as other data-types. This is because Attributes can store any Python entity and is actually stored as _strings_ on the database side. So while you can use `db_value=2` in the above example, you will not be able to use `dbvalue__eq=2` or `__lt=2`. See [Attributes](../../../Components/Attributes.md#querying-by-attribute) for more information on dealing with Attributes.
```
## Queries with OR or NOT
All examples so far used `AND` relations. The arguments to `.filter` are added together with `AND`
("we want tag room to be "monlit" _and_ lycantrhopy be > 2").
("we want tag room to be "monlit" _and_ lycanthropy be > 2").
For queries using `OR` and `NOT` we need Django's [Q object](https://docs.djangoproject.com/en/4.1/topics/db/queries/#complex-lookups-with-q-objects). It is imported from Django directly:
@ -228,7 +230,7 @@ works like `NOT`.
Would get all Characters that are either named "Dalton" _or_ which is _not_ in prison. The result is a mix
of Daltons and non-prisoners.
Let us expand our original werewolf query. Not only do we want to find all Characters in a moonlit room with a certain level of `lycanthrophy` - we decide that if they have been _newly bitten_, they should also turn, _regardless_ of their lycantrophy level (more dramatic that way!).
Let us expand our original werewolf query. Not only do we want to find all Characters in a moonlit room with a certain level of `lycanthropy` - we decide that if they have been _newly bitten_, they should also turn, _regardless_ of their lycanthropy level (more dramatic that way!).
Let's say that getting bitten means that you'll get assigned a Tag `recently_bitten`.
@ -242,8 +244,8 @@ will_transform = (
.filter(
Q(db_location__db_tags__db_key__iexact="moonlit")
& (
Q(db_attributes__db_key="lycantrophy",
db_attributes__db_value__eq=2)
Q(db_attributes__db_key="lycanthropy",
db_attributes__db_value=2)
| Q(db_tags__db_key__iexact="recently_bitten")
))
.distinct()
@ -256,12 +258,12 @@ That's quite compact. It may be easier to see what's going on if written this wa
from django.db.models import Q
q_moonlit = Q(db_location__db_tags__db_key__iexact="moonlit")
q_lycantropic = Q(db_attributes__db_key="lycantrophy", db_attributes__db_value__eq=2)
q_lycanthropic = Q(db_attributes__db_key="lycanthropy", db_attributes__db_value=2)
q_recently_bitten = Q(db_tags__db_key__iexact="recently_bitten")
will_transform = (
Character.objects
.filter(q_moonlit & (q_lycantropic | q_recently_bitten))
.filter(q_moonlit & (q_lycanthropic | q_recently_bitten))
.distinct()
)
```
@ -276,7 +278,7 @@ the same object with different relations.
```
This reads as "Find all Characters in a moonlit room that either has the
Attribute `lycantrophy` higher than two, _or_ which has the Tag
Attribute `lycanthropy` equal to two, _or_ which has the Tag
`recently_bitten`". With an OR-query like this it's possible to find the same
Character via different paths, so we add `.distinct()` at the end. This makes
sure that there is only one instance of each Character in the result.
@ -406,4 +408,4 @@ in a format like the following:
## Conclusions
We have covered a lot of ground in this lesson and covered several more complex topics. Knowing how to query using Django is a powerful skill to have.
We have covered a lot of ground in this lesson and covered several more complex topics. Knowing how to query using Django is a powerful skill to have.

View file

@ -81,7 +81,7 @@ Common for the settings is that you generally will never import them directly vi
- `at_server_startstop.py` - This allows to inject code to execute every time the server starts, stops or reloads in different ways.
- `connection_screens.py` - This allows for changing the connection screen you see when you first connect to your game.
- `inlinefuncs.py` - [Inlinefuncs](../../../Concepts/Inline-Functions.md) are optional and limited 'functions' that can be embedded in any strings being sent to a player. They are written as `$funcname(args)` and are used to customize the output depending on the user receiving it. For example sending people the text `"Let's meet at $realtime(13:00, GMT)!` would show every player seeing that string the time given in their own time zone. The functions added to this module will become new inlinefuncs in the game. See also the [FuncParser](../../../Components/FuncParser.md).
- `inputfucs.py` - When a command like `look` is received by the server, it is handled by an [Inputfunc](InputFuncs) that redirects it to the cmdhandler system. But there could be other inputs coming from the clients, like button-presses or the request to update a health-bar. While most common cases are already covered, this is where one adds new functions to process new types of input.
- `inputfuncs.py` - When a command like `look` is received by the server, it is handled by an [Inputfunc](InputFuncs) that redirects it to the cmdhandler system. But there could be other inputs coming from the clients, like button-presses or the request to update a health-bar. While most common cases are already covered, this is where one adds new functions to process new types of input.
- `lockfuncs.py` - [Locks](../../../Components/Locks.md) and their component _LockFuncs_ restrict access to things in-game. Lock funcs are used in a mini-language to defined more complex locks. For example you could have a lockfunc that checks if the user is carrying a given item, is bleeding or has a certain skill value. New functions added in this modules will become available for use in lock definitions.
- `mssp.py` - Mud Server Status Protocol is a way for online MUD archives/listings (which you usually have to sign up for) to track which MUDs are currently online, how many players they have etc. While Evennia handles the dynamic information automatically, this is where you set up the meta-info about your game, such as its theme, if player-killing is allowed and so on. This is a more generic form of the Evennia Game directory.
- `portal_services_plugins.py` - If you want to add new external connection protocols to Evennia, this is the place to add them.

View file

@ -132,7 +132,7 @@ It's fine to sit 'on' a chair. But what if our Sittable is an armchair?
You sit on armchair.
```
This is not grammatically correct, you actually sit "in" an armchair rather than "on" it. It's also possible to both sit 'in' or 'on' a chair depending on the type of chair (English is weird). We want to be able to control this.
This is not grammatically correct, you actually sit "in" an armchair rather than "on" it. The type of chair matters (English is weird). We want to be able to control this.
We _could_ make a child class of `Sittable` named `SittableIn` that makes this change, but that feels excessive. Instead we will modify what we have:
@ -154,19 +154,19 @@ class Sittable(Object):
sitter (Object): The one trying to sit down.
"""
adjective = self.db.adjective or "on"
preposition = self.db.preposition or "on"
current = self.db.sitter
if current:
if current == sitter:
sitter.msg(f"You are already sitting {adjective} {self.key}.")
sitter.msg(f"You are already sitting {preposition} {self.key}.")
else:
sitter.msg(
f"You can't sit {adjective} {self.key} "
f"You can't sit {preposition} {self.key} "
f"- {current.key} is already sitting there!")
return
self.db.sitter = sitter
sitter.db.is_sitting = self
sitter.msg(f"You sit {adjective} {self.key}")
sitter.msg(f"You sit {preposition} {self.key}")
def do_stand(self, stander):
"""
@ -178,20 +178,20 @@ class Sittable(Object):
"""
current = self.db.sitter
if not stander == current:
stander.msg(f"You are not sitting {self.db.adjective} {self.key}.")
stander.msg(f"You are not sitting {self.db.preposition} {self.key}.")
else:
self.db.sitter = None
del stander.db.is_sitting
stander.msg(f"You stand up from {self.key}.")
```
- **Line 15**: We grab the `adjective` Attribute. Using `self.db.adjective or "on"` here means that if the Attribute is not set (is `None`/falsy) the default "on" string will be assumed.
- **Lines 19,22,27,39, and 43**: We use this adjective to modify the return text we see.
- **Line 15**: We grab the `preposition` Attribute. Using `self.db.preposition or "on"` here means that if the Attribute is not set (is `None`/falsy) the default "on" string will be assumed. This is because the `or` relation will return the first true condition. A more explicit way to write this would be to use a [ternary operator](https://www.dataquest.io/blog/python-ternary-operator/) `self.db.preposition if self.db.preposition else "on"`.
- **Lines 19,22,27,39, and 43**: We use this preposition to modify the return text we see.
`reload` the server. An advantage of using Attributes like this is that they can be modified on the fly, in-game. Let's look at how a builder could use this with normal building commands (no need for `py`):
```
> set armchair/adjective = in
> set armchair/preposition = in
```
Since we haven't added the `sit` command yet, we must still use `py` to test:
@ -386,7 +386,7 @@ We don't need a new CmdSet for this, instead we will add this to the default Cha
# ...
from commands import sittables
class CharacterCmdSet(CmdSet):
class CharacterCmdSet(default_cmds.CharacterCmdSet):
"""
(docstring)
"""

View file

@ -377,7 +377,6 @@ class ObjectParent:
"""
class docstring
"""
pass
class Object(ObjectParent, DefaultObject):
"""

View file

@ -382,10 +382,8 @@ from evennia import search_object
# we assume only one match of each
dungeons = search_object("dungeon", typeclass="typeclasses.rooms.Room")
chests = search_object("chest", location=dungeons[0])
# find if there are any skulls in the chest
# find out how much coin are in the chest
coins = search_object("coin", candidates=chests[0].contents)
```
This would work but is both quite inefficient, fragile and a lot to type. This kind of thing is better done by directly querying the database.
In the next lesson we will dive further into more complex searching when we look at Django queries and querysets in earnest.
This would work but is both quite inefficient, fragile and a lot to type. This kind of thing is better done by *directly querying the database*. We will get to this in the next lesson. There we will dive into more complex searching using Django database queries and querysets.

View file

@ -172,7 +172,7 @@ In game:
Or in code:
exit_obj.locks.add(
"traverse:attr(is_ic, True)")
"traverse:attr(is_pc, True)")
See [Locks](../../../Components/Locks.md) for a lot more information about Evennia locks.
```

View file

@ -402,18 +402,19 @@ class EquipmentHandler:
to_backpack = [obj]
else:
# for others (body, head), just replace whatever's there
replaced = [obj]
to_backpack = [slots[use_slot]]
slots[use_slot] = obj
for to_backpack_obj in to_backpack:
# put stuff in backpack
slots[use_slot].append(to_backpack_obj)
if to_backpack_obj:
slots[WieldLocation.BACKPACK].append(to_backpack_obj)
# store new state
self._save()
```
Here we remember that every `EvAdventureObject` has an `inventory_use_slot` property that tells us where it goes. So we just need to move the object to that slot, replacing whatever is in that place from before. Anything we replace goes back to the backpack.
Here we remember that every `EvAdventureObject` has an `inventory_use_slot` property that tells us where it goes. So we just need to move the object to that slot, replacing whatever is in that place from before. Anything we replace goes back to the backpack, as long as it's actually an item and not `None`, in the case where we are moving an item into an empty slot.
## Get everything
@ -612,4 +613,4 @@ _Handlers_ are useful for grouping functionality together. Now that we spent our
We also learned to use _hooks_ to tie _Knave_'s custom equipment handling into Evennia.
With `Characters`, `Objects` and now `Equipment` in place, we should be able to move on to character generation - where players get to make their own character!
With `Characters`, `Objects` and now `Equipment` in place, we should be able to move on to character generation - where players get to make their own character!

View file

@ -27,7 +27,7 @@ from evennia import DefaultCharacter, AttributeProperty
from .characters import LivingMixin
from .enums import Ability
from .objects import get_bare_hands
class EvAdventureNPC(LivingMixin, DefaultCharacter):
"""Base class for NPCs"""
@ -40,7 +40,7 @@ class EvAdventureNPC(LivingMixin, DefaultCharacter):
morale = AttributeProperty(default=9, autocreate=False)
allegiance = AttributeProperty(default=Ability.ALLEGIANCE_HOSTILE, autocreate=False)
weapon = AttributeProperty(default=BARE_HANDS, autocreate=False) # instead of inventory
weapon = AttributeProperty(default=get_bare_hands, autocreate=False) # instead of inventory
coins = AttributeProperty(default=1, autocreate=False) # coin loot
is_idle = AttributeProperty(default=False, autocreate=False)

View file

@ -248,7 +248,7 @@ class EvAdventureWeapon(EvAdventureObject):
quality = AttributeProperty(3, autocreate=False)
attack_type = AttributeProperty(Ability.STR, autocreate=False)
defend_type = AttributeProperty(Ability.ARMOR, autocreate=False)
defense_type = AttributeProperty(Ability.ARMOR, autocreate=False)
damage_roll = AttributeProperty("1d6", autocreate=False)
@ -387,7 +387,7 @@ class EvAdventureRuneStone(EvAdventureWeapon, EvAdventureConsumable):
quality = AttributeProperty(3, autocreate=False)
attack_type = AttributeProperty(Ability.INT, autocreate=False)
defend_type = AttributeProperty(Ability.DEX, autocreate=False)
defense_type = AttributeProperty(Ability.DEX, autocreate=False)
damage_roll = AttributeProperty("1d8", autocreate=False)
@ -488,4 +488,4 @@ Well, we just figured out all we need! You can go back and update `get_obj_stats
When you change this function you must also update the related unit test - so your existing test becomes a nice way to test your new Objects as well! Add more tests showing the output of feeding different object-types to `get_obj_stats`.
Try it out yourself. If you need help, a finished utility example is found in [evennia/contrib/tutorials/evadventure/utils.py](get_obj_stats).
Try it out yourself. If you need help, a finished utility example is found in [evennia/contrib/tutorials/evadventure/utils.py](get_obj_stats).

View file

@ -1,5 +1,9 @@
# Game Quests
```{warning}
This tutorial lesson is not yet complete, and has some serious bugs in its implementation. So use this as a reference, but the code is not yet ready to use directly.
```
A _quest_ is a common feature of games. From classic fetch-quests like retrieving 10 flowers to complex quest chains involving drama and intrigue, quests need to be properly tracked in our game.
A quest follows a specific development:

View file

@ -135,9 +135,9 @@ ABILITY_REVERSE_MAP = {
Above, the `Ability` class holds some basic properties of a character sheet.
The `ABILITY_REVERSE_MAP` is a convenient map to go the other way &mdash; if in some command we were to enter the string 'cha', we could use this mapping to directly convert your input to the correct `Ability`. For example:
The `ABILITY_REVERSE_MAP` is a convenient map to convert a string to an Enum. The most common use of this would be in a Command; the Player don't know anything about Enums, they can only send strings. So we'd only get the string "cha". Using this `ABILITY_REVERSE_MAP` we can conveniently convert this input to an `Ability.CHA` Enum you can then pass around in code
ability = ABILITY_REVERSE_MAP.get(your_input)
ability = ABILITY_REVERSE_MAP.get(user_input)
## Utility Module
@ -151,22 +151,7 @@ An example of the utility module is found in
The utility module is used to contain general functions we may need to call repeatedly from various other modules. In this tutorial example, we only crate one utility: a function that produces a pretty display of any object we pass to it.
Below is an example of the string we want to see:
```
Chipped Sword
Value: ~10 coins [wielded in Weapon hand]
A simple sword used by mercenaries all over
the world.
Slots: 1, Used from: weapon hand
Quality: 3, Uses: None
Attacks using strength against armor.
Damage roll: 1d6
```
And, here's the start of how the function might look:
Here's how it could look:
```python
# in mygame/evadventure/utils.py
@ -210,11 +195,33 @@ def get_obj_stats(obj, owner=None):
damage_roll="1d6"
)
```
In our new `get_obj_stats` function above, we set up a string template with place holders for where every element of stats information should go. Study this string so that you understand what it does. The `|c`, `|y`, `|w` and `|n` markers are [Evennia color markup](../../../Concepts/Colors.md) for making the text cyan, yellow, white and neutral-color, respectively.
Previously throughout these tutorial lessons, we have seen the `""" ... """` multi-line string used mainly for function help strings, but a triple-quoted string in Python is used for any multi-line string.
Above, we set up a string template (`_OBJ_STATS`) with place holders (`{...}`) for where every element of stats information should go. In the `_OBJ_STATS.format(...)` call, we then dynamically fill those place holders with data from the object we pass into `get_obj_stats`.
Here's what you'd get back if you were to pass a 'chipped sword' to `get_obj_stats` (note that these docs don't show the text colors):
```
Chipped Sword
Value: ~10 coins [wielded in Weapon hand]
A simple sword used by mercenaries all over
the world.
Slots: 1, Used from: weapon hand
Quality: 3, Uses: None
Attacks using strength against armor.
Damage roll: 1d6
```
We will later use this to let the player inspect any object without us having to make a new utility for every object type.
Study the `_OBJ_STATS` template string so that you understand what it does. The `|c`, `|y`, `|w` and `|n` markers are [Evennia color markup](../../../Concepts/Colors.md) for making the text cyan, yellow, white and neutral-color, respectively.
Some stats elements are easy to identify in the above code. For instance, `obj.key` is the name of an object and `obj.db.desc` will hold an object's description &mdash; this is also how default Evennia works.
So far, here in our tutorial, we have not yet established how to get any of the other properties like `size` or `attack_type`. For our current purposes, we will just set them to dummy values and we'll need to revisit them later when we have more code in place!
So far, here in our tutorial, we have not yet established how to get any of the other properties like `size`, `damage_roll` or `attack_type_name`. For our current purposes, we will just set them to fixed dummy values so they work. We'll need to revisit them later when we have more code in place!
## Testing
@ -259,7 +266,7 @@ class TestUtils(EvenniaTest):
result,
"""
|ctestobj|n
Value: ~|y10|n coins[not carried]
Value: ~|y10|n coins[Not carried]
A test object

View file

@ -22,7 +22,7 @@ class NameChanger:
self.obj = obj
def add_to_key(self, suffix):
self.obj.key = f"self.obj.key_{suffix}"
self.obj.key = f"{self.obj.key}_{suffix}"
# make a test object
class MyObject(DefaultObject):

View file

@ -56,7 +56,7 @@ After this, we will get into defining our *models* (the description of the datab
### Installing - Checkpoint:
* you should have a folder named `chargen` or whatever you chose in your mygame/web/ directory
* you should have your application name added to your INSTALLED_APPS in settings.py
* you should have your application name added to your INSTALLED_APPS in `settings.py`
## Create Models
@ -350,7 +350,7 @@ urlpatterns = [
### URLs - Checkpoint:
* Youve created a urls.py file in the `mygame/web/chargen` directory
* Youve created a `urls.py` file in the `mygame/web/chargen` directory
* You have edited the main `mygame/web/urls.py` file to include urls to the `chargen` directory.
## HTML Templates
@ -416,7 +416,7 @@ This page should show a detailed character sheet of their application. This will
### create.html
Our create HTML template will use the Django form we defined back in views.py/forms.py to drive the majority of the application process. There will be a form input for every field we defined in forms.py, which is handy. We have used POST as our method because we are sending information to the server that will update the database. As an alternative, GET would be much less secure. You can read up on documentation elsewhere on the web for GET vs. POST.
Our create HTML template will use the Django form we defined back in views.py/forms.py to drive the majority of the application process. There will be a form input for every field we defined in `forms.py`, which is handy. We have used POST as our method because we are sending information to the server that will update the database. As an alternative, GET would be much less secure. You can read up on documentation elsewhere on the web for GET vs. POST.
```html
<!-- file mygame/web/chargen/templates/chargen/create.html-->
@ -546,4 +546,4 @@ And you should put it at the bottom of the page. Just before the closing body w
{% endblock %}
```
Reload and open [http://localhost:4001/chargen/create](http://localhost:4001/chargen/create/) and you should see your beautiful CAPCHA just before the "submit" button. Try not to check the checkbox to see what happens. And do the same while checking the checkbox!
Reload and open [http://localhost:4001/chargen/create](http://localhost:4001/chargen/create/) and you should see your beautiful CAPCHA just before the "submit" button. Try not to check the checkbox to see what happens. And do the same while checking the checkbox!

View file

@ -62,10 +62,10 @@ At this point, our new *app* contains mostly empty files that you can explore.
### Create a view
A *view* in Django is a simple Python function placed in the "views.py" file in your app. It will
A *view* in Django is a simple Python function placed in the `views.py` file in your app. It will
handle the behavior that is triggered when a user asks for this information by entering a *URL* (the connection between *views* and *URLs* will be discussed later).
So let's create our view. You can open the "web/help_system/views.py" file and paste the following lines:
So let's create our view. You can open the `web/help_system/views.py` file and paste the following lines:
```python
from django.shortcuts import render
@ -108,7 +108,7 @@ Here's a little explanation line by line of what this template does:
### Create a new URL
Last step to add our page: we need to add a *URL* leading to it... otherwise users won't be able to access it. The URLs of our apps are stored in the app's directory "urls.py" file.
Last step to add our page: we need to add a *URL* leading to it... otherwise users won't be able to access it. The URLs of our apps are stored in the app's directory `urls.py` file.
Open the `web/help_system/urls.py` file (you might have to create it) and make it look like this:

View file

@ -94,7 +94,7 @@ We create a database user 'evennia' and a new database named `evennia` (you can
### Evennia PostgreSQL configuration
Edit `mygame/server/conf/secret_settings.py and add the following section:
Edit `mygame/server/conf/secret_settings.py` and add the following section:
```python
#

View file

@ -12,14 +12,13 @@ Any system that supports Python3.10+ should work.
- Windows (Win7, Win8, Win10, Win11)
- Mac OSX (>10.5 recommended)
- [Python](https://www.python.org) (3.10 and 3.11 are tested. 3.11 is recommended)
- [Twisted](https://twistedmatrix.com) (v22.3+)
- [Python](https://www.python.org) (3.10, 3.11 and 3.12 are tested. 3.12 is recommended)
- [Twisted](https://twistedmatrix.com) (v23.10+)
- [ZopeInterface](https://www.zope.org/Products/ZopeInterface) (v3.0+) - usually included in Twisted packages
- Linux/Mac users may need the `gcc` and `python-dev` packages or equivalent.
- Windows users need [MS Visual C++](https://aka.ms/vs/16/release/vs_buildtools.exe) and *maybe* [pypiwin32](https://pypi.python.org/pypi/pypiwin32).
- [Django](https://www.djangoproject.com) (v4.2+), be warned that latest dev version is usually untested with Evennia.
- [GIT](https://git-scm.com/) - version control software used if you want to install the sources
(but also useful to track your own code)
- [GIT](https://git-scm.com/) - version control software used if you want to install the sources (but also useful to track your own code)
- Mac users can use the [git-osx-installer](https://code.google.com/p/git-osx-installer/) or the [MacPorts version](https://git-scm.com/book/en/Getting-Started-Installing-Git#Installing-on-Mac).
## Confusion of location (GIT installation)
@ -33,27 +32,26 @@ muddev/
mygame/
```
The evennia code itself is found inside `evennia/evennia/` (so two levels down). Your settings file
is `mygame/server/conf/settings.py` and the _parent_ setting file is `evennia/evennia/settings_default.py`.
The evennia library code itself is found inside `evennia/evennia/` (so two levels down). You shouldn't change this; do all work in `mygame/`. Your settings file is `mygame/server/conf/settings.py` and the _parent_ setting file is `evennia/evennia/settings_default.py`.
## Virtualenv setup fails
When doing the `python3.11 -m venv evenv` step, some users report getting an error; something like:
When doing the `python3.x -m venv evenv` (where x is the python3 version) step, some users report getting an error; something like:
Error: Command '['evenv', '-Im', 'ensurepip', '--upgrade', '--default-pip']'
returned non-zero exit status 1
You can solve this by installing the `python3.11-venv` package or equivalent for your OS. Alternatively you can bootstrap it in this way:
You can solve this by installing the `python3.11-venv` (or later) package (or equivalent for your OS). Alternatively you can bootstrap it in this way:
python3.11 -m --without-pip evenv
python3.x -m --without-pip evenv
This should set up the virtualenv without `pip`. Activate the new virtualenv and then install pip from within it:
This should set up the virtualenv without `pip`. Activate the new virtualenv and then install pip from within it (you don't need to specify the python version once virtualenv is active):
python -m ensurepip --upgrade
If that fails, a worse alternative to try is
curl https://bootstrap.pypa.io/get-pip.py | python3.10 (linux/unix/WSL only)
curl https://bootstrap.pypa.io/get-pip.py | python3.x (linux/unix/WSL only)
Either way, you should now be able to continue with the installation.
@ -72,29 +70,19 @@ If `localhost` doesn't work when trying to connect to your local game, try `127.
- Users of Fedora (notably Fedora 24) has reported a `gcc` error saying the directory
`/usr/lib/rpm/redhat/redhat-hardened-cc1` is missing, despite `gcc` itself being installed. [The
confirmed work-around](https://gist.github.com/yograterol/99c8e123afecc828cb8c) seems to be to install the `redhat-rpm-config` package with e.g. `sudo dnf install redhat-rpm-config`.
- Some users trying to set up a virtualenv on an NTFS filesystem find that it fails due to issues
with symlinks not being supported. Answer is to not use NTFS (seriously, why would you do that to yourself?)
- Some users trying to set up a virtualenv on an NTFS filesystem find that it fails due to issues with symlinks not being supported. Answer is to not use NTFS (seriously, why would you do that to yourself?)
## Mac Troubleshooting
- Some Mac users have reported not being able to connect to `localhost` (i.e. your own computer). If so, try to connect to `127.0.0.1` instead, which is the same thing. Use port 4000 from mud clients and port 4001 from the web browser as usual.
- If you get a `MemoryError` when starting Evennia, or when looking at the log, this may be due to an sqlite versioning issue. [A user in our forums](https://github.com/evennia/evennia/discussions/2637) found a working solution for this. [Here](https://github.com/evennia/evennia/issues/2854) is another variation to solve it.
- If you get a `MemoryError` when starting Evennia, or when looking at the log, this may be due to an sqlite versioning issue. [A user in our forums](https://github.com/evennia/evennia/discussions/2637) found a working solution for this. [Here](https://github.com/evennia/evennia/issues/2854) is another variation to solve it. [Another user](https://github.com/evennia/evennia/issues/3704) also wrote an extensive summary of the issue, along with troubleshooting instructions.
## Windows Troubleshooting
- If you install with `pip install evennia` and find that the `evennia` command is not available, run `py -m evennia` once. This should add the evennia binary to your environment. If this fails, make sure you are using a [virtualenv](./Installation-Git.md#virtualenv). Worst case, you can keep using `py -m evennia` in the places where the `evennia` command is used.
- Install Python [from the Python homepage](https://www.python.org/downloads/windows/). You will need to be a Windows Administrator to install packages.
- When installing Python, make sure to check-mark *all* install options, especially the one about making Python available on the path (you may have to scroll to see it). This allows you to
just write `python` in any console without first finding where the `python` program actually sits on your hard drive.
- If you get a `command not found` when trying to run the `evennia` program after installation, try closing the Console and starting it again (remember to re-activate the virtualenv if you use one!). Sometimes Windows is not updating its environment properly and `evennia` will be available only in the new console.
- If you installed Python but the `python` command is not available (even in a new console), then
you might have missed installing Python on the path. In the Windows Python installer you get a list of options for what to install. Most or all options are pre-checked except this one, and you may even have to scroll down to see it. Reinstall Python and make sure it's checked.
- If your MUD client cannot connect to `localhost:4000`, try the equivalent `127.0.0.1:4000`
instead. Some MUD clients on Windows does not appear to understand the alias `localhost`.
- Some Windows users get an error installing the Twisted 'wheel'. A wheel is a pre-compiled binary
package for Python. A common reason for this error is that you are using a 32-bit version of Python, but Twisted has not yet uploaded the latest 32-bit wheel. Easiest way to fix this is to install a slightly older Twisted version. So if, say, version `22.1` failed, install `22.0` manually with `pip install twisted==22.0`. Alternatively you could check that you are using the 64-bit version of Python and uninstall any 32bit one. If so, you must then `deactivate` the virtualenv, delete the `evenv` folder and recreate it anew with your new Python.
- - If you get a `command not found` when trying to run the `evennia` program directly after installation, try closing the Windows Console and starting it again (remember to re-activate the virtualenv if you use one!). Sometimes Windows is not updating its environment properly and `evennia` will be available only in the new console.
- If you installed Python but the `python` command is not available (even in a new console), then you might have missed installing Python on the path. In the Windows Python installer you get a list of options for what to install. Most or all options are pre-checked except this one, and you may even have to scroll down to see it. Reinstall Python and make sure it's checked. Install Python [from the Python homepage](https://www.python.org/downloads/windows/). You will need to be a Windows Administrator to install packages.
- If your MUD client cannot connect to `localhost:4000`, try the equivalent `127.0.0.1:4000` instead. Some MUD clients on Windows does not appear to understand the alias `localhost`.
- Some Windows users get an error installing the Twisted 'wheel'. A wheel is a pre-compiled binary package for Python. A common reason for this error is that you are using a 32-bit version of Python, but Twisted has not yet uploaded the latest 32-bit wheel. Easiest way to fix this is to install a slightly older Twisted version. So if, say, version `22.1` failed, install `22.0` manually with `pip install twisted==22.0`. Alternatively you could check that you are using the 64-bit version of Python and uninstall any 32bit one. If so, you must then `deactivate` the virtualenv, delete the `evenv` folder and recreate it anew with your new Python.
- If you've done a git installation, and your server won't start with an error message like `AttributeError: module 'evennia' has no attribute '_init'`, it may be a python path issue. In a terminal, cd to `(your python directory)\site-packages` and run the command `echo "C:\absolute\path\to\evennia" > local-vendors.pth`. Open the created file in your favorite IDE and make sure it is saved with *UTF-8* encoding and not *UTF-8 with BOM*.
- If your server won't start, with no error messages (and no log files at all when starting from
scratch), try to start with `evennia ipstart` instead. If you then see an error about `system cannot find the path specified`, it may be that the file `evennia\evennia\server\twistd.bat` has the wrong path to the `twistd` executable. This file is auto-generated, so try to delete it and then run `evennia start` to rebuild it and see if it works. If it still doesn't work you need to open it in a text editor like Notepad. It's just one line containing the path to the `twistd.exe` executable as determined by Evennia. If you installed Twisted in a non-standard location this might be wrong and you should update the line to the real location.
- Some users have reported issues with Windows WSL and anti-virus software during Evennia
development. Timeout errors and the inability to run `evennia connections` may be due to your anti-virus software interfering. Try disabling or changing your anti-virus software settings.
- Some users have reported issues with Windows WSL and anti-virus software during Evennia development. Timeout errors and the inability to run `evennia connections` may be due to your anti-virus software interfering. Try disabling or changing your anti-virus software settings.

View file

@ -320,13 +320,13 @@ DATABASES = {
}
}
# 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.
# performance for your setup. Don't change this unless you know what you are doing. Also
# be careful to change for an already populated database.
SQLITE3_PRAGMAS = (
"PRAGMA cache_size=10000",
"PRAGMA synchronous=1",
"PRAGMA synchronous=OFF",
"PRAGMA count_changes=OFF",
"PRAGMA temp_store=2",
"PRAGMA journal_mode=WAL",
)
# How long the django-database connection should be kept open, in seconds.

View file

@ -55,5 +55,5 @@ Apart from the main `settings.py` file,
Some other Evennia systems can be customized by plugin modules but has no explicit template in `conf/`:
- *cmdparser.py* - a custom module can be used to totally replace Evennia's default command parser. All this does is to split the incoming string into "command name" and "the rest". It also handles things like error messages for no-matches and multiple-matches among other things that makes this more complex than it sounds. The default parser is *very* generic, so you are most often best served by modifying things further down the line (on the command parse level) than here.
- *at_search.py* - this allows for replacing the way Evennia handles search results. It allows to change how errors are echoed and how multi-matches are resolved and reported (like how the default understands that "2-ball" should match the second "ball" object if there are two of them in the room).
- `cmdparser.py` - a custom module can be used to totally replace Evennia's default command parser. All this does is to split the incoming string into "command name" and "the rest". It also handles things like error messages for no-matches and multiple-matches among other things that makes this more complex than it sounds. The default parser is *very* generic, so you are most often best served by modifying things further down the line (on the command parse level) than here.
- `at_search.py` - this allows for replacing the way Evennia handles search results. It allows to change how errors are echoed and how multi-matches are resolved and reported (like how the default understands that "2-ball" should match the second "ball" object if there are two of them in the room).

View file

@ -21,6 +21,7 @@ evennia.contrib.game\_systems
evennia.contrib.game_systems.mail
evennia.contrib.game_systems.multidescer
evennia.contrib.game_systems.puzzles
evennia.contrib.game_systems.storage
evennia.contrib.game_systems.turnbattle
```

View file

@ -0,0 +1,18 @@
```{eval-rst}
evennia.contrib.game\_systems.storage
=============================================
.. automodule:: evennia.contrib.game_systems.storage
:members:
:undoc-members:
:show-inheritance:
.. toctree::
:maxdepth: 6
evennia.contrib.game_systems.storage.storage
evennia.contrib.game_systems.storage.tests
```

View file

@ -0,0 +1,10 @@
```{eval-rst}
evennia.contrib.game\_systems.storage.storage
====================================================
.. automodule:: evennia.contrib.game_systems.storage.storage
:members:
:undoc-members:
:show-inheritance:
```

View file

@ -0,0 +1,10 @@
```{eval-rst}
evennia.contrib.game\_systems.storage.tests
==================================================
.. automodule:: evennia.contrib.game_systems.storage.tests
:members:
:undoc-members:
:show-inheritance:
```

View file

@ -1,6 +1,6 @@
# 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 manual was last updated August 25, 2024, see the [Evennia Changelog](Coding/Changelog.md). Latest released Evennia version is 4.3.0.
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 outubro 26, 2024, see the [Evennia Changelog](Coding/Changelog.md). Latest released Evennia version is 4.5.0.
- [Introduction](./Evennia-Introduction.md) - what is this Evennia thing?
- [Evennia in Pictures](./Evennia-In-Pictures.md) - a visual overview of Evennia

View file

@ -1 +1 @@
4.3.0
4.5.0

View file

@ -3,8 +3,8 @@
# when people upgrade outside regular channels). This file only supports lines of
# `value = number` and only specific names supported by the handler.
PYTHON_MIN = 3.10
PYTHON_MAX_TESTED = 3.12.100
TWISTED_MIN = 23.10
PYTHON_MIN = 3.11
PYTHON_MAX_TESTED = 3.13.100
TWISTED_MIN = 24.11
DJANGO_MIN = 4.0.2
DJANGO_MAX_TESTED = 4.2.100
DJANGO_MAX_TESTED = 5.1.100

View file

@ -478,11 +478,11 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
raise RuntimeError("Session not found")
if self.get_puppet(session) == obj:
# already puppeting this object
self.msg("You are already puppeting this object.")
self.msg(_("You are already puppeting this object."))
return
if not obj.access(self, "puppet"):
# no access
self.msg(f"You don't have permission to puppet '{obj.key}'.")
self.msg(_("You don't have permission to puppet '{key}'.").format(key=obj.key))
return
if obj.account:
# object already puppeted
@ -491,13 +491,21 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
# we may take over another of our sessions
# output messages to the affected sessions
if _MULTISESSION_MODE in (1, 3):
txt1 = f"Sharing |c{obj.name}|n with another of your sessions."
txt2 = f"|c{obj.name}|n|G is now shared from another of your sessions.|n"
txt1 = _("Sharing |c{name}|n with another of your sessions.").format(
name=obj.name
)
txt2 = _(
"|c{name}|n|G is now shared from another of your sessions.|n"
).format(name=obj.name)
self.msg(txt1, session=session)
self.msg(txt2, session=obj.sessions.all())
else:
txt1 = f"Taking over |c{obj.name}|n from another of your sessions."
txt2 = f"|c{obj.name}|n|R is now acted from another of your sessions.|n"
txt1 = _("Taking over |c{name}|n from another of your sessions.").format(
name=obj.name
)
txt2 = _(
"|c{name}|n|R is now acted from another of your sessions.|n"
).format(name=obj.name)
self.msg(txt1, session=session)
self.msg(txt2, session=obj.sessions.all())
self.unpuppet_object(obj.sessions.get())
@ -523,7 +531,9 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
and len(self.get_all_puppets()) >= _MAX_NR_SIMULTANEOUS_PUPPETS
):
self.msg(
_(f"You cannot control any more puppets (max {_MAX_NR_SIMULTANEOUS_PUPPETS})")
_("You cannot control any more puppets (max {max_puppets})").format(
max_puppets=_MAX_NR_SIMULTANEOUS_PUPPETS
)
)
return
@ -778,6 +788,9 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
In this case we're simply piggybacking on this feature to apply
additional normalization per Evennia's standards.
"""
if not isinstance(username, str):
username = str(username)
username = super(DefaultAccount, cls).normalize_username(username)
# strip excessive spaces in accountname
@ -939,7 +952,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
# parse inputs
character_key = kwargs.pop("key", self.key)
character_ip = kwargs.pop("ip", self.db.creator_ip)
character_permissions = kwargs.pop("permissions", self.permissions)
character_permissions = kwargs.pop("permissions", self.permissions.all())
# Load the appropriate Character class
character_typeclass = kwargs.pop("typeclass", self.default_character_typeclass)
@ -1010,8 +1023,8 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
account = None
errors = []
username = kwargs.get("username")
password = kwargs.get("password")
username = kwargs.get("username", "")
password = kwargs.get("password", "")
email = kwargs.get("email", "").strip()
guest = kwargs.get("guest", False)

View file

@ -556,24 +556,28 @@ class DiscordBot(Bot):
factory_path = "evennia.server.portal.discord.DiscordWebsocketServerFactory"
def at_init(self):
"""
Load required channels back into memory
"""
def _load_channels(self):
self.ndb.ev_channels = {}
if channel_links := self.db.channels:
# this attribute contains a list of evennia<->discord links in the form
# of ("evennia_channel", "discord_chan_id")
# grab Evennia channels, cache and connect
channel_set = {evchan for evchan, dcid in channel_links}
self.ndb.ev_channels = {}
for channel_name in list(channel_set):
channel = search.search_channel(channel_name)
if not channel:
raise RuntimeError(f"Evennia Channel {channel_name} not found.")
logger.log_err(f"Evennia Channel {channel_name} not found; skipping.")
continue
channel = channel[0]
self.ndb.ev_channels[channel_name] = channel
def at_init(self):
"""
Load required channels back into memory
"""
self._load_channels()
def start(self):
"""
Tell the Discord protocol to connect.
@ -583,27 +587,20 @@ class DiscordBot(Bot):
self.delete()
return
if self.ndb.ev_channels:
for channel in self.ndb.ev_channels.values():
channel.connect(self)
if not self.ndb.ev_channels:
self._load_channels()
elif channel_links := self.db.channels:
# this attribute contains a list of evennia<->discord links in the form
# of ("evennia_channel", "discord_chan_id")
# grab Evennia channels, cache and connect
channel_set = {evchan for evchan, dcid in channel_links}
self.ndb.ev_channels = {}
for channel_name in list(channel_set):
channel = search.search_channel(channel_name)
if not channel:
raise RuntimeError(f"Evennia Channel {channel_name} not found.")
channel = channel[0]
self.ndb.ev_channels[channel_name] = channel
channel.connect(self)
for channel in self.ndb.ev_channels.values():
if not channel.connect(self):
logger.log_warn(f"{self} could not connect to Evennia channel {channel}.")
if not channel.access(self, "send"):
logger.log_warn(
f"{self} doesn't have permission to send messages to Evennia channel {channel}."
)
# connect
# these will be made available as properties on the protocol factory
configdict = {"uid": self.dbid}
# finally, connect
evennia.SESSION_HANDLER.start_bot_session(self.factory_path, configdict)
def at_pre_channel_msg(self, message, channel, senders=None, **kwargs):

View file

@ -37,7 +37,7 @@ from weakref import WeakValueDictionary
from django.conf import settings
from django.utils.translation import gettext as _
from twisted.internet import reactor
from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.internet.defer import inlineCallbacks
from twisted.internet.task import deferLater
from evennia.commands.cmdset import CmdSet
@ -261,6 +261,7 @@ def _progressive_cmd_run(cmd, generator, response=None):
class NoCmdSets(Exception):
"No cmdsets found. Critical error."
pass
@ -361,7 +362,12 @@ def get_and_merge_cmdsets(
local_objlist = yield (
location.contents_get(exclude=obj) + obj.contents_get() + [location]
)
local_objlist = [o for o in local_objlist if not o._is_deleted]
local_objlist = [
o
for o in local_objlist
if not o._is_deleted
and o.access(caller, access_type="call", no_superuser_bypass=True)
]
for lobj in local_objlist:
try:
# call hook in case we need to do dynamic changing to cmdset
@ -375,12 +381,7 @@ def get_and_merge_cmdsets(
chain.from_iterable(
lobj.cmdset.cmdset_stack
for lobj in local_objlist
if (
lobj.cmdset.current
and lobj.access(
caller, access_type="call", no_superuser_bypass=True
)
)
if lobj.cmdset.current
)
)
for cset in local_obj_cmdsets:
@ -390,7 +391,7 @@ def get_and_merge_cmdsets(
# explicitly.
cset.old_duplicates = cset.duplicates
cset.duplicates = True if cset.duplicates is None else cset.duplicates
returnValue(local_obj_cmdsets)
return local_obj_cmdsets
except Exception:
_msg_err(caller, _ERROR_CMDSETS)
raise ErrorReported(raw_string)
@ -408,9 +409,9 @@ def get_and_merge_cmdsets(
_msg_err(caller, _ERROR_CMDSETS)
raise ErrorReported(raw_string)
try:
returnValue(obj.get_cmdsets(caller=caller, current=current))
return obj.get_cmdsets(caller=caller, current=current)
except AttributeError:
returnValue((CmdSet(), []))
return (CmdSet(), [])
local_obj_cmdsets = []
@ -486,7 +487,7 @@ def get_and_merge_cmdsets(
# if cmdset:
# caller.cmdset.current = cmdset
returnValue(cmdset)
return cmdset
except ErrorReported:
raise
except Exception:
@ -600,7 +601,7 @@ def cmdhandler(
if _testing:
# only return the command instance
returnValue(cmd)
return cmd
# assign custom kwargs to found cmd object
for key, val in kwargs.items():
@ -619,7 +620,7 @@ def cmdhandler(
abort = yield cmd.at_pre_cmd()
if abort:
# abort sequence
returnValue(abort)
return abort
# Parse and execute
yield cmd.parse()
@ -630,14 +631,12 @@ def cmdhandler(
if isinstance(ret, types.GeneratorType):
# cmd.func() is a generator, execute progressively
_progressive_cmd_run(cmd, ret)
ret = yield ret
# note that the _progressive_cmd_run will itself run
# the at_post_cmd etc as it finishes; this is a bit of
# code duplication but there seems to be no way to
# catch the StopIteration here (it's not in the same
# frame since this is in a deferred chain)
else:
ret = yield ret
# post-command hook
yield cmd.at_post_cmd()
@ -648,9 +647,6 @@ def cmdhandler(
else:
caller.ndb.last_cmd = None
# return result to the deferred
returnValue(ret)
except InterruptCommand:
# Do nothing, clean exit
pass
@ -759,10 +755,9 @@ def cmdhandler(
cmd = copy(cmd)
# A normal command.
ret = yield _run_command(
yield _run_command(
cmd, cmdname, args, raw_cmdname, cmdset, session, account, cmdset_providers
)
returnValue(ret)
except ErrorReported as exc:
# this error was already reported, so we
@ -776,7 +771,7 @@ def cmdhandler(
sysarg = exc.sysarg
if syscmd:
ret = yield _run_command(
yield _run_command(
syscmd,
syscmd.key,
sysarg,
@ -786,7 +781,7 @@ def cmdhandler(
account,
cmdset_providers,
)
returnValue(ret)
return
elif sysarg:
# return system arg
error_to.msg(err_helper(exc.sysarg, cmdid=cmdid))

View file

@ -19,6 +19,7 @@ from evennia.utils.evtable import EvTable
from evennia.utils.utils import fill, is_iter, lazy_property, make_iter
CMD_IGNORE_PREFIXES = settings.CMD_IGNORE_PREFIXES
_RE_CMD_LOCKFUNC_IN_LOCKSTRING = re.compile(r"(^|;|\s)cmd\:\w+", re.DOTALL)
class InterruptCommand(Exception):
@ -74,7 +75,7 @@ def _init_command(cls, **kwargs):
if not hasattr(cls, "locks"):
# default if one forgets to define completely
cls.locks = "cmd:all()"
if "cmd:" not in cls.locks:
if not _RE_CMD_LOCKFUNC_IN_LOCKSTRING.search(cls.locks):
cls.locks = "cmd:all();" + cls.locks
for lockstring in cls.locks.split(";"):
if lockstring and ":" not in lockstring:
@ -575,7 +576,7 @@ Command \"{cmdname}\" has no defined `func()` method. Available properties on th
ex.
::
url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/$',
url(r'characters/(?P<slug>[\\w\\d\\-]+)/(?P<pk>[0-9]+)/$',
CharDetailView.as_view(), name='character-detail')
If no View has been created and defined in urls.py, returns an
@ -623,6 +624,22 @@ Command \"{cmdname}\" has no defined `func()` method. Available properties on th
)[0]
return settings.CLIENT_DEFAULT_WIDTH
def _get_account_option(self, option):
"""
Retrieve the value of a specified account option.
Args:
option (str): The name of the option to retrieve.
Returns:
The value of the specified account option if the account exists,
otherwise the default value from settings.OPTIONS_ACCOUNT_DEFAULT.
"""
if self.account:
return self.account.options.get(option)
return settings.OPTIONS_ACCOUNT_DEFAULT.get(option)
def styled_table(self, *args, **kwargs):
"""
Create an EvTable styled by on user preferences.
@ -638,8 +655,8 @@ Command \"{cmdname}\" has no defined `func()` method. Available properties on th
or incomplete and ready for use with `.add_row` or `.add_collumn`.
"""
border_color = self.account.options.get("border_color")
column_color = self.account.options.get("column_names_color")
border_color = self._get_account_option("border_color")
column_color = self._get_account_option("column_names_color")
colornames = ["|%s%s|n" % (column_color, col) for col in args]
@ -699,9 +716,9 @@ Command \"{cmdname}\" has no defined `func()` method. Available properties on th
"""
colors = dict()
colors["border"] = self.account.options.get("border_color")
colors["headertext"] = self.account.options.get("%s_text_color" % mode)
colors["headerstar"] = self.account.options.get("%s_star_color" % mode)
colors["border"] = self._get_account_option("border_color")
colors["headertext"] = self._get_account_option("%s_text_color" % mode)
colors["headerstar"] = self._get_account_option("%s_star_color" % mode)
width = width or self.client_width()
if edge_character:
@ -722,7 +739,7 @@ Command \"{cmdname}\" has no defined `func()` method. Available properties on th
else:
center_string = ""
fill_character = self.account.options.get("%s_fill" % mode)
fill_character = self._get_account_option("%s_fill" % mode)
remain_fill = width - len(center_string)
if remain_fill % 2 == 0:

View file

@ -212,7 +212,7 @@ class CmdBan(COMMAND_DEFAULT_CLASS):
typ = "IP"
ban = ipban[0]
# replace * with regex form and compile it
ipregex = ban.replace(".", "\.")
ipregex = ban.replace(".", r"\.")
ipregex = ipregex.replace("*", "[0-9]{1,3}")
ipregex = re.compile(r"%s" % ipregex)
bantup = ("", ban, ipregex, now, reason)

View file

@ -21,7 +21,6 @@ therefore always be limited to superusers only.
import re
from django.conf import settings
from evennia.commands.cmdset import CmdSet
from evennia.utils import logger, utils
from evennia.utils.batchprocessors import BATCHCMD, BATCHCODE
@ -394,7 +393,7 @@ class CmdStateAbort(_COMMAND_DEFAULT_CLASS):
key = "abort"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
locks = "cmd:perm(batchcommands) or perm(Developer)"
def func(self):
"""Exit back to default."""
@ -412,7 +411,7 @@ class CmdStateLL(_COMMAND_DEFAULT_CLASS):
key = "ll"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
locks = "cmd:perm(batchcommands) or perm(Developer)"
def func(self):
show_curr(self.caller, showall=True)
@ -427,7 +426,7 @@ class CmdStatePP(_COMMAND_DEFAULT_CLASS):
key = "pp"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
locks = "cmd:perm(batchcommands) or perm(Developer)"
def func(self):
"""
@ -450,7 +449,7 @@ class CmdStateRR(_COMMAND_DEFAULT_CLASS):
key = "rr"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
locks = "cmd:perm(batchcommands) or perm(Developer)"
def func(self):
caller = self.caller
@ -473,7 +472,7 @@ class CmdStateRRR(_COMMAND_DEFAULT_CLASS):
key = "rrr"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
locks = "cmd:perm(batchcommands) or perm(Developer)"
def func(self):
caller = self.caller
@ -495,7 +494,7 @@ class CmdStateNN(_COMMAND_DEFAULT_CLASS):
key = "nn"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
locks = "cmd:perm(batchcommands) or perm(Developer)"
def func(self):
caller = self.caller
@ -518,7 +517,7 @@ class CmdStateNL(_COMMAND_DEFAULT_CLASS):
key = "nl"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
locks = "cmd:perm(batchcommands) or perm(Developer)"
def func(self):
caller = self.caller
@ -541,7 +540,7 @@ class CmdStateBB(_COMMAND_DEFAULT_CLASS):
key = "bb"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
locks = "cmd:perm(batchcommands) or perm(Developer)"
def func(self):
caller = self.caller
@ -564,7 +563,7 @@ class CmdStateBL(_COMMAND_DEFAULT_CLASS):
key = "bl"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
locks = "cmd:perm(batchcommands) or perm(Developer)"
def func(self):
caller = self.caller
@ -588,7 +587,7 @@ class CmdStateSS(_COMMAND_DEFAULT_CLASS):
key = "ss"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
locks = "cmd:perm(batchcommands) or perm(Developer)"
def func(self):
caller = self.caller
@ -618,7 +617,7 @@ class CmdStateSL(_COMMAND_DEFAULT_CLASS):
key = "sl"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
locks = "cmd:perm(batchcommands) or perm(Developer)"
def func(self):
caller = self.caller
@ -647,7 +646,7 @@ class CmdStateCC(_COMMAND_DEFAULT_CLASS):
key = "cc"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
locks = "cmd:perm(batchcommands) or perm(Developer)"
def func(self):
caller = self.caller
@ -676,7 +675,7 @@ class CmdStateJJ(_COMMAND_DEFAULT_CLASS):
key = "jj"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
locks = "cmd:perm(batchcommands) or perm(Developer)"
def func(self):
caller = self.caller
@ -701,7 +700,7 @@ class CmdStateJL(_COMMAND_DEFAULT_CLASS):
key = "jl"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
locks = "cmd:perm(batchcommands) or perm(Developer)"
def func(self):
caller = self.caller
@ -726,7 +725,7 @@ class CmdStateQQ(_COMMAND_DEFAULT_CLASS):
key = "qq"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
locks = "cmd:perm(batchcommands) or perm(Developer)"
def func(self):
purge_processor(self.caller)
@ -738,7 +737,7 @@ class CmdStateHH(_COMMAND_DEFAULT_CLASS):
key = "hh"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
locks = "cmd:perm(batchcommands) or perm(Developer)"
def func(self):
string = """

View file

@ -5,10 +5,11 @@ 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
@ -167,7 +168,7 @@ class ObjManipCommand(COMMAND_DEFAULT_CLASS):
attrs = _attrs
# store data
obj_defs[iside].append({"name": objdef, "option": option, "aliases": aliases})
obj_attrs[iside].append({"name": objdef, "attrs": attrs})
obj_attrs[iside].append({"name": objdef, "attrs": attrs, "category": option})
# store for future access
self.lhs_objs = obj_defs[0]
@ -397,7 +398,10 @@ class CmdCopy(ObjManipCommand):
if not from_obj:
return
to_obj_name = "%s_copy" % from_obj_name
to_obj_aliases = ["%s_copy" % alias for alias in from_obj.aliases.all()]
to_obj_aliases = [
(f"{alias}_copy", category)
for alias, category in from_obj.aliases.all(return_key_and_category=True)
]
copiedobj = ObjectDB.objects.copy_object(
from_obj, new_key=to_obj_name, new_aliases=to_obj_aliases
)
@ -447,7 +451,7 @@ class CmdCpAttr(ObjManipCommand):
Usage:
cpattr[/switch] <obj>/<attr> = <obj1>/<attr1> [,<obj2>/<attr2>,<obj3>/<attr3>,...]
cpattr[/switch] <obj>/<attr> = <obj1> [,<obj2>,<obj3>,...]
cpattr[/switch] <attr> = <obj1>/<attr1> [,<obj2>/<attr2>,<obj3>/<attr3>,...]
cpattr[/switch] <attr>[:category] = <obj1>/<attr1>[:category] [,<obj2>/<attr2>,<obj3>/<attr3>,...]
cpattr[/switch] <attr> = <obj1>[,<obj2>,<obj3>,...]
Switches:
@ -459,6 +463,11 @@ class CmdCpAttr(ObjManipCommand):
copies the coolness attribute (defined on yourself), to attributes
on Anna and Tom.
cpattr box/width:dimension = tube/width:dimension
->
copies the box's width attribute in the dimension category, to be the
tube's width attribute in the dimension category
Copy the attribute one object to one or more attributes on another object.
If you don't supply a source object, yourself is used.
"""
@ -468,7 +477,7 @@ class CmdCpAttr(ObjManipCommand):
locks = "cmd:perm(cpattr) or perm(Builder)"
help_category = "Building"
def check_from_attr(self, obj, attr, clear=False):
def check_from_attr(self, obj, attr, category=None, clear=False):
"""
Hook for overriding on subclassed commands. Checks to make sure a
caller can copy the attr from the object in question. If not, return a
@ -479,7 +488,7 @@ class CmdCpAttr(ObjManipCommand):
"""
return True
def check_to_attr(self, obj, attr):
def check_to_attr(self, obj, attr, category=None):
"""
Hook for overriding on subclassed commands. Checks to make sure a
caller can write to the specified attribute on the specified object.
@ -488,22 +497,24 @@ class CmdCpAttr(ObjManipCommand):
"""
return True
def check_has_attr(self, obj, attr):
def check_has_attr(self, obj, attr, category=None):
"""
Hook for overriding on subclassed commands. Do any preprocessing
required and verify an object has an attribute.
"""
if not obj.attributes.has(attr):
self.msg(f"{obj.name} doesn't have an attribute {attr}.")
if not obj.attributes.has(attr, category=category):
self.msg(
f"{obj.name} doesn't have an attribute {attr}{f'[{category}]' if category else ''}."
)
return False
return True
def get_attr(self, obj, attr):
def get_attr(self, obj, attr, category=None):
"""
Hook for overriding on subclassed commands. Do any preprocessing
required and get the attribute from the object.
"""
return obj.attributes.get(attr)
return obj.attributes.get(attr, category=category)
def func(self):
"""
@ -513,7 +524,7 @@ class CmdCpAttr(ObjManipCommand):
if not self.rhs:
string = """Usage:
cpattr[/switch] <obj>/<attr> = <obj1>/<attr1> [,<obj2>/<attr2>,<obj3>/<attr3>,...]
cpattr[/switch] <obj>/<attr>[:category] = <obj1>/<attr1> [,<obj2>/<attr2>,<obj3>/<attr3>,...]
cpattr[/switch] <obj>/<attr> = <obj1> [,<obj2>,<obj3>,...]
cpattr[/switch] <attr> = <obj1>/<attr1> [,<obj2>/<attr2>,<obj3>/<attr3>,...]
cpattr[/switch] <attr> = <obj1>[,<obj2>,<obj3>,...]"""
@ -524,6 +535,8 @@ class CmdCpAttr(ObjManipCommand):
to_objs = self.rhs_objattr
from_obj_name = lhs_objattr[0]["name"]
from_obj_attrs = lhs_objattr[0]["attrs"]
from_obj_category = lhs_objattr[0]["category"] # None if unset
from_obj_category_str = f"[{from_obj_category}]" if from_obj_category else ""
if not from_obj_attrs:
# this means the from_obj_name is actually an attribute
@ -540,11 +553,13 @@ class CmdCpAttr(ObjManipCommand):
clear = True
else:
clear = False
if not self.check_from_attr(from_obj, from_obj_attrs[0], clear=clear):
if not self.check_from_attr(
from_obj, from_obj_attrs[0], clear=clear, category=from_obj_category
):
return
for attr in from_obj_attrs:
if not self.check_has_attr(from_obj, attr):
if not self.check_has_attr(from_obj, attr, category=from_obj_category):
return
if (len(from_obj_attrs) != len(set(from_obj_attrs))) and clear:
@ -552,10 +567,11 @@ class CmdCpAttr(ObjManipCommand):
return
result = []
for to_obj in to_objs:
to_obj_name = to_obj["name"]
to_obj_attrs = to_obj["attrs"]
to_obj_category = to_obj.get("category")
to_obj_category_str = f"[{to_obj_category}]" if to_obj_category else ""
to_obj = caller.search(to_obj_name)
if not to_obj:
result.append(f"\nCould not find object '{to_obj_name}'")
@ -567,19 +583,19 @@ class CmdCpAttr(ObjManipCommand):
# if there are too few attributes given
# on the to_obj, we copy the original name instead.
to_attr = from_attr
if not self.check_to_attr(to_obj, to_attr):
if not self.check_to_attr(to_obj, to_attr, to_obj_category):
continue
value = self.get_attr(from_obj, from_attr)
to_obj.attributes.add(to_attr, value)
value = self.get_attr(from_obj, from_attr, from_obj_category)
to_obj.attributes.add(to_attr, value, category=to_obj_category)
if clear and not (from_obj == to_obj and from_attr == to_attr):
from_obj.attributes.remove(from_attr)
from_obj.attributes.remove(from_attr, category=from_obj_category)
result.append(
f"\nMoved {from_obj.name}.{from_attr} -> {to_obj_name}.{to_attr}. (value:"
f"\nMoved {from_obj.name}.{from_attr}{from_obj_category_str} -> {to_obj_name}.{to_attr}{to_obj_category_str}. (value:"
f" {repr(value)})"
)
else:
result.append(
f"\nCopied {from_obj.name}.{from_attr} -> {to_obj.name}.{to_attr}. (value:"
f"\nCopied {from_obj.name}.{from_attr}{from_obj_category_str} -> {to_obj.name}.{to_attr}{to_obj_category_str}. (value:"
f" {repr(value)})"
)
caller.msg("".join(result))
@ -693,6 +709,7 @@ class CmdCreate(ObjManipCommand):
)
if errors:
self.msg(errors)
if not obj:
continue
@ -702,9 +719,7 @@ class CmdCreate(ObjManipCommand):
)
else:
string = f"You create a new {obj.typename}: {obj.name}."
# set a default desc
if not obj.db.desc:
obj.db.desc = "You see nothing special."
if "drop" in self.switches:
if caller.location:
obj.home = caller.location
@ -1473,7 +1488,7 @@ class CmdName(ObjManipCommand):
obj = None
if self.lhs_objs:
objname = self.lhs_objs[0]["name"]
if objname.startswith("*"):
if objname.startswith("*") and caller.account:
# account mode
obj = caller.account.search(objname.lstrip("*"))
if obj:
@ -3238,7 +3253,7 @@ class CmdFind(COMMAND_DEFAULT_CLASS):
try:
# Check that rhs is either a valid dbref or dbref range
bounds = tuple(
sorted(dbref(x, False) for x in re.split("[-\s]+", self.rhs.strip()))
sorted(dbref(x, False) for x in re.split(r"[-\s]+", self.rhs.strip()))
)
# dbref() will return either a valid int or None

View file

@ -1382,18 +1382,18 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
if target and target.isnumeric():
# a number to specify a historic page
number = int(target)
elif target:
elif message:
target_obj = self.caller.search(target, quiet=True)
if target_obj:
# a proper target
targets = [target_obj[0]]
message = message[0].strip()
else:
# a message with a space in it - put it back together
message = target + " " + (message[0] if message else "")
# a message with a space in it - use the original args
message = self.args.strip()
else:
# a single-word message
message = message[0].strip()
# a single-word message - use the original args
message = self.args.strip()
pages = list(pages_we_sent) + list(pages_we_got)
pages = sorted(pages, key=lambda page: page.date_created)
@ -2030,7 +2030,7 @@ class CmdDiscord2Chan(COMMAND_DEFAULT_CLASS):
# show all connections
if channel_list := discord_bot.db.channels:
table = self.styled_table(
"|wLink ID|n",
"|wLink Index|n",
"|wEvennia|n",
"|wDiscord|n",
border="cells",
@ -2076,7 +2076,7 @@ class CmdDiscord2Chan(COMMAND_DEFAULT_CLASS):
# show all discord channels linked to self.lhs
if channel_list := discord_bot.db.channels:
table = self.styled_table(
"|wLink ID|n",
"|wLink Index|n",
"|wEvennia|n",
"|wDiscord|n",
border="cells",

View file

@ -6,7 +6,7 @@ import re
from django.conf import settings
import evennia
from evennia.objects.objects import DefaultObject
from evennia.typeclasses.attributes import NickTemplateInvalid
from evennia.utils import utils
@ -118,7 +118,7 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
nick build $1 $2 = create/drop $1;$2
nick tell $1 $2=page $1=$2
nick tm?$1=page tallman=$1
nick tm\=$1=page tallman=$1
nick tm\\\\=$1=page tallman=$1
A 'nick' is a personal string replacement. Use $1, $2, ... to catch arguments.
Put the last $-marker without an ending space to catch all remaining text. You
@ -128,7 +128,7 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
? - matches 0 or 1 single characters
[abcd] - matches these chars in any order
[!abcd] - matches everything not among these chars
\= - escape literal '=' you want in your <string>
\\\\= - escape literal '=' you want in your <string>
Note that no objects are actually renamed or changed by this command - your nicks
are only available to you. If you want to permanently add keywords to an object
@ -143,7 +143,7 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
def parse(self):
"""
Support escaping of = with \=
Support escaping of = with \\=
"""
super().parse()
args = (self.lhs or "") + (" = %s" % self.rhs if self.rhs else "")
@ -153,7 +153,7 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
self.lhs = parts[0].strip()
else:
self.lhs, self.rhs = [part.strip() for part in parts]
self.lhs = self.lhs.replace("\=", "=")
self.lhs = self.lhs.replace("\\=", "=")
def func(self):
"""Create the nickname"""
@ -190,7 +190,8 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
if "clearall" in switches:
caller.nicks.clear()
caller.account.nicks.clear()
if caller.account:
caller.account.nicks.clear()
caller.msg("Cleared all nicks.")
return
@ -790,15 +791,18 @@ class CmdAccess(COMMAND_DEFAULT_CLASS):
hierarchy_full = settings.PERMISSION_HIERARCHY
string = "\n|wPermission Hierarchy|n (climbing):\n %s" % ", ".join(hierarchy_full)
if self.caller.account.is_superuser:
if caller.account and caller.account.is_superuser:
cperms = "<Superuser>"
pperms = "<Superuser>"
else:
cperms = ", ".join(caller.permissions.all())
pperms = ", ".join(caller.account.permissions.all())
if caller.account:
pperms = ", ".join(caller.account.permissions.all())
else:
pperms = "<No account>"
string += "\n|wYour access|n:"
string += f"\nCharacter |c{caller.key}|n: {cperms}"
if utils.inherits_from(caller, evennia.DefaultObject):
if utils.inherits_from(caller, DefaultObject) and caller.account:
string += f"\nAccount |c{caller.account.key}|n: {pperms}"
caller.msg(string)

View file

@ -477,6 +477,7 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
tuple: A tuple (match, suggestions).
"""
def strip_prefix(query):
if query and query[0] in settings.CMD_IGNORE_PREFIXES:
return query[1:]
@ -742,7 +743,7 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
if not fuzzy_match:
# no match found - give up
checked_topic = topic + f"/{subtopic_query}"
checked_topic = topic + f"{self.subtopic_separator_char}{subtopic_query}"
output = self.format_help_entry(
topic=topic,
help_text=f"No help entry found for '{checked_topic}'",
@ -757,7 +758,7 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
subtopic_map = subtopic_map.pop(subtopic_query)
subtopic_index = [subtopic for subtopic in subtopic_map if subtopic is not None]
# keep stepping down into the tree, append path to show position
topic = topic + f"/{subtopic_query}"
topic = topic + f"{self.subtopic_separator_char}{subtopic_query}"
# we reached the bottom of the topic tree
help_text = subtopic_map[None]

View file

@ -7,6 +7,7 @@ System commands
import code
import datetime
import os
import subprocess
import sys
import time
import traceback
@ -48,6 +49,11 @@ __all__ = (
)
class PrintRecursionError(RecursionError):
# custom error for recursion in print
pass
class CmdReload(COMMAND_DEFAULT_CLASS):
"""
reload the server
@ -203,7 +209,14 @@ def _run_code_snippet(
self.caller = caller
def write(self, string):
self.caller.msg(text=(string.rstrip("\n"), {"type": "py_output"}))
try:
self.caller.msg(text=(string.rstrip("\n"), {"type": "py_output"}))
except RecursionError:
tb = traceback.extract_tb(sys.exc_info()[2])
if any("print(" in frame.line for frame in tb):
# We are in a print loop (likely because msg() contains a print),
# exit the stdout reroute prematurely
raise PrintRecursionError
fake_std = FakeStd(caller)
sys.stdout = fake_std
@ -225,6 +238,12 @@ def _run_code_snippet(
else:
ret = eval(pycode_compiled, {}, available_vars)
except PrintRecursionError:
ret = (
"<<< Error: Recursive print() found (probably in custom msg()). "
"Since `py` reroutes `print` to `msg()`, this causes a loop. Remove `print()` "
"from msg-related code to resolve."
)
except Exception:
errlist = traceback.format_exc().split("\n")
if len(errlist) > 4:
@ -679,8 +698,7 @@ class CmdAbout(COMMAND_DEFAULT_CLASS):
|wHomepage|n https://evennia.com
|wCode|n https://github.com/evennia/evennia
|wDemo|n https://demo.evennia.com
|wGame listing|n https://games.evennia.com
|wGame listing|n http://games.evennia.com
|wChat|n https://discord.gg/AJJpcRUhtF
|wForum|n https://github.com/evennia/evennia/discussions
|wLicence|n https://opensource.org/licenses/BSD-3-Clause
@ -845,16 +863,26 @@ class CmdServerLoad(COMMAND_DEFAULT_CLASS):
if not _RESOURCE:
import resource as _RESOURCE
env = os.environ.copy()
env["LC_NUMERIC"] = "C" # use default locale instead of system locale
loadavg = os.getloadavg()[0]
rmem = (
float(os.popen("ps -p %d -o %s | tail -1" % (pid, "rss")).read()) / 1000.0
) # resident memory
vmem = (
float(os.popen("ps -p %d -o %s | tail -1" % (pid, "vsz")).read()) / 1000.0
) # virtual memory
pmem = float(
os.popen("ps -p %d -o %s | tail -1" % (pid, "%mem")).read()
) # % of resident memory to total
# Helper function to run the ps command with a modified environment
def run_ps_command(command):
result = subprocess.run(
command, shell=True, env=env, stdout=subprocess.PIPE, text=True
)
return result.stdout.strip()
# Resident memory
rmem = float(run_ps_command(f"ps -p {pid} -o rss | tail -1")) / 1000.0
# Virtual memory
vmem = float(run_ps_command(f"ps -p {pid} -o vsz | tail -1")) / 1000.0
# Percentage of resident memory to total
pmem = float(run_ps_command(f"ps -p {pid} -o %mem | tail -1"))
rusage = _RESOURCE.getrusage(_RESOURCE.RUSAGE_SELF)
if "mem" in self.switches:
@ -930,7 +958,7 @@ class CmdTickers(COMMAND_DEFAULT_CLASS):
locks = "cmd:perm(tickers) or perm(Builder)"
def func(self):
from evennia import TICKER_HANDLER
from evennia.scripts.tickerhandler import TICKER_HANDLER
all_subs = TICKER_HANDLER.all_display()
if not all_subs:

View file

@ -14,32 +14,39 @@ 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 evennia import (
DefaultCharacter,
DefaultExit,
DefaultObject,
DefaultRoom,
ObjectDB,
search_object,
)
from parameterized import parameterized
from twisted.internet import task
import evennia
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
from evennia.commands.default.muxcommand import MuxCommand
from evennia.objects.models import ObjectDB
from evennia.objects.objects import (
DefaultCharacter,
DefaultExit,
DefaultObject,
DefaultRoom,
)
from evennia.prototypes import prototypes as protlib
from evennia.utils import create, gametime, utils
from evennia.utils.search import search_object
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
@ -543,7 +550,9 @@ class TestCmdTasks(BaseEvenniaCommandTest):
self.call(system.CmdTasks(), f"/cancel {self.task.get_id()}")
self.task_handler.clock.advance(self.timedelay + 1)
self.assertFalse(self.task.exists())
self.task = self.task_handler.add(self.timedelay, func_test_cmd_tasks)
# the +1 time delay is to fix a timing issue with the test on Windows
# (see https://github.com/evennia/evennia/issues/3596)
self.task = self.task_handler.add(self.timedelay + 1, func_test_cmd_tasks)
self.assertTrue(self.task.get_id(), 1)
self.char1.msg = Mock()
self.char1.execute_cmd("y")
@ -1265,6 +1274,15 @@ class TestBuilding(BaseEvenniaCommandTest):
)
self.call(building.CmdName(), "Obj4=", "No names or aliases defined!")
def test_name_clears_plural(self):
box, _ = DefaultObject.create("Opened Box", location=self.char1)
# Force update of plural aliases (set in get_numbered_name)
self.char1.execute_cmd("inventory")
self.assertIn("one opened box", box.aliases.get(category=box.plural_category))
self.char1.execute_cmd("@name box=closed box")
self.assertIsNone(box.aliases.get(category=box.plural_category))
def test_desc(self):
oid = self.obj2.id
self.call(building.CmdDesc(), "Obj2=TestDesc", "The description was set on Obj2.")

View file

@ -1307,3 +1307,24 @@ class TestIssue3090(BaseEvenniaTest):
self.assertEqual(result[3], 8)
self.assertEqual(result[4], 1.0)
self.assertEqual(result[5], "smile at")
class _TestCmd1(Command):
key = "testcmd"
locks = "usecmd:false()"
def func():
pass
class TestIssue3643(BaseEvenniaTest):
"""
Commands with a 'cmd:' anywhere in its string, even `funccmd:` is assumed to
be a cmd: type lock, meaning it will not auto-insert `cmd:all()` into the
lockstring as intended.
"""
def test_issue_3643(self):
cmd = _TestCmd1()
self.assertEqual(cmd.locks, "cmd:all();usecmd:false()")

View file

@ -9,9 +9,9 @@ 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.objects.objects import DefaultObject
from evennia.typeclasses.models import TypeclassBase
from evennia.utils import create, logger
from evennia.utils.utils import inherits_from, make_iter
@ -231,7 +231,7 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
"""
has_sub = self.subscriptions.has(subscriber)
if not has_sub and inherits_from(subscriber, evennia.DefaultObject):
if not has_sub and inherits_from(subscriber, DefaultObject):
# it's common to send an Object when we
# by default only allow Accounts to subscribe.
has_sub = self.subscriptions.has(subscriber.account)
@ -814,7 +814,7 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
return "#"
def web_get_detail_url(self):
"""
r"""
Returns the URI path for a View that allows users to view details for
this object.
@ -850,7 +850,7 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
return "#"
def web_get_update_url(self):
"""
r"""
Returns the URI path for a View that allows users to update this
object.
@ -886,7 +886,7 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
return "#"
def web_get_delete_url(self):
"""
r"""
Returns the URI path for a View that allows users to delete this object.
ex. Oscar (Character) = '/characters/oscar/1/delete/'

View file

@ -184,6 +184,7 @@ class Msg(SharedMemoryModel):
class Meta(object):
"Define Django meta options"
verbose_name = "Msg"
@lazy_property
@ -712,6 +713,7 @@ class ChannelDB(TypedObject):
class Meta:
"Define Django meta options"
verbose_name = "Channel"
verbose_name_plural = "Channels"

View file

@ -1,7 +1,7 @@
from django.test import SimpleTestCase
from evennia import DefaultChannel
from evennia.commands.default.comms import CmdChannel
from evennia.comms.comms import DefaultChannel
from evennia.utils.create import create_message
from evennia.utils.test_resources import BaseEvenniaTest

View file

@ -76,7 +76,7 @@ from tempfile import SpooledTemporaryFile
from django.core.files.base import File
from django.core.files.storage import Storage
from django.utils.deconstruct import deconstructible
from django.utils.encoding import filepath_to_uri, force_bytes, force_text, smart_text
from django.utils.encoding import filepath_to_uri, force_bytes, force_str, smart_str
from django.utils.timezone import is_naive, make_naive
try:
@ -127,9 +127,9 @@ def safe_join(base, *paths):
final_path (str): A joined path, base + filepath
"""
base_path = force_text(base)
base_path = force_str(base)
base_path = base_path.rstrip("/")
paths = [force_text(p) for p in paths]
paths = [force_str(p) for p in paths]
final_path = base_path + "/"
for path in paths:
@ -252,7 +252,7 @@ class S3Boto3StorageFile(File):
self._storage = storage
self.name = name[len(self._storage.location) :].lstrip("/")
self._mode = mode
self._force_mode = (lambda b: b) if "b" in mode else force_text
self._force_mode = (lambda b: b) if "b" in mode else force_str
self.obj = storage.bucket.Object(storage._encode_name(name))
if "w" not in mode:
# Force early RAII-style exception if object does not exist
@ -632,10 +632,10 @@ class S3Boto3Storage(Storage):
raise SuspiciousOperation("Attempted access to '%s' denied." % name)
def _encode_name(self, name):
return smart_text(name, encoding=self.file_name_charset)
return smart_str(name, encoding=self.file_name_charset)
def _decode_name(self, name):
return force_text(name, encoding=self.file_name_charset)
return force_str(name, encoding=self.file_name_charset)
def _compress_content(self, content):
"""Gzip a given string content."""

View file

@ -8,7 +8,7 @@ from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.core.files.base import ContentFile
from django.test import TestCase, override_settings
from django.utils.timezone import is_aware, utc
from django.utils.timezone import is_aware
_SKIP = False
try:
@ -533,7 +533,7 @@ class S3Boto3StorageTests(S3Boto3TestCase):
def _test_storage_mtime(self, use_tz):
obj = self.storage.bucket.Object.return_value
obj.last_modified = datetime.datetime.now(utc)
obj.last_modified = datetime.datetime.now(datetime.timezone.utc)
name = "file.txt"
self.assertFalse(

View file

@ -9,7 +9,7 @@ You can use Godot to provide advanced functionality with proper Evennia support.
## Installation
You need to add the following settings in your settings.py and restart evennia.
You need to add the following settings in your `settings.py` and restart evennia.
```python
PORTAL_SERVICES_PLUGIN_MODULES.append('evennia.contrib.base_systems.godotwebsocket.webclient')
@ -64,7 +64,7 @@ This will connect when the Scene is ready, poll and print the data when we recei
extends Node
# The URL we will connect to.
var websocket_url = "ws://localhost:4008"
var websocket_url = "ws://127.0.0.1:4008"
var socket := WebSocketPeer.new()
func _ready():
@ -145,7 +145,7 @@ func _on_button_pressed():
extends Node
# The URL we will connect to.
var websocket_url = "ws://localhost:4008"
var websocket_url = "ws://127.0.0.1:4008"
var socket := WebSocketPeer.new()
@onready var output_label = $"../Panel/VBoxContainer/RichTextLabel"
@ -193,4 +193,4 @@ func _on_button_pressed():
func _exit_tree():
socket.close()
```
```

View file

@ -1,4 +1,4 @@
"""Tests for text2bbcode """
"""Tests for text2bbcode"""
import mock
from django.test import TestCase
@ -58,7 +58,7 @@ class TestText2Bbcode(TestCase):
mocked_match = mock.Mock()
mocked_match.groups.return_value = ["cmd", "text"]
self.assertEqual("[mxp=send cmd=cmd]text[/mxp]", parser.sub_mxp_links(mocked_match))
self.assertEqual("[url=send cmd=cmd]text[/url]", parser.sub_mxp_links(mocked_match))
def test_sub_text(self):
parser = text2bbcode.BBCODE_PARSER

View file

@ -705,7 +705,7 @@ class TextToBBCODEparser(TextToHTMLparser):
"""
cmd, text = [grp.replace('"', "\\&quot;") for grp in match.groups()]
val = f"[mxp=send cmd={cmd}]{text}[/mxp]"
val = f"[url=send cmd={cmd}]{text}[/url]"
return val

View file

@ -65,6 +65,7 @@ class GodotWebSocketClient(webclient.WebSocketClient):
def start_plugin_services(portal):
class GodotWebsocket(WebSocketServerFactory):
"Only here for better naming in logs"
pass
factory = GodotWebsocket()

View file

@ -7,14 +7,19 @@ default ones in evennia core.
"""
from evennia import DefaultCharacter, DefaultExit, DefaultObject, DefaultRoom, ScriptDB
from evennia.contrib.base_systems.ingame_python.callbackhandler import CallbackHandler
from evennia.contrib.base_systems.ingame_python.utils import (
phrase_event,
register_events,
time_event,
)
from evennia.utils.utils import delay, inherits_from, lazy_property
from evennia.objects.objects import (
DefaultCharacter,
DefaultExit,
DefaultObject,
DefaultRoom,
)
from evennia.utils.utils import inherits_from, lazy_property
# Character help
CHARACTER_CAN_DELETE = """

View file

@ -7,11 +7,15 @@ These functions are to be used by developers to customize events and callbacks.
from django.conf import settings
from evennia import ScriptDB, logger
from evennia.contrib.base_systems.custom_gametime import UNITS, gametime_to_realtime
from evennia.contrib.base_systems.custom_gametime import (
UNITS,
gametime_to_realtime,
)
from evennia.contrib.base_systems.custom_gametime import (
real_seconds_until as custom_rsu,
)
from evennia.scripts.models import ScriptDB
from evennia.utils import logger
from evennia.utils.create import create_script
from evennia.utils.gametime import real_seconds_until as standard_rsu
from evennia.utils.utils import class_from_module

View file

@ -77,7 +77,7 @@ The contrib is designed to make adding new types of reports to the system as sim
#### 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.
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

View file

@ -24,11 +24,6 @@ if hasattr(settings, "INGAME_REPORT_STATUS_TAGS"):
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}"
@ -54,6 +49,11 @@ def menunode_list_reports(caller, raw_string, **kwargs):
if not report_list:
return "There is nothing there for you to manage.", {}
page = kwargs.get("page", 0)
start = page * _REPORTS_PER_PAGE
end = start + _REPORTS_PER_PAGE
report_slice = report_list[start:end]
options = [
{
"desc": f"{datetime_format(report.date_created)} - {crop(report.message, 50)}",

View file

@ -23,7 +23,7 @@ To install, just add the provided cmdset to your default AccountCmdSet:
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.
@ -34,10 +34,10 @@ The contrib can be further configured through two settings, `INGAME_REPORT_TYPES
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 evennia.utils import create, evmenu, logger, search
from evennia.utils.utils import class_from_module, datetime_format, is_iter, iter_to_str
from . import menu
@ -67,8 +67,9 @@ def _get_report_hub(report_type):
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)):
from evennia import GLOBAL_SCRIPTS
if not (hub := GLOBAL_SCRIPTS.get(hub_key)):
hub = create.create_script(key=hub_key)
return hub or None
@ -92,7 +93,7 @@ class CmdManageReports(_DEFAULT_COMMAND_CLASS):
aliases = tuple(f"manage {report_type}" for report_type in _REPORT_TYPES)
locks = "cmd:pperm(Admin)"
def get_help(self):
def get_help(self, caller, cmdset):
"""Returns a help string containing the configured available report types"""
report_types = iter_to_str("\n ".join(_REPORT_TYPES))
@ -102,7 +103,7 @@ manage the various reports
Usage:
manage [report type]
Available report types:
{report_types}
@ -157,7 +158,7 @@ class ReportCmdBase(_DEFAULT_COMMAND_CLASS):
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
@ -212,7 +213,11 @@ class ReportCmdBase(_DEFAULT_COMMAND_CLASS):
receivers.append(target)
if self.create_report(
self.account, self.report_message, receivers=receivers, locks=self.report_locks, tags=["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)

View file

@ -1,6 +1,7 @@
from unittest.mock import Mock, patch, MagicMock
from evennia.utils import create
from unittest.mock import MagicMock, Mock, patch
from evennia.comms.models import TempMsg
from evennia.utils import create
from evennia.utils.test_resources import EvenniaCommandTest
from . import menu, reports

View file

@ -232,6 +232,7 @@ class MenuLoginEvMenu(EvMenu):
class UnloggedinCmdSet(CmdSet):
"Cmdset for the unloggedin state"
key = "DefaultUnloggedin"
priority = 0

View file

@ -31,14 +31,10 @@ import re
from django.conf import settings
from evennia import (
SESSION_HANDLER,
CmdSet,
Command,
InterruptCommand,
default_cmds,
syscmdkeys,
)
import evennia
from evennia import default_cmds, syscmdkeys
from evennia.commands.cmdset import CmdSet
from evennia.commands.command import Command, InterruptCommand
from evennia.utils import variable_from_module
from .utils import create_evscaperoom_object
@ -300,7 +296,7 @@ class CmdWho(CmdEvscapeRoom, default_cmds.CmdWho):
if self.args == "all":
table = self.style_table("|wName", "|wRoom")
sessions = SESSION_HANDLER.get_sessions()
sessions = evennia.SESSION_HANDLER.get_sessions()
for session in sessions:
puppet = session.get_puppet()
if puppet:

View file

@ -23,7 +23,7 @@ The recognized fields for an achievement are:
- name (str): The name of the achievement. This is not the key and does not need to be unique.
- desc (str): The longer description of the achievement. Common uses for this would be flavor text
or hints on how to complete it.
- category (str): The category of conditions which this achievement tracks. It will most likely be
- category (str): The category of conditions which this achievement tracks. It will most likely be
an action and you will most likely specify it based on where you're checking from.
e.g. killing 10 rats might have a category of "defeat", which you'd then check from your code
that runs when a player defeats something.

View file

@ -21,7 +21,7 @@ Would result in this added description:
## Installation
To install, import this module and have your default character
inherit from ClothedCharacter in your game's characters.py file:
inherit from ClothedCharacter in your game's `characters.py` file:
```python

View file

@ -527,6 +527,9 @@ class CmdRemove(MuxCommand):
help_category = "clothing"
def func(self):
if not self.args:
self.caller.msg("Usage: remove <worn clothing object>")
return
clothing = self.caller.search(self.args, candidates=self.caller.contents)
if not clothing:
self.caller.msg("You don't have anything like that.")

View file

@ -85,7 +85,9 @@ class TestClothingCmd(BaseEvenniaCommandTest):
)
# Test remove command.
self.call(clothing.CmdRemove(), "", "Could not find ''.", caller=self.wearer)
self.call(
clothing.CmdRemove(), "", "Usage: remove <worn clothing object>", caller=self.wearer
)
self.call(
clothing.CmdRemove(),
"hat",

View file

@ -11,7 +11,7 @@ To install, import and add the `ContainerCmdSet` to `CharacterCmdSet` in your `d
class CharacterCmdSet(default_cmds.CharacterCmdSet):
# ...
def at_cmdset_creation(self):
# ...
self.add(ContainerCmdSet)

View file

@ -41,6 +41,7 @@ _RE_KEYS = re.compile(r"([\w\s]+)(?:\+*?)", re.U + re.I)
class DescValidateError(ValueError):
"Used for tracebacks from desc systems"
pass

View file

@ -0,0 +1,37 @@
# Item Storage
Contribution by helpme (2024)
This module allows certain rooms to be marked as storage locations.
In those rooms, players can `list`, `store`, and `retrieve` items. Storages can be shared or individual.
## Installation
This utility adds the storage-related commands. Import the module into your commands and add it to your command set to make it available.
Specifically, in `mygame/commands/default_cmdsets.py`:
```python
...
from evennia.contrib.game_systems.storage import StorageCmdSet # <---
class CharacterCmdset(default_cmds.Character_CmdSet):
...
def at_cmdset_creation(self):
...
self.add(StorageCmdSet) # <---
```
Then `reload` to make the `list`, `retrieve`, `store`, and `storage` commands available.
## Usage
To mark a location as having item storage, use the `storage` command. By default this is a builder-level command. Storage can be shared, which means everyone using the storage can access all items stored there, or individual, which means only the person who stores an item can retrieve it. See `help storage` for further details.
## Technical info
This is a tag-based system. Rooms set as storage rooms are tagged with an identifier marking them as shared or not. Items stored in those rooms are tagged with the storage room identifier and, if the storage room is not shared, the character identifier, and then they are removed from the grid i.e. their location is set to `None`. Upon retrieval, items are untagged and moved back to character inventories.
When a room is unmarked as storage with the `storage` command, all stored objects are untagged and dropped to the room. You should use the `storage` command to create and remove storages, as otherwise stored objects may become lost.

View file

@ -0,0 +1,5 @@
"""
Item storage integration - helpme 2024
"""
from .storage import StorageCmdSet # noqa

View file

@ -0,0 +1,190 @@
from evennia import CmdSet
from evennia.commands.default.muxcommand import MuxCommand
from evennia.utils import list_to_string
from evennia.utils.search import search_object_by_tag
SHARED_TAG_PREFIX = "shared"
class StorageCommand(MuxCommand):
"""
Shared functionality for storage-related commands
"""
def at_pre_cmd(self):
"""
Check if the current location is tagged as a storage location
Every stored object is tagged on storage, and untagged on retrieval
Returns:
bool: True if the command is to be stopped here
"""
if super().at_pre_cmd():
return True
self.storage_location_id = self.caller.location.tags.get(category="storage_location")
if not self.storage_location_id:
self.caller.msg(f"You cannot {self.cmdstring} anything here.")
return True
self.object_tag = (
SHARED_TAG_PREFIX
if self.storage_location_id.startswith(SHARED_TAG_PREFIX)
else self.caller.pk
)
self.currently_stored = search_object_by_tag(
self.object_tag, category=self.storage_location_id
)
class CmdStore(StorageCommand):
"""
Store something in a storage location.
Usage:
store <obj>
"""
key = "store"
locks = "cmd:all()"
help_category = "Storage"
def func(self):
"""
Find the item in question to store, then store it
"""
caller = self.caller
if not self.args:
self.caller.msg("Store what?")
return
obj = caller.search(self.args.strip(), candidates=caller.contents)
if not obj:
return
"""
We first check at_pre_move before setting the location to None, in case
anything should stymie its movement.
"""
if obj.at_pre_move(caller.location):
obj.tags.add(self.object_tag, self.storage_location_id)
obj.location = None
caller.msg(f"You store {obj.get_display_name(caller)} here.")
else:
caller.msg(f"You fail to store {obj.get_display_name(caller)} here.")
class CmdRetrieve(StorageCommand):
"""
Retrieve something from a storage location.
Usage:
retrieve <obj>
"""
key = "retrieve"
locks = "cmd:all()"
help_category = "Storage"
def func(self):
"""
Retrieve the item in question if possible
"""
caller = self.caller
if not self.args:
self.caller.msg("Retrieve what?")
return
obj = caller.search(self.args.strip(), candidates=self.currently_stored)
if not obj:
return
if obj.at_pre_move(caller):
obj.tags.remove(self.object_tag, self.storage_location_id)
caller.msg(f"You retrieve {obj.get_display_name(caller)}.")
else:
caller.msg(f"You fail to retrieve {obj.get_display_name(caller)}.")
class CmdList(StorageCommand):
"""
List items in the storage location.
Usage:
list
"""
key = "list"
locks = "cmd:all()"
help_category = "Storage"
def func(self):
"""
List items in the storage
"""
caller = self.caller
if not self.currently_stored:
caller.msg("You find nothing stored here.")
return
caller.msg(f"Stored here:\n{list_to_string(self.currently_stored)}")
class CmdStorage(MuxCommand):
"""
Make the current location a storage room, or delete it as a storage and move all stored objects into the room contents.
Shared storage locations can be used by all interchangeably.
The default storage identifier will be its primary key in the database, but you can supply a new one in case you want linked storages.
Usage:
storage [= [storage identifier]]
storage/shared [= [storage identifier]]
storage/delete
"""
key = "@storage"
locks = "cmd:perm(Builder)"
def func(self):
"""Set the storage location."""
caller = self.caller
location = caller.location
current_storage_id = location.tags.get(category="storage_location")
storage_id = self.lhs or location.pk
if "delete" in self.switches:
if not current_storage_id:
caller.msg("This is not tagged as a storage location.")
return
# Move the stored objects, if any, into the room
currently_stored_here = search_object_by_tag(category=current_storage_id)
for obj in currently_stored_here:
obj.tags.remove(category=current_storage_id)
obj.location = location
caller.msg("You remove the storage capabilities of the room.")
location.tags.remove(current_storage_id, category="storage_location")
return
if current_storage_id:
caller.msg("This is already a storage location: |wstorage/delete|n to remove the tag.")
return
new_storage_id = (
f"{SHARED_TAG_PREFIX if SHARED_TAG_PREFIX in self.switches else ''}{storage_id}"
)
location.tags.add(new_storage_id, category="storage_location")
caller.msg(f"This is now a storage location with id: {new_storage_id}.")
class StorageCmdSet(CmdSet):
"""
CmdSet for all storage-related commands
"""
def at_cmdset_creation(self):
self.add(CmdStore)
self.add(CmdRetrieve)
self.add(CmdList)
self.add(CmdStorage)

View file

@ -0,0 +1,121 @@
from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.utils.create import create_object
from . import storage
class TestStorage(BaseEvenniaCommandTest):
def setUp(self):
super().setUp()
self.obj1.location = self.char1
self.room1.tags.add("storage_1", "storage_location")
self.room2.tags.add("shared_storage_2", "storage_location")
def test_store_and_retrieve(self):
self.call(
storage.CmdStore(),
"",
"Store what?",
caller=self.char1,
)
self.call(
storage.CmdStore(),
"obj",
f"You store {self.obj1.get_display_name(self.char1)} here.",
caller=self.char1,
)
self.call(
storage.CmdList(),
"",
f"Stored here:\n{self.obj1.get_display_name(self.char1)}",
caller=self.char1,
)
self.call(
storage.CmdRetrieve(),
"obj2",
"Could not find 'obj2'.",
caller=self.char1,
)
self.call(
storage.CmdRetrieve(),
"obj",
f"You retrieve {self.obj1.get_display_name(self.char1)}.",
caller=self.char1,
)
def test_store_retrieve_while_not_in_storeroom(self):
self.char2.location = self.char1
self.call(storage.CmdStore(), "obj", "You cannot store anything here.", caller=self.char2)
self.call(
storage.CmdRetrieve(), "obj", "You cannot retrieve anything here.", caller=self.char2
)
def test_store_retrieve_nonexistent_obj(self):
self.call(storage.CmdStore(), "asdasd", "Could not find 'asdasd'.", caller=self.char1)
self.call(storage.CmdRetrieve(), "asdasd", "Could not find 'asdasd'.", caller=self.char1)
def test_list_nothing_stored(self):
self.call(
storage.CmdList(),
"",
"You find nothing stored here.",
caller=self.char1,
)
def test_shared_storage(self):
self.char1.location = self.room2
self.char2.location = self.room2
self.call(
storage.CmdStore(),
"obj",
f"You store {self.obj1.get_display_name(self.char1)} here.",
caller=self.char1,
)
self.call(
storage.CmdRetrieve(),
"obj",
f"You retrieve {self.obj1.get_display_name(self.char1)}.",
caller=self.char2,
)
def test_remove_add_storage(self):
self.char1.permissions.add("builder")
self.call(
storage.CmdStorage(),
"",
"This is already a storage location: storage/delete to remove the tag.",
caller=self.char1,
)
self.call(
storage.CmdStore(),
"obj",
f"You store {self.obj1.get_display_name(self.char1)} here.",
caller=self.char1,
)
self.assertEqual(self.obj1.location, None)
self.call(
storage.CmdStorage(),
"/delete",
"You remove the storage capabilities of the room.",
caller=self.char1,
)
self.assertEqual(self.obj1.location, self.room1)
self.call(
storage.CmdStorage(),
"",
f"This is now a storage location with id: {self.room1.id}.",
caller=self.char1,
)
self.call(
storage.CmdStorage(),
"/delete",
"You remove the storage capabilities of the room.",
caller=self.char1,
)
self.call(
storage.CmdStorage(),
"/shared",
f"This is now a storage location with id: shared{self.room1.id}.",
caller=self.char1,
)

View file

@ -125,16 +125,11 @@ class Map(object):
Returns:
string: The exit name as a compass direction or an empty string.
"""
exit_name = ex.name
if exit_name not in _COMPASS_DIRECTIONS:
compass_aliases = [
direction in ex.aliases.all() for direction in _COMPASS_DIRECTIONS.keys()
]
if compass_aliases[0]:
exit_name = compass_aliases[0]
if exit_name not in _COMPASS_DIRECTIONS:
return ""
return exit_name
return (
ex.name
if ex.name in _COMPASS_DIRECTIONS
else next((alias for alias in ex.aliases.all() if alias in _COMPASS_DIRECTIONS), "")
)
def update_pos(self, room, exit_name):
"""

View file

@ -32,8 +32,8 @@ class TestIngameMap(BaseEvenniaCommandTest):
)
create_object(
exits.Exit,
key="west",
aliases=["w"],
key="shopfront",
aliases=["w", "west"],
location=self.east_room,
destination=self.west_room,
)

View file

@ -86,7 +86,7 @@ For example:
Below are two examples showcasing the use of automatic exit generation and
custom exit generation. Whilst located, and can be used, from this module for
convenience The below example code should be in mymap.py in mygame/world.
convenience The below example code should be in `mymap.py` in mygame/world.
### Example One

View file

@ -1,7 +1,7 @@
"""
Buffs - Tegiminis 2022
A buff is a timed object, attached to a game entity, that modifies values, triggers
A buff is a timed object, attached to a game entity, that modifies values, triggers
code, or both. It is a common design pattern in RPGs, particularly action games.
This contrib gives you a buff handler to apply to your objects, a buff class to extend them,
@ -25,7 +25,7 @@ To make use of the handler, you will need:
### Applying a Buff
Call the handler `add(BuffClass)` method. This requires a class reference, and also contains a number of
Call the handler `add(BuffClass)` method. This requires a class reference, and also contains a number of
optional arguments to customize the buff's duration, stacks, and so on.
```python
@ -36,8 +36,8 @@ self.buffs.add(ReflectBuff, to_cache={'reflect': 0.5}) # A single stack of Refl
### Modify
Call the handler `check(value, stat)` method wherever you want to see the modified value.
This will return the value, modified by and relevant buffs on the handler's owner (identified by
Call the handler `check(value, stat)` method wherever you want to see the modified value.
This will return the value, modified by and relevant buffs on the handler's owner (identified by
the `stat` string). For example:
```python
@ -49,7 +49,7 @@ def take_damage(self, source, damage):
### Trigger
Call the handler `trigger(triggerstring)` method wherever you want an event call. This
Call the handler `trigger(triggerstring)` method wherever you want an event call. This
will call the `at_trigger` hook method on all buffs with the relevant trigger.
```python

View file

@ -82,7 +82,7 @@ class ContribCmdCharCreate(MuxAccountCommand):
)
if errors:
self.msg(errors)
self.msg("\n".join(errors))
if not new_character:
return
# initalize the new character to the beginning of the chargen menu

View file

@ -66,7 +66,8 @@ LLM_PATH = "/api/v1/generate"
# if you wanted to authenticated to some external service, you could
# add an Authenticate header here with a token
LLM_HEADERS = {"Content-Type": "application/json"}
# note that the content of each header must be an iterable
LLM_HEADERS = {"Content-Type": ["application/json"]}
# this key will be inserted in the request, with your user-input
LLM_PROMPT_KEYNAME = "prompt"
@ -77,7 +78,7 @@ LLM_REQUEST_BODY = {
"temperature": 0.7, # 0-2. higher=more random, lower=predictable
}
# helps guide the NPC AI. See the LLNPC section.
LLM_PROMPT_PREFIx = (
LLM_PROMPT_PREFIX = (
"You are roleplaying as {name}, a {desc} existing in {location}. "
"Answer with short sentences. Only respond as {name} would. "
"From here on, the conversation between {name} and {character} begins."
@ -148,8 +149,8 @@ Here is an untested example of the Evennia setting for calling [OpenAI's v1/comp
```python
LLM_HOST = "https://api.openai.com"
LLM_PATH = "/v1/completions"
LLM_HEADERS = {"Content-Type": "application/json",
"Authorization": "Bearer YOUR_OPENAI_API_KEY"}
LLM_HEADERS = {"Content-Type": ["application/json"],
"Authorization": ["Bearer YOUR_OPENAI_API_KEY"]}
LLM_PROMPT_KEYNAME = "prompt"
LLM_REQUEST_BODY = {
"model": "gpt-3.5-turbo",

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