diff --git a/CHANGELOG.md b/CHANGELOG.md index 669b4ea2e5..f92cdab5ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ ## Main branch +- [Feature][pull3470]: New `exit_order` kwarg to + `DefaultObject.get_display_exits` to easier customize the order in which + standard exits are displayed in a room (chiizujin) + +[pull3470]: https://github.com/evennia/evennia/pull/3470 + +## Evennia 4.1.1 + +April 6, 2024 + - [Fix][pull3438]: Error with 'you' mapping in third-person style of `msg_contents` (InspectorCaracal) - [Fix][pull3472]: The new `filter_visible` didn't exclude oneself by default @@ -10,8 +20,26 @@ `.get_extra_display_name_info` (the #dbref display by default) (Griatch) - Fix: Add `DefaultAccount.get_extra_display_name_info` method for API compliance with `DefaultObject` in commands. (Griatch) +- Fix: Show `XYZRoom` subclass when repr() it. (Griatch) +- [Fix][pull3485]: Typo in `sethome` message (chiizujin) +- [Fix][pull3487]: Fix traceback when using `get`,`drop` and `give` with no + arguments (InspectorCaracal) +- [Fix][issue3476]: Don't ignore EvEditor commands with wrong capitalization (Griatch) +- [Fix][issue3477]: The `at_server_reload_start()` hook was not firing on + a reload (regression). +- [Fix][issue3488]: `AttributeProperty(, autocreate=False)`, where + `` was mutable would not update/save properly in-place (Griatch) +- [Docs] Added new [Server-Lifecycle][doc-server-lifecycle] page to describe + the hooks called on server start/stop/reload (Griatch) +- [Docs] Doc typo fixes (Griatch, chiizujin) [pull3438]: https://github.com/evennia/evennia/pull/3446 +[pull3485]: https://github.com/evennia/evennia/pull/3485 +[pull3487]: https://github.com/evennia/evennia/pull/3487 +[issue3476]: https://github.com/evennia/evennia/issues/3476 +[issue3477]: https://github.com/evennia/evennia/issues/3477 +[issue3488]: https://github.com/evennia/evennia/issues/3488 +[doc-server-lifecycle]: https://www.evennia.com/docs/latest/Concepts/Server-Lifecycle.html ## Evennia 4.1.0 diff --git a/docs/source/Coding/Changelog.md b/docs/source/Coding/Changelog.md index 7e0137fef9..b603ec36ca 100644 --- a/docs/source/Coding/Changelog.md +++ b/docs/source/Coding/Changelog.md @@ -1,5 +1,39 @@ # Changelog +## Evennia 4.1.1 + +April 6, 2024 + +- [Fix][pull3438]: Error with 'you' mapping in third-person style of + `msg_contents` (InspectorCaracal) +- [Fix][pull3472]: The new `filter_visible` didn't exclude oneself by default + (InspectorCaracal) +- Fix: `find #dbref` results didn't include the results of + `.get_extra_display_name_info` (the #dbref display by default) (Griatch) +- Fix: Add `DefaultAccount.get_extra_display_name_info` method for API + compliance with `DefaultObject` in commands. (Griatch) +- Fix: Show `XYZRoom` subclass when repr() it. (Griatch) +- [Fix][pull3485]: Typo in `sethome` message (chiizujin) +- [Fix][pull3487]: Fix traceback when using `get`,`drop` and `give` with no + arguments (InspectorCaracal) +- [Fix][issue3476]: Don't ignore EvEditor commands with wrong capitalization (Griatch) +- [Fix][issue3477]: The `at_server_reload_start()` hook was not firing on + a reload (regression). +- [Fix][issue3488]: `AttributeProperty(, autocreate=False)`, where + `` was mutable would not update/save properly in-place (Griatch) +- [Docs] Added new [Server-Lifecycle][doc-server-lifecycle] page to describe + the hooks called on server start/stop/reload (Griatch) +- [Docs] Doc typo fixes (Griatch, chiizujin) + +[pull3438]: https://github.com/evennia/evennia/pull/3446 +[pull3485]: https://github.com/evennia/evennia/pull/3485 +[pull3487]: https://github.com/evennia/evennia/pull/3487 +[issue3476]: https://github.com/evennia/evennia/issues/3476 +[issue3477]: https://github.com/evennia/evennia/issues/3477 +[issue3488]: https://github.com/evennia/evennia/issues/3488 +[doc-server-lifecycle]: https://www.evennia.com/docs/latest/Concepts/Server-Lifecycle.html + + ## Evennia 4.1.0 April 1, 2024 @@ -49,7 +83,8 @@ April 1, 2024 differentiating from their lower-case alternatives (Griatch) - [Fix][issue3460]: The `menu_login` contrib regression caused it to error out when creating a new character (Griatch) -- Doc: Added Beginner Tutorial lessons for AI, Quests and Procedural dungeon (Griatch) +- Doc: Added Beginner Tutorial lessons for [Monster and NPC AI][docAI], + [Quests][docQuests] and [Making a Procedural dungeon][docDungeon] (Griatch) - Doc fixes (Griatch, InspectorCaracal, homeofpoe) [pull3421]: https://github.com/evennia/evennia/pull/3421 @@ -70,6 +105,9 @@ April 1, 2024 [issue3462]: https://github.com/evennia/evennia/issues/3462 [issue3460]: https://github.com/evennia/evennia/issues/3460 [issue3461]: https://github.com/evennia/evennia/issues/3461 +[docAI]: https://www.evennia.com/docs/latest/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-AI.html +[docQuests]: https://www.evennia.com/docs/latest/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Quests.html +[docDungeon]: https://www.evennia.com/docs/latest/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Dungeon.html ## Evennia 4.0.0 diff --git a/docs/source/Concepts/Concepts-Overview.md b/docs/source/Concepts/Concepts-Overview.md index 84d0aa5ca4..60cb066e7f 100644 --- a/docs/source/Concepts/Concepts-Overview.md +++ b/docs/source/Concepts/Concepts-Overview.md @@ -37,6 +37,7 @@ Banning.md ```{toctree} :maxdepth: 2 +Server-Lifecycle Protocols.md Models.md Zones.md diff --git a/docs/source/Concepts/Server-Lifecycle.md b/docs/source/Concepts/Server-Lifecycle.md new file mode 100644 index 0000000000..274d0e1d63 --- /dev/null +++ b/docs/source/Concepts/Server-Lifecycle.md @@ -0,0 +1,80 @@ + +# Evennia Server Lifecycle + +As part of your game design you may want to change how Evennia behaves when starting or stopping. A common use case would be to start up some piece of custom code you want to always have available once the server is up. + +Evennia has three main life cycles, all of which you can add custom behavior for: + +- **Database life cycle**: Evennia uses a database. This exists in parallel to the code changes you do. The database exists until you choose to reset or delete it. Doing so doesn't require re-downloading Evennia. +- **Reboot life cycle**: From When Evennia starts to it being fully shut down, which means both Portal and Server are stopped. At the end of this cycle, all players are disconnected. +- **Reload life cycle:** This is the main runtime, until a "reload" event. Reloads refreshes game code but do not kick any players. + +## When Evennia starts for the first time + +This is the beginning of the **Database life cycle**, just after the database is created and migrated for the first time (or after it was deleted and re-built). See [Choosing a Database](../Setup/Choosing-a-Database.md) for instructions on how to reset a database, should you want to re-run this sequence after the first time. + +Hooks called, in sequence: + +1. `evennia.server.initial_setup.handle_setup(last_step=None)`: Evennia's core initialization function. This is what creates the #1 Character (tied to the superuser account) and `Limbo` room. It calls the next hook below and also understands to restart at the last failed step if there was some issue. You should normally not override this function unless you _really_ know what you are doing. To override, change `settings.INITIAL_SETUP_MODULE` to your own module with a `handle_setup` function in it. +2. `mygame/server/conf/at_initial_setup.py` contains a single function, `at_initial_setup()`, which will be called without arguments. It's called last in the setup sequence by the above function. Use this to add your own custom behavior or to tweak the initialization. If you for example wanted to change the auto-generated Limbo room, you should do it from here. If you want to change where this function is found, you can do so by changing `settings.AT_INITIAL_SETUP_HOOK_MODULE`. + +## When Evennia starts and shutdowns + +This is part of the **Reboot life cycle**. Evennia consists of two main processes, the [Portal and the Server](../Components/Portal-And-Server.md). On a reboot or shutdown, both Portal and Server shuts down, which means all players are disconnected. + +Each process call a series of hooks located in `mygame/server/conf/at_server_startstop.py`. You can customize the module used with `settings.AT_SERVER_STARTSTOP_MODULE` - this can even be a list of modules, if so, the appropriately-named functions will be called from each module, in sequence. + +All hooks are called without arguments. + +> The use of the term 'server' in the hook-names indicate the whole of Evennia, not just the `Server` component. + +### Server cold start + +Starting the server from zero, after a full stop. This is done with `evennia start` from the terminal. + +1. `at_server_init()` - Always called first in the startup sequence. +2. `at_server_cold_start()` - Only called on cold starts. +3. `at_server_start()` - Always called last in the startup sequece. + +### Server cold shutdown + +Shutting everything down. Done with `shutdown` in-game or `evennia stop` from the terminal. + +1. `at_server_cold_stop()` - Only called on cold stops. +2. `at_server_stop()` - Always called last in the stopping sequence. + +### Server reboots + +This is done with `evennia reboot` and effectively constitutes an automatic cold shutdown followed by a cold start controlled from the `evennia` launcher. There are no special `reboot` hooks for this, instead it looks like you'd expect: + +1. `at_server_cold_stop()` +2. `at_server_stop()` (after this, both `Server` + `Portal` have both shut down) +3. `at_server_init()` (like a cold start) +4. `at_server_cold_start()` +5. `at_server_start()` + +## When Evennia reloads and resets + +This is the **Reload life cycle**. As mentioned above, Evennia consists of two components, the [Portal and Server](../Components/Portal-And-Server.md). During a reload, only the `Server` component is shut down and restarted. Since the Portal stays up, players are not disconnected. + +All hooks are called without arguments. + +### Server reload + +Reloads are initiated with the `reload` command in-game, or with `evennia reload` from the terminal. + +1. `at_server_reload_stop()` - Only called on reload stops. +2. `at_server_stop` - Always called last in the stopping sequence. +3. `at_server_init()` - Always called first in startup sequence. +4. `at_server_reload_start()` - Only called on a reload (re)start. +5. `at_server_start()` - Always called last in the startup sequence. + +### Server reset + +A 'reset' is a hybrid reload state, where the reload is treated as a cold shutdown only for the sake of running hooks (players are not disconnected). It's run with `reset` in-game or with `evennia reset` from the terminal. + +1. `at_server_cold_stop()` +2. `at_server_stop()` (after this, only `Server` has shut down) +3. `at_server_init()` (`Server` coming back up) +4. `at_server_cold_start()` +5. `at_server_start()` diff --git a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Characters.md b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Characters.md index eb1576452e..e9ebe2826a 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Characters.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Characters.md @@ -1,13 +1,11 @@ # Player Characters -In the [previous lesson about rules and dice rolling](./Beginner-Tutorial-Rules.md) we made some -assumptions about the "Player Character" entity: +In the [previous lesson about rules and dice rolling](./Beginner-Tutorial-Rules.md) we made some assumptions about the "Player Character" entity: - It should store Abilities on itself as `character.strength`, `character.constitution` etc. - It should have a `.heal(amount)` method. -So we have some guidelines of how it should look! A Character is a database entity with values that -should be able to be changed over time. It makes sense to base it off Evennia's +So we have some guidelines of how it should look! A Character is a database entity with values that should be able to be changed over time. It makes sense to base it off Evennia's [DefaultCharacter Typeclass](../../../Components/Typeclasses.md). The Character class is like a 'character sheet' in a tabletop RPG, it will hold everything relevant to that PC. @@ -16,8 +14,7 @@ RPG, it will hold everything relevant to that PC. Player Characters (PCs) are not the only "living" things in our world. We also have _NPCs_ (like shopkeepers and other friendlies) as well as _monsters_ (mobs) that can attack us. -In code, there are a few ways we could structure this. If NPCs/monsters were just special cases of PCs, -we could use a class inheritance like this: +In code, there are a few ways we could structure this. If NPCs/monsters were just special cases of PCs, we could use a class inheritance like this: ```python from evennia import DefaultCharacter @@ -34,9 +31,7 @@ class EvAdventureMob(EvAdventureNPC): All code we put on the `Character` class would now be inherited to `NPC` and `Mob` automatically. -However, in _Knave_, NPCs and particularly monsters are _not_ using the same rules as PCs - they are -simplified to use a Hit-Die (HD) concept. So while still character-like, NPCs should be separate from -PCs like this: +However, in _Knave_, NPCs and particularly monsters are _not_ using the same rules as PCs - they are simplified to use a Hit-Die (HD) concept. So while still character-like, NPCs should be separate from PCs like this: ```python from evennia import DefaultCharacter @@ -60,8 +55,7 @@ Nevertheless, there are some things that _should_ be common for all 'living thin - All can loot their fallen foes. - All can get looted when defeated. -We don't want to code this separately for every class but we no longer have a common parent -class to put it on. So instead we'll use the concept of a _mixin_ class: +We don't want to code this separately for every class but we no longer have a common parent class to put it on. So instead we'll use the concept of a _mixin_ class: ```python from evennia import DefaultCharacter @@ -83,10 +77,7 @@ class EvAdventureMob(LivingMixin, EvadventureNPC): In [evennia/contrib/tutorials/evadventure/characters.py](../../../api/evennia.contrib.tutorials.evadventure.characters.md) is an example of a character class structure. ``` -Above, the `LivingMixin` class cannot work on its own - it just 'patches' the other classes with some -extra functionality all living things should be able to do. This is an example of -_multiple inheritance_. It's useful to know about, but one should not over-do multiple inheritance -since it can also get confusing to follow the code. +Above, the `LivingMixin` class cannot work on its own - it just 'patches' the other classes with some extra functionality all living things should be able to do. This is an example of _multiple inheritance_. It's useful to know about, but one should not over-do multiple inheritance since it can also get confusing to follow the code. ## Living mixin class @@ -178,7 +169,6 @@ Most of these are empty since they will behave differently for characters and np Once we create more of our game, we will need to remember to actually call these hook methods so they serve a purpose. For example, once we implement combat, we must remember to call `at_attacked` as well as the other methods involving taking damage, getting defeated or dying. - ## Character class We will now start making the basic Character class, based on what we need from _Knave_. @@ -234,8 +224,7 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter): We make an assumption about our rooms here - that they have a property `.allow_death`. We need to make a note to actually add such a property to rooms later! -In our `Character` class we implement all attributes we want to simulate from the _Knave_ ruleset. -The `AttributeProperty` is one way to add an Attribute in a field-like way; these will be accessible on every character in several ways: +In our `Character` class we implement all attributes we want to simulate from the _Knave_ ruleset. The `AttributeProperty` is one way to add an Attribute in a field-like way; these will be accessible on every character in several ways: - As `character.strength` - As `character.db.strength` @@ -249,7 +238,7 @@ We implement the Player Character versions of `at_defeat` and `at_death`. We als ### Funcparser inlines -This piece of code is worth some more explanation: +This piece of code in the `at_defeat` method above is worth some more extra explanation: ```python self.location.msg_contents( @@ -259,8 +248,7 @@ self.location.msg_contents( Remember that `self` is the Character instance here. So `self.location.msg_contents` means "send a message to everything inside my current location". In other words, send a message to everyone in the same place as the character. -The `$You() $conj(collapse)` are [FuncParser inlines](../../../Components/FuncParser.md). These are functions that -execute in the string. The resulting string may look different for different audiences. The `$You()` inline function will use `from_obj` to figure out who 'you' are and either show your name or 'You'. The `$conj()` (verb conjugator) will tweak the (English) verb to match. +The `$You() $conj(collapse)` are [FuncParser inlines](../../../Components/FuncParser.md). These are functions that execute in the string. The resulting string may look different for different audiences. The `$You()` inline function will use `from_obj` to figure out who 'you' are and either show your name or 'You'. The `$conj()` (verb conjugator) will tweak the (English) verb to match. - You will see: `"You collapse in a heap, alive but beaten."` - Others in the room will see: `"Thomas collapses in a heap, alive but beaten."` @@ -303,10 +291,7 @@ You can easily make yourself an `EvAdventureCharacter` in-game by using the You can now do `examine self` to check your type updated. -If you want _all_ new Characters to be of this type you need to tell Evennia about it. Evennia -uses a global setting `BASE_CHARACTER_TYPECLASS` to know which typeclass to use when creating -Characters (when logging in, for example). This defaults to `typeclasses.characters.Character` (that is, -the `Character` class in `mygame/typeclasses/characters.py`). +If you want _all_ new Characters to be of this type you need to tell Evennia about it. Evennia uses a global setting `BASE_CHARACTER_TYPECLASS` to know which typeclass to use when creating Characters (when logging in, for example). This defaults to `typeclasses.characters.Character` (that is, the `Character` class in `mygame/typeclasses/characters.py`). There are thus two ways to weave your new Character class into Evennia: @@ -327,8 +312,7 @@ instead. > Create a new module `mygame/evadventure/tests/test_characters.py` -For testing, we just need to create a new EvAdventure character and check -that calling the methods on it doesn't error out. +For testing, we just need to create a new EvAdventure character and check that calling the methods on it doesn't error out. ```python # mygame/evadventure/tests/test_characters.py @@ -368,22 +352,18 @@ class TestCharacters(BaseEvenniaTest): # tests for other methods ... ``` -If you followed the previous lessons, these tests should look familiar. Consider adding -tests for other methods as practice. Refer to previous lessons for details. +If you followed the previous lessons, these tests should look familiar. Consider adding tests for other methods as practice. Refer to previous lessons for details. For running the tests you do: - evennia test --settings settings.py .evadventure.tests.test_character + evennia test --settings settings.py .evadventure.tests.test_characters ## About races and classes -_Knave_ doesn't have any D&D-style _classes_ (like Thief, Fighter etc). It also does not bother with -_races_ (like dwarves, elves etc). This makes the tutorial shorter, but you may ask yourself how you'd -add these functions. +_Knave_ doesn't have any D&D-style _classes_ (like Thief, Fighter etc). It also does not bother with _races_ (like dwarves, elves etc). This makes the tutorial shorter, but you may ask yourself how you'd add these functions. -In the framework we have sketched out for _Knave_, it would be simple - you'd add your race/class as -an Attribute on your Character: +In the framework we have sketched out for _Knave_, it would be simple - you'd add your race/class as an Attribute on your Character: ```python # mygame/evadventure/characters.py @@ -399,8 +379,7 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter): charrace = AttributeProperty("Human") ``` -We use `charclass` rather than `class` here, because `class` is a reserved Python keyword. Naming -`race` as `charrace` thus matches in style. +We use `charclass` rather than `class` here, because `class` is a reserved Python keyword. Naming `race` as `charrace` thus matches in style. We'd then need to expand our [rules module](./Beginner-Tutorial-Rules.md) (and later [character generation](./Beginner-Tutorial-Chargen.md) to check and include what these classes mean. @@ -409,23 +388,16 @@ We'd then need to expand our [rules module](./Beginner-Tutorial-Rules.md) (and l ## Summary -With the `EvAdventureCharacter` class in place, we have a better understanding of how our PCs will look -like under _Knave_. +With the `EvAdventureCharacter` class in place, we have a better understanding of how our PCs will look like under _Knave_. -For now, we only have bits and pieces and haven't been testing this code in-game. But if you want -you can swap yourself into `EvAdventureCharacter` right now. Log into your game and run -the command +For now, we only have bits and pieces and haven't been testing this code in-game. But if you want you can swap yourself into `EvAdventureCharacter` right now. Log into your game and run the command type self = evadventure.characters.EvAdventureCharacter -If all went well, `ex self` will now show your typeclass as being `EvAdventureCharacter`. -Check out your strength with +If all went well, `ex self` will now show your typeclass as being `EvAdventureCharacter`. Check out your strength with py self.strength = 3 ```{important} -When doing `ex self` you will _not_ see all your Abilities listed yet. That's because -Attributes added with `AttributeProperty` are not available until they have been accessed at -least once. So once you set (or look at) `.strength` above, `strength` will show in `examine` from -then on. +When doing `ex self` you will _not_ see all your Abilities listed yet. That's because Attributes added with `AttributeProperty` are not available until they have been accessed at least once. So once you set (or look at) `.strength` above, `strength` will show in `examine` from then on. ``` diff --git a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Objects.md b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Objects.md index a1ffb51526..449367eb0f 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Objects.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Objects.md @@ -92,7 +92,7 @@ class EvAdventureObject(DefaultObject): """The top of the description""" return "" - def get_display_desc(self, looker, **kwargs) + def get_display_desc(self, looker, **kwargs): """The main display - show object stats""" return get_obj_stats(self, owner=looker) @@ -216,7 +216,7 @@ class EvAdventureConsumable(EvAdventureObject): """Called when using the item""" pass - def at_post_use(self. user, *args, **kwargs): + def at_post_use(self, user, *args, **kwargs): """Called after using the item""" # detract a usage, deleting the item if used up. self.uses -= 1 @@ -452,7 +452,7 @@ _BARE_HANDS = None # ... -class WeaponBareHands(EvAdventureWeapon) +class WeaponBareHands(EvAdventureWeapon): obj_type = ObjType.WEAPON inventory_use_slot = WieldLocation.WEAPON_HAND attack_type = Ability.STR diff --git a/docs/source/Setup/Settings.md b/docs/source/Setup/Settings.md index 4d549f7549..766d24281e 100644 --- a/docs/source/Setup/Settings.md +++ b/docs/source/Setup/Settings.md @@ -1,7 +1,6 @@ # Changing Game Settings -Evennia runs out of the box without any changes to its settings. But there are several important -ways to customize the server and expand it with your own plugins. +Evennia runs out of the box without any changes to its settings. But there are several important ways to customize the server and expand it with your own plugins. All game-specific settings are located in the `mygame/server/conf/` directory. @@ -17,13 +16,9 @@ heavily documented and up-to-date, so you should refer to this file directly for Since `mygame/server/conf/settings.py` is a normal Python module, it simply imports `evennia/settings_default.py` into itself at the top. -This means that if any setting you want to change were to depend on some *other* default setting, -you might need to copy & paste both in order to change them and get the effect you want (for most -commonly changed settings, this is not something you need to worry about). +This means that if any setting you want to change were to depend on some *other* default setting, you might need to copy & paste both in order to change them and get the effect you want (for most commonly changed settings, this is not something you need to worry about). -You should never edit `evennia/settings_default.py`. Rather you should copy&paste the select -variables you want to change into your `settings.py` and edit them there. This will overload the -previously imported defaults. +You should never edit `evennia/settings_default.py`. Rather you should copy&paste the select variables you want to change into your `settings.py` and edit them there. This will overload the previously imported defaults. ```{warning} Don't copy everything! It may be tempting to copy *everything* from `settings_default.py` into your own settings file just to have it all in one place. Don't do this. By copying only what you need, you can easier track what you changed. @@ -41,45 +36,24 @@ In code, the settings is accessed through Each setting appears as a property on the imported `settings` object. You can also explore all possible options with `evennia.settings_full` (this also includes advanced Django defaults that are not touched in default Evennia). -> When importing `settings` into your code like this, it will be *read -only*. You *cannot* edit your settings from your code! The only way to change an Evennia setting is -to edit `mygame/server/conf/settings.py` directly. You will also need to restart the server -(possibly also the Portal) before a changed setting becomes available. +> When importing `settings` into your code like this, it will be *read only*. You *cannot* edit your settings from your code! The only way to change an Evennia setting is to edit `mygame/server/conf/settings.py` directly. You will also need to restart the server (possibly also the Portal) before a changed setting becomes available. ## Other files in the `server/conf` directory Apart from the main `settings.py` file, -- `at_initial_setup.py` - this allows you to add a custom startup method to be called (only) the -very first time Evennia starts (at the same time as user #1 and Limbo is created). It can be made to -start your own global scripts or set up other system/world-related things your game needs to have -running from the start. -- `at_server_startstop.py` - this module contains two functions that Evennia will call every time -the Server starts and stops respectively - this includes stopping due to reloading and resetting as -well as shutting down completely. It's a useful place to put custom startup code for handlers and -other things that must run in your game but which has no database persistence. -- `connection_screens.py` - all global string variables in this module are interpreted by Evennia as -a greeting screen to show when an Account first connects. If more than one string variable is -present in the module a random one will be picked. +- `at_initial_setup.py` - this allows you to add a custom startup method to be called (only) the very first time Evennia starts (at the same time as user #1 and Limbo is created). It can be made to start your own global scripts or set up other system/world-related things your game needs to have running from the start. +- `at_server_startstop.py` - this module contains functions that Evennia will call every time the Server starts and stops respectively - this includes stopping due to reloading and resetting as well as shutting down completely. It's a useful place to put custom startup code for handlers and other things that must run in your game but which has no database persistence. +- `connection_screens.py` - all global string variables in this module are interpreted by Evennia as a greeting screen to show when an Account first connects. If more than one string variable is present in the module a random one will be picked. - `inlinefuncs.py` - this is where you can define custom [FuncParser functions](../Components/FuncParser.md). -- `inputfuncs.py` - this is where you define custom [Input functions](../Components/Inputfuncs.md) to handle data -from the client. -- `lockfuncs.py` - this is one of many possible modules to hold your own "safe" *lock functions* to -make available to Evennia's [Locks](../Components/Locks.md). -- `mssp.py` - this holds meta information about your game. It is used by MUD search engines (which -you often have to register with) in order to display what kind of game you are running along with - statistics such as number of online accounts and online status. +- `inputfuncs.py` - this is where you define custom [Input functions](../Components/Inputfuncs.md) to handle data from the client. +- `lockfuncs.py` - this is one of many possible modules to hold your own "safe" *lock functions* to make available to Evennia's [Locks](../Components/Locks.md). +- `mssp.py` - this holds meta information about your game. It is used by MUD search engines (which you often have to register with) in order to display what kind of game you are running along with statistics such as number of online accounts and online status. - `oobfuncs.py` - in here you can define custom [OOB functions](../Concepts/OOB.md). -- `portal_services_plugin.py` - this allows for adding your own custom services/protocols to the -Portal. It must define one particular function that will be called by Evennia at startup. There can -be any number of service plugin modules, all will be imported and used if defined. More info can be -found [here](https://code.google.com/p/evennia/wiki/SessionProtocols#Adding_custom_Protocols). -- `server_services_plugin.py` - this is equivalent to the previous one, but used for adding new -services to the Server instead. More info can be found -[here](https://code.google.com/p/evennia/wiki/SessionProtocols#Adding_custom_Protocols). +- `portal_services_plugin.py` - this allows for adding your own custom services/protocols to the Portal. It must define one particular function that will be called by Evennia at startup. There can be any number of service plugin modules, all will be imported and used if defined. More info can be found [here](https://code.google.com/p/evennia/wiki/SessionProtocols#Adding_custom_Protocols). +- `server_services_plugin.py` - this is equivalent to the previous one, but used for adding new services to the Server instead. More info can be found [here](https://code.google.com/p/evennia/wiki/SessionProtocols#Adding_custom_Protocols). -Some other Evennia systems can be customized by plugin modules but has no explicit template in -`conf/`: +Some other Evennia systems can be customized by plugin modules but has no explicit template in `conf/`: - *cmdparser.py* - a custom module can be used to totally replace Evennia's default command parser. All this does is to split the incoming string into "command name" and "the rest". It also handles things like error messages for no-matches and multiple-matches among other things that makes this more complex than it sounds. The default parser is *very* generic, so you are most often best served by modifying things further down the line (on the command parse level) than here. - *at_search.py* - this allows for replacing the way Evennia handles search results. It allows to change how errors are echoed and how multi-matches are resolved and reported (like how the default understands that "2-ball" should match the second "ball" object if there are two of them in the room). \ No newline at end of file diff --git a/evennia/VERSION.txt b/evennia/VERSION.txt index ee74734aa2..627a3f43a6 100644 --- a/evennia/VERSION.txt +++ b/evennia/VERSION.txt @@ -1 +1 @@ -4.1.0 +4.1.1 diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index 594424f269..89137ff672 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -16,13 +16,14 @@ import time import typing from random import getrandbits -import evennia from django.conf import settings from django.contrib.auth import authenticate, password_validation from django.core.exceptions import ImproperlyConfigured, ValidationError from django.utils import timezone from django.utils.module_loading import import_string from django.utils.translation import gettext as _ + +import evennia from evennia.accounts.manager import AccountManager from evennia.accounts.models import AccountDB from evennia.commands.cmdsethandler import CmdSetHandler @@ -30,17 +31,24 @@ from evennia.comms.models import ChannelDB from evennia.objects.models import ObjectDB from evennia.scripts.scripthandler import ScriptHandler from evennia.server.models import ServerConfig -from evennia.server.signals import (SIGNAL_ACCOUNT_POST_CREATE, - SIGNAL_ACCOUNT_POST_LOGIN_FAIL, - SIGNAL_OBJECT_POST_PUPPET, - SIGNAL_OBJECT_POST_UNPUPPET) +from evennia.server.signals import ( + SIGNAL_ACCOUNT_POST_CREATE, + SIGNAL_ACCOUNT_POST_LOGIN_FAIL, + SIGNAL_OBJECT_POST_PUPPET, + SIGNAL_OBJECT_POST_UNPUPPET, +) from evennia.server.throttle import Throttle from evennia.typeclasses.attributes import ModelAttributeBackend, NickHandler from evennia.typeclasses.models import TypeclassBase from evennia.utils import class_from_module, create, logger from evennia.utils.optionhandler import OptionHandler -from evennia.utils.utils import (is_iter, lazy_property, make_iter, to_str, - variable_from_module) +from evennia.utils.utils import ( + is_iter, + lazy_property, + make_iter, + to_str, + variable_from_module, +) __all__ = ("DefaultAccount", "DefaultGuest") diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index d8059c9a50..558d96b3b5 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -5,13 +5,13 @@ Building and world design commands import re import typing -import evennia from django.conf import settings from django.core.paginator import Paginator from django.db.models import Max, Min, Q + +import evennia from evennia import InterruptCommand -from evennia.commands.cmdhandler import (generate_cmdset_providers, - get_and_merge_cmdsets) +from evennia.commands.cmdhandler import generate_cmdset_providers, get_and_merge_cmdsets from evennia.locks.lockhandler import LockException from evennia.objects.models import ObjectDB from evennia.prototypes import menus as olc_menus @@ -24,10 +24,18 @@ from evennia.utils.dbserialize import deserialize from evennia.utils.eveditor import EvEditor from evennia.utils.evmore import EvMore from evennia.utils.evtable import EvTable -from evennia.utils.utils import (class_from_module, crop, dbref, display_len, - format_grid, get_all_typeclasses, - inherits_from, interactive, list_to_string, - variable_from_module) +from evennia.utils.utils import ( + class_from_module, + crop, + dbref, + display_len, + format_grid, + get_all_typeclasses, + inherits_from, + interactive, + list_to_string, + variable_from_module, +) COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) @@ -1397,7 +1405,7 @@ class CmdSetHome(CmdLink): obj.home = new_home if old_home: string = ( - f"Home location of {obj} was changed from {old_home}({old_home.dbref} to" + f"Home location of {obj} was changed from {old_home}({old_home.dbref}) to" f" {new_home}({new_home.dbref})." ) else: @@ -3274,11 +3282,15 @@ class CmdFind(COMMAND_DEFAULT_CLASS): string += f"\n |RNo match found for '{searchstring}' in #dbref interval.|n" else: result = result[0] - string += (f"\n|g {result.get_display_name(caller)}" - f"{result.get_extra_display_name_info(caller)} - {result.path}|n") + string += ( + f"\n|g {result.get_display_name(caller)}" + f"{result.get_extra_display_name_info(caller)} - {result.path}|n" + ) if "loc" in self.switches and not is_account and result.location: - string += (f" (|wlocation|n: |g{result.location.get_display_name(caller)}" - f"{result.get_extra_display_name_info(caller)}|n)") + string += ( + f" (|wlocation|n: |g{result.location.get_display_name(caller)}" + f"{result.get_extra_display_name_info(caller)}|n)" + ) else: # Not an account/dbref search but a wider search; build a queryset. # Searches for key and aliases diff --git a/evennia/commands/default/general.py b/evennia/commands/default/general.py index 88c15f74f4..06aae08f75 100644 --- a/evennia/commands/default/general.py +++ b/evennia/commands/default/general.py @@ -4,8 +4,9 @@ General Character commands usually available to all characters import re -import evennia from django.conf import settings + +import evennia from evennia.typeclasses.attributes import NickTemplateInvalid from evennia.utils import utils @@ -397,7 +398,7 @@ class NumberedTargetCommand(COMMAND_DEFAULT_CLASS): """ super().parse() self.number = 0 - if hasattr(self, "lhs"): + if getattr(self, "lhs", None): # handle self.lhs but don't require it count, *args = self.lhs.split(maxsplit=1) # we only use the first word as a count if it's a number and diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 6bc69f602a..f7fc7f1fd2 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -134,6 +134,17 @@ class TestGeneral(BaseEvenniaCommandTest): self.obj2.location = self.char1 self.call(general.CmdGive(), "2 Obj = Char2", "You give two Objs") + def test_numbered_target_command(self): + class CmdTest(general.NumberedTargetCommand): + key = "test" + + def func(self): + self.msg(f"Number: {self.number} Args: {self.args}") + + self.call(CmdTest(), "", "Number: 0 Args: ") + self.call(CmdTest(), "obj", "Number: 0 Args: obj") + self.call(CmdTest(), "1 obj", "Number: 1 Args: obj") + def test_mux_command(self): class CmdTest(MuxCommand): key = "test" diff --git a/evennia/contrib/base_systems/components/component.py b/evennia/contrib/base_systems/components/component.py index 48c7dfa738..6397b77a07 100644 --- a/evennia/contrib/base_systems/components/component.py +++ b/evennia/contrib/base_systems/components/component.py @@ -155,6 +155,22 @@ class Component(metaclass=BaseComponent): """ return self.host.attributes + @property + def pk(self): + """ + Shortcut property returning the host's primary key. + + Returns: + int: The Host's primary key. + + Notes: + This is requried to allow AttributeProperties to correctly update `_SaverMutable` data + (like lists) in-place (since the DBField sits on the Component which doesn't itself + have a primary key, this save operation would otherwise fail). + + """ + return self.host.pk + @property def nattributes(self): """ diff --git a/evennia/contrib/base_systems/components/dbfield.py b/evennia/contrib/base_systems/components/dbfield.py index 4b9c6d4fa8..67f812b484 100644 --- a/evennia/contrib/base_systems/components/dbfield.py +++ b/evennia/contrib/base_systems/components/dbfield.py @@ -6,7 +6,8 @@ This file contains the Descriptors used to set Fields in Components import typing -from evennia.typeclasses.attributes import AttributeProperty, NAttributeProperty +from evennia.typeclasses.attributes import (AttributeProperty, + NAttributeProperty) if typing.TYPE_CHECKING: from .components import Component diff --git a/evennia/contrib/base_systems/components/tests.py b/evennia/contrib/base_systems/components/tests.py index b11ce68937..57d072b8f7 100644 --- a/evennia/contrib/base_systems/components/tests.py +++ b/evennia/contrib/base_systems/components/tests.py @@ -268,7 +268,7 @@ class TestComponents(EvenniaTest): def test_mutables_are_not_shared_when_autocreate(self): self.char1.test_a.my_list.append(1) - self.assertNotEqual(self.char1.test_a.my_list, self.char2.test_a.my_list) + self.assertIsNot(self.char1.test_a.my_list, self.char2.test_a.my_list) def test_replacing_class_component_slot_with_runtime_component(self): self.char1.components.add_default("replacement_inherited_test_a") diff --git a/evennia/contrib/grid/xyzgrid/xymap_legend.py b/evennia/contrib/grid/xyzgrid/xymap_legend.py index 29be2cca66..973f96a1d5 100644 --- a/evennia/contrib/grid/xyzgrid/xymap_legend.py +++ b/evennia/contrib/grid/xyzgrid/xymap_legend.py @@ -20,11 +20,11 @@ import uuid from collections import defaultdict from django.core import exceptions as django_exceptions + from evennia.prototypes import spawner from evennia.utils.utils import class_from_module -from .utils import (BIGVAL, MAPSCAN, REVERSE_DIRECTIONS, MapError, - MapParserError) +from .utils import BIGVAL, MAPSCAN, REVERSE_DIRECTIONS, MapError, MapParserError NodeTypeclass = None ExitTypeclass = None @@ -331,7 +331,8 @@ class MapNode: raise MapError( f"Multiple objects found: {NodeTypeclass.objects.filter_xyz(xyz=xyz)}. " "This may be due to manual creation of XYZRooms at this position. " - "Delete duplicates.", self + "Delete duplicates.", + self, ) else: self.log(f" updating existing room (if changed) at xyz={xyz}") diff --git a/evennia/contrib/grid/xyzgrid/xyzroom.py b/evennia/contrib/grid/xyzgrid/xyzroom.py index 6bf4161620..21de4aa874 100644 --- a/evennia/contrib/grid/xyzgrid/xyzroom.py +++ b/evennia/contrib/grid/xyzgrid/xyzroom.py @@ -9,6 +9,7 @@ used as stand-alone XYZ-coordinate-aware rooms. from django.conf import settings from django.db.models import Q + from evennia.objects.manager import ObjectManager from evennia.objects.objects import DefaultExit, DefaultRoom @@ -282,7 +283,7 @@ class XYZRoom(DefaultRoom): def __repr__(self): x, y, z = self.xyz - return f"" + return f"<{self.__class__.__name__} '{self.db_key}', XYZ=({x},{y},{z})>" @property def xyz(self): @@ -307,8 +308,7 @@ class XYZRoom(DefaultRoom): def xyzgrid(self): global GET_XYZGRID if not GET_XYZGRID: - from evennia.contrib.grid.xyzgrid.xyzgrid import \ - get_xyzgrid as GET_XYZGRID + from evennia.contrib.grid.xyzgrid.xyzgrid import get_xyzgrid as GET_XYZGRID return GET_XYZGRID() @property @@ -532,8 +532,7 @@ class XYZExit(DefaultExit): def xyzgrid(self): global GET_XYZGRID if not GET_XYZGRID: - from evennia.contrib.grid.xyzgrid.xyzgrid import \ - get_xyzgrid as GET_XYZGRID + from evennia.contrib.grid.xyzgrid.xyzgrid import get_xyzgrid as GET_XYZGRID return GET_XYZGRID() @property diff --git a/evennia/contrib/tutorials/evadventure/chargen.py b/evennia/contrib/tutorials/evadventure/chargen.py index 6c773a0116..a1b429e3d1 100644 --- a/evennia/contrib/tutorials/evadventure/chargen.py +++ b/evennia/contrib/tutorials/evadventure/chargen.py @@ -4,6 +4,7 @@ EvAdventure character generation. """ from django.conf import settings + from evennia.objects.models import ObjectDB from evennia.prototypes.spawner import spawn from evennia.utils.create import create_object diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 0e613e8482..150c95f5d1 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -10,10 +10,11 @@ import time import typing from collections import defaultdict -import evennia import inflect from django.conf import settings from django.utils.translation import gettext as _ + +import evennia from evennia.commands import cmdset from evennia.commands.cmdsethandler import CmdSetHandler from evennia.objects.manager import ObjectManager @@ -23,9 +24,17 @@ from evennia.server.signals import SIGNAL_EXIT_TRAVERSED from evennia.typeclasses.attributes import ModelAttributeBackend, NickHandler from evennia.typeclasses.models import TypeclassBase from evennia.utils import ansi, create, funcparser, logger, search -from evennia.utils.utils import (class_from_module, compress_whitespace, dbref, - is_iter, iter_to_str, lazy_property, - make_iter, to_str, variable_from_module) +from evennia.utils.utils import ( + class_from_module, + compress_whitespace, + dbref, + is_iter, + iter_to_str, + lazy_property, + make_iter, + to_str, + variable_from_module, +) _INFLECT = inflect.engine() _MULTISESSION_MODE = settings.MULTISESSION_MODE @@ -1425,7 +1434,8 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): return [ obj for obj in obj_list - if obj != looker and (obj.access(looker, "view") and obj.access(looker, "search", default=True)) + if obj != looker + and (obj.access(looker, "view") and obj.access(looker, "search", default=True)) ] # name and return_appearance hooks @@ -1563,12 +1573,34 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Args: looker (DefaultObject): Object doing the looking. **kwargs: Arbitrary data for use when overriding. + + Keyword Args: + exit_order (iterable of str): The order in which exits should be listed, with + unspecified exits appearing at the end, alphabetically. + Returns: str: The exits display data. + Examples: + :: + + For a room with exits in the order 'portal', 'south', 'north', and 'out': + obj.get_display_name(looker, exit_order=('north', 'south')) + -> "Exits: north, south, out, and portal." (markup not shown here) """ + def _sort_exit_names(names): + exit_order = kwargs.get("exit_order") + if not exit_order: + return names + sort_index = {name: key for key, name in enumerate(exit_order)} + names = sorted(names) + end_pos = len(names) + 1 + names.sort(key=lambda name:sort_index.get(name, end_pos)) + return names + exits = self.filter_visible(self.contents_get(content_type="exit"), looker, **kwargs) - exit_names = iter_to_str(exi.get_display_name(looker, **kwargs) for exi in exits) + exit_names = (exi.get_display_name(looker, **kwargs) for exi in exits) + exit_names = iter_to_str(_sort_exit_names(exit_names)) return f"|wExits:|n {exit_names}" if exit_names else "" diff --git a/evennia/objects/tests.py b/evennia/objects/tests.py index de23269cdf..5c300865dc 100644 --- a/evennia/objects/tests.py +++ b/evennia/objects/tests.py @@ -3,13 +3,10 @@ from unittest import skip from evennia import DefaultCharacter, DefaultExit, DefaultObject, DefaultRoom from evennia.objects.models import ObjectDB from evennia.typeclasses.attributes import AttributeProperty -from evennia.typeclasses.tags import ( - AliasProperty, - PermissionProperty, - TagCategoryProperty, - TagProperty, -) +from evennia.typeclasses.tags import (AliasProperty, PermissionProperty, + TagCategoryProperty, TagProperty) from evennia.utils import create, search +from evennia.utils.ansi import strip_ansi from evennia.utils.test_resources import BaseEvenniaTest, EvenniaTestCase @@ -94,6 +91,21 @@ class DefaultObjectTest(BaseEvenniaTest): all_return_exit = ex1.get_return_exit(return_all=True) self.assertEqual(len(all_return_exit), 2) + def test_exit_order(self): + DefaultExit.create("south", self.room1, self.room2, account=self.account) + DefaultExit.create("portal", self.room1, self.room2, account=self.account) + DefaultExit.create("north", self.room1, self.room2, account=self.account) + DefaultExit.create("aperture", self.room1, self.room2, account=self.account) + + # in creation order + exits = strip_ansi(self.room1.get_display_exits(self.char1)) + self.assertEqual(exits, "Exits: out, south, portal, north, and aperture") + + # in specified order with unspecified exits alpbabetically on the end + exit_order = ('north', 'south', 'out') + exits = strip_ansi(self.room1.get_display_exits(self.char1, exit_order=exit_order)) + self.assertEqual(exits, "Exits: north, south, out, aperture, and portal") + def test_urls(self): "Make sure objects are returning URLs" self.assertTrue(self.char1.get_absolute_url()) @@ -356,6 +368,10 @@ class TestObjectPropertiesClass(DefaultObject): attr2 = AttributeProperty(default="attr2", category="attrcategory") attr3 = AttributeProperty(default="attr3", autocreate=False) attr4 = SubAttributeProperty(default="attr4") + attr5 = AttributeProperty(default=list, autocreate=False) + attr6 = AttributeProperty(default=[None], autocreate=False) + attr7 = AttributeProperty(default=list) + attr8 = AttributeProperty(default=[None]) cusattr = CustomizedProperty(default=5) tag1 = TagProperty() tag2 = TagProperty(category="tagcategory") @@ -541,3 +557,99 @@ class TestProperties(EvenniaTestCase): obj1.delete() obj2.delete() + + def test_not_create_attribute_with_autocreate_false(self): + """ + Test that AttributeProperty with autocreate=False does not create an attribute in the database. + + """ + obj = create.create_object(TestObjectPropertiesClass, key="obj1") + + self.assertEqual(obj.attr3, "attr3") + self.assertEqual(obj.attributes.get("attr3"), None) + + self.assertEqual(obj.attr5, []) + self.assertEqual(obj.attributes.get("attr5"), None) + + obj.delete() + + def test_callable_defaults__autocreate_false(self): + """ + Test https://github.com/evennia/evennia/issues/3488, where a callable default value like `list` + would produce an infinitely empty result even when appended to. + + """ + obj1 = create.create_object(TestObjectPropertiesClass, key="obj1") + obj2 = create.create_object(TestObjectPropertiesClass, key="obj2") + + self.assertEqual(obj1.attr5, []) + obj1.attr5.append(1) + self.assertEqual(obj1.attr5, [1]) + + # check cross-instance sharing + self.assertEqual(obj2.attr5, [], "cross-instance sharing detected") + + + def test_mutable_defaults__autocreate_false(self): + """ + Test https://github.com/evennia/evennia/issues/3488, where a mutable default value (like a + list `[]` or `[None]`) would not be updated in the database when appended to. + + Note that using a mutable default value is not recommended, as the mutable will share the + same memory space across all instances of the class. This means that if one instance modifiesA + the mutable, all instances will be affected. + + """ + obj1 = create.create_object(TestObjectPropertiesClass, key="obj1") + obj2 = create.create_object(TestObjectPropertiesClass, key="obj2") + + self.assertEqual(obj1.attr6, [None]) + obj1.attr6.append(1) + self.assertEqual(obj1.attr6, [None, 1]) + + obj1.attr6[1] = 2 + self.assertEqual(obj1.attr6, [None, 2]) + + # check cross-instance sharing + self.assertEqual(obj2.attr6, [None], "cross-instance sharing detected") + + obj1.delete() + obj2.delete() + + def test_callable_defaults__autocreate_true(self): + """ + Test callables with autocreate=True. + + """ + obj1 = create.create_object(TestObjectPropertiesClass, key="obj1") + obj2 = create.create_object(TestObjectPropertiesClass, key="obj1") + + self.assertEqual(obj1.attr7, []) + obj1.attr7.append(1) + self.assertEqual(obj1.attr7, [1]) + + # check cross-instance sharing + self.assertEqual(obj2.attr7, []) + + + def test_mutable_defaults__autocreate_true(self): + """ + Test mutable defaults with autocreate=True. + + """ + obj1 = create.create_object(TestObjectPropertiesClass, key="obj1") + obj2 = create.create_object(TestObjectPropertiesClass, key="obj2") + + self.assertEqual(obj1.attr8, [None]) + obj1.attr8.append(1) + self.assertEqual(obj1.attr8, [None, 1]) + + obj1.attr8[1] = 2 + self.assertEqual(obj1.attr8, [None, 2]) + + # check cross-instance sharing + self.assertEqual(obj2.attr8, [None]) + + obj1.delete() + obj2.delete() + diff --git a/evennia/server/evennia_launcher.py b/evennia/server/evennia_launcher.py index 60ed76ab03..7578da9e52 100644 --- a/evennia/server/evennia_launcher.py +++ b/evennia/server/evennia_launcher.py @@ -642,6 +642,8 @@ def send_instruction(operation, arguments, callback=None, errback=None): """ global AMP_CONNECTION, REACTOR_RUN + # print("launcher: Sending to portal: {} + {}".format(ord(operation), arguments)) + if None in (AMP_HOST, AMP_PORT, AMP_INTERFACE): print(ERROR_AMP_UNCONFIGURED) sys.exit() diff --git a/evennia/server/portal/amp_server.py b/evennia/server/portal/amp_server.py index 67fbd1d1e7..007bddd7cb 100644 --- a/evennia/server/portal/amp_server.py +++ b/evennia/server/portal/amp_server.py @@ -197,8 +197,6 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): if process and not _is_windows(): # avoid zombie-process on Unix/BSD process.wait() - # unset the reset-mode flag on the portal - self.factory.portal.server_restart_mode = None return def wait_for_disconnect(self, callback, *args, **kwargs): @@ -232,11 +230,18 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): """ if mode == "reload": - self.send_AdminPortal2Server(amp.DUMMYSESSION, operation=amp.SRELOAD) + self.send_AdminPortal2Server( + amp.DUMMYSESSION, operation=amp.SRELOAD, server_restart_mode=mode + ) elif mode == "reset": - self.send_AdminPortal2Server(amp.DUMMYSESSION, operation=amp.SRESET) + self.send_AdminPortal2Server( + amp.DUMMYSESSION, operation=amp.SRESET, server_restart_mode=mode + ) elif mode == "shutdown": - self.send_AdminPortal2Server(amp.DUMMYSESSION, operation=amp.SSHUTD) + self.send_AdminPortal2Server( + amp.DUMMYSESSION, operation=amp.SSHUTD, server_restart_mode=mode + ) + # store the mode for use once server comes back up again self.factory.portal.server_restart_mode = mode # sending amp data @@ -326,7 +331,6 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): _, server_connected, _, _, _, _ = self.get_status() # logger.log_msg("Evennia Launcher->Portal operation %s:%s received" % (ord(operation), arguments)) - # logger.log_msg("operation == amp.SSTART: {}: {}".format(operation == amp.SSTART, amp.loads(arguments))) if operation == amp.SSTART: # portal start #15 @@ -405,11 +409,11 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): sessid, kwargs = self.data_in(packed_data) - # logger.log_msg("Evennia Server->Portal admin data %s:%s received" % (sessid, kwargs)) - operation = kwargs.pop("operation") portal_sessionhandler = evennia.PORTAL_SESSION_HANDLER + # logger.log_msg(f"Evennia Server->Portal admin data operation {ord(operation)}") + if operation == amp.SLOGIN: # server_session_login # a session has authenticated; sync it. session = portal_sessionhandler.get(sessid) @@ -427,22 +431,28 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): portal_sessionhandler.server_disconnect_all(reason=kwargs.get("reason")) elif operation == amp.SRELOAD: # server reload + # set up callback to restart server once it has disconnected self.factory.server_connection.wait_for_disconnect( self.start_server, self.factory.portal.server_twistd_cmd ) + # tell server to reload self.stop_server(mode="reload") elif operation == amp.SRESET: # server reset + # set up callback to restart server once it has disconnected self.factory.server_connection.wait_for_disconnect( self.start_server, self.factory.portal.server_twistd_cmd ) + # tell server to reset self.stop_server(mode="reset") elif operation == amp.SSHUTD: # server-only shutdown self.stop_server(mode="shutdown") elif operation == amp.PSHUTD: # full server+server shutdown + # set up callback to shut down portal once server has disconnected self.factory.server_connection.wait_for_disconnect(self.factory.portal.shutdown) + # tell server to shut down self.stop_server(mode="shutdown") elif operation == amp.PSYNC: # portal sync @@ -451,6 +461,7 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): self.factory.portal.server_process_id = kwargs.get("spid", None) # this defaults to 'shutdown' or whatever value set in server_stop server_restart_mode = self.factory.portal.server_restart_mode + # print("Server has connected. Sending session data to Server ... mode: {}".format(server_restart_mode)) sessdata = evennia.PORTAL_SESSION_HANDLER.get_all_sync_data() self.send_AdminPortal2Server( @@ -461,6 +472,7 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): portal_start_time=self.factory.portal.start_time, ) evennia.PORTAL_SESSION_HANDLER.at_server_connection() + self.factory.portal.server_restart_mode = None if self.factory.server_connection: # this is an indication the server has successfully connected, so @@ -480,7 +492,7 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): ) # set a flag in case we are about to shut down soon - self.factory.server_restart_mode = True + self.factory.server_restart_mode = "shutdown" elif operation == amp.SCONN: # server_force_connection (for irc/etc) portal_sessionhandler.server_connect(**kwargs) diff --git a/evennia/typeclasses/attributes.py b/evennia/typeclasses/attributes.py index 5aedc03262..bdef3bdc33 100644 --- a/evennia/typeclasses/attributes.py +++ b/evennia/typeclasses/attributes.py @@ -12,11 +12,11 @@ which is a non-db version of Attributes. import fnmatch import re from collections import defaultdict +from copy import copy from django.conf import settings from django.db import models from django.utils.encoding import smart_str - from evennia.locks.lockhandler import LockHandler from evennia.utils.dbserialize import from_pickle, to_pickle from evennia.utils.idmapper.models import SharedMemoryModel @@ -166,6 +166,7 @@ class AttributeProperty: """ attrhandler_name = "attributes" + cached_default_name_template = "_property_attribute_default_{key}" def __init__(self, default=None, category=None, strattr=False, lockstring="", autocreate=True): """ @@ -207,21 +208,6 @@ class AttributeProperty: self._autocreate = autocreate self._key = "" - @property - def _default(self): - """ - Tries returning a new instance of default if callable. - - """ - if callable(self.__default): - return self.__default() - - return self.__default - - @_default.setter - def _default(self, value): - self.__default = value - def __set_name__(self, cls, name): """ Called when descriptor is first assigned to the class. It is called with @@ -230,17 +216,35 @@ class AttributeProperty: """ self._key = name + def _get_and_cache_default(self, instance): + """ + Get and cache the default value for this attribute. We make sure to convert any mutables + into _Saver* equivalent classes here and cache the result on the instance's AttributeHandler. + + """ + attrhandler = getattr(instance, self.attrhandler_name) + value = getattr(attrhandler, self.cached_default_name_template.format(key=self._key), None) + if not value: + if callable(self._default): + value = self._default() + else: + value = copy(self._default) + value = from_pickle(value, db_obj=instance) + setattr(attrhandler, self.cached_default_name_template.format(key=self._key), value) + return value + def __get__(self, instance, owner): """ Called when the attrkey is retrieved from the instance. """ - value = self._default + value = self._get_and_cache_default(instance) + try: value = self.at_get( getattr(instance, self.attrhandler_name).get( key=self._key, - default=self._default, + default=value, category=self._category, strattr=self._strattr, raise_exception=self._autocreate, @@ -250,7 +254,7 @@ class AttributeProperty: except AttributeError: if self._autocreate: # attribute didn't exist and autocreate is set - self.__set__(instance, self._default) + self.__set__(instance, value) else: raise return value diff --git a/evennia/utils/eveditor.py b/evennia/utils/eveditor.py index 7b1e775d1f..c7a7a04c92 100644 --- a/evennia/utils/eveditor.py +++ b/evennia/utils/eveditor.py @@ -98,7 +98,7 @@ _HELP_TEXT = _( :s - search/replace word or regex in buffer or on line :j - justify buffer or line . is f, c, l or r. Default f (full) - :f - flood-fill entire buffer or line : Equivalent to :j left + :f - flood-fill entire buffer or line . Equivalent to :j l :fi - indent entire buffer or line :fd - de-indent entire buffer or line @@ -351,6 +351,35 @@ class CmdEditorBase(_COMMAND_DEFAULT_CLASS): self.arg1 = arg1 self.arg2 = arg2 + def insert_raw_string_into_buffer(self): + """ + Insert a line into the buffer. Used by both CmdLineInput and CmdEditorGroup. + + """ + caller = self.caller + editor = caller.ndb._eveditor + buf = editor.get_buffer() + + # add a line of text to buffer + line = self.raw_string.strip("\r\n") + if editor._codefunc and editor._indent >= 0: + # if automatic indentation is active, add spaces + line = editor.deduce_indent(line, buf) + buf = line if not buf else buf + "\n%s" % line + self.editor.update_buffer(buf) + if self.editor._echo_mode: + # need to do it here or we will be off one line + cline = len(self.editor.get_buffer().split("\n")) + if editor._codefunc: + # display the current level of identation + indent = editor._indent + if indent < 0: + indent = "off" + + self.caller.msg("|b%02i|||n (|g%s|n) %s" % (cline, indent, raw(line))) + else: + self.caller.msg("|b%02i|||n %s" % (cline, raw(line))) + def _load_editor(caller): """ @@ -394,29 +423,7 @@ class CmdLineInput(CmdEditorBase): If the editor handles code, it might add automatic indentation. """ - caller = self.caller - editor = caller.ndb._eveditor - buf = editor.get_buffer() - - # add a line of text to buffer - line = self.raw_string.strip("\r\n") - if editor._codefunc and editor._indent >= 0: - # if automatic indentation is active, add spaces - line = editor.deduce_indent(line, buf) - buf = line if not buf else buf + "\n%s" % line - self.editor.update_buffer(buf) - if self.editor._echo_mode: - # need to do it here or we will be off one line - cline = len(self.editor.get_buffer().split("\n")) - if editor._codefunc: - # display the current level of identation - indent = editor._indent - if indent < 0: - indent = "off" - - self.caller.msg("|b%02i|||n (|g%s|n) %s" % (cline, indent, raw(line))) - else: - self.caller.msg("|b%02i|||n %s" % (cline, raw(line))) + self.insert_raw_string_into_buffer() class CmdEditorGroup(CmdEditorBase): @@ -806,6 +813,9 @@ class CmdEditorGroup(CmdEditorBase): caller.msg(_("Auto-indentation turned off.")) else: caller.msg(_("This command is only available in code editor mode.")) + else: + # no match - insert as line in buffer + self.insert_raw_string_into_buffer() class EvEditorCmdSet(CmdSet): diff --git a/pyproject.toml b/pyproject.toml index 17a9871c10..7c507aada5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "evennia" -version = "4.1.0" +version = "4.1.1" 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"