diff --git a/.github/workflows/github_action_issue_to_project.yml b/.github/workflows/github_action_issue_to_project.yml new file mode 100644 index 0000000000..4ecf9b6ea9 --- /dev/null +++ b/.github/workflows/github_action_issue_to_project.yml @@ -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 }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 6df071efc2..5e6c49ca3b 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/docs/requirements.txt b/docs/requirements.txt index 9b371f313a..cddfd1b970 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -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 diff --git a/docs/source/Coding/Changelog.md b/docs/source/Coding/Changelog.md index 7ed3c09e34..5e6c49ca3b 100644 --- a/docs/source/Coding/Changelog.md +++ b/docs/source/Coding/Changelog.md @@ -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 diff --git a/docs/source/Components/Batch-Command-Processor.md b/docs/source/Components/Batch-Command-Processor.md index dda41e610c..2718d3a40b 100644 --- a/docs/source/Components/Batch-Command-Processor.md +++ b/docs/source/Components/Batch-Command-Processor.md @@ -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 diff --git a/docs/source/Components/EvMenu.md b/docs/source/Components/EvMenu.md index 8a1342c7ae..01159d0cd8 100644 --- a/docs/source/Components/EvMenu.md +++ b/docs/source/Components/EvMenu.md @@ -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) diff --git a/docs/source/Components/Tags.md b/docs/source/Components/Tags.md index 42aaa92455..9e8643113a 100644 --- a/docs/source/Components/Tags.md +++ b/docs/source/Components/Tags.md @@ -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). diff --git a/docs/source/Components/Web-API.md b/docs/source/Components/Web-API.md index da4b70e31c..0a11445d0c 100644 --- a/docs/source/Components/Web-API.md +++ b/docs/source/Components/Web-API.md @@ -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. diff --git a/docs/source/Components/Website.md b/docs/source/Components/Website.md index c4dddad948..de784b58cc 100644 --- a/docs/source/Components/Website.md +++ b/docs/source/Components/Website.md @@ -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. ``` diff --git a/docs/source/Concepts/Internationalization.md b/docs/source/Concepts/Internationalization.md index a4f81a3ad7..2dbe226572 100644 --- a/docs/source/Concepts/Internationalization.md +++ b/docs/source/Concepts/Internationalization.md @@ -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 | +---------------+----------------------+--------------+ ``` diff --git a/docs/source/Contribs/Contrib-Clothing.md b/docs/source/Contribs/Contrib-Clothing.md index 3a5dcf6502..219fee2dcc 100644 --- a/docs/source/Contribs/Contrib-Clothing.md +++ b/docs/source/Contribs/Contrib-Clothing.md @@ -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 diff --git a/docs/source/Contribs/Contrib-Godotwebsocket.md b/docs/source/Contribs/Contrib-Godotwebsocket.md index ffe10e1d68..efdbe67e28 100644 --- a/docs/source/Contribs/Contrib-Godotwebsocket.md +++ b/docs/source/Contribs/Contrib-Godotwebsocket.md @@ -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') diff --git a/docs/source/Contribs/Contrib-Ingame-Reports.md b/docs/source/Contribs/Contrib-Ingame-Reports.md index 20157af3f9..e7958e773e 100644 --- a/docs/source/Contribs/Contrib-Ingame-Reports.md +++ b/docs/source/Contribs/Contrib-Ingame-Reports.md @@ -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 diff --git a/docs/source/Contribs/Contrib-Llm.md b/docs/source/Contribs/Contrib-Llm.md index ffae9c1a84..85d33bfbb0 100644 --- a/docs/source/Contribs/Contrib-Llm.md +++ b/docs/source/Contribs/Contrib-Llm.md @@ -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", diff --git a/docs/source/Contribs/Contrib-Mapbuilder.md b/docs/source/Contribs/Contrib-Mapbuilder.md index ae18378f24..c2d365f865 100644 --- a/docs/source/Contribs/Contrib-Mapbuilder.md +++ b/docs/source/Contribs/Contrib-Mapbuilder.md @@ -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 diff --git a/docs/source/Contribs/Contrib-Storage.md b/docs/source/Contribs/Contrib-Storage.md new file mode 100644 index 0000000000..fa720ece20 --- /dev/null +++ b/docs/source/Contribs/Contrib-Storage.md @@ -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. + +---- + +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. diff --git a/docs/source/Contribs/Contribs-Overview.md b/docs/source/Contribs/Contribs-Overview.md index f1d7308eaf..232284616e 100644 --- a/docs/source/Contribs/Contribs-Overview.md +++ b/docs/source/Contribs/Contribs-Overview.md @@ -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_ diff --git a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Making-A-Sittable-Object.md b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Making-A-Sittable-Object.md index d2266364e7..cb3a6e3d4c 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Making-A-Sittable-Object.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Making-A-Sittable-Object.md @@ -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) """ diff --git a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Python-classes-and-objects.md b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Python-classes-and-objects.md index f664cdcedb..668db4e283 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Python-classes-and-objects.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Python-classes-and-objects.md @@ -377,7 +377,6 @@ class ObjectParent: """ class docstring """ - pass class Object(ObjectParent, DefaultObject): """ diff --git a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-AI.md b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-AI.md index 4e35e2b2c4..58661a1ddd 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-AI.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-AI.md @@ -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. ``` diff --git a/docs/source/Howtos/Web-Character-Generation.md b/docs/source/Howtos/Web-Character-Generation.md index e7ef7c823b..98190f6cb8 100644 --- a/docs/source/Howtos/Web-Character-Generation.md +++ b/docs/source/Howtos/Web-Character-Generation.md @@ -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 @@ -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! \ No newline at end of file +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! diff --git a/docs/source/Howtos/Web-Help-System-Tutorial.md b/docs/source/Howtos/Web-Help-System-Tutorial.md index 37788f963d..116bb35bf5 100644 --- a/docs/source/Howtos/Web-Help-System-Tutorial.md +++ b/docs/source/Howtos/Web-Help-System-Tutorial.md @@ -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: diff --git a/docs/source/Setup/Choosing-a-Database.md b/docs/source/Setup/Choosing-a-Database.md index 630a176cde..65db157449 100644 --- a/docs/source/Setup/Choosing-a-Database.md +++ b/docs/source/Setup/Choosing-a-Database.md @@ -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 # diff --git a/docs/source/Setup/Installation-Troubleshooting.md b/docs/source/Setup/Installation-Troubleshooting.md index 7eb9d77a31..3f29ac8cf3 100644 --- a/docs/source/Setup/Installation-Troubleshooting.md +++ b/docs/source/Setup/Installation-Troubleshooting.md @@ -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. \ No newline at end of file diff --git a/docs/source/Setup/Settings.md b/docs/source/Setup/Settings.md index 766d24281e..afe4abbbfb 100644 --- a/docs/source/Setup/Settings.md +++ b/docs/source/Setup/Settings.md @@ -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). \ No newline at end of file +- `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). diff --git a/docs/source/api/evennia.contrib.game_systems.md b/docs/source/api/evennia.contrib.game_systems.md index 07ddfa33e1..de3da5f352 100644 --- a/docs/source/api/evennia.contrib.game_systems.md +++ b/docs/source/api/evennia.contrib.game_systems.md @@ -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 ``` \ No newline at end of file diff --git a/docs/source/api/evennia.contrib.game_systems.storage.md b/docs/source/api/evennia.contrib.game_systems.storage.md new file mode 100644 index 0000000000..82abd8d66a --- /dev/null +++ b/docs/source/api/evennia.contrib.game_systems.storage.md @@ -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 + +``` \ No newline at end of file diff --git a/docs/source/api/evennia.contrib.game_systems.storage.storage.md b/docs/source/api/evennia.contrib.game_systems.storage.storage.md new file mode 100644 index 0000000000..882eaf6984 --- /dev/null +++ b/docs/source/api/evennia.contrib.game_systems.storage.storage.md @@ -0,0 +1,10 @@ +```{eval-rst} +evennia.contrib.game\_systems.storage.storage +==================================================== + +.. automodule:: evennia.contrib.game_systems.storage.storage + :members: + :undoc-members: + :show-inheritance: + +``` \ No newline at end of file diff --git a/docs/source/api/evennia.contrib.game_systems.storage.tests.md b/docs/source/api/evennia.contrib.game_systems.storage.tests.md new file mode 100644 index 0000000000..fccdd5fe58 --- /dev/null +++ b/docs/source/api/evennia.contrib.game_systems.storage.tests.md @@ -0,0 +1,10 @@ +```{eval-rst} +evennia.contrib.game\_systems.storage.tests +================================================== + +.. automodule:: evennia.contrib.game_systems.storage.tests + :members: + :undoc-members: + :show-inheritance: + +``` \ No newline at end of file diff --git a/docs/source/index.md b/docs/source/index.md index 0a55d908c0..cb4faf4117 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -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 diff --git a/evennia/VERSION.txt b/evennia/VERSION.txt index cca25a93cd..a84947d6ff 100644 --- a/evennia/VERSION.txt +++ b/evennia/VERSION.txt @@ -1 +1 @@ -4.4.1 +4.5.0 diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index e92c245fb6..43870ce1fa 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -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) diff --git a/evennia/accounts/bots.py b/evennia/accounts/bots.py index 82bd3e2d57..c7a020dc62 100644 --- a/evennia/accounts/bots.py +++ b/evennia/accounts/bots.py @@ -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): diff --git a/evennia/commands/command.py b/evennia/commands/command.py index 8dbac1c3a7..f676df49bb 100644 --- a/evennia/commands/command.py +++ b/evennia/commands/command.py @@ -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[\w\d\-]+)/(?P[0-9]+)/$', + url(r'characters/(?P[\\w\\d\\-]+)/(?P[0-9]+)/$', CharDetailView.as_view(), name='character-detail') If no View has been created and defined in urls.py, returns an diff --git a/evennia/commands/default/batchprocess.py b/evennia/commands/default/batchprocess.py index 694fd20972..67035e87d4 100644 --- a/evennia/commands/default/batchprocess.py +++ b/evennia/commands/default/batchprocess.py @@ -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 = """ diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index cffd59a6a6..710e6f87cb 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -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] / = / [,/,/,...] cpattr[/switch] / = [,,,...] - cpattr[/switch] = / [,/,/,...] + cpattr[/switch] [:category] = /[:category] [,/,/,...] cpattr[/switch] = [,,,...] 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] / = / [,/,/,...] + cpattr[/switch] /[:category] = / [,/,/,...] cpattr[/switch] / = [,,,...] cpattr[/switch] = / [,/,/,...] cpattr[/switch] = [,,,...]""" @@ -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 diff --git a/evennia/commands/default/comms.py b/evennia/commands/default/comms.py index 5c5df209b2..a5d1da3e19 100644 --- a/evennia/commands/default/comms.py +++ b/evennia/commands/default/comms.py @@ -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", diff --git a/evennia/commands/default/general.py b/evennia/commands/default/general.py index 06aae08f75..e1eff225ee 100644 --- a/evennia/commands/default/general.py +++ b/evennia/commands/default/general.py @@ -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 + \\\\= - escape literal '=' you want in your 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 diff --git a/evennia/commands/default/system.py b/evennia/commands/default/system.py index ceecfd27f3..df493f366a 100644 --- a/evennia/commands/default/system.py +++ b/evennia/commands/default/system.py @@ -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: diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index f9c6d356b1..5db4c14acd 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -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.") diff --git a/evennia/commands/tests.py b/evennia/commands/tests.py index 10eb5143a7..ea9c954b2e 100644 --- a/evennia/commands/tests.py +++ b/evennia/commands/tests.py @@ -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()") diff --git a/evennia/contrib/base_systems/godotwebsocket/README.md b/evennia/contrib/base_systems/godotwebsocket/README.md index b80068024c..856138577d 100644 --- a/evennia/contrib/base_systems/godotwebsocket/README.md +++ b/evennia/contrib/base_systems/godotwebsocket/README.md @@ -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') diff --git a/evennia/contrib/base_systems/ingame_reports/README.md b/evennia/contrib/base_systems/ingame_reports/README.md index 00c1cbf905..7376390325 100644 --- a/evennia/contrib/base_systems/ingame_reports/README.md +++ b/evennia/contrib/base_systems/ingame_reports/README.md @@ -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 diff --git a/evennia/contrib/base_systems/ingame_reports/reports.py b/evennia/contrib/base_systems/ingame_reports/reports.py index 296f14b62c..b5758baacc 100644 --- a/evennia/contrib/base_systems/ingame_reports/reports.py +++ b/evennia/contrib/base_systems/ingame_reports/reports.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) diff --git a/evennia/contrib/game_systems/clothing/README.md b/evennia/contrib/game_systems/clothing/README.md index d84a259f01..d92bdd7b72 100644 --- a/evennia/contrib/game_systems/clothing/README.md +++ b/evennia/contrib/game_systems/clothing/README.md @@ -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 diff --git a/evennia/contrib/grid/mapbuilder/README.md b/evennia/contrib/grid/mapbuilder/README.md index 8a249c9cd6..f2750350b0 100644 --- a/evennia/contrib/grid/mapbuilder/README.md +++ b/evennia/contrib/grid/mapbuilder/README.md @@ -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 diff --git a/evennia/contrib/rpg/character_creator/character_creator.py b/evennia/contrib/rpg/character_creator/character_creator.py index 694393b75a..35f1ce4a41 100644 --- a/evennia/contrib/rpg/character_creator/character_creator.py +++ b/evennia/contrib/rpg/character_creator/character_creator.py @@ -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 diff --git a/evennia/contrib/rpg/llm/README.md b/evennia/contrib/rpg/llm/README.md index 762895b96f..2960a72b56 100644 --- a/evennia/contrib/rpg/llm/README.md +++ b/evennia/contrib/rpg/llm/README.md @@ -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", diff --git a/evennia/contrib/tutorials/evadventure/tests/test_rooms.py b/evennia/contrib/tutorials/evadventure/tests/test_rooms.py index d661390785..94fa338520 100644 --- a/evennia/contrib/tutorials/evadventure/tests/test_rooms.py +++ b/evennia/contrib/tutorials/evadventure/tests/test_rooms.py @@ -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")) diff --git a/evennia/contrib/tutorials/tutorial_world/rooms.py b/evennia/contrib/tutorials/tutorial_world/rooms.py index 42c4f6fc2d..4e346c4bd2 100644 --- a/evennia/contrib/tutorials/tutorial_world/rooms.py +++ b/evennia/contrib/tutorials/tutorial_world/rooms.py @@ -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 diff --git a/evennia/help/filehelp.py b/evennia/help/filehelp.py index 4c9cf3d9f1..ff2d18f6b8 100644 --- a/evennia/help/filehelp.py +++ b/evennia/help/filehelp.py @@ -120,6 +120,9 @@ class FileHelpEntry: def __repr__(self): return f"" + def __hash__(self): + return hash(self.key) + @lazy_property def locks(self): return LockHandler(self) diff --git a/evennia/help/tests.py b/evennia/help/tests.py index 0f7f31f242..086c10a141 100644 --- a/evennia/help/tests.py +++ b/evennia/help/tests.py @@ -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) diff --git a/evennia/help/utils.py b/evennia/help/utils.py index 716122accf..5ad469a508 100644 --- a/evennia/help/utils.py +++ b/evennia/help/utils.py @@ -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\#{2,6})\s*?(?P.*?)$", 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 = [] diff --git a/evennia/locale/zh/LC_MESSAGES/django.po b/evennia/locale/zh/LC_MESSAGES/django.po index 0b30ba017b..1bacd24476 100644 --- a/evennia/locale/zh/LC_MESSAGES/django.po +++ b/evennia/locale/zh/LC_MESSAGES/django.po @@ -1,105 +1,89 @@ -# The Simplified Chinese translation for the Evennia server. -# Copyright (C) 2019 MaxAlex -# This file is distributed under the same license as the Evennia package. -# FIRST AUTHOR: MaxAlex , 2018- -# msgid "" msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-10-29 18:53+0000\n" -"PO-Revision-Date: 2019-05-03 17:04+0800\n" -"Last-Translator: \n" -"Language-Team: \n" -"Language: zh-hans\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: Evennia\n" +"Language: zh-Hans\n" #: accounts/accounts.py:341 -#, python-brace-format msgid "|c{key}|R is already puppeted by another Account." -msgstr "" +msgstr "|c{key}|R 已被另一个帐户使用。" #: accounts/accounts.py:361 -#, python-brace-format -msgid "" -"You cannot control any more puppets (max {_MAX_NR_SIMULTANEOUS_PUPPETS})" -msgstr "" +msgid "You cannot control any more puppets (max {_MAX_NR_SIMULTANEOUS_PUPPETS})" +msgstr "您无法再操作更多实体(最多{_MAX_NR_SIMULTANEOUS_PUPPETS}个)" #: accounts/accounts.py:555 msgid "Too many login failures; please try again in a few minutes." -msgstr "" +msgstr "登录失败次数过多;请稍后再试。" #: accounts/accounts.py:568 accounts/accounts.py:832 -msgid "" -"|rYou have been banned and cannot continue from here.\n" +msgid "|rYou have been banned and cannot continue from here.\n" "If you feel this ban is in error, please email an admin.|x" -msgstr "" +msgstr "|r您已被封禁。\n" +"如有疑问或想要解禁,请向管理员发送电子邮件说明。|x" #: accounts/accounts.py:580 msgid "Username and/or password is incorrect." -msgstr "" +msgstr "用户名或密码不正确。" #: accounts/accounts.py:587 msgid "Too many authentication failures." -msgstr "" +msgstr "身份验证失败次数过多。" #: accounts/accounts.py:803 -msgid "" -"You are creating too many accounts. Please log into an existing account." -msgstr "" +msgid "You are creating too many accounts. Please log into an existing account." +msgstr "您创建的账户过多,请先登录至现有账户。" #: accounts/accounts.py:849 -msgid "" -"There was an error creating the Account. If this problem persists, contact " -"an admin." -msgstr "" +msgid "There was an error creating the Account. If this problem persists, contact an admin." +msgstr "创建帐户时出错:如果您发现此问题重复出现,请汇报至管理员。" #: accounts/accounts.py:885 accounts/accounts.py:1801 msgid "An error occurred. Please e-mail an admin if the problem persists." -msgstr "" +msgstr "发生错误:如果问题仍然存在,请向管理员发送电子邮件。" #: accounts/accounts.py:918 msgid "Account being deleted." msgstr "用户已删除。" #: accounts/accounts.py:1475 accounts/accounts.py:1819 -#, python-brace-format msgid "|G{key} connected|n" -msgstr "" +msgstr "|G{key} 已连接|n" #: accounts/accounts.py:1481 -#, fuzzy -#| msgid "The destination doesn't exist." msgid "The Character does not exist." -msgstr "目的地不存在。" +msgstr "该角色不存在。" #: accounts/accounts.py:1520 -#, python-brace-format msgid "|R{key} disconnected{reason}|n" -msgstr "" +msgstr "|R{key} 断开连接{reason}|n" #: accounts/accounts.py:1754 msgid "Guest accounts are not enabled on this server." -msgstr "" +msgstr "服务器未启用访客帐户。" #: accounts/accounts.py:1764 msgid "All guest accounts are in use. Please try again later." -msgstr "" +msgstr "所有访客帐户均在使用中。请稍后重试。" #: commands/cmdhandler.py:84 msgid "" "\n" "An untrapped error occurred.\n" msgstr "" +"\n" +"发生了未捕获的错误。\n" #: commands/cmdhandler.py:89 msgid "" "\n" -"An untrapped error occurred. Please file a bug report detailing the steps to " -"reproduce.\n" +"An untrapped error occurred. Please file a bug report detailing the steps to reproduce.\n" msgstr "" +"\n" +"发生未捕获的错误。请提交错误报告并详细说明重现步骤。\n" #: commands/cmdhandler.py:97 msgid "" @@ -107,6 +91,8 @@ msgid "" "A cmdset merger-error occurred. This is often due to a syntax\n" "error in one of the cmdsets to merge.\n" msgstr "" +"\n" +"发生 cmaset 合并错误。这通常是由于要合并的某个 cmdset 中存在语法错误。\n" #: commands/cmdhandler.py:103 msgid "" @@ -114,6 +100,9 @@ msgid "" "A cmdset merger-error occurred. Please file a bug report detailing the\n" "steps to reproduce.\n" msgstr "" +"\n" +"发生 cmdset 合并错误。\n" +"请提交错误报告,详细说明重现步骤。\n" #: commands/cmdhandler.py:112 msgid "" @@ -121,6 +110,9 @@ msgid "" "No command sets found! This is a critical bug that can have\n" "multiple causes.\n" msgstr "" +"\n" +"未找到命令集!\n" +"这是一个严重的错误,可能由多种原因造成。\n" #: commands/cmdhandler.py:118 msgid "" @@ -129,6 +121,10 @@ msgid "" "disconnecting/reconnecting doesn't\" solve the problem, try to contact\n" "the server admin through\" some other means for assistance.\n" msgstr "" +"\n" +"未找到命令集!\n" +"这意味着出现严重错误。\n" +"如果断开/重新连接无法解决问题,请尝试通过其他方式联系服务器管理员寻求帮助。\n" #: commands/cmdhandler.py:128 msgid "" @@ -137,6 +133,10 @@ msgid "" "please file a bug report with the Evennia project, including the\n" "traceback and steps to reproduce.\n" msgstr "" +"\n" +"发生命令处理程序错误。\n" +"如果这不是由于本地更改造成的, 请向 Evennia官方提交错误报告,\n" +"包括回溯和重现步骤。\n" #: commands/cmdhandler.py:135 msgid "" @@ -144,44 +144,33 @@ msgid "" "A command handler bug occurred. Please notify staff - they should\n" "likely file a bug report with the Evennia project.\n" msgstr "" +"\n" +"出现命令处理程序错误。请通知工作人员 \n" +"---他们应该 向 Evennia 项目提交错误报告。\n" #: commands/cmdhandler.py:143 -#, python-brace-format -msgid "" -"Command recursion limit ({recursion_limit}) reached for " -"'{raw_cmdname}' ({cmdclass})." -msgstr "" +msgid "Command recursion limit ({recursion_limit}) reached for '{raw_cmdname}' ({cmdclass})." +msgstr "‘{raw_cmdname}’ ({cmdclass}) 的命令递归已达到限制 ({recursion_limit}) 。" #: commands/cmdhandler.py:165 -#, fuzzy, python-brace-format -#| msgid "" -#| "{traceback}\n" -#| "Error loading cmdset '{path}'\n" -#| "(Traceback was logged {timestamp})" -msgid "" -"{traceback}\n" +msgid "{traceback}\n" "{errmsg}\n" "(Traceback was logged {timestamp})." -msgstr "" -"{traceback}\n" -"读取CmdSet '{path}' 时发生错误 \n" -"(已记录 Traceback {timestamp})" +msgstr "{traceback}\n" +"读取CmdSet '{errmsg}' 时发生错误 \n" +"(已记录回溯 {timestamp})。" #: commands/cmdhandler.py:715 msgid "There were multiple matches." msgstr "发现多个匹配项。" #: commands/cmdhandler.py:740 -#, fuzzy, python-brace-format -#| msgid "Command '%s' is not available." msgid "Command '{command}' is not available." -msgstr "命令 '%s' 不可用。" +msgstr "命令 '{command}' 不可用。" #: commands/cmdhandler.py:750 -#, fuzzy, python-brace-format -#| msgid " Maybe you meant %s?" msgid " Maybe you meant {command}?" -msgstr " 您指的是 %s 吗?" +msgstr " 您指的是{command}吗?" #: commands/cmdhandler.py:751 msgid "or" @@ -189,56 +178,39 @@ msgstr "或" #: commands/cmdhandler.py:754 msgid " Type \"help\" for help." -msgstr " 键入 \"help\" 获得帮助。" +msgstr " 输入 \"help\" 获得帮助。" #: commands/cmdsethandler.py:89 -#, python-brace-format -msgid "" -"{traceback}\n" +msgid "{traceback}\n" "Error loading cmdset '{path}'\n" "(Traceback was logged {timestamp})" -msgstr "" -"{traceback}\n" +msgstr "{traceback}\n" "读取CmdSet '{path}' 时发生错误 \n" "(已记录 Traceback {timestamp})" #: commands/cmdsethandler.py:95 -#, python-brace-format -msgid "" -"Error loading cmdset: No cmdset class '{classname}' in '{path}'.\n" +msgid "Error loading cmdset: No cmdset class '{classname}' in '{path}'.\n" "(Traceback was logged {timestamp})" -msgstr "" -"读取 CmdSet 时发生错误:在 '{path}' 处未找到 CmdSet '{classname}' 。\n" +msgstr "读取 CmdSet 时发生错误:在 '{path}' 处未找到 CmdSet '{classname}' 。\n" "(已记录 Traceback {timestamp})" #: commands/cmdsethandler.py:100 -#, python-brace-format -msgid "" -"{traceback}\n" +msgid "{traceback}\n" "SyntaxError encountered when loading cmdset '{path}'.\n" "(Traceback was logged {timestamp})" -msgstr "" -"{traceback}\n" +msgstr "{traceback}\n" "读取在 '{path}' 处的 CmdSet 时发生 语法 错误。\n" "(已记录 Traceback {timestamp})" #: commands/cmdsethandler.py:106 -#, fuzzy, python-brace-format -#| msgid "" -#| "{traceback}\n" -#| "Compile/Run error when loading cmdset '{path}'.\",\n" -#| "(Traceback was logged {timestamp})" -msgid "" -"{traceback}\n" +msgid "{traceback}\n" "Compile/Run error when loading cmdset '{path}'.\n" "(Traceback was logged {timestamp})" -msgstr "" -"{traceback}\n" -"读取在 '{path}' 处的 CmdSet 时发生 编译/运行 错误。\",\n" +msgstr "{traceback}\n" +"读取在 '{path}' 处的 CmdSet 时发生 编译/运行 错误。\n" "(已记录 Traceback {timestamp})" #: commands/cmdsethandler.py:112 -#, python-brace-format msgid "" "\n" "Error encountered for cmdset at path '{path}'.\n" @@ -249,24 +221,18 @@ msgstr "" "使用备选路径 '{fallback_path}' 。\n" #: commands/cmdsethandler.py:118 -#, python-brace-format msgid "Fallback path '{fallback_path}' failed to generate a cmdset." msgstr "在备选路径 '{fallback_path}' 处创建 CmdSet 失败。" #: commands/cmdsethandler.py:188 commands/cmdsethandler.py:200 -#, fuzzy, python-brace-format -#| msgid "" -#| "\n" -#| "(Unsuccessfully tried '%s')." msgid "" "\n" "(Unsuccessfully tried '{path}')." msgstr "" "\n" -"(尝试 '%s' 失败)。" +"(尝试‘{path}’未成功)。" #: commands/cmdsethandler.py:331 -#, python-brace-format msgid "custom {mergetype} on cmdset '{cmdset}'" msgstr "CmdSet '{cmdset}' 的自定义 {mergetype}" @@ -275,305 +241,226 @@ msgid "Only CmdSets can be added to the cmdsethandler!" msgstr "只有 CmdSet 可以被添加给 cmdsethandler!" #: locks/lockhandler.py:239 -#, fuzzy, python-brace-format -#| msgid "Lock: lock-function '%s' is not available." msgid "Lock: lock-function '{lockfunc}' is not available." -msgstr "Lock:Lock函数 '%s' 不可用。" +msgstr "Lock:Lock函数'{lockfunc}'不可用。" #: locks/lockhandler.py:262 -#, fuzzy, python-brace-format -#| msgid "Lock: definition '%s' has syntax errors." msgid "Lock: definition '{lock_string}' has syntax errors." -msgstr "Lock:定义 '%s' 发生语法错误。" +msgstr "Lock:定义 '{lock_string}' 发现语法错误。" #: locks/lockhandler.py:271 -#, fuzzy, python-brace-format -#| msgid "" -#| "LockHandler on %(obj)s: access type '%(access_type)s' changed from " -#| "'%(source)s' to '%(goal)s' " -msgid "" -"LockHandler on {obj}: access type '{access_type}' changed from '{source}' to " -"'{goal}' " -msgstr "" -"%(obj)s 上的 LockHandler: 访问类型 '%(access_type)s' 由 '%(source)s' 改变为 " -"'%(goal)s' " +msgid "LockHandler on {obj}: access type '{access_type}' changed from '{source}' to '{goal}' " +msgstr "{obj} 上的 LockHandler:访问类型'{access_type}'从'{source}'更改为'{goal}' " #: locks/lockhandler.py:347 -#, python-brace-format msgid "Lock: '{lockdef}' contains no colon (:)." msgstr "Lock:'{lockdef}' 缺少英文冒号 (:) 。" #: locks/lockhandler.py:356 -#, python-brace-format msgid "Lock: '{lockdef}' has no access_type (left-side of colon is empty)." msgstr "Lock: '{lockdef}' 无访问类型(冒号左侧缺少数据)。" #: locks/lockhandler.py:364 -#, python-brace-format msgid "Lock: '{lockdef}' has mismatched parentheses." msgstr "Lock: '{lockdef}' 英文括号不匹配。" #: locks/lockhandler.py:371 -#, python-brace-format msgid "Lock: '{lockdef}' has no valid lock functions." msgstr "Lock: '{lockdef}' 缺少合法Lock函数。" #: objects/objects.py:856 -#, fuzzy, python-brace-format -#| msgid "Couldn't perform move ('%s'). Contact an admin." msgid "Couldn't perform move ({err}). Contact an admin." -msgstr "无法做出行动 ('%s')。请联系管理员。" +msgstr "无法执行操作({err})。请联系管理员。" #: objects/objects.py:866 msgid "The destination doesn't exist." msgstr "目的地不存在。" #: objects/objects.py:978 -#, fuzzy, python-brace-format -#| msgid "Could not find default home '(#%d)'." msgid "Could not find default home '(#{dbid})'." -msgstr "无法定位默认寓所 '(#%d)' 。" +msgstr "未找到默认位置‘(#{dbid})’。" #: objects/objects.py:992 msgid "Something went wrong! You are dumped into nowhere. Contact an admin." -msgstr "出现错误!您进入了错误的地点。请联系管理员。" +msgstr "发生了意外!您目前处于不恰当的地点。请联系管理员处理。" #: objects/objects.py:1145 -#, fuzzy, python-brace-format -#| msgid "Your character %s has been destroyed." msgid "Your character {key} has been destroyed." -msgstr "您的角色 %s 被摧毁了。" +msgstr "你的角色{key}已被删除。" #: objects/objects.py:1853 -#, python-brace-format msgid "You now have {name} in your possession." -msgstr "" +msgstr "您得到了{name}。" #: objects/objects.py:1863 -#, python-brace-format msgid "{object} arrives to {destination} from {origin}." -msgstr "" +msgstr "{object}从{origin}来到了{destination}。" #: objects/objects.py:1865 -#, python-brace-format msgid "{object} arrives to {destination}." -msgstr "" +msgstr "{object}来到了{destination}。" #: objects/objects.py:2530 msgid "Invalid character name." -msgstr "" +msgstr "无效的角色名称。" #: objects/objects.py:2549 msgid "There are too many characters associated with this account." -msgstr "" +msgstr "与此帐户关联的角色过多。" #: objects/objects.py:2575 -#, fuzzy -#| msgid "This is User #1." msgid "This is a character." -msgstr "这是管理员。" +msgstr "这是一个角色。" #: objects/objects.py:2664 -#, python-brace-format msgid "|r{obj} has no location and no home is set.|n" -msgstr "" +msgstr "|r{obj} 目前没有位置信息且没有设置默认归属地。|n" #: objects/objects.py:2682 -#, python-brace-format msgid "" "\n" "You become |c{name}|n.\n" msgstr "" +"\n" +"你化身为|c{name}|n。\n" #: objects/objects.py:2687 -#, python-brace-format msgid "{name} has entered the game." -msgstr "" +msgstr "{name}已加入游戏。" #: objects/objects.py:2716 -#, python-brace-format msgid "{name} has left the game{reason}." -msgstr "" +msgstr "{name}已离开游戏{reason}。" #: objects/objects.py:2838 -#, fuzzy -#| msgid "This is User #1." msgid "This is a room." -msgstr "这是管理员。" +msgstr "这是一个房间。" #: objects/objects.py:3045 -#, fuzzy -#| msgid "This is User #1." msgid "This is an exit." -msgstr "这是管理员。" +msgstr "这是一个出口。" #: objects/objects.py:3142 msgid "You cannot go there." -msgstr "" +msgstr "你不能到那里去。" #: prototypes/prototypes.py:55 msgid "Error" -msgstr "" +msgstr "错误" #: prototypes/prototypes.py:56 msgid "Warning" -msgstr "" +msgstr "警告" #: prototypes/prototypes.py:389 msgid "Prototype requires a prototype_key" -msgstr "" +msgstr "原型需要一个'prototype_key'" #: prototypes/prototypes.py:397 prototypes/prototypes.py:466 #: prototypes/prototypes.py:1092 -#, python-brace-format msgid "{protkey} is a read-only prototype (defined as code in {module})." -msgstr "" +msgstr "{protkey} 是一个只读原型(在 {module} 中定义为代码)。" #: prototypes/prototypes.py:399 prototypes/prototypes.py:468 #: prototypes/prototypes.py:1094 -#, python-brace-format msgid "{protkey} is a read-only prototype (passed directly as a dict)." -msgstr "" +msgstr "{protkey} 是一个只读原型(直接作为dict传递)。" #: prototypes/prototypes.py:475 -#, python-brace-format msgid "Prototype {prototype_key} was not found." -msgstr "" +msgstr "未找到原型 {prototype_key}。" #: prototypes/prototypes.py:483 -#, python-brace-format -msgid "" -"{caller} needs explicit 'edit' permissions to delete prototype " -"{prototype_key}." -msgstr "" +msgid "{caller} needs explicit 'edit' permissions to delete prototype {prototype_key}." +msgstr "{caller} 需要明确的'编辑'权限才能删除原型 {prototype_key}。" #: prototypes/prototypes.py:605 -#, python-brace-format msgid "Found {num} matching prototypes among {module_prototypes}." -msgstr "" +msgstr "在 {module_prototypes} 中找到 {num} 个匹配的原型。" #: prototypes/prototypes.py:765 msgid "No prototypes found." -msgstr "" +msgstr "未找到原型。" #: prototypes/prototypes.py:816 msgid "Prototype lacks a 'prototype_key'." -msgstr "" +msgstr "原型缺少'prototype_key'。" #: prototypes/prototypes.py:825 -#, python-brace-format msgid "Prototype {protkey} requires `typeclass` or 'prototype_parent'." -msgstr "" +msgstr "原型 {protkey} 需要'typeclass'或'prototype_parent'。" #: prototypes/prototypes.py:832 -#, python-brace-format -msgid "" -"Prototype {protkey} can only be used as a mixin since it lacks 'typeclass' " -"or 'prototype_parent' keys." -msgstr "" +msgid "Prototype {protkey} can only be used as a mixin since it lacks 'typeclass' or 'prototype_parent' keys." +msgstr "原型 {protkey} 由于缺少 'typeclass'或 'prototype_parent'键,只能作为混合元素(mixin)使用。" #: prototypes/prototypes.py:843 -#, python-brace-format -msgid "" -"{err}: Prototype {protkey} is based on typeclass {typeclass}, which could " -"not be imported!" -msgstr "" +msgid "{err}: Prototype {protkey} is based on typeclass {typeclass}, which could not be imported!" +msgstr "{err}原型: {protkey} 基于类型类 {typeclass},无法导入!" #: prototypes/prototypes.py:862 -#, python-brace-format msgid "Prototype {protkey} tries to parent itself." -msgstr "" +msgstr "原型 {protkey} 尝试将自己作为父节点。" #: prototypes/prototypes.py:868 -#, python-brace-format -msgid "" -"Prototype {protkey}'s `prototype_parent` (named '{parent}') was not found." -msgstr "" +msgid "Prototype {protkey}'s `prototype_parent` (named '{parent}') was not found." +msgstr "未找到原型 {protkey} 的 `prototype_parent` (名为 '{parent}')。" #: prototypes/prototypes.py:875 -#, python-brace-format msgid "{protkey} has infinite nesting of prototypes." -msgstr "" +msgstr "{protkey} 具有无限的原型嵌套。" #: prototypes/prototypes.py:900 -#, python-brace-format -msgid "" -"Prototype {protkey} has no `typeclass` defined anywhere in its parent\n" -" chain. Add `typeclass`, or a `prototype_parent` pointing to a prototype " -"with a typeclass." -msgstr "" +msgid "Prototype {protkey} has no `typeclass` defined anywhere in its parent\n" +" chain. Add `typeclass`, or a `prototype_parent` pointing to a prototype with a typeclass." +msgstr "原型 {protkey} 在其父链的任何地方均未定义 `typeclass`。\n" +"添加 `typeclass`,或指向具有 typeclass 的原型的 `protype_parent`。" #: prototypes/spawner.py:495 -#, python-brace-format -msgid "" -"Diff contains non-dicts that are not on the form (old, new, action_to_take): " -"{diffpart}" -msgstr "" +msgid "Diff contains non-dicts that are not on the form (old, new, action_to_take): {diffpart}" +msgstr "Diff 包含不属于 (old, new, action_to_take) 形式的非字典:{diffpart}" #: scripts/scripthandler.py:51 -#, fuzzy, python-brace-format -#| msgid "" -#| "\n" -#| " '%(key)s' (%(next_repeat)s/%(interval)s, %(repeats)s repeats): %(desc)s" msgid "" "\n" " '{key}' ({next_repeat}/{interval}, {repeats} repeats): {desc}" msgstr "" "\n" -" '%(key)s' (%(next_repeat)s/%(interval)s, %(repeats)s repeats): %(desc)s" +"'{key}'({next_repeat}/{interval},{repeats} 重复):{desc}" #: scripts/scripts.py:344 -#, fuzzy, python-brace-format -#| msgid "" -#| "Script %(key)s(#%(dbid)s) of type '%(cname)s': at_repeat() error " -#| "'%(err)s'." msgid "Script {key}(#{dbid}) of type '{name}': at_repeat() error '{err}'." -msgstr "" -"'%(cname)s' 的脚本 %(key)s(#%(dbid)s): at_repeat() 出现 '%(err)s' 错误。" +msgstr "类型为‘{name}’的脚本 {key}(#{dbid}):at_repeat() 错误‘{err}’。" #: server/initial_setup.py:29 -#, fuzzy -#| msgid "" -#| "\n" -#| "Welcome to your new |wEvennia|n-based game! Visit http://www.evennia.com " -#| "if you need\n" -#| "help, want to contribute, report issues or just join the community.\n" -#| "As Account #1 you can create a demo/tutorial area with |w@batchcommand " -#| "tutorial_world.build|n.\n" -#| " " msgid "" "\n" -"Welcome to your new |wEvennia|n-based game! Visit https://www.evennia.com if " -"you need\n" +"Welcome to your new |wEvennia|n-based game! Visit https://www.evennia.com if you need\n" "help, want to contribute, report issues or just join the community.\n" "\n" "As a privileged user, write |wbatchcommand tutorial_world.build|n to build\n" -"tutorial content. Once built, try |wintro|n for starting help and |wtutorial|" -"n to\n" +"tutorial content. Once built, try |wintro|n for starting help and |wtutorial|n to\n" "play the demo game.\n" msgstr "" "\n" -"欢迎进入您的基于 |wEvennia|n 的游戏! 如果需要帮助、想要做些贡献、报告错误的" -"话,请访问 http://www.evennia.com 。\n" +"--欢迎来到基于|wEvennia|n的新游戏!\n" +"如果您需要帮助,或是想贡献自己的力量、报告问题或只是想加入社区,\n" +"请访问 https://www.evennia.com\n" "\n" -"作为管理员,你可以使用 |w@batchcommand tutorial_world.build|n 来创建一个演示/" -"教程区域。\n" -" " +"作为超级用户,输入|wbatchcommand tutorial_world.build|n来构建教程内容。\n" +"构建完成后,请尝试使用 |wintro|n 启动帮助,随后使用 |wtutorial|n 玩演示游戏。\n" #: server/initial_setup.py:108 msgid "This is User #1." -msgstr "这是管理员。" +msgstr "这是首个用户。" #: server/initial_setup.py:128 msgid "Limbo" -msgstr "边境" +msgstr "Limbo" #: server/portal/portalsessionhandler.py:41 -#, python-brace-format -msgid "" -"{servername} DoS protection is active.You are queued to connect in {num} " -"seconds ..." -msgstr "" +msgid "{servername} DoS protection is active.You are queued to connect in {num} seconds ..." +msgstr "{servername} DoS 保护已激活。您将在 {num} 秒内排队连接..." #: server/server.py:156 msgid "idle timeout exceeded" @@ -581,48 +468,39 @@ msgstr "连接超时" #: server/server.py:177 msgid " (connection lost)" -msgstr "" +msgstr " (连接丢失)" #: server/sessionhandler.py:41 msgid "Your client sent an incorrect UTF-8 sequence." -msgstr "" +msgstr "您的客户端发送了一个不正确的 UTF-8 序列。" #: server/sessionhandler.py:410 msgid " ... Server restarted." -msgstr " ... 服务器已启动。" +msgstr " ... 服务器已重启。" #: server/sessionhandler.py:634 msgid "Logged in from elsewhere. Disconnecting." -msgstr "异地登录。已断线。" +msgstr "已从其他地方登录。正在断开连接。" #: server/sessionhandler.py:662 msgid "Idle timeout exceeded, disconnecting." -msgstr "连接超时。已断线。" +msgstr "连接超时,正在断开连接。" #: server/throttle.py:21 -msgid "" -"Too many failed attempts; you must wait a few minutes before trying again." -msgstr "" +msgid "Too many failed attempts; you must wait a few minutes before trying again." +msgstr "失败尝试次数过多;您必须等待几分钟才能重试。" #: server/validators.py:31 msgid "Sorry, that username is reserved." -msgstr "" +msgstr "抱歉,该用户名已被保留。" #: server/validators.py:38 msgid "Sorry, that username is already taken." -msgstr "" +msgstr "抱歉,该用户名已被使用。" #: server/validators.py:88 -#, fuzzy, python-brace-format -#| msgid "" -#| "%s From a terminal client, you can also use a phrase of multiple words if " -#| "you enclose the password in double quotes." -msgid "" -"{policy} From a terminal client, you can also use a phrase of multiple words " -"if you enclose the password in double quotes." -msgstr "" -"(%s) 在命令行客户端中,您可以使用英文引号将输入内容扩起,来使用包含空格的词" -"组。" +msgid "{policy} From a terminal client, you can also use a phrase of multiple words if you enclose the password in double quotes." +msgstr "{policy} 在命令行客户端中,如果用双引号括起密码,也可以使用由多个单词组成的短语。" #: utils/eveditor.py:68 msgid "" @@ -649,23 +527,56 @@ msgid "" " :y - yank (copy) line(s) to the copy buffer\n" " :x - cut line(s) and store it in the copy buffer\n" " :p - put (paste) previously copied line(s) directly after \n" -" :i - insert new text at line . Old line will move " -"down\n" +" :i - insert new text at line . Old line will move down\n" " :r - replace line with text \n" " :I - insert text at the beginning of line \n" " :A - append text after the end of line \n" "\n" -" :s - search/replace word or regex in buffer or on line " -"\n" +" :s - search/replace word or regex in buffer or on line \n" "\n" -" :j - justify buffer or line . is f, c, l or r. Default f " -"(full)\n" +" :j - justify buffer or line . is f, c, l or r. Default f (full)\n" " :f - flood-fill entire buffer or line : Equivalent to :j left\n" " :fi - indent entire buffer or line \n" " :fd - de-indent entire buffer or line \n" "\n" " :echo - turn echoing of the input on/off (helpful for some clients)\n" msgstr "" +"\n" +" - 任何非命令都会附加到缓冲区的末尾。\n" +" : - 查看缓冲区或仅查看行\n" +" :: - 原始视图缓冲区或仅行数 \n" +" ::: - escape - 输入': '作为该行的唯一字符。\n" +" :h - 此帮助。\n" +"\n" +" :w - 保存缓冲区(不退出)\n" +" :wq - 保存缓冲区并退出\n" +" :q - 退出(如果缓冲区已更改,将要求保存)\n" +" :q! - 不保存退出,不问任何问题\n" +"\n" +" :u - (撤销)在撤销历史中后退一步\n" +" :uu - (重做)在撤销历史中向前移动一步\n" +" :UU - 将所有更改重置回初始状态\n" +"\n" +" :dd - 删除最后一行或几行 \n" +" :dw - 删除整个缓冲区或行 中的单词或 regex \n" +" :DD - 清除整个缓冲区\n" +"\n" +" :y - 将行拖入(复制)到复制缓冲区\n" +" :x - 剪切行 ,并将其存储在复制缓冲区中\n" +" :p - 将先前复制的行直接粘贴到 之后\n" +" :i - 在 行插入新文本 。旧行将下移\n" +" :r - 用文本 替换 行\n" +" :I - 在 行开头插入文本\n" +" :A - 在 行结束后附加文本\n" +"\n" +" :s - 在缓冲区或行 中搜索/替换单词或 regex \n" +"\n" +" :j - 对齐缓冲区或 行。 可以是 f、c、l 或 r。默认为 f(全选)\n" +" :f - 填充整个缓冲区或行 :等同于 :j left\n" +" :fi - 缩进整个缓冲区或行 \n" +" :fd - 取消整个缓冲区或行 的缩进\n" +"\n" +" :echo - 打开/关闭输入的回显(对某些客户端有帮助)\n" #: utils/eveditor.py:108 msgid "" @@ -675,6 +586,11 @@ msgid "" " - a single word, or multiple words with quotes around them.\n" " - longer string, usually not needing quotes.\n" msgstr "" +"\n" +"图例: \n" +" - 行号,如 '5' 或范围,如 '3:7'。 \n" +" - 单个单词,或多个单词,用引号括起来。 \n" +" - 较长的字符串,通常不需要引号。\n" #: utils/eveditor.py:117 msgid "" @@ -684,48 +600,61 @@ msgid "" " :> - Increase the level of automatic indentation for the next lines\n" " := - Switch automatic indentation on/off\n" msgstr "" +"\n" +":! - 执行代码缓冲区而不保存 \n" +":< - 降低下一行的自动缩进级别 \n" +":> - 增加下一行的自动缩进级别 \n" +":= - 打开/关闭自动缩进\n" #: utils/eveditor.py:128 -#, python-brace-format msgid "" "\n" "{error}\n" "\n" "|rBuffer load function error. Could not load initial data.|n\n" msgstr "" +"\n" +"{error} \n" +"\n" +"|rBuffer 加载函数错误。无法加载初始数据。|n\n" #: utils/eveditor.py:136 -#, python-brace-format msgid "" "\n" "{error}\n" "\n" "|rSave function returned an error. Buffer not saved.|n\n" msgstr "" +"\n" +"{error} \n" +"\n" +"|rSave 函数返回错误。缓冲区未保存。|n\n" #: utils/eveditor.py:143 msgid "|rNo save function defined. Buffer cannot be saved.|n" -msgstr "" +msgstr "|r未定义保存函数。无法保存至缓冲区。|n" #: utils/eveditor.py:145 msgid "No changes need saving" -msgstr "" +msgstr "无需保存任何更改" #: utils/eveditor.py:146 msgid "Exited editor." -msgstr "" +msgstr "退出编辑器。" #: utils/eveditor.py:149 -#, python-brace-format msgid "" "\n" "{error}\n" "\n" "|rQuit function gave an error. Skipping.|n\n" msgstr "" +"\n" +"{error} \n" +"\n" +"|r退出函数出错。已跳过。|n\n" #: utils/eveditor.py:157 -#, python-brace-format msgid "" "\n" "{error}\n" @@ -734,238 +663,209 @@ msgid "" "to non-persistent mode (which means the editor session won't survive\n" "an eventual server reload - so save often!)|n\n" msgstr "" +"\n" +"{error}\n" +"\n" +"|r编辑器状态无法保存为持久模式,切换到非持久模式。\n" +"(这意味着编辑器会话将无法在最终的服务器重载后继续存在\n" +"--所以要经常保存!)|n\n" #: utils/eveditor.py:167 -msgid "" -"EvEditor persistent-mode error. Commonly, this is because one or more of the " -"EvEditor callbacks could not be pickled, for example because it's a class " -"method or is defined inside another function." -msgstr "" +msgid "EvEditor persistent-mode error. Commonly, this is because one or more of the EvEditor callbacks could not be pickled, for example because it's a class method or is defined inside another function." +msgstr "EvEditor 持久模式错误。通常情况下,这是因为一个或多个 EvEditor 回调无法被拾取,例如因为它是一个类方法或定义在另一个函数内。" #: utils/eveditor.py:173 msgid "Nothing to undo." -msgstr "" +msgstr "沒有需要撤消的操作。" #: utils/eveditor.py:174 msgid "Nothing to redo." -msgstr "" +msgstr "沒有需要重做的操作。" #: utils/eveditor.py:175 msgid "Undid one step." -msgstr "" +msgstr "撤消了一个步骤。" #: utils/eveditor.py:176 msgid "Redid one step." -msgstr "" +msgstr "重做了一个步骤。" #: utils/eveditor.py:494 msgid "Single ':' added to buffer." -msgstr "" +msgstr "单个‘:’已添加到缓冲区。" #: utils/eveditor.py:509 msgid "Save before quitting?" -msgstr "" +msgstr "退出前要保存吗?" #: utils/eveditor.py:524 msgid "Reverted all changes to the buffer back to original state." -msgstr "" +msgstr "将缓冲区的所有更改恢复为原始状态。" #: utils/eveditor.py:529 -#, python-brace-format msgid "Deleted {string}." -msgstr "" +msgstr "已删除 {string}。" #: utils/eveditor.py:534 msgid "You must give a search word to delete." -msgstr "" +msgstr "您必须输入要删除的搜索词。" #: utils/eveditor.py:540 -#, python-brace-format msgid "Removed {arg1} for lines {l1}-{l2}." -msgstr "" +msgstr "删除了第 {l1}-{l2} 行的 {arg1}。" #: utils/eveditor.py:546 -#, python-brace-format msgid "Removed {arg1} for {line}." -msgstr "" +msgstr "已删除 {line} 的 {arg1}。" #: utils/eveditor.py:562 -#, python-brace-format msgid "Cleared {nlines} lines from buffer." -msgstr "" +msgstr "从缓冲区清除了 {nlines} 行。" #: utils/eveditor.py:567 -#, python-brace-format msgid "{line}, {cbuf} yanked." -msgstr "" +msgstr "{line}, {cbuf} 被删除。" #: utils/eveditor.py:574 -#, python-brace-format msgid "{line}, {cbuf} cut." -msgstr "" +msgstr "{line},{cbuf} 被剪切。" #: utils/eveditor.py:578 msgid "Copy buffer is empty." -msgstr "" +msgstr "复制缓冲区为空。" #: utils/eveditor.py:583 -#, python-brace-format msgid "Pasted buffer {cbuf} to {line}." -msgstr "" +msgstr "已将缓冲区 {cbuf} 粘贴至 {line}。" #: utils/eveditor.py:591 msgid "You need to enter a new line and where to insert it." -msgstr "" +msgstr "您需要输入新行以及插入位置。" #: utils/eveditor.py:596 -#, python-brace-format msgid "Inserted {num} new line(s) at {line}." -msgstr "" +msgstr "在 {line} 处插入 {num} 个新行。" #: utils/eveditor.py:604 msgid "You need to enter a replacement string." -msgstr "" +msgstr "您需要输入替换字符串。" #: utils/eveditor.py:609 -#, python-brace-format msgid "Replaced {num} line(s) at {line}." -msgstr "" +msgstr "替换了第 {line} 行中的 {num} 行。" #: utils/eveditor.py:616 msgid "You need to enter text to insert." -msgstr "" +msgstr "您需要输入要插入的文本。" #: utils/eveditor.py:624 -#, python-brace-format msgid "Inserted text at beginning of {line}." -msgstr "" +msgstr "在{line}开头插入文本。" #: utils/eveditor.py:628 msgid "You need to enter text to append." -msgstr "" +msgstr "您需要输入要附加的文本。" #: utils/eveditor.py:636 -#, python-brace-format msgid "Appended text to end of {line}." -msgstr "" +msgstr "将文本附加至{line}的末尾。" #: utils/eveditor.py:641 msgid "You must give a search word and something to replace it with." -msgstr "" +msgstr "您必须给出一个搜索词以及要替换它的词。" #: utils/eveditor.py:647 -#, python-brace-format msgid "Search-replaced {arg1} -> {arg2} for lines {l1}-{l2}." -msgstr "" +msgstr "搜索替换行 {l1}-{l2} 的 {arg1} -> {arg2}。" #: utils/eveditor.py:653 -#, python-brace-format msgid "Search-replaced {arg1} -> {arg2} for {line}." -msgstr "" +msgstr "搜索替换 {arg1} -> {arg2} 中的 {line}。" #: utils/eveditor.py:677 -#, python-brace-format msgid "Flood filled lines {l1}-{l2}." -msgstr "" +msgstr "填充行 {l1}-{l2}。" #: utils/eveditor.py:679 -#, python-brace-format msgid "Flood filled {line}." -msgstr "" +msgstr "填充了{line}。" #: utils/eveditor.py:701 msgid "Valid justifications are" -msgstr "" +msgstr "有效的理由如下" #: utils/eveditor.py:710 -#, python-brace-format msgid "{align}-justified lines {l1}-{l2}." -msgstr "" +msgstr "{align}-对齐行 {l1}-{l2}。" #: utils/eveditor.py:716 -#, python-brace-format msgid "{align}-justified {line}." -msgstr "" +msgstr "{align}-对齐{line}。" #: utils/eveditor.py:728 -#, python-brace-format msgid "Indented lines {l1}-{l2}." -msgstr "" +msgstr "缩进行 {l1}-{l2}。" #: utils/eveditor.py:730 -#, python-brace-format msgid "Indented {line}." -msgstr "" +msgstr "缩进{line}。" #: utils/eveditor.py:740 -#, python-brace-format msgid "Removed left margin (dedented) lines {l1}-{l2}." -msgstr "" +msgstr "删除行 {l1}-{l2}的左侧边距(dedented)。" #: utils/eveditor.py:745 -#, python-brace-format msgid "Removed left margin (dedented) {line}." -msgstr "" +msgstr "删除了左侧边距(dedented){line}。" #: utils/eveditor.py:753 -#, python-brace-format msgid "Echo mode set to {mode}" -msgstr "" +msgstr "回显模式设置为 {mode}" #: utils/eveditor.py:758 utils/eveditor.py:773 utils/eveditor.py:788 #: utils/eveditor.py:799 msgid "This command is only available in code editor mode." -msgstr "" +msgstr "此命令仅在代码编辑器模式下可用。" #: utils/eveditor.py:766 -#, python-brace-format msgid "Decreased indentation: new indentation is {indent}." -msgstr "" +msgstr "减少缩进:新缩进为{indent}。" #: utils/eveditor.py:771 utils/eveditor.py:786 msgid "|rManual indentation is OFF.|n Use := to turn it on." -msgstr "" +msgstr "|r手动缩进已关闭。|n 使用 := 将其打开。" #: utils/eveditor.py:781 -#, python-brace-format msgid "Increased indentation: new indentation is {indent}." -msgstr "" +msgstr "增加缩进:新缩进为 {indent}。" #: utils/eveditor.py:795 msgid "Auto-indentation turned on." -msgstr "" +msgstr "自动缩进已打开。" #: utils/eveditor.py:797 msgid "Auto-indentation turned off." -msgstr "" +msgstr "自动缩进已关闭。" #: utils/eveditor.py:1093 -#, python-brace-format msgid "Line Editor [{name}]" -msgstr "" +msgstr "行编辑器 [{name}]" #: utils/eveditor.py:1101 msgid "(:h for help)" -msgstr "" +msgstr "(:h 获取帮助)" #: utils/evmenu.py:302 -#, fuzzy, python-brace-format -#| msgid "" -#| "Menu node '{nodename}' is either not implemented or caused an error. Make " -#| "another choice." -msgid "" -"Menu node '{nodename}' is either not implemented or caused an error. Make " -"another choice or try 'q' to abort." -msgstr "菜单节点 '{nodename}' 未实现或发生错误。请尝试其他选项。" +msgid "Menu node '{nodename}' is either not implemented or caused an error. Make another choice or try 'q' to abort." +msgstr "菜单节点’{nodename}‘未实现或导致错误。请做出其他选择或尝试’q‘中止。" #: utils/evmenu.py:305 -#, python-brace-format msgid "Error in menu node '{nodename}'." msgstr "菜单节点 '{nodename}' 发生错误。" #: utils/evmenu.py:306 msgid "No description." -msgstr "无描述。" +msgstr "没有描述。" #: utils/evmenu.py:307 msgid "Commands: , help, quit" @@ -985,219 +885,168 @@ msgstr "命令: help" #: utils/evmenu.py:311 utils/evmenu.py:1850 msgid "Choose an option or try 'help'." -msgstr "" +msgstr "选择一个选项或尝试“帮助”。" #: utils/evmenu.py:1375 msgid "|rInvalid choice.|n" -msgstr "" +msgstr "|r无效的选择。|n" #: utils/evmenu.py:1439 msgid "|Wcurrent|n" -msgstr "" +msgstr "|W当前|n" #: utils/evmenu.py:1447 msgid "|wp|Wrevious page|n" -msgstr "" +msgstr "|wp|W上一页|n" #: utils/evmenu.py:1454 msgid "|wn|Wext page|n" -msgstr "" +msgstr "|wn|下一页|n" #: utils/evmenu.py:1689 msgid "Aborted." -msgstr "" +msgstr "已中止。" #: utils/evmenu.py:1712 msgid "|rError in ask_yes_no. Choice not confirmed (report to admin)|n" -msgstr "" +msgstr "|rask_yes_no 错误。选择尚未确认(请向管理员汇报)|n" #: utils/evmore.py:235 msgid "|xExited pager.|n" -msgstr "" +msgstr "|x退出呼叫器。|n" #: utils/optionhandler.py:138 utils/optionhandler.py:162 msgid "Option not found!" -msgstr "" +msgstr "未找到选项!" #: utils/optionhandler.py:159 msgid "Option field blank!" -msgstr "" +msgstr "选项字段空白!" #: utils/optionhandler.py:165 #, fuzzy -#| msgid "There were multiple matches." msgid "Multiple matches:" msgstr "发现多个匹配项。" #: utils/optionhandler.py:165 msgid "Please be more specific." -msgstr "" +msgstr "请输入更有指向性的名称。" #: utils/utils.py:2127 -#, python-brace-format -msgid "" -"{obj}.{handlername} is a handler and can't be set directly. To add values, " -"use `{obj}.{handlername}.add()` instead." -msgstr "" +msgid "{obj}.{handlername} is a handler and can't be set directly. To add values, use `{obj}.{handlername}.add()` instead." +msgstr "{obj}.{handlername} 是一个处理程序,不能直接设置。要添加值,请使用 `{obj}.{handlername}.add()`。" #: utils/utils.py:2137 -#, python-brace-format -msgid "" -"{obj}.{handlername} is a handler and can't be deleted directly. To remove " -"values, use `{obj}.{handlername}.remove()` instead." -msgstr "" +msgid "{obj}.{handlername} is a handler and can't be deleted directly. To remove values, use `{obj}.{handlername}.remove()` instead." +msgstr "{obj}.{handlername} 是一个处理程序,无法直接删除。要删除值,请使用 `{obj}.{handlername}.remove()`。" #: utils/utils.py:2278 -#, fuzzy, python-brace-format -#| msgid "Could not find '%s'." +#, fuzzy msgid "Could not find '{query}'." -msgstr "无法找到 '%s'" +msgstr "无法找到“{query}”。" #: utils/utils.py:2285 -#, fuzzy, python-brace-format -#| msgid "More than one match for '%s' (please narrow target):\n" +#, fuzzy msgid "More than one match for '{query}' (please narrow target):\n" -msgstr "发现多个符合 '%s' 的匹配项 (请缩小范围):\n" +msgstr "‘{query}’ 有多个匹配项(请缩小目标):\n" #: utils/validatorfuncs.py:25 -#, python-brace-format msgid "Input could not be converted to text ({err})" -msgstr "" +msgstr "输入无法转换为文本({err})" #: utils/validatorfuncs.py:34 -#, python-brace-format msgid "Nothing entered for a {option_key}!" -msgstr "" +msgstr "{option_key} 未输入任何内容!" #: utils/validatorfuncs.py:38 -#, python-brace-format msgid "'{entry}' is not a valid {option_key}." -msgstr "" +msgstr "'{entry}' 不是有效的 {option_key}。" #: utils/validatorfuncs.py:63 utils/validatorfuncs.py:236 -#, python-brace-format msgid "No {option_key} entered!" -msgstr "" +msgstr "未输入{option_key}!" #: utils/validatorfuncs.py:72 -#, python-brace-format msgid "Timezone string '{acct_tz}' is not a valid timezone ({err})" -msgstr "" +msgstr "时区字符串“{acct_tz}”不是有效的时区({err})" #: utils/validatorfuncs.py:89 utils/validatorfuncs.py:97 -#, python-brace-format msgid "{option_key} must be entered in a 24-hour format such as: {timeformat}" -msgstr "" +msgstr "{option_key} 必须采用 24 小时格式输入,例如:{timeformat}" #: utils/validatorfuncs.py:141 -#, python-brace-format msgid "Could not convert section '{interval}' to a {option_key}." -msgstr "" +msgstr "无法将’{interval}‘部分转换为{option_key}。" #: utils/validatorfuncs.py:153 -#, python-brace-format msgid "That {option_key} is in the past! Must give a Future datetime!" -msgstr "" +msgstr "{option_key}是过去时!必须给出一个未来日期时间!" #: utils/validatorfuncs.py:163 -#, python-brace-format msgid "Must enter a whole number for {option_key}!" -msgstr "" +msgstr "必须为{option_key}输入一个整数!" #: utils/validatorfuncs.py:169 -#, python-brace-format msgid "Could not convert '{entry}' to a whole number for {option_key}!" -msgstr "" +msgstr "无法将“{entry}”转换为{option_key}的整数!" #: utils/validatorfuncs.py:180 -#, python-brace-format msgid "Must enter a whole number greater than 0 for {option_key}!" -msgstr "" +msgstr "必须为 {option_key} 输入一个大于 0 的整数!" #: utils/validatorfuncs.py:191 -#, python-brace-format msgid "{option_key} must be a whole number greater than or equal to 0!" -msgstr "" +msgstr "{option_key} 必须是大于或等于 0 的整数!" #: utils/validatorfuncs.py:210 -#, python-brace-format msgid "Must enter a true/false input for {option_key}. Accepts {alternatives}." -msgstr "" +msgstr "必须为 {option_key} 输入真/假值。接受 {alternatives}。" #: utils/validatorfuncs.py:240 -#, python-brace-format msgid "That matched: {matches}. Please be more specific!" -msgstr "" +msgstr "匹配项:{matches}。请更具体一些!" #: utils/validatorfuncs.py:247 -#, python-brace-format msgid "Could not find timezone '{entry}' for {option_key}!" -msgstr "" +msgstr "无法找到 {option_key} 的时区“{entry}”!" #: utils/validatorfuncs.py:255 msgid "Email address field empty!" -msgstr "" +msgstr "电子邮件地址字段为空!" #: utils/validatorfuncs.py:258 -#, python-brace-format msgid "That isn't a valid {option_key}!" -msgstr "" +msgstr "这不是一个有效的{option_key}!" #: utils/validatorfuncs.py:265 -#, python-brace-format msgid "No {option_key} entered to set!" -msgstr "" +msgstr "未输入要设置的{option_key}!" #: utils/validatorfuncs.py:269 msgid "Must enter an access type!" -msgstr "" +msgstr "必须输入访问类型!" #: utils/validatorfuncs.py:273 -#, python-brace-format msgid "Access type must be one of: {alternatives}" -msgstr "" +msgstr "访问类型必须是其中之一:{alternatives}" #: utils/validatorfuncs.py:278 msgid "Lock func not entered." -msgstr "" +msgstr "未输入锁定函数。" #: web/templates/admin/app_list.html:19 msgid "Add" -msgstr "" +msgstr "添加" #: web/templates/admin/app_list.html:26 msgid "View" -msgstr "" +msgstr "视图" #: web/templates/admin/app_list.html:28 msgid "Change" -msgstr "" +msgstr "更改" #: web/templates/admin/app_list.html:39 msgid "You don’t have permission to view or edit anything." -msgstr "" +msgstr "您无权查看或编辑任何内容。" -#~ msgid " : {current}" -#~ msgstr "<合并 {mergelist} {mergetype},优先级 {prio}>: {current}" - -#~ msgid "" -#~ " <{key} ({mergetype}, prio {prio}, {permstring})>:\n" -#~ " {keylist}" -#~ msgstr "" -#~ " <{key} ({mergetype}, 优先级 {prio}, {permstring})>:\n" -#~ " {keylist}" - -#~ msgid "Say what?" -#~ msgstr "您想说?" - -#~ msgid "Channel '%s' not found." -#~ msgstr "未找到频道 '%s' 。" - -#~ msgid "You are not connected to channel '%s'." -#~ msgstr "未连接至频道 '%s' 。" - -#~ msgid "You are not permitted to send to channel '%s'." -#~ msgstr "您未被允许在频道 '%s' 发送信息。" - -#~ msgid " (channel)" -#~ msgstr " (频道)" diff --git a/evennia/objects/manager.py b/evennia/objects/manager.py index afddc809a8..b41805d318 100644 --- a/evennia/objects/manager.py +++ b/evennia/objects/manager.py @@ -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 ( diff --git a/evennia/objects/models.py b/evennia/objects/models.py index 544f33fd2d..5db789a007 100644 --- a/evennia/objects/models.py +++ b/evennia/objects/models.py @@ -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: diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 2906ecbed2..08acc74876 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -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}") diff --git a/evennia/objects/tests.py b/evennia/objects/tests.py index 4c73bc82a6..9daaf0dcf5 100644 --- a/evennia/objects/tests.py +++ b/evennia/objects/tests.py @@ -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") diff --git a/evennia/scripts/ondemandhandler.py b/evennia/scripts/ondemandhandler.py index a86eb91016..c7191d501f 100644 --- a/evennia/scripts/ondemandhandler.py +++ b/evennia/scripts/ondemandhandler.py @@ -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): diff --git a/evennia/server/portal/mccp.py b/evennia/server/portal/mccp.py index ba748e0010..8f7aa00de5 100644 --- a/evennia/server/portal/mccp.py +++ b/evennia/server/portal/mccp.py @@ -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() diff --git a/evennia/server/portal/mssp.py b/evennia/server/portal/mssp.py index 03714cf9da..2b0a3bbe9f 100644 --- a/evennia/server/portal/mssp.py +++ b/evennia/server/portal/mssp.py @@ -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() diff --git a/evennia/server/portal/mxp.py b/evennia/server/portal/mxp.py index a445168c97..b9952864c3 100644 --- a/evennia/server/portal/mxp.py +++ b/evennia/server/portal/mxp.py @@ -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() diff --git a/evennia/server/portal/naws.py b/evennia/server/portal/naws.py index 71d3a75352..d3c62a4131 100644 --- a/evennia/server/portal/naws.py +++ b/evennia/server/portal/naws.py @@ -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) diff --git a/evennia/server/portal/suppress_ga.py b/evennia/server/portal/suppress_ga.py index cadf007fa9..f933b359e5 100644 --- a/evennia/server/portal/suppress_ga.py +++ b/evennia/server/portal/suppress_ga.py @@ -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() diff --git a/evennia/server/portal/telnet.py b/evennia/server/portal/telnet.py index 46d1de0baa..e561953f18 100644 --- a/evennia/server/portal/telnet.py +++ b/evennia/server/portal/telnet.py @@ -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): diff --git a/evennia/server/portal/telnet_oob.py b/evennia/server/portal/telnet_oob.py index ffc2283067..c8c3365ab0 100644 --- a/evennia/server/portal/telnet_oob.py +++ b/evennia/server/portal/telnet_oob.py @@ -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) diff --git a/evennia/server/portal/ttype.py b/evennia/server/portal/ttype.py index d1b4c738b2..5427cd40e8 100644 --- a/evennia/server/portal/ttype.py +++ b/evennia/server/portal/ttype.py @@ -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 diff --git a/evennia/server/serversession.py b/evennia/server/serversession.py index 4f76960592..165769b7f2 100644 --- a/evennia/server/serversession.py +++ b/evennia/server/serversession.py @@ -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 diff --git a/evennia/server/service.py b/evennia/server/service.py index 80eba12bb0..cfda892709 100644 --- a/evennia/server/service.py +++ b/evennia/server/service.py @@ -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 diff --git a/evennia/utils/dbserialize.py b/evennia/utils/dbserialize.py index cc79eed89e..8984c876e8 100644 --- a/evennia/utils/dbserialize.py +++ b/evennia/utils/dbserialize.py @@ -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): diff --git a/evennia/utils/eveditor.py b/evennia/utils/eveditor.py index 2657d61393..debd63c7de 100644 --- a/evennia/utils/eveditor.py +++ b/evennia/utils/eveditor.py @@ -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("=") diff --git a/evennia/utils/evmore.py b/evennia/utils/evmore.py index 57192e2e90..5aebc899d9 100644 --- a/evennia/utils/evmore.py +++ b/evennia/utils/evmore.py @@ -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) diff --git a/evennia/utils/funcparser.py b/evennia/utils/funcparser.py index c0de83838a..2ed3243004 100644 --- a/evennia/utils/funcparser.py +++ b/evennia/utils/funcparser.py @@ -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 diff --git a/evennia/utils/tests/test_dbserialize.py b/evennia/utils/tests/test_dbserialize.py index f70bf702a0..61fac175b7 100644 --- a/evennia/utils/tests/test_dbserialize.py +++ b/evennia/utils/tests/test_dbserialize.py @@ -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 diff --git a/evennia/utils/tests/test_evtable.py b/evennia/utils/tests/test_evtable.py index 54869e8da9..34fb3d40fc 100644 --- a/evennia/utils/tests/test_evtable.py +++ b/evennia/utils/tests/test_evtable.py @@ -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)) diff --git a/evennia/utils/tests/test_funcparser.py b/evennia/utils/tests/test_funcparser.py index 7756a95999..589219163e 100644 --- a/evennia/utils/tests/test_funcparser.py +++ b/evennia/utils/tests/test_funcparser.py @@ -231,6 +231,9 @@ class TestFuncParser(TestCase): ("Test literal3 $typ($lit(1)aaa)", "Test literal3 "), ("Test literal4 $typ(aaa$lit(1))", "Test literal4 "), ("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): diff --git a/evennia/utils/verb_conjugation/verbs.txt b/evennia/utils/verb_conjugation/verbs.txt index 9cfae2cc47..ae297860fd 100644 --- a/evennia/utils/verb_conjugation/verbs.txt +++ b/evennia/utils/verb_conjugation/verbs.txt @@ -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,,,,,,,,,,,, diff --git a/evennia/web/static/webclient/js/plugins/goldenlayout.js b/evennia/web/static/webclient/js/plugins/goldenlayout.js index 68ea5710bd..aca6f8b836 100644 --- a/evennia/web/static/webclient/js/plugins/goldenlayout.js +++ b/evennia/web/static/webclient/js/plugins/goldenlayout.js @@ -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(); } diff --git a/evennia/web/static/webclient/js/plugins/options2.js b/evennia/web/static/webclient/js/plugins/options2.js index 24acd4b314..ef62d62a54 100644 --- a/evennia/web/static/webclient/js/plugins/options2.js +++ b/evennia/web/static/webclient/js/plugins/options2.js @@ -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() { diff --git a/pyproject.toml b/pyproject.toml index bbc6c74a32..35c49b43b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"