Merge branch 'main' of https://github.com/evennia/evennia into editor_search_traceback

This commit is contained in:
Chiizujin 2024-04-07 22:06:14 +10:00
commit c21c5667c9
25 changed files with 503 additions and 188 deletions

View file

@ -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(<default>, autocreate=False)`, where
`<default>` 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

View file

@ -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(<default>, autocreate=False)`, where
`<default>` 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

View file

@ -37,6 +37,7 @@ Banning.md
```{toctree}
:maxdepth: 2
Server-Lifecycle
Protocols.md
Models.md
Zones.md

View file

@ -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()`

View file

@ -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.
```

View file

@ -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

View file

@ -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).

View file

@ -1 +1 @@
4.1.0
4.1.1

View file

@ -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")

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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):
"""

View file

@ -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

View file

@ -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")

View file

@ -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}")

View file

@ -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"<XYZRoom '{self.db_key}', XYZ=({x},{y},{z})>"
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

View file

@ -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

View file

@ -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 ""

View file

@ -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()

View file

@ -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()

View file

@ -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)

View file

@ -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

View file

@ -98,7 +98,7 @@ _HELP_TEXT = _(
:s <l> <w> <txt> - search/replace word or regex <w> in buffer or on line <l>
:j <l> <w> - justify buffer or line <l>. <w> is f, c, l or r. Default f (full)
:f <l> - flood-fill entire buffer or line <l>: Equivalent to :j left
:f <l> - flood-fill entire buffer or line <l>. Equivalent to :j <l> l
:fi <l> - indent entire buffer or line <l>
:fd <l> - de-indent entire buffer or line <l>
@ -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):

View file

@ -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"