mirror of
https://github.com/evennia/evennia.git
synced 2026-04-05 15:37:17 +02:00
Merge branch 'evennia:main' into database_backup_contrib
This commit is contained in:
commit
47442beaf7
80 changed files with 1367 additions and 843 deletions
19
.github/workflows/github_action_issue_to_project.yml
vendored
Normal file
19
.github/workflows/github_action_issue_to_project.yml
vendored
Normal 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 }}
|
||||
105
CHANGELOG.md
105
CHANGELOG.md
|
|
@ -1,16 +1,107 @@
|
|||
# Changelog
|
||||
|
||||
|
||||
## Main branch
|
||||
|
||||
- [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)
|
||||
- [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][issue3688]: Made TutorialWorld possible to build cleanly without being a superuser (Griatch)
|
||||
- [Fix][issue3687]: Fixed batchcommand/interactive with developer perms (Griatch)
|
||||
- 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
|
||||
|
||||
|
||||
[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
|
||||
[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
|
||||
- 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
|
||||
|
|
@ -28,6 +119,10 @@ Oct 1, 2024
|
|||
|
||||
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)
|
||||
|
|
@ -44,7 +139,7 @@ with dynamic keys (rather than just relying on typeclass' key) (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
|
||||
|
|
@ -114,7 +209,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)
|
||||
|
||||
|
|
@ -1468,7 +1563,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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,109 @@
|
|||
# Changelog
|
||||
|
||||
## Main branch
|
||||
|
||||
- [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)
|
||||
- [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][issue3688]: Made TutorialWorld possible to build cleanly without being a superuser (Griatch)
|
||||
- [Fix][issue3687]: Fixed batchcommand/interactive with developer perms (Griatch)
|
||||
- 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
|
||||
|
||||
|
||||
[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
|
||||
[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
|
||||
|
|
@ -15,6 +119,10 @@ Oct 1, 2024
|
|||
|
||||
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)
|
||||
|
|
@ -31,7 +139,7 @@ with dynamic keys (rather than just relying on typeclass' key) (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
|
||||
|
|
@ -101,7 +209,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)
|
||||
|
||||
|
|
@ -1455,7 +1563,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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
+---------------+----------------------+--------------+
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
42
docs/source/Contribs/Contrib-Storage.md
Normal file
42
docs/source/Contribs/Contrib-Storage.md
Normal 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>
|
||||
|
|
@ -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_
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -377,7 +377,6 @@ class ObjectParent:
|
|||
"""
|
||||
class docstring
|
||||
"""
|
||||
pass
|
||||
|
||||
class Object(ObjectParent, DefaultObject):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
* You’ve created a urls.py file in the `mygame/web/chargen` directory
|
||||
* You’ve 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!
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
#
|
||||
|
|
|
|||
|
|
@ -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,20 @@ 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.
|
||||
- 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.
|
||||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
```
|
||||
18
docs/source/api/evennia.contrib.game_systems.storage.md
Normal file
18
docs/source/api/evennia.contrib.game_systems.storage.md
Normal 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
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
```{eval-rst}
|
||||
evennia.contrib.game\_systems.storage.storage
|
||||
====================================================
|
||||
|
||||
.. automodule:: evennia.contrib.game_systems.storage.storage
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
```{eval-rst}
|
||||
evennia.contrib.game\_systems.storage.tests
|
||||
==================================================
|
||||
|
||||
.. automodule:: evennia.contrib.game_systems.storage.tests
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
```
|
||||
|
|
@ -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 October 01, 2024, see the [Evennia Changelog](Coding/Changelog.md). Latest released Evennia version is 4.4.1.
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
4.4.1
|
||||
4.5.0
|
||||
|
|
|
|||
|
|
@ -778,6 +778,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 +942,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 +1013,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)
|
||||
|
||||
|
|
|
|||
|
|
@ -555,24 +555,28 @@ class DiscordBot(Bot):
|
|||
"""
|
||||
|
||||
factory_path = "evennia.server.portal.discord.DiscordWebsocketServerFactory"
|
||||
|
||||
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}
|
||||
for channel_name in list(channel_set):
|
||||
channel = search.search_channel(channel_name)
|
||||
if not channel:
|
||||
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
|
||||
|
||||
"""
|
||||
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.")
|
||||
channel = channel[0]
|
||||
self.ndb.ev_channels[channel_name] = channel
|
||||
self._load_channels()
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
|
|
@ -583,27 +587,18 @@ 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):
|
||||
|
|
|
|||
|
|
@ -12,13 +12,13 @@ import re
|
|||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from django.utils.text import slugify
|
||||
|
||||
from evennia.locks.lockhandler import LockHandler
|
||||
from evennia.utils.ansi import ANSIString
|
||||
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 +74,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 +575,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
|
||||
|
|
|
|||
|
|
@ -394,7 +394,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."""
|
||||
|
|
@ -427,7 +427,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 +450,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 +473,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 +495,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 +518,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 +541,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 +564,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 +588,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 +618,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 +647,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 +676,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 +701,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 +726,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 +738,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 = """
|
||||
|
|
|
|||
|
|
@ -167,7 +167,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]
|
||||
|
|
@ -447,7 +447,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 +459,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 +473,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 +484,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 +493,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 +520,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 +531,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 +549,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 +563,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 +579,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 +705,7 @@ class CmdCreate(ObjManipCommand):
|
|||
)
|
||||
if errors:
|
||||
self.msg(errors)
|
||||
|
||||
if not obj:
|
||||
continue
|
||||
|
||||
|
|
@ -702,9 +715,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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import traceback
|
|||
|
||||
import django
|
||||
import evennia
|
||||
import subprocess
|
||||
import twisted
|
||||
from django.conf import settings
|
||||
from evennia.accounts.models import AccountDB
|
||||
|
|
@ -696,8 +697,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
|
||||
|
|
@ -862,16 +862,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:
|
||||
|
|
|
|||
|
|
@ -1267,6 +1267,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.")
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ Unit testing for the Command system itself.
|
|||
"""
|
||||
|
||||
from django.test import override_settings
|
||||
|
||||
from evennia.commands import cmdparser
|
||||
from evennia.commands.cmdset import CmdSet
|
||||
from evennia.commands.command import Command
|
||||
|
|
@ -991,9 +990,8 @@ class TestOptionTransferReplace(TestCase):
|
|||
|
||||
import sys
|
||||
|
||||
from twisted.trial.unittest import TestCase as TwistedTestCase
|
||||
|
||||
from evennia.commands import cmdhandler
|
||||
from twisted.trial.unittest import TestCase as TwistedTestCase
|
||||
|
||||
|
||||
def _mockdelay(time, func, *args, **kwargs):
|
||||
|
|
@ -1307,3 +1305,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()")
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
@ -32,12 +32,11 @@ 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
|
||||
|
||||
|
|
@ -68,6 +67,7 @@ def _get_report_hub(report_type):
|
|||
"""
|
||||
hub_key = f"{report_type}_reports"
|
||||
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 +92,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 +102,7 @@ manage the various reports
|
|||
|
||||
Usage:
|
||||
manage [report type]
|
||||
|
||||
|
||||
Available report types:
|
||||
{report_types}
|
||||
|
||||
|
|
@ -157,7 +157,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 +212,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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ class EvAdventureRoomTest(EvenniaTestCase):
|
|||
/|\
|
||||
o o o
|
||||
room_center
|
||||
You see nothing special.
|
||||
This is a room.
|
||||
Exits: north, northeast, east, southeast, south, southwest, west, and northwest"""
|
||||
|
||||
result = "\n".join(part.rstrip() for part in strip_ansi(desc).split("\n"))
|
||||
|
|
|
|||
|
|
@ -280,7 +280,11 @@ class TutorialRoom(DefaultRoom):
|
|||
source_location (Object): the previous location of new_arrival.
|
||||
|
||||
"""
|
||||
if new_arrival.has_account and not new_arrival.is_superuser:
|
||||
if new_arrival.ndb.batch_batchmode:
|
||||
# currently running batchcommand
|
||||
return
|
||||
|
||||
if new_arrival.has_account and not new_arrival.ndb.batch_batchmode:
|
||||
# this is a character
|
||||
for obj in self.contents_get(exclude=new_arrival):
|
||||
if hasattr(obj, "at_new_arrival"):
|
||||
|
|
@ -465,6 +469,10 @@ class IntroRoom(TutorialRoom):
|
|||
Assign properties on characters
|
||||
"""
|
||||
|
||||
if character.ndb.batch_batchmode:
|
||||
# currently running batchcommand
|
||||
return
|
||||
|
||||
# setup character for the tutorial
|
||||
health = self.db.char_health or 20
|
||||
|
||||
|
|
@ -476,8 +484,8 @@ class IntroRoom(TutorialRoom):
|
|||
string = "-" * 78 + SUPERUSER_WARNING + "-" * 78
|
||||
character.msg("|r%s|n" % string.format(name=character.key, quell="|wquell|r"))
|
||||
else:
|
||||
# quell user
|
||||
if character.account:
|
||||
# quell user if they have account and is not currently running the batch processor
|
||||
if character.account and not character.ndb.batch_batchmode:
|
||||
character.account.execute_cmd("quell")
|
||||
character.msg("(Auto-quelling while in tutorial-world)")
|
||||
|
||||
|
|
@ -784,6 +792,10 @@ class BridgeRoom(WeatherRoom):
|
|||
This hook is called by the engine whenever the player is moved
|
||||
into this room.
|
||||
"""
|
||||
if character.ndb.batch_batchmode:
|
||||
# currently running batchcommand
|
||||
return
|
||||
|
||||
if character.has_account:
|
||||
# we only run this if the entered object is indeed a player object.
|
||||
# check so our east/west exits are correctly defined.
|
||||
|
|
@ -1007,6 +1019,7 @@ class DarkRoom(TutorialRoom):
|
|||
"""
|
||||
return (
|
||||
obj.is_superuser
|
||||
or obj.ndb.batch_batchmode
|
||||
or obj.db.is_giving_light
|
||||
or any(o for o in obj.contents if o.db.is_giving_light)
|
||||
)
|
||||
|
|
@ -1051,6 +1064,10 @@ class DarkRoom(TutorialRoom):
|
|||
"""
|
||||
Called when an object enters the room.
|
||||
"""
|
||||
if obj.ndb.batch_batchmode:
|
||||
# currently running batchcommand
|
||||
self.check_light_state() # this should remove the DarkCmdSet
|
||||
|
||||
if obj.has_account:
|
||||
# a puppeted object, that is, a Character
|
||||
self._heal(obj)
|
||||
|
|
@ -1117,9 +1134,10 @@ class TeleportRoom(TutorialRoom):
|
|||
This hook is called by the engine whenever the player is moved into
|
||||
this room.
|
||||
"""
|
||||
if not character.has_account:
|
||||
# only act on player characters.
|
||||
if not character.has_account or character.ndb.batch_batchmode:
|
||||
# only act on player characters or when not building.
|
||||
return
|
||||
|
||||
# determine if the puzzle is a success or not
|
||||
is_success = str(character.db.puzzle_clue) == str(self.db.puzzle_value)
|
||||
teleport_to = self.db.success_teleport_to if is_success else self.db.failure_teleport_to
|
||||
|
|
@ -1180,6 +1198,10 @@ class OutroRoom(TutorialRoom):
|
|||
"""
|
||||
Do cleanup.
|
||||
"""
|
||||
if character.ndb.batch_batchmode:
|
||||
# currently running batchcommand
|
||||
return
|
||||
|
||||
if character.has_account:
|
||||
del character.db.health_max
|
||||
del character.db.health
|
||||
|
|
|
|||
|
|
@ -120,6 +120,9 @@ class FileHelpEntry:
|
|||
def __repr__(self):
|
||||
return f"<FileHelpEntry {self.key}>"
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.key)
|
||||
|
||||
@lazy_property
|
||||
def locks(self):
|
||||
return LockHandler(self)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ command test-suite).
|
|||
"""
|
||||
|
||||
from unittest import mock
|
||||
from parameterized import parameterized
|
||||
|
||||
from evennia.help import filehelp
|
||||
from evennia.help import utils as help_utils
|
||||
|
|
@ -140,3 +141,56 @@ class TestFileHelp(TestCase):
|
|||
self.assertEqual(HELP_ENTRY_DICTS[inum].get("aliases", []), helpentry.aliases)
|
||||
self.assertEqual(HELP_ENTRY_DICTS[inum]["category"].lower(), helpentry.help_category)
|
||||
self.assertEqual(HELP_ENTRY_DICTS[inum]["text"], helpentry.entrytext)
|
||||
|
||||
|
||||
class HelpUtils(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.candidate_entries = [
|
||||
filehelp.FileHelpEntry(
|
||||
key="*examine",
|
||||
aliases=["*exam", "*ex", "@examine"],
|
||||
help_category="building",
|
||||
entrytext="Lorem ipsum examine",
|
||||
lock_storage="",
|
||||
),
|
||||
filehelp.FileHelpEntry(
|
||||
key="inventory",
|
||||
aliases=[],
|
||||
help_category="general",
|
||||
entrytext="A character's inventory",
|
||||
lock_storage="",
|
||||
),
|
||||
filehelp.FileHelpEntry(
|
||||
key="userpassword",
|
||||
aliases=[],
|
||||
help_category="admin",
|
||||
entrytext="change the password of an account",
|
||||
lock_storage="",
|
||||
),
|
||||
]
|
||||
|
||||
@parameterized.expand(
|
||||
[
|
||||
("*examine", "*examine", "Leading wildcard should return exact matches."),
|
||||
("@examine", "*examine", "Aliases should return an entry."),
|
||||
("inventory", "inventory", "It should return exact matches."),
|
||||
("inv*", "inventory", "Trailing wildcard search should return an entry."),
|
||||
("userpaZZword~2", "userpassword", "Fuzzy matching should return an entry."),
|
||||
(
|
||||
"*word",
|
||||
"userpassword",
|
||||
"Leading wildcard should return an entry when no exact match.",
|
||||
),
|
||||
]
|
||||
)
|
||||
def test_help_search_with_index(self, search_term, expected_entry_key, error_msg):
|
||||
"""Test search terms return correct entries"""
|
||||
|
||||
expected_entry = [
|
||||
entry for entry in self.candidate_entries if entry.key == expected_entry_key
|
||||
]
|
||||
|
||||
entries, _ = help_utils.help_search_with_index(search_term, self.candidate_entries)
|
||||
|
||||
self.assertEqual(entries, expected_entry, error_msg)
|
||||
|
|
|
|||
|
|
@ -9,26 +9,8 @@ This is used primarily by the default `help` command.
|
|||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from lunr.stemmer import stemmer
|
||||
|
||||
# these are words that Lunr normally ignores but which we want to find
|
||||
# since we use them (e.g. as command names).
|
||||
# Lunr's default ignore-word list is found here:
|
||||
# https://github.com/yeraydiazdiaz/lunr.py/blob/master/lunr/stop_word_filter.py
|
||||
_LUNR_STOP_WORD_FILTER_EXCEPTIONS = [
|
||||
"about",
|
||||
"might",
|
||||
"get",
|
||||
"who",
|
||||
"say",
|
||||
"where",
|
||||
] + settings.LUNR_STOP_WORD_FILTER_EXCEPTIONS
|
||||
|
||||
|
||||
_LUNR = None
|
||||
_LUNR_EXCEPTION = None
|
||||
|
||||
_LUNR_GET_BUILDER = None
|
||||
_LUNR_BUILDER_PIPELINE = None
|
||||
|
||||
_RE_HELP_SUBTOPICS_START = re.compile(r"^\s*?#\s*?subtopics\s*?$", re.I + re.M)
|
||||
_RE_HELP_SUBTOPIC_SPLIT = re.compile(r"^\s*?(\#{2,6}\s*?\w+?[a-z0-9 \-\?!,\.]*?)$", re.M + re.I)
|
||||
|
|
@ -37,6 +19,123 @@ _RE_HELP_SUBTOPIC_PARSE = re.compile(r"^(?P<nesting>\#{2,6})\s*?(?P<name>.*?)$",
|
|||
MAX_SUBTOPIC_NESTING = 5
|
||||
|
||||
|
||||
def wildcard_stemmer(token, i, tokens):
|
||||
"""
|
||||
Custom LUNR stemmer that returns both the original and stemmed token
|
||||
if the token contains a leading wildcard (*).
|
||||
|
||||
Args:
|
||||
token (str): The input token to be stemmed
|
||||
i (int): Index of current token. Unused here but required by LUNR.
|
||||
tokens (list): List of tokens being processed. Unused here but required by LUNR.
|
||||
|
||||
Returns:
|
||||
list: A list containing the stemmed tokens and original token if it has leading '*'.
|
||||
"""
|
||||
|
||||
original_token = token.clone()
|
||||
# Then apply the standard Lunr stemmer
|
||||
stemmed_token = stemmer(token)
|
||||
|
||||
if original_token.string.startswith("*"):
|
||||
# Return both tokens
|
||||
return [original_token, stemmed_token]
|
||||
return stemmed_token
|
||||
|
||||
|
||||
class LunrSearch:
|
||||
"""
|
||||
Singleton class for managing Lunr search index configuration and initialization.
|
||||
"""
|
||||
|
||||
# these are words that Lunr normally ignores but which we want to find
|
||||
# since we use them (e.g. as command names).
|
||||
# Lunr's default ignore-word list is found here:
|
||||
# https://github.com/yeraydiazdiaz/lunr.py/blob/master/lunr/stop_word_filter.py
|
||||
_LUNR_STOP_WORD_FILTER_EXCEPTIONS = [
|
||||
"about",
|
||||
"might",
|
||||
"get",
|
||||
"who",
|
||||
"say",
|
||||
"where",
|
||||
] + settings.LUNR_STOP_WORD_FILTER_EXCEPTIONS
|
||||
|
||||
_instance = None
|
||||
|
||||
def __new__(cls):
|
||||
"""
|
||||
Ensure only one instance of the class is created (Singleton)
|
||||
"""
|
||||
if not cls._instance:
|
||||
cls._instance = super(LunrSearch, cls).__new__(cls)
|
||||
cls._instance._initialize()
|
||||
return cls._instance
|
||||
|
||||
def _initialize(self):
|
||||
"""
|
||||
Lazy load Lunr libraries and set up custom configuration
|
||||
|
||||
we have to delay-load lunr because it messes with logging if it's imported
|
||||
before twisted's logging has been set up
|
||||
"""
|
||||
# Lunr-related imports
|
||||
from lunr import get_default_builder
|
||||
from lunr import lunr
|
||||
from lunr import stop_word_filter
|
||||
from lunr.exceptions import QueryParseError
|
||||
from lunr.stemmer import stemmer
|
||||
from lunr.pipeline import Pipeline
|
||||
|
||||
# Store imported modules as instance attributes
|
||||
self.get_default_builder = get_default_builder
|
||||
self.lunr = lunr
|
||||
self.stop_word_filter = stop_word_filter
|
||||
self.QueryParseError = QueryParseError
|
||||
self.default_stemmer = stemmer
|
||||
|
||||
self._setup_stop_words_filter()
|
||||
self.custom_builder_pipeline = (self.custom_stop_words_filter, wildcard_stemmer)
|
||||
|
||||
# Register custom stemmer if we want to serialize.
|
||||
Pipeline.register_function(wildcard_stemmer, "wildcard_stemmer")
|
||||
|
||||
def _setup_stop_words_filter(self):
|
||||
"""
|
||||
Create a custom stop words filter, removing specified exceptions
|
||||
"""
|
||||
stop_words = self.stop_word_filter.WORDS.copy()
|
||||
|
||||
for ignore_word in self._LUNR_STOP_WORD_FILTER_EXCEPTIONS:
|
||||
try:
|
||||
stop_words.remove(ignore_word)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
self.custom_stop_words_filter = self.stop_word_filter.generate_stop_word_filter(stop_words)
|
||||
|
||||
def index(self, ref, fields, documents):
|
||||
"""
|
||||
Creates a Lunr searchable index.
|
||||
|
||||
Args:
|
||||
ref (str): Unique identifier field within a document
|
||||
fields (list): A list of Lunr field mappings
|
||||
``{"field_name": str, "boost": int}``. See the Lunr documentation
|
||||
for more details.
|
||||
documents (list[dict]): This is the body of possible entities to search.
|
||||
Each dict should have all keys in the `fields` arg.
|
||||
Returns: A lunr.Index object
|
||||
"""
|
||||
|
||||
# Create and configure builder
|
||||
builder = self.get_default_builder()
|
||||
builder.pipeline.reset()
|
||||
builder.pipeline.add(*self.custom_builder_pipeline)
|
||||
|
||||
return self.lunr(ref, fields, documents, builder=builder)
|
||||
|
||||
|
||||
def help_search_with_index(query, candidate_entries, suggestion_maxnum=5, fields=None):
|
||||
"""
|
||||
Lunr-powered fast index search and suggestion wrapper. See https://lunrjs.com/.
|
||||
|
|
@ -57,31 +156,7 @@ def help_search_with_index(query, candidate_entries, suggestion_maxnum=5, fields
|
|||
how many suggestions are included.
|
||||
|
||||
"""
|
||||
global _LUNR, _LUNR_EXCEPTION, _LUNR_BUILDER_PIPELINE, _LUNR_GET_BUILDER
|
||||
if not _LUNR:
|
||||
# we have to delay-load lunr because it messes with logging if it's imported
|
||||
# before twisted's logging has been set up
|
||||
from lunr import get_default_builder as _LUNR_GET_BUILDER
|
||||
from lunr import lunr as _LUNR
|
||||
from lunr import stop_word_filter
|
||||
from lunr.exceptions import QueryParseError as _LUNR_EXCEPTION
|
||||
from lunr.stemmer import stemmer
|
||||
|
||||
# from lunr.trimmer import trimmer
|
||||
# pre-create a lunr index-builder pipeline where we've removed some of
|
||||
# the stop-words from the default in lunr.
|
||||
|
||||
stop_words = stop_word_filter.WORDS
|
||||
|
||||
for ignore_word in _LUNR_STOP_WORD_FILTER_EXCEPTIONS:
|
||||
try:
|
||||
stop_words.remove(ignore_word)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
custom_stop_words_filter = stop_word_filter.generate_stop_word_filter(stop_words)
|
||||
# _LUNR_BUILDER_PIPELINE = (trimmer, custom_stop_words_filter, stemmer)
|
||||
_LUNR_BUILDER_PIPELINE = (custom_stop_words_filter, stemmer)
|
||||
from lunr.exceptions import QueryParseError
|
||||
|
||||
indx = [cnd.search_index_entry for cnd in candidate_entries]
|
||||
mapping = {indx[ix]["key"]: cand for ix, cand in enumerate(candidate_entries)}
|
||||
|
|
@ -94,16 +169,13 @@ def help_search_with_index(query, candidate_entries, suggestion_maxnum=5, fields
|
|||
{"field_name": "tags", "boost": 5},
|
||||
]
|
||||
|
||||
# build the search index
|
||||
builder = _LUNR_GET_BUILDER()
|
||||
builder.pipeline.reset()
|
||||
builder.pipeline.add(*_LUNR_BUILDER_PIPELINE)
|
||||
lunr_search = LunrSearch()
|
||||
|
||||
search_index = _LUNR(ref="key", fields=fields, documents=indx, builder=builder)
|
||||
search_index = lunr_search.index(ref="key", fields=fields, documents=indx)
|
||||
|
||||
try:
|
||||
matches = search_index.search(query)[:suggestion_maxnum]
|
||||
except _LUNR_EXCEPTION:
|
||||
except QueryParseError:
|
||||
# this is a user-input problem
|
||||
matches = []
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -322,7 +322,7 @@ class ObjectDBManager(TypedObjectManager):
|
|||
)
|
||||
|
||||
# convert search term to partial-match regex
|
||||
search_regex = r".* ".join(re.escape(word) for word in ostring.split()) + r'.*'
|
||||
search_regex = r".* ".join(r"\b" + re.escape(word) for word in ostring.split()) + r'.*'
|
||||
|
||||
# do the fuzzy search and return whatever it matches
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ class ContentsHandler:
|
|||
|
||||
"""
|
||||
objects = self.load()
|
||||
self._typecache = defaultdict(dict)
|
||||
self._pkcache = {obj.pk: True for obj in objects}
|
||||
for obj in objects:
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -231,6 +231,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
has_account (bool, read-only) - True is this object has an associated account.
|
||||
is_superuser (bool, read-only): True if this object has an account and that
|
||||
account is a superuser.
|
||||
plural_category (string) - Alias category for the plural strings of this object
|
||||
|
||||
* Handlers available
|
||||
|
||||
|
|
@ -382,6 +383,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
|
||||
at_look(target, **kwargs)
|
||||
at_desc(looker=None)
|
||||
at_rename(oldname, newname)
|
||||
|
||||
|
||||
"""
|
||||
|
|
@ -397,6 +399,9 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
|
||||
objects = ObjectManager()
|
||||
|
||||
# Used by get_display_desc when self.db.desc is None
|
||||
default_description = _("You see nothing special.")
|
||||
|
||||
# populated by `return_appearance`
|
||||
appearance_template = """
|
||||
{header}
|
||||
|
|
@ -407,6 +412,8 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
{things}
|
||||
{footer}
|
||||
"""
|
||||
|
||||
plural_category = "plural_key"
|
||||
# on-object properties
|
||||
|
||||
@lazy_property
|
||||
|
|
@ -545,11 +552,13 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
|
||||
"""
|
||||
if isinstance(searchdata, str):
|
||||
candidates = kwargs.get("candidates") or []
|
||||
global_search = kwargs.get("global_search", False)
|
||||
match searchdata.lower():
|
||||
case "me" | "self":
|
||||
return True, self
|
||||
return global_search or self in candidates, self
|
||||
case "here":
|
||||
return True, self.location
|
||||
return global_search or self.location in candidates, self.location
|
||||
return False, searchdata
|
||||
|
||||
def get_search_candidates(self, searchdata, **kwargs):
|
||||
|
|
@ -829,8 +838,14 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
# replace incoming searchdata string with a potentially modified version
|
||||
searchdata = self.get_search_query_replacement(searchdata, **input_kwargs)
|
||||
|
||||
# get candidates
|
||||
candidates = self.get_search_candidates(searchdata, **input_kwargs)
|
||||
|
||||
# handle special input strings, like "me" or "here".
|
||||
should_return, searchdata = self.get_search_direct_match(searchdata, **input_kwargs)
|
||||
# we also want to include the identified candidates here instead of input, to account for defaults
|
||||
should_return, searchdata = self.get_search_direct_match(
|
||||
searchdata, **(input_kwargs | {"candidates": candidates})
|
||||
)
|
||||
if should_return:
|
||||
# we got an actual result, return it immediately
|
||||
return [searchdata] if quiet else searchdata
|
||||
|
|
@ -850,9 +865,6 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
# always use exact match for dbref/global searches
|
||||
exact = True if global_search or dbref(searchdata) else exact
|
||||
|
||||
# get candidates
|
||||
candidates = self.get_search_candidates(searchdata, **input_kwargs)
|
||||
|
||||
# do the actual search
|
||||
results = self.get_search_result(
|
||||
searchdata,
|
||||
|
|
@ -1464,10 +1476,9 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
if account:
|
||||
obj.db.creator_id = account.id
|
||||
|
||||
# Set description if there is none, or update it if provided
|
||||
if description or not obj.db.desc:
|
||||
desc = description if description else "You see nothing special."
|
||||
obj.db.desc = desc
|
||||
# Set description if provided
|
||||
if description:
|
||||
obj.db.desc = description
|
||||
|
||||
except Exception as e:
|
||||
errors.append(f"An error occurred while creating this '{key}' object: {e}")
|
||||
|
|
@ -1693,7 +1704,6 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
obj.get_numbered_name(1, looker, key="Foobert", return_string=True, no_article=True)
|
||||
-> "Foobert"
|
||||
"""
|
||||
plural_category = "plural_key"
|
||||
key = kwargs.get("key", self.get_display_name(looker))
|
||||
raw_key = self.name
|
||||
key = ansi.ANSIString(key) # this is needed to allow inflection of colored names
|
||||
|
|
@ -1704,13 +1714,13 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
# this is raised by inflect if the input is not a proper noun
|
||||
plural = key
|
||||
singular = _INFLECT.an(key)
|
||||
if not self.aliases.get(plural, category=plural_category):
|
||||
if not self.aliases.get(plural, category=self.plural_category):
|
||||
# we need to wipe any old plurals/an/a in case key changed in the interrim
|
||||
self.aliases.clear(category=plural_category)
|
||||
self.aliases.add(plural, category=plural_category)
|
||||
self.aliases.clear(category=self.plural_category)
|
||||
self.aliases.add(plural, category=self.plural_category)
|
||||
# save the singular form as an alias here too so we can display "an egg" and also
|
||||
# look at 'an egg'.
|
||||
self.aliases.add(singular, category=plural_category)
|
||||
self.aliases.add(singular, category=self.plural_category)
|
||||
|
||||
if kwargs.get("no_article") and count == 1:
|
||||
if kwargs.get("return_string"):
|
||||
|
|
@ -1746,7 +1756,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
str: The desc display string.
|
||||
|
||||
"""
|
||||
return self.db.desc or "You see nothing special."
|
||||
return self.db.desc or self.default_description
|
||||
|
||||
def get_display_exits(self, looker, **kwargs):
|
||||
"""
|
||||
|
|
@ -1928,7 +1938,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
|
||||
if hasattr(self, "_createdict"):
|
||||
# this will be set if the object was created by the utils.create function
|
||||
# or the spawner. We want these kwargs to override the values set by
|
||||
# or the spawner. We want these kwargs to override the values set by
|
||||
# the initial hooks.
|
||||
cdict = self._createdict
|
||||
updates = []
|
||||
|
|
@ -1972,7 +1982,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
self.nattributes.add(key, value)
|
||||
|
||||
del self._createdict
|
||||
|
||||
|
||||
# run the post-setup hook
|
||||
self.at_object_post_creation()
|
||||
|
||||
|
|
@ -2055,7 +2065,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
"""
|
||||
Called when this object is spawned or updated from a prototype, after all other
|
||||
hooks have been run.
|
||||
|
||||
|
||||
Keyword Args:
|
||||
prototype (dict): The prototype that was used to spawn or update this object.
|
||||
"""
|
||||
|
|
@ -2980,6 +2990,19 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
mapping=location_mapping,
|
||||
)
|
||||
|
||||
def at_rename(self, oldname, newname):
|
||||
"""
|
||||
This Hook is called by @name on a successful rename.
|
||||
|
||||
Args:
|
||||
oldname (str): The instance's original name.
|
||||
newname (str): The new name for the instance.
|
||||
|
||||
"""
|
||||
|
||||
# Clear plural aliases set by DefaultObject.get_numbered_name
|
||||
self.aliases.clear(category=self.plural_category)
|
||||
|
||||
|
||||
#
|
||||
# Base Character object
|
||||
|
|
@ -3004,6 +3027,9 @@ class DefaultCharacter(DefaultObject):
|
|||
"edit:pid({account_id}) or perm(Admin)"
|
||||
)
|
||||
|
||||
# Used by get_display_desc when self.db.desc is None
|
||||
default_description = _("This is a character.")
|
||||
|
||||
@classmethod
|
||||
def get_default_lockstring(
|
||||
cls, account: "DefaultAccount" = None, caller: "DefaultObject" = None, **kwargs
|
||||
|
|
@ -3117,9 +3143,9 @@ class DefaultCharacter(DefaultObject):
|
|||
if locks:
|
||||
obj.locks.add(locks)
|
||||
|
||||
# If no description is set, set a default description
|
||||
if description or not obj.db.desc:
|
||||
obj.db.desc = description if description else _("This is a character.")
|
||||
# Set description if provided
|
||||
if description:
|
||||
obj.db.desc = description
|
||||
|
||||
except Exception as e:
|
||||
errors.append(f"An error occurred while creating object '{key} object: {e}")
|
||||
|
|
@ -3330,6 +3356,9 @@ class DefaultRoom(DefaultObject):
|
|||
# Generally, a room isn't expected to HAVE a location, but maybe in some games?
|
||||
_content_types = ("room",)
|
||||
|
||||
# Used by get_display_desc when self.db.desc is None
|
||||
default_description = _("This is a room.")
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
|
|
@ -3400,9 +3429,9 @@ class DefaultRoom(DefaultObject):
|
|||
if account:
|
||||
obj.db.creator_id = account.id
|
||||
|
||||
# If no description is set, set a default description
|
||||
if description or not obj.db.desc:
|
||||
obj.db.desc = description if description else _("This is a room.")
|
||||
# Set description if provided
|
||||
if description:
|
||||
obj.db.desc = description
|
||||
|
||||
except Exception as e:
|
||||
errors.append(f"An error occurred while creating this '{key}' object: {e}")
|
||||
|
|
@ -3495,6 +3524,9 @@ class DefaultExit(DefaultObject):
|
|||
exit_command = ExitCommand
|
||||
priority = 101
|
||||
|
||||
# Used by get_display_desc when self.db.desc is None
|
||||
default_description = _("This is an exit.")
|
||||
|
||||
# Helper classes and methods to implement the Exit. These need not
|
||||
# be overloaded unless one want to change the foundation for how
|
||||
# Exits work. See the end of the class for hook methods to overload.
|
||||
|
|
@ -3609,9 +3641,9 @@ class DefaultExit(DefaultObject):
|
|||
if account:
|
||||
obj.db.creator_id = account.id
|
||||
|
||||
# If no description is set, set a default description
|
||||
if description or not obj.db.desc:
|
||||
obj.db.desc = description if description else _("This is an exit.")
|
||||
# Set description if provided
|
||||
if description:
|
||||
obj.db.desc = description
|
||||
|
||||
except Exception as e:
|
||||
errors.append(f"An error occurred while creating this '{key}' object: {e}")
|
||||
|
|
|
|||
|
|
@ -30,6 +30,13 @@ class DefaultObjectTest(BaseEvenniaTest):
|
|||
self.assertEqual(obj.db.creator_ip, self.ip)
|
||||
self.assertEqual(obj.db_home, self.room1)
|
||||
|
||||
def test_object_default_description(self):
|
||||
obj, errors = DefaultObject.create("void")
|
||||
self.assertTrue(obj, errors)
|
||||
self.assertFalse(errors, errors)
|
||||
self.assertIsNone(obj.db.desc)
|
||||
self.assertEqual(obj.default_description, obj.get_display_desc(obj))
|
||||
|
||||
def test_character_create(self):
|
||||
description = "A furry green monster, reeking of garbage."
|
||||
home = self.room1.dbref
|
||||
|
|
@ -57,6 +64,13 @@ class DefaultObjectTest(BaseEvenniaTest):
|
|||
self.assertFalse(errors, errors)
|
||||
self.assertEqual(obj.name, "SigurXurXorarinsson")
|
||||
|
||||
def test_character_default_description(self):
|
||||
obj, errors = DefaultCharacter.create("dementor")
|
||||
self.assertTrue(obj, errors)
|
||||
self.assertFalse(errors, errors)
|
||||
self.assertIsNone(obj.db.desc)
|
||||
self.assertEqual(obj.default_description, obj.get_display_desc(obj))
|
||||
|
||||
def test_room_create(self):
|
||||
description = "A dimly-lit alley behind the local Chinese restaurant."
|
||||
obj, errors = DefaultRoom.create("alley", self.account, description=description, ip=self.ip)
|
||||
|
|
@ -65,6 +79,13 @@ class DefaultObjectTest(BaseEvenniaTest):
|
|||
self.assertEqual(description, obj.db.desc)
|
||||
self.assertEqual(obj.db.creator_ip, self.ip)
|
||||
|
||||
def test_room_default_description(self):
|
||||
obj, errors = DefaultRoom.create("black hole")
|
||||
self.assertTrue(obj, errors)
|
||||
self.assertFalse(errors, errors)
|
||||
self.assertIsNone(obj.db.desc)
|
||||
self.assertEqual(obj.default_description, obj.get_display_desc(obj))
|
||||
|
||||
def test_exit_create(self):
|
||||
description = (
|
||||
"The steaming depths of the dumpster, ripe with refuse in various states of"
|
||||
|
|
@ -78,6 +99,13 @@ class DefaultObjectTest(BaseEvenniaTest):
|
|||
self.assertEqual(description, obj.db.desc)
|
||||
self.assertEqual(obj.db.creator_ip, self.ip)
|
||||
|
||||
def test_exit_default_description(self):
|
||||
obj, errors = DefaultExit.create("the nothing")
|
||||
self.assertTrue(obj, errors)
|
||||
self.assertFalse(errors, errors)
|
||||
self.assertIsNone(obj.db.desc)
|
||||
self.assertEqual(obj.default_description, obj.get_display_desc(obj))
|
||||
|
||||
def test_exit_get_return_exit(self):
|
||||
ex1, _ = DefaultExit.create("north", self.room1, self.room2, account=self.account)
|
||||
single_return_exit = ex1.get_return_exit()
|
||||
|
|
@ -266,13 +294,37 @@ class TestObjectManager(BaseEvenniaTest):
|
|||
query = ObjectDB.objects.get_objs_with_key_or_alias("")
|
||||
self.assertFalse(query)
|
||||
query = ObjectDB.objects.get_objs_with_key_or_alias("", exact=False)
|
||||
self.assertEqual(list(query), list(ObjectDB.objects.all().order_by('id')))
|
||||
self.assertEqual(list(query), list(ObjectDB.objects.all().order_by("id")))
|
||||
|
||||
query = ObjectDB.objects.get_objs_with_key_or_alias(
|
||||
"", exact=False, typeclasses="evennia.objects.objects.DefaultCharacter"
|
||||
)
|
||||
self.assertEqual(list(query), [self.char1, self.char2])
|
||||
|
||||
def test_key_alias_search_partial_match(self):
|
||||
"""
|
||||
verify that get_objs_with_key_or_alias will partial match the first part of
|
||||
any words in the name, when given in the correct order
|
||||
"""
|
||||
self.obj1.key = "big sword"
|
||||
self.obj2.key = "shiny sword"
|
||||
|
||||
# beginning of "sword", should match both
|
||||
query = ObjectDB.objects.get_objs_with_key_or_alias("sw", exact=False)
|
||||
self.assertEqual(list(query), [self.obj1, self.obj2])
|
||||
|
||||
# middle of "sword", should NOT match
|
||||
query = ObjectDB.objects.get_objs_with_key_or_alias("wor", exact=False)
|
||||
self.assertEqual(list(query), [])
|
||||
|
||||
# beginning of "big" then "sword", should match obj1
|
||||
query = ObjectDB.objects.get_objs_with_key_or_alias("b sw", exact=False)
|
||||
self.assertEqual(list(query), [self.obj1])
|
||||
|
||||
# beginning of "sword" then "big", should NOT match
|
||||
query = ObjectDB.objects.get_objs_with_key_or_alias("sw b", exact=False)
|
||||
self.assertEqual(list(query), [])
|
||||
|
||||
def test_search_object(self):
|
||||
self.char1.tags.add("test tag")
|
||||
self.obj1.tags.add("test tag")
|
||||
|
|
|
|||
|
|
@ -398,6 +398,10 @@ class OnDemandHandler:
|
|||
Save the on-demand timers to ServerConfig storage. Should be called when Evennia shuts down.
|
||||
|
||||
"""
|
||||
for key, category in list(self.tasks.keys()):
|
||||
# in case an object was used for categories, and were since deleted, drop the task
|
||||
if hasattr(category, "id") and category.id is None:
|
||||
self.tasks.pop((key, category))
|
||||
ServerConfig.objects.conf(ONDEMAND_HANDLER_SAVE_NAME, self.tasks)
|
||||
|
||||
def _build_key(self, key, category):
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ This protocol is implemented by the telnet protocol importing
|
|||
mccp_compress and calling it from its write methods.
|
||||
"""
|
||||
|
||||
import weakref
|
||||
import zlib
|
||||
|
||||
# negotiations for v1 and v2 of the protocol
|
||||
|
|
@ -57,10 +58,10 @@ class Mccp:
|
|||
|
||||
"""
|
||||
|
||||
self.protocol = protocol
|
||||
self.protocol.protocol_flags["MCCP"] = False
|
||||
self.protocol = weakref.ref(protocol)
|
||||
self.protocol().protocol_flags["MCCP"] = False
|
||||
# ask if client will mccp, connect callbacks to handle answer
|
||||
self.protocol.will(MCCP).addCallbacks(self.do_mccp, self.no_mccp)
|
||||
self.protocol().will(MCCP).addCallbacks(self.do_mccp, self.no_mccp)
|
||||
|
||||
def no_mccp(self, option):
|
||||
"""
|
||||
|
|
@ -70,10 +71,10 @@ class Mccp:
|
|||
option (Option): Option dict (not used).
|
||||
|
||||
"""
|
||||
if hasattr(self.protocol, "zlib"):
|
||||
del self.protocol.zlib
|
||||
self.protocol.protocol_flags["MCCP"] = False
|
||||
self.protocol.handshake_done()
|
||||
if hasattr(self.protocol(), "zlib"):
|
||||
del self.protocol().zlib
|
||||
self.protocol().protocol_flags["MCCP"] = False
|
||||
self.protocol().handshake_done()
|
||||
|
||||
def do_mccp(self, option):
|
||||
"""
|
||||
|
|
@ -84,7 +85,7 @@ class Mccp:
|
|||
option (Option): Option dict (not used).
|
||||
|
||||
"""
|
||||
self.protocol.protocol_flags["MCCP"] = True
|
||||
self.protocol.requestNegotiation(MCCP, b"")
|
||||
self.protocol.zlib = zlib.compressobj(9)
|
||||
self.protocol.handshake_done()
|
||||
self.protocol().protocol_flags["MCCP"] = True
|
||||
self.protocol().requestNegotiation(MCCP, b"")
|
||||
self.protocol().zlib = zlib.compressobj(9)
|
||||
self.protocol().handshake_done()
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ active players and so on.
|
|||
|
||||
"""
|
||||
|
||||
import weakref
|
||||
from django.conf import settings
|
||||
|
||||
from evennia.utils import utils
|
||||
|
|
@ -39,8 +40,8 @@ class Mssp:
|
|||
protocol (Protocol): The active protocol instance.
|
||||
|
||||
"""
|
||||
self.protocol = protocol
|
||||
self.protocol.will(MSSP).addCallbacks(self.do_mssp, self.no_mssp)
|
||||
self.protocol = weakref.ref(protocol)
|
||||
self.protocol().will(MSSP).addCallbacks(self.do_mssp, self.no_mssp)
|
||||
|
||||
def get_player_count(self):
|
||||
"""
|
||||
|
|
@ -50,7 +51,7 @@ class Mssp:
|
|||
count (int): The number of players in the MUD.
|
||||
|
||||
"""
|
||||
return str(self.protocol.sessionhandler.count_loggedin())
|
||||
return str(self.protocol().sessionhandler.count_loggedin())
|
||||
|
||||
def get_uptime(self):
|
||||
"""
|
||||
|
|
@ -60,7 +61,7 @@ class Mssp:
|
|||
uptime (int): Number of seconds of uptime.
|
||||
|
||||
"""
|
||||
return str(self.protocol.sessionhandler.uptime)
|
||||
return str(self.protocol().sessionhandler.uptime)
|
||||
|
||||
def no_mssp(self, option):
|
||||
"""
|
||||
|
|
@ -71,7 +72,7 @@ class Mssp:
|
|||
option (Option): Not used.
|
||||
|
||||
"""
|
||||
self.protocol.handshake_done()
|
||||
self.protocol().handshake_done()
|
||||
|
||||
def do_mssp(self, option):
|
||||
"""
|
||||
|
|
@ -132,5 +133,5 @@ class Mssp:
|
|||
)
|
||||
|
||||
# send to crawler by subnegotiation
|
||||
self.protocol.requestNegotiation(MSSP, varlist)
|
||||
self.protocol.handshake_done()
|
||||
self.protocol().requestNegotiation(MSSP, varlist)
|
||||
self.protocol().handshake_done()
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ http://www.gammon.com.au/mushclient/addingservermxp.htm
|
|||
"""
|
||||
|
||||
import re
|
||||
import weakref
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
|
@ -61,10 +62,10 @@ class Mxp:
|
|||
protocol (Protocol): The active protocol instance.
|
||||
|
||||
"""
|
||||
self.protocol = protocol
|
||||
self.protocol.protocol_flags["MXP"] = False
|
||||
self.protocol = weakref.ref(protocol)
|
||||
self.protocol().protocol_flags["MXP"] = False
|
||||
if settings.MXP_ENABLED:
|
||||
self.protocol.will(MXP).addCallbacks(self.do_mxp, self.no_mxp)
|
||||
self.protocol().will(MXP).addCallbacks(self.do_mxp, self.no_mxp)
|
||||
|
||||
def no_mxp(self, option):
|
||||
"""
|
||||
|
|
@ -74,8 +75,8 @@ class Mxp:
|
|||
option (Option): Not used.
|
||||
|
||||
"""
|
||||
self.protocol.protocol_flags["MXP"] = False
|
||||
self.protocol.handshake_done()
|
||||
self.protocol().protocol_flags["MXP"] = False
|
||||
self.protocol().handshake_done()
|
||||
|
||||
def do_mxp(self, option):
|
||||
"""
|
||||
|
|
@ -86,8 +87,8 @@ class Mxp:
|
|||
|
||||
"""
|
||||
if settings.MXP_ENABLED:
|
||||
self.protocol.protocol_flags["MXP"] = True
|
||||
self.protocol.requestNegotiation(MXP, b"")
|
||||
self.protocol().protocol_flags["MXP"] = True
|
||||
self.protocol().requestNegotiation(MXP, b"")
|
||||
else:
|
||||
self.protocol.wont(MXP)
|
||||
self.protocol.handshake_done()
|
||||
self.protocol().wont(MXP)
|
||||
self.protocol().handshake_done()
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ client and update it when the size changes
|
|||
"""
|
||||
|
||||
from codecs import encode as codecs_encode
|
||||
import weakref
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
|
@ -41,13 +42,13 @@ class Naws:
|
|||
|
||||
"""
|
||||
self.naws_step = 0
|
||||
self.protocol = protocol
|
||||
self.protocol.protocol_flags["SCREENWIDTH"] = {
|
||||
self.protocol = weakref.ref(protocol)
|
||||
self.protocol().protocol_flags["SCREENWIDTH"] = {
|
||||
0: DEFAULT_WIDTH
|
||||
} # windowID (0 is root):width
|
||||
self.protocol.protocol_flags["SCREENHEIGHT"] = {0: DEFAULT_HEIGHT} # windowID:width
|
||||
self.protocol.negotiationMap[NAWS] = self.negotiate_sizes
|
||||
self.protocol.do(NAWS).addCallbacks(self.do_naws, self.no_naws)
|
||||
self.protocol().protocol_flags["SCREENHEIGHT"] = {0: DEFAULT_HEIGHT} # windowID:width
|
||||
self.protocol().negotiationMap[NAWS] = self.negotiate_sizes
|
||||
self.protocol().do(NAWS).addCallbacks(self.do_naws, self.no_naws)
|
||||
|
||||
def no_naws(self, option):
|
||||
"""
|
||||
|
|
@ -58,8 +59,8 @@ class Naws:
|
|||
option (Option): Not used.
|
||||
|
||||
"""
|
||||
self.protocol.protocol_flags["AUTORESIZE"] = False
|
||||
self.protocol.handshake_done()
|
||||
self.protocol().protocol_flags["AUTORESIZE"] = False
|
||||
self.protocol().handshake_done()
|
||||
|
||||
def do_naws(self, option):
|
||||
"""
|
||||
|
|
@ -69,8 +70,8 @@ class Naws:
|
|||
option (Option): Not used.
|
||||
|
||||
"""
|
||||
self.protocol.protocol_flags["AUTORESIZE"] = True
|
||||
self.protocol.handshake_done()
|
||||
self.protocol().protocol_flags["AUTORESIZE"] = True
|
||||
self.protocol().handshake_done()
|
||||
|
||||
def negotiate_sizes(self, options):
|
||||
"""
|
||||
|
|
@ -83,6 +84,6 @@ class Naws:
|
|||
if len(options) == 4:
|
||||
# NAWS is negotiated with 16bit words
|
||||
width = options[0] + options[1]
|
||||
self.protocol.protocol_flags["SCREENWIDTH"][0] = int(codecs_encode(width, "hex"), 16)
|
||||
self.protocol().protocol_flags["SCREENWIDTH"][0] = int(codecs_encode(width, "hex"), 16)
|
||||
height = options[2] + options[3]
|
||||
self.protocol.protocol_flags["SCREENHEIGHT"][0] = int(codecs_encode(height, "hex"), 16)
|
||||
self.protocol().protocol_flags["SCREENHEIGHT"][0] = int(codecs_encode(height, "hex"), 16)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ http://www.faqs.org/rfcs/rfc858.html
|
|||
|
||||
"""
|
||||
|
||||
import weakref
|
||||
|
||||
SUPPRESS_GA = bytes([3]) # b"\x03"
|
||||
|
||||
# default taken from telnet specification
|
||||
|
|
@ -36,14 +38,14 @@ class SuppressGA:
|
|||
protocol (Protocol): The active protocol instance.
|
||||
|
||||
"""
|
||||
self.protocol = protocol
|
||||
self.protocol = weakref.ref(protocol)
|
||||
|
||||
self.protocol.protocol_flags["NOGOAHEAD"] = True
|
||||
self.protocol.protocol_flags["NOPROMPTGOAHEAD"] = (
|
||||
self.protocol().protocol_flags["NOGOAHEAD"] = True
|
||||
self.protocol().protocol_flags["NOPROMPTGOAHEAD"] = (
|
||||
True # Used to send a GA after a prompt line only, set in TTYPE (per client)
|
||||
)
|
||||
# tell the client that we prefer to suppress GA ...
|
||||
self.protocol.will(SUPPRESS_GA).addCallbacks(self.will_suppress_ga, self.wont_suppress_ga)
|
||||
self.protocol().will(SUPPRESS_GA).addCallbacks(self.will_suppress_ga, self.wont_suppress_ga)
|
||||
|
||||
def wont_suppress_ga(self, option):
|
||||
"""
|
||||
|
|
@ -53,8 +55,8 @@ class SuppressGA:
|
|||
option (Option): Not used.
|
||||
|
||||
"""
|
||||
self.protocol.protocol_flags["NOGOAHEAD"] = False
|
||||
self.protocol.handshake_done()
|
||||
self.protocol().protocol_flags["NOGOAHEAD"] = False
|
||||
self.protocol().handshake_done()
|
||||
|
||||
def will_suppress_ga(self, option):
|
||||
"""
|
||||
|
|
@ -64,5 +66,5 @@ class SuppressGA:
|
|||
option (Option): Not used.
|
||||
|
||||
"""
|
||||
self.protocol.protocol_flags["NOGOAHEAD"] = True
|
||||
self.protocol.handshake_done()
|
||||
self.protocol().protocol_flags["NOGOAHEAD"] = True
|
||||
self.protocol().handshake_done()
|
||||
|
|
|
|||
|
|
@ -306,6 +306,8 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS):
|
|||
|
||||
"""
|
||||
self.sessionhandler.disconnect(self)
|
||||
if self.nop_keep_alive and self.nop_keep_alive.running:
|
||||
self.toggle_nop_keepalive()
|
||||
self.transport.loseConnection()
|
||||
|
||||
def applicationDataReceived(self, data):
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ This implements the following telnet OOB communication protocols:
|
|||
|
||||
import json
|
||||
import re
|
||||
import weakref
|
||||
|
||||
# General Telnet
|
||||
from twisted.conch.telnet import IAC, SB, SE
|
||||
|
|
@ -84,16 +85,16 @@ class TelnetOOB:
|
|||
protocol (Protocol): The active protocol.
|
||||
|
||||
"""
|
||||
self.protocol = protocol
|
||||
self.protocol.protocol_flags["OOB"] = False
|
||||
self.protocol = weakref.ref(protocol)
|
||||
self.protocol().protocol_flags["OOB"] = False
|
||||
self.MSDP = False
|
||||
self.GMCP = False
|
||||
# ask for the available protocols and assign decoders
|
||||
# (note that handshake_done() will be called twice!)
|
||||
self.protocol.negotiationMap[MSDP] = self.decode_msdp
|
||||
self.protocol.negotiationMap[GMCP] = self.decode_gmcp
|
||||
self.protocol.will(MSDP).addCallbacks(self.do_msdp, self.no_msdp)
|
||||
self.protocol.will(GMCP).addCallbacks(self.do_gmcp, self.no_gmcp)
|
||||
self.protocol().negotiationMap[MSDP] = self.decode_msdp
|
||||
self.protocol().negotiationMap[GMCP] = self.decode_gmcp
|
||||
self.protocol().will(MSDP).addCallbacks(self.do_msdp, self.no_msdp)
|
||||
self.protocol().will(GMCP).addCallbacks(self.do_gmcp, self.no_gmcp)
|
||||
self.oob_reported = {}
|
||||
|
||||
def no_msdp(self, option):
|
||||
|
|
@ -105,7 +106,7 @@ class TelnetOOB:
|
|||
|
||||
"""
|
||||
# no msdp, check GMCP
|
||||
self.protocol.handshake_done()
|
||||
self.protocol().handshake_done()
|
||||
|
||||
def do_msdp(self, option):
|
||||
"""
|
||||
|
|
@ -116,8 +117,8 @@ class TelnetOOB:
|
|||
|
||||
"""
|
||||
self.MSDP = True
|
||||
self.protocol.protocol_flags["OOB"] = True
|
||||
self.protocol.handshake_done()
|
||||
self.protocol().protocol_flags["OOB"] = True
|
||||
self.protocol().handshake_done()
|
||||
|
||||
def no_gmcp(self, option):
|
||||
"""
|
||||
|
|
@ -128,7 +129,7 @@ class TelnetOOB:
|
|||
option (Option): Not used.
|
||||
|
||||
"""
|
||||
self.protocol.handshake_done()
|
||||
self.protocol().handshake_done()
|
||||
|
||||
def do_gmcp(self, option):
|
||||
"""
|
||||
|
|
@ -139,8 +140,8 @@ class TelnetOOB:
|
|||
|
||||
"""
|
||||
self.GMCP = True
|
||||
self.protocol.protocol_flags["OOB"] = True
|
||||
self.protocol.handshake_done()
|
||||
self.protocol().protocol_flags["OOB"] = True
|
||||
self.protocol().handshake_done()
|
||||
|
||||
# encoders
|
||||
|
||||
|
|
@ -375,7 +376,7 @@ class TelnetOOB:
|
|||
cmds["msdp_{}".format(remap)] = cmds.pop(lower_case[remap])
|
||||
|
||||
# print("msdp data in:", cmds) # DEBUG
|
||||
self.protocol.data_in(**cmds)
|
||||
self.protocol().data_in(**cmds)
|
||||
|
||||
def decode_gmcp(self, data):
|
||||
"""
|
||||
|
|
@ -424,7 +425,7 @@ class TelnetOOB:
|
|||
if cmdname.lower().startswith(b"core_"):
|
||||
# if Core.cmdname, then use cmdname
|
||||
cmdname = cmdname[5:]
|
||||
self.protocol.data_in(**{cmdname.lower().decode(): [args, kwargs]})
|
||||
self.protocol().data_in(**{cmdname.lower().decode(): [args, kwargs]})
|
||||
|
||||
# access methods
|
||||
|
||||
|
|
@ -441,8 +442,8 @@ class TelnetOOB:
|
|||
|
||||
if self.MSDP:
|
||||
encoded_oob = self.encode_msdp(cmdname, *args, **kwargs)
|
||||
self.protocol._write(IAC + SB + MSDP + encoded_oob + IAC + SE)
|
||||
self.protocol()._write(IAC + SB + MSDP + encoded_oob + IAC + SE)
|
||||
|
||||
if self.GMCP:
|
||||
encoded_oob = self.encode_gmcp(cmdname, *args, **kwargs)
|
||||
self.protocol._write(IAC + SB + GMCP + encoded_oob + IAC + SE)
|
||||
self.protocol()._write(IAC + SB + GMCP + encoded_oob + IAC + SE)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ under the 'TTYPE' key.
|
|||
|
||||
"""
|
||||
|
||||
import weakref
|
||||
|
||||
# telnet option codes
|
||||
TTYPE = bytes([24]) # b"\x18"
|
||||
IS = bytes([0]) # b"\x00"
|
||||
|
|
@ -55,16 +57,16 @@ class Ttype:
|
|||
|
||||
"""
|
||||
self.ttype_step = 0
|
||||
self.protocol = protocol
|
||||
self.protocol = weakref.ref(protocol)
|
||||
# we set FORCEDENDLINE for clients not supporting ttype
|
||||
self.protocol.protocol_flags["FORCEDENDLINE"] = True
|
||||
self.protocol.protocol_flags["TTYPE"] = False
|
||||
self.protocol().protocol_flags["FORCEDENDLINE"] = True
|
||||
self.protocol().protocol_flags["TTYPE"] = False
|
||||
# is it a safe bet to assume ANSI is always supported?
|
||||
self.protocol.protocol_flags["ANSI"] = True
|
||||
self.protocol().protocol_flags["ANSI"] = True
|
||||
# setup protocol to handle ttype initialization and negotiation
|
||||
self.protocol.negotiationMap[TTYPE] = self.will_ttype
|
||||
self.protocol().negotiationMap[TTYPE] = self.will_ttype
|
||||
# ask if client will ttype, connect callback if it does.
|
||||
self.protocol.do(TTYPE).addCallbacks(self.will_ttype, self.wont_ttype)
|
||||
self.protocol().do(TTYPE).addCallbacks(self.will_ttype, self.wont_ttype)
|
||||
|
||||
def wont_ttype(self, option):
|
||||
"""
|
||||
|
|
@ -74,8 +76,8 @@ class Ttype:
|
|||
option (Option): Not used.
|
||||
|
||||
"""
|
||||
self.protocol.protocol_flags["TTYPE"] = False
|
||||
self.protocol.handshake_done()
|
||||
self.protocol().protocol_flags["TTYPE"] = False
|
||||
self.protocol().handshake_done()
|
||||
|
||||
def will_ttype(self, option):
|
||||
"""
|
||||
|
|
@ -91,7 +93,7 @@ class Ttype:
|
|||
stored on protocol.protocol_flags under the TTYPE key.
|
||||
|
||||
"""
|
||||
options = self.protocol.protocol_flags
|
||||
options = self.protocol().protocol_flags
|
||||
|
||||
if options and options.get("TTYPE", False) or self.ttype_step > 3:
|
||||
return
|
||||
|
|
@ -104,7 +106,7 @@ class Ttype:
|
|||
|
||||
if self.ttype_step == 0:
|
||||
# just start the request chain
|
||||
self.protocol.requestNegotiation(TTYPE, SEND)
|
||||
self.protocol().requestNegotiation(TTYPE, SEND)
|
||||
|
||||
elif self.ttype_step == 1:
|
||||
# this is supposed to be the name of the client/terminal.
|
||||
|
|
@ -125,9 +127,9 @@ class Ttype:
|
|||
xterm256 = clientname.split("MUDLET", 1)[1].strip() >= "1.1"
|
||||
# Mudlet likes GA's on a prompt line for the prompt trigger to
|
||||
# match, if it's not wanting NOGOAHEAD.
|
||||
if not self.protocol.protocol_flags["NOGOAHEAD"]:
|
||||
self.protocol.protocol_flags["NOGOAHEAD"] = True
|
||||
self.protocol.protocol_flags["NOPROMPTGOAHEAD"] = False
|
||||
if not self.protocol().protocol_flags["NOGOAHEAD"]:
|
||||
self.protocol().protocol_flags["NOGOAHEAD"] = True
|
||||
self.protocol().protocol_flags["NOPROMPTGOAHEAD"] = False
|
||||
|
||||
if (
|
||||
clientname.startswith("XTERM")
|
||||
|
|
@ -153,11 +155,11 @@ class Ttype:
|
|||
truecolor = True
|
||||
|
||||
# all clients supporting TTYPE at all seem to support ANSI
|
||||
self.protocol.protocol_flags["ANSI"] = True
|
||||
self.protocol.protocol_flags["XTERM256"] = xterm256
|
||||
self.protocol.protocol_flags["TRUECOLOR"] = truecolor
|
||||
self.protocol.protocol_flags["CLIENTNAME"] = clientname
|
||||
self.protocol.requestNegotiation(TTYPE, SEND)
|
||||
self.protocol().protocol_flags["ANSI"] = True
|
||||
self.protocol().protocol_flags["XTERM256"] = xterm256
|
||||
self.protocol().protocol_flags["TRUECOLOR"] = truecolor
|
||||
self.protocol().protocol_flags["CLIENTNAME"] = clientname
|
||||
self.protocol().requestNegotiation(TTYPE, SEND)
|
||||
|
||||
elif self.ttype_step == 2:
|
||||
# this is a term capabilities flag
|
||||
|
|
@ -170,11 +172,11 @@ class Ttype:
|
|||
and not tupper.endswith("-COLOR") # old Tintin, Putty
|
||||
)
|
||||
if xterm256:
|
||||
self.protocol.protocol_flags["ANSI"] = True
|
||||
self.protocol.protocol_flags["XTERM256"] = xterm256
|
||||
self.protocol.protocol_flags["TERM"] = term
|
||||
self.protocol().protocol_flags["ANSI"] = True
|
||||
self.protocol().protocol_flags["XTERM256"] = xterm256
|
||||
self.protocol().protocol_flags["TERM"] = term
|
||||
# request next information
|
||||
self.protocol.requestNegotiation(TTYPE, SEND)
|
||||
self.protocol().requestNegotiation(TTYPE, SEND)
|
||||
|
||||
elif self.ttype_step == 3:
|
||||
# the MTTS bitstring identifying term capabilities
|
||||
|
|
@ -186,12 +188,12 @@ class Ttype:
|
|||
support = dict(
|
||||
(capability, True) for bitval, capability in MTTS if option & bitval > 0
|
||||
)
|
||||
self.protocol.protocol_flags.update(support)
|
||||
self.protocol().protocol_flags.update(support)
|
||||
else:
|
||||
# some clients send erroneous MTTS as a string. Add directly.
|
||||
self.protocol.protocol_flags[option.upper()] = True
|
||||
self.protocol().protocol_flags[option.upper()] = True
|
||||
|
||||
self.protocol.protocol_flags["TTYPE"] = True
|
||||
self.protocol().protocol_flags["TTYPE"] = True
|
||||
# we must sync ttype once it'd done
|
||||
self.protocol.handshake_done()
|
||||
self.protocol().handshake_done()
|
||||
self.ttype_step += 1
|
||||
|
|
|
|||
|
|
@ -161,9 +161,6 @@ class ServerSession(_BASE_SESSION_CLASS):
|
|||
account = self.account
|
||||
if self.puppet:
|
||||
account.unpuppet_object(self)
|
||||
uaccount = account
|
||||
uaccount.last_login = timezone.now()
|
||||
uaccount.save()
|
||||
# calling account hook
|
||||
account.at_disconnect(reason)
|
||||
self.logged_in = False
|
||||
|
|
|
|||
|
|
@ -673,12 +673,6 @@ class EvenniaServerService(MultiService):
|
|||
shutdown or a reset.
|
||||
|
||||
"""
|
||||
# We need to do this just in case the server was killed in a way where
|
||||
# the normal cleanup operations did not have time to run.
|
||||
from evennia.objects.models import ObjectDB
|
||||
|
||||
ObjectDB.objects.clear_all_sessids()
|
||||
|
||||
# Remove non-persistent scripts
|
||||
from evennia.scripts.models import ScriptDB
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ from django.utils.safestring import SafeString
|
|||
import evennia
|
||||
from evennia.utils import logger
|
||||
from evennia.utils.utils import is_iter, to_bytes, uses_database
|
||||
from enum import IntFlag
|
||||
|
||||
__all__ = ("to_pickle", "from_pickle", "do_pickle", "do_unpickle", "dbserialize", "dbunserialize")
|
||||
|
||||
|
|
@ -672,6 +673,8 @@ def to_pickle(data):
|
|||
|
||||
if dtype in (str, int, float, bool, bytes, SafeString):
|
||||
return item
|
||||
elif isinstance(item, IntFlag):
|
||||
return item.value
|
||||
elif dtype == tuple:
|
||||
return tuple(process_item(val) for val in item)
|
||||
elif dtype in (list, _SaverList):
|
||||
|
|
|
|||
|
|
@ -731,7 +731,7 @@ class CmdEditorGroup(CmdEditorBase):
|
|||
+ " [f]ull (default), [c]enter, [r]right or [l]eft"
|
||||
)
|
||||
return
|
||||
align = align_map[self.arg1.lower()] if self.arg1 else "f"
|
||||
align = align_map[self.arg1.lower()] if self.arg1 else "l"
|
||||
width = _DEFAULT_WIDTH
|
||||
if self.arg2:
|
||||
value = self.arg2.lstrip("=")
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ class CmdMore(Command):
|
|||
Implement the command
|
||||
"""
|
||||
more = self.caller.ndb._more
|
||||
if not more and inherits_from(self.caller, evennia.DefaultObject):
|
||||
if not more and hasattr(self.caller, 'account') and self.caller.account:
|
||||
more = self.caller.account.ndb._more
|
||||
if not more:
|
||||
self.caller.msg("Error in loading the pager. Contact an admin.")
|
||||
|
|
@ -111,9 +111,13 @@ class CmdMoreExit(Command):
|
|||
def func(self):
|
||||
"""
|
||||
Exit pager and re-fire the failed command.
|
||||
|
||||
"""
|
||||
more = self.caller.ndb._more
|
||||
if not more and hasattr(self.caller, 'account') and self.caller.account:
|
||||
more = self.caller.account.ndb._more
|
||||
if not more:
|
||||
self.caller.msg("Error in exiting the pager. Contact an admin.")
|
||||
return
|
||||
more.page_quit()
|
||||
|
||||
# re-fire the command (in new cmdset)
|
||||
|
|
|
|||
|
|
@ -334,18 +334,20 @@ class FuncParser:
|
|||
infuncstr = "" # string parts inside the current level of $funcdef (including $)
|
||||
literal_infuncstr = False
|
||||
|
||||
for char in string:
|
||||
for ichar, char in enumerate(string):
|
||||
if escaped:
|
||||
# always store escaped characters verbatim
|
||||
if curr_func:
|
||||
infuncstr += char
|
||||
curr_func.rawstr += char
|
||||
else:
|
||||
fullstr += char
|
||||
escaped = False
|
||||
continue
|
||||
|
||||
if char == escape_char:
|
||||
# don't store the escape-char itself
|
||||
if char == escape_char and string[ichar + 1 : ichar + 2] != escape_char:
|
||||
# don't store the escape-char itself, but keep one escape-char,
|
||||
# if it's followed by another escape-char
|
||||
escaped = True
|
||||
continue
|
||||
|
||||
|
|
@ -372,7 +374,8 @@ class FuncParser:
|
|||
curr_func.open_lsquare = open_lsquare
|
||||
curr_func.open_lcurly = open_lcurly
|
||||
# we must strip the remaining funcstr so it's not counted twice
|
||||
curr_func.rawstr = curr_func.rawstr[: -len(infuncstr)]
|
||||
if len(infuncstr) > 0:
|
||||
curr_func.rawstr = curr_func.rawstr[: -len(infuncstr)]
|
||||
current_kwarg = ""
|
||||
infuncstr = ""
|
||||
double_quoted = -1
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from parameterized import parameterized
|
|||
|
||||
from evennia.objects.objects import DefaultObject
|
||||
from evennia.utils import dbserialize
|
||||
from enum import IntFlag, auto
|
||||
|
||||
|
||||
class TestDbSerialize(TestCase):
|
||||
|
|
@ -20,6 +21,13 @@ class TestDbSerialize(TestCase):
|
|||
self.obj = DefaultObject(db_key="Tester")
|
||||
self.obj.save()
|
||||
|
||||
def test_intflag(self):
|
||||
class TestFlag(IntFlag):
|
||||
foo = auto()
|
||||
self.obj.db.test = TestFlag.foo
|
||||
self.assertEqual(self.obj.db.test, TestFlag.foo)
|
||||
self.obj.save()
|
||||
|
||||
def test_constants(self):
|
||||
self.obj.db.test = 1
|
||||
self.obj.db.test += 1
|
||||
|
|
|
|||
|
|
@ -404,3 +404,48 @@ class TestEvTable(EvenniaTestCase):
|
|||
|
||||
self.assertEqual(table1b, table1a)
|
||||
self.assertEqual(table2b, table2a)
|
||||
|
||||
@skip("Needs to be further invstigated")
|
||||
def test_formatting_with_carriage_return_marker_3693_a(self):
|
||||
"""
|
||||
Testing of issue https://github.com/evennia/evennia/issues/3693
|
||||
|
||||
Adding a |/ marker causes a misalignment of the side border.
|
||||
|
||||
"""
|
||||
data = "This is a test |/on a separate line"
|
||||
table = evtable.EvTable("", table=[[data]], width=20, border="cols")
|
||||
|
||||
expected = """
|
||||
| |
|
||||
+~~~~~~~~~~~~~~~~~~+
|
||||
| This is a test |
|
||||
| on a separate |
|
||||
| line |
|
||||
"""
|
||||
self._validate(expected, str(table))
|
||||
|
||||
@skip("Needs to be further invstigated")
|
||||
def test_formatting_with_carriage_return_marker_3693_b(self):
|
||||
"""
|
||||
Testing of issue https://github.com/evennia/evennia/issues/3693
|
||||
|
||||
Adding a |/ marker causes a misalignment of the side border.
|
||||
|
||||
"""
|
||||
data = "This is a test |/on a separate line"
|
||||
data = "Welcome to your new Evennia-based game! Visit https://www.evennia.com if you need help, want to contribute, report issues or just join the community. |/|/As a privileged user, write batchcommand tutorial_world.build to build tutorial content. Once built, try intro for starting help and tutorial to play the demo game." # noqa
|
||||
|
||||
table = evtable.EvTable("", table=[[data]], width=80, border="cols")
|
||||
|
||||
expected = """
|
||||
| |
|
||||
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
|
||||
| Welcome to your new Evennia-based game! Visit https://www.evennia.com if |
|
||||
| you need help, want to contribute, report issues or just join the community. |
|
||||
| |
|
||||
| As a privileged user, write batchcommand tutorial_world.build to build |
|
||||
| tutorial content. Once built, try intro for starting help and tutorial to |
|
||||
| play the demo game. |
|
||||
"""
|
||||
self._validate(expected, str(table))
|
||||
|
|
|
|||
|
|
@ -231,6 +231,9 @@ class TestFuncParser(TestCase):
|
|||
("Test literal3 $typ($lit(1)aaa)", "Test literal3 <class 'str'>"),
|
||||
("Test literal4 $typ(aaa$lit(1))", "Test literal4 <class 'str'>"),
|
||||
("Test spider's thread", "Test spider's thread"),
|
||||
("Test escape syntax $a=$b", "Test escape syntax $a=$b"),
|
||||
(r"Test escape syntax $a\= b", "Test escape syntax $a= b"),
|
||||
(r"Test escape syntax $a\\= $b", r"Test escape syntax $a\= $b"),
|
||||
]
|
||||
)
|
||||
def test_parse(self, string, expected):
|
||||
|
|
|
|||
|
|
@ -2004,7 +2004,6 @@ nidify,,,nidifies,,nidifying,,,,,nidified,nidified,,,,,,,,,,,,
|
|||
expand,,,expands,,expanding,,,,,expanded,expanded,,,,,,,,,,,,
|
||||
audit,,,audits,,auditing,,,,,audited,audited,,,,,,,,,,,,
|
||||
dislocate,,,dislocates,,dislocating,,,,,dislocated,dislocated,,,,,,,,,,,,
|
||||
offer,,,,,,,,,,,,,,,,,,,,,,,
|
||||
fascinate,,,fascinates,,fascinating,,,,,fascinated,fascinated,,,,,,,,,,,,
|
||||
trudge,,,trudges,,trudging,,,,,trudged,trudged,,,,,,,,,,,,
|
||||
shotgun,,,shotguns,,shotgunning,,,,,shotgunned,shotgunned,,,,,,,,,,,,
|
||||
|
|
@ -4660,7 +4659,7 @@ recuperate,,,recuperates,,recuperating,,,,,recuperated,recuperated,,,,,,,,,,,,
|
|||
womanize,,,womanizes,,womanizing,,,,,womanized,womanized,,,,,,,,,,,,
|
||||
remount,,,remounts,,remounting,,,,,remounted,remounted,,,,,,,,,,,,
|
||||
jess,,,jesses,,jessing,,,,,jessed,jessed,,,,,,,,,,,,
|
||||
canter,,,cants,,canting,,,,,canted,canted,,,,,,,,,,,,
|
||||
cant,,,cants,,canting,,,,,canted,canted,,,,,,,,,,,,
|
||||
lyophilize,,,lyophilizes,,lyophilizing,,,,,lyophilized,lyophilized,,,,,,,,,,,,
|
||||
jest,,,jests,,jesting,,,,,jested,jested,,,,,,,,,,,,
|
||||
mouse,,,mouses,,mousing,,,,,moused,moused,,,,,,,,,,,,
|
||||
|
|
@ -6559,7 +6558,7 @@ micturate,,,micturates,,micturating,,,,,micturated,micturated,,,,,,,,,,,,
|
|||
outgain,,,outgains,,outgaining,,,,,outgained,outgained,,,,,,,,,,,,
|
||||
declassify,,,declassifies,,declassifying,,,,,declassified,declassified,,,,,,,,,,,,
|
||||
tissue,,,tissues,,tissuing,,,,,tissued,tissued,,,,,,,,,,,,
|
||||
install,,,instals,,installing,,,,,installed,installed,,,,,,,,,,,,
|
||||
install,,,installs,,installing,,,,,installed,installed,,,,,,,,,,,,
|
||||
salvage,,,salvages,,salvaging,,,,,salvaged,salvaged,,,,,,,,,,,,
|
||||
aggrandize,,,aggrandizes,,aggrandizing,,,,,aggrandized,aggrandized,,,,,,,,,,,,
|
||||
quarrel,,,quarrels,,quarrelling,,,,,quarrelled,quarrelled,,,,,,,,,,,,
|
||||
|
|
@ -7405,7 +7404,6 @@ hint,,,hints,,hinting,,,,,hinted,hinted,,,,,,,,,,,,
|
|||
except,,,excepts,,excepting,,,,,excepted,excepted,,,,,,,,,,,,
|
||||
enfilade,,,enfilades,,enfilading,,,,,enfiladed,enfiladed,,,,,,,,,,,,
|
||||
blob,,,blobs,,blobbing,,,,,blobbed,blobbed,,,,,,,,,,,,
|
||||
hinder,,,,,,,,,,,,,,,,,,,,,,,
|
||||
backbite,,,backbites,,backbiting,,,,,backbit,backbitten,,,,,,,,,,,,
|
||||
disrupt,,,disrupts,,disrupting,,,,,disrupted,disrupted,,,,,,,,,,,,
|
||||
impound,,,impounds,,impounding,,,,,impounded,impounded,,,,,,,,,,,,
|
||||
|
|
@ -7529,7 +7527,6 @@ disc,,,discs,,discing,,,,,disced,disced,,,,,,,,,,,,
|
|||
antagonize,,,antagonizes,,antagonizing,,,,,antagonized,antagonized,,,,,,,,,,,,
|
||||
dish,,,dishes,,dishing,,,,,dished,dished,,,,,,,,,,,,
|
||||
follow,,,follows,,following,,,,,followed,followed,,,,,,,,,,,,
|
||||
alter,,,,,,,,,,,,,,,,,,,,,,,
|
||||
glimpse,,,glimpses,,glimpsing,,,,,glimpsed,glimpsed,,,,,,,,,,,,
|
||||
depressurize,,,depressurizes,,depressurizing,,,,,depressurized,depressurized,,,,,,,,,,,,
|
||||
homage,,,homages,,homaging,,,,,homaged,homaged,,,,,,,,,,,,
|
||||
|
|
|
|||
|
|
@ -300,6 +300,14 @@ let goldenlayout = (function () {
|
|||
let typelist = document.getElementById("typelist");
|
||||
let updatelist = document.getElementById("updatelist");
|
||||
|
||||
if(tab?.componentName !== 'options')
|
||||
{
|
||||
window.plugins["default_in"].setKeydownFocus(true);
|
||||
}
|
||||
else {
|
||||
window.plugins["default_in"].setKeydownFocus(false);
|
||||
}
|
||||
|
||||
if( renamebox ) {
|
||||
closeRenameDropdown();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,14 +75,12 @@ let options2 = (function () {
|
|||
.click( function () {
|
||||
optionsContainer = null;
|
||||
tab.contentItem.remove();
|
||||
window.plugins["default_in"].setKeydownFocus(true);
|
||||
});
|
||||
optionsContainer = tab.contentItem;
|
||||
}
|
||||
});
|
||||
main.parent.addChild( optionsComponent );
|
||||
|
||||
window.plugins["default_in"].setKeydownFocus(false);
|
||||
} else {
|
||||
optionsContainer.remove();
|
||||
optionsContainer = null;
|
||||
|
|
@ -151,7 +149,6 @@ let options2 = (function () {
|
|||
// don't claim this Prompt as completed.
|
||||
return false;
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
var init = function() {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||
|
||||
[project]
|
||||
name = "evennia"
|
||||
version = "4.4.1"
|
||||
version = "4.5.0"
|
||||
maintainers = [{ name = "Griatch", email = "griatch@gmail.com" }]
|
||||
description = "A full-featured toolkit and server for text-based multiplayer games (MUDs, MU*, etc)."
|
||||
requires-python = ">=3.10"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue