diff --git a/docs/source/Components/Typeclasses.md b/docs/source/Components/Typeclasses.md index 6c9c872986..073bcd0803 100644 --- a/docs/source/Components/Typeclasses.md +++ b/docs/source/Components/Typeclasses.md @@ -350,3 +350,13 @@ object seeing the relationship may not reliably update but will still see its ol Due to typeclasses staying so long in memory, stale caches of such relationships could be more visible than common in Django. See the [closed issue #1098 and its comments](https://github.com/evennia/evennia/issues/1098) for examples and solutions. + +## Will I run out of dbrefs? + +Evennia does not re-use its `#dbrefs`. This means new objects get an ever-increasing `#dbref`, also if you delete older objects. There are technical and safety reasons for this. But you may wonder if this means you have to worry about a big game 'running out' of dbref integers eventually. + +The answer is simply **no**. + +For example, the max dbref value for the default sqlite3 database is `2**64`. If you *created 10 000 new objects every second of every minute of every day of the year it would take about **60 million years** for you to run out of dbref numbers*. That's a database of 140 TeraBytes, just to store the dbrefs, no other data. + +If you are still using Evennia at that point and has this concern, get back to us and we can discuss adding dbref reuse then. \ No newline at end of file diff --git a/docs/source/Concepts/Colors.md b/docs/source/Concepts/Colors.md index 71b03e2d07..2fdc6c23e6 100644 --- a/docs/source/Concepts/Colors.md +++ b/docs/source/Concepts/Colors.md @@ -23,7 +23,7 @@ Evennia supports two color standards: To see which colours your client support, use the default `color` command. This will list all available colours for ANSI and Xterm256 along with the codes you use for them. The -central ansi/xterm256 parser is located in [evennia/utils/ansi.py](api:evennia.utils.ansi). +central ansi/xterm256 parser is located in [evennia/utils/ansi.py](evennia.utils.ansi). ## ANSI colours diff --git a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md index cdfde5b7f0..20a2c082d5 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md @@ -247,6 +247,7 @@ Still in `mygame/commands/mycommands.py`, add a new class, between `CmdEcho` and ```{code-block} python :linenos: +:emphasize-lines: 3,4,11,14,15,17,18,19,21 # ... diff --git a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Creating-Things.md b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Creating-Things.md index f8a1260724..f93d8188ec 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Creating-Things.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Creating-Things.md @@ -1,9 +1,18 @@ # Creating things -We have already created some things - dragons for example. There are many different things to create -in Evennia though. In the last lesson we learned about typeclasses, the way to make objects persistent in the database. +We have already created some things - dragons for example. There are many different things to create in Evennia though. In the [Typeclasses tutorial](./Beginner-Tutorial-Learning-Typeclasses.md), we noted that there are 7 default Typeclasses coming with Evennia out of the box: -Given the path to a Typeclass, there are three ways to create an instance of it: +| Evennia base typeclass | mygame.typeclasses child | description | +| --------------- | --------------| ------------- | +| `evennia.DefaultObject` | `typeclasses.objects.Object` | Everything with a location | +| `evennia.DefaultCharacter` (child of `DefaultObject`) | `typeclasses.characters.Character` | Player avatars | +| `evennia.DefaultRoom` (child of `DefaultObject`) | `typeclasses.rooms.Room` | In-game locations | +| `evennia.DefaultExit` (chld of `DefaultObject`) | `typeclasses.exits.Exit` | Links between rooms | +| `evennia.DefaultAccount` | `typeclasses.accounts.Account` | A player account | +| `evennia.DefaultChannel` | `typeclasses.channels.Channel` | In-game comms | +| `evennia.DefaultScript` | `typeclasses.scripts.Script` | Entities with no location | + +Given you have an imported Typeclass, there are four ways to create an instance of it: - Firstly, you can call the class directly, and then `.save()` it: @@ -11,38 +20,156 @@ Given the path to a Typeclass, there are three ways to create an instance of it: obj.save() This has the drawback of being two operations; you must also import the class and have to pass - the actual database field names, such as `db_key` instead of `key` as keyword arguments. + the actual database field names, such as `db_key` instead of `key` as keyword arguments. This is closest to how a 'normal' Python class works, but is not recommended. - Secondly you can use the Evennia creation helpers: obj = evennia.create_object(SomeTypeClass, key=...) - This is the recommended way if you are trying to create things in Python. The first argument can either be - the class _or_ the python-path to the typeclass, like `"path.to.SomeTypeClass"`. It can also be `None` in which - case the Evennia default will be used. While all the creation methods - are available on `evennia`, they are actually implemented in [evennia/utils/create.py](../../../api/evennia.utils.create.md). + This is the recommended way if you are trying to create things in Python. The first argument can either be the class _or_ the python-path to the typeclass, like `"path.to.SomeTypeClass"`. It can also be `None` in which case the Evennia default will be used. While all the creation methods + are available on `evennia`, they are actually implemented in [evennia/utils/create.py](../../../api/evennia.utils.create.md). Each of the different base classes have their own creation function, like `create_account` and `create_script` etc. +- Thirdly, you can use the `.create` method on the Typeclass itself: + + ```python + obj, err = SomeTypeClass.create(key=...) + ``` + Since `.create` is a method on the typeclass, this form is useful if you want to customize how the creation process works for your custom typeclasses. Note that it returns _two_ values - the `obj` is either the new object or `None`, in which case `err` should be a list of error-strings detailing what went wrong. - Finally, you can create objects using an in-game command, such as - create/drop obj:path.to.SomeTypeClass + create obj:path.to.SomeTypeClass + + As a developer you are usually best off using the other methods, but a command is usually the only way to let regular players or builders without Python-access help build the game world. - As a developer you are usually best off using the two other methods, but a command is usually the only way - to let regular players or builders without Python-access help build the game world. - ## Creating Objects -This is one of the most common creation-types. These are entities that inherits from `DefaultObject` at any distance. -They have an existence in the game world and includes rooms, characters, exits, weapons, flower pots and castles. +An [Object](../../../Components/Objects.md) is one of the most common creation-types. These are entities that inherits from `DefaultObject` at any distance. They have an existence in the game world and includes rooms, characters, exits, weapons, flower pots and castles. > py > import evennia > rose = evennia.create_object(key="rose") -Since we didn't specify the `typeclass` as the first argument, the default given by `settings.BASE_OBJECT_TYPECLASS` -(`typeclasses.objects.Object`) will be used. +Since we didn't specify the `typeclass` as the first argument, the default given by `settings.BASE_OBJECT_TYPECLASS` (`typeclasses.objects.Object` out of the box) will be used. + +The `create_object` has [a lot of options](evennia.utils.create.create_object). A more detailed example in code: + +```python +from evennia import create_object, search_object + +meadow = search_object("Meadow")[0] + +lasgun = create_object("typeclasses.objects.guns.LasGun", + key="lasgun", + location=meadow, + attributes=[("desc", "A fearsome Lasgun.")]) + +``` + +Here we set the location of a weapon as well as gave it an [Attribute](../../../Components/Attributes.md) `desc`, which is what the `look` command will use when looking this and other things. + +## Creating Rooms, Characters and Exits + +`Characters`, `Rooms` and `Exits` are all subclasses of `DefaultObject`. So there is for example no separate `create_character`, you just create characters with `create_object` pointing to the `Character` typeclass. + +### Linking Exits and Rooms in code + +An `Exit` is a one-way link between rooms. For example, `east` could be an `Exit` between the `Forest` room and the `Meadow` room. + + Meadow -> east -> Forest + +The `east` exit has a `key` of `east`, a `location` of `Meadow` and a `destination` of `Forest`. If you wanted to be able to go back from Forest to Meadow, you'd need to create a new `Exit`, say, `west`, where `location` is `Forest` and `destination` is `Meadow`. + + Meadow -> east -> Forest + Forest -> west -> Meadow + +In-game you do this with `tunnel` and `dig` commands, bit if you want to ever set up these links in code, you can do it like this: + +```python +from evennia import create_object +from mygame.typeclasses import rooms, exits + +# rooms +meadow = create_object(rooms.Room, key="Meadow") +forest = create_object(rooms.Room, key="Forest") + +# exits +create_object(exits.Exit, key="east", location=meadow, destination=forest) +create_object(exits.Exit, key="west", location=forest, destination=meadow) +``` ## Creating Accounts -An _Account_ is an out-of-character (OOC) entity, with no existence in the game world. +An [Account](../../../Components/Accounts.md) is an out-of-character (OOC) entity, with no existence in the game world. You can find the parent class for Accounts in `typeclasses/accounts.py`. -_TODO_ +Normally, you want to create the Account when a user authenticates. By default, this happens in the `create account` and `login` default commands in the `UnloggedInCmdSet`. This means that customizing this just means replacing those commands! + +So normally you'd modify those commands rather than make something from scratch. But here's the principle: + +```python +from evennia import create_account + +new_account = create_account( + accountname, email, password, + permissions=["Player"], + typeclass="typeclasses.accounts.MyAccount" + ) +``` +The inputs are usually taken from the player via the command. The `email` must be given, but can be `None` if you are not using it. The `accountname` must be globally unique on the server. The `password` is stored encrypted in the database. If `typeclass` is not given, the `settings.BASE_ACCOUNT_TYPECLASS` will be used (`typeclasses.accounts.Account`). + + +## Creating Channels + +A [Channel](../../../Components/Channels.md) acts like a switchboard for sending in-game messages between users; like an IRC- or discord channel but inside the game. + +Users interact with channels via the `channel` command: + + channel/all + channel/create channelname + channel/who channelname + channel/sub channel name + ... + (see 'help channel') + +If a channel named, say, `myguild` exists, a user can send a message to it just by writing the channel name: + + > myguild Hello! I have some questions ... + +Creating channels follows a familiar syntax: + +```python +from evennia import create_channel + +new_channel = create_channel(channelname) +``` + +Channels can also be auto-created by the server by setting the `DEFAULT_CHANNELS` setting. See [Channels documentation](../../../Components/Channels.md) for details. + + +## Creating Scripts + +A [Script](../../../Components/Scripts.md) is an entity that has no in-game location. It can be used to store arbitrary data and is often used for game systems that need persistent storage but which you can't 'look' at in-game. Examples are economic systems, weather and combat handlers. + +Scripts are multi-use and depending on what they do, a given script can either be 'global' or be attached "to" another object (like a Room or Character). + +```python +from evennia import create_script, search_object +# global script +new_script = create_script("typeclasses.scripts.MyScript", key="myscript") + +# on-object script +meadow = search_object("Meadow")[0] +new_script = create_script("typeclasses.scripts.MyScripts", + key"myscript2", obj=meadow) + +``` + +A convenient way to create global scripts is define them in the `GLOBAL_SCRIPTS` setting; Evennia will then make sure to initialize them. Scripts also have an optional 'timer' component. See the dedicated [Script](../../../Components/Scripts.md) documentation for more info. + +## Conclusion + +Any game will need peristent storage of data. This was a quick run-down of how to create each default type of typeclassed entity. If you make your own typeclasses (as children of the default ones), you create them in the same way. + +Next we'll learn how to find them again by _searching_ for them in the database. + + + diff --git a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Django-queries.md b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Django-queries.md index 2119886f23..579e981a08 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Django-queries.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Django-queries.md @@ -22,34 +22,23 @@ and if statements. But for something non-standard like this, querying the databa much more efficient. Evennia uses [Django](https://www.djangoproject.com/) to handle its connection to the database. -A [django queryset](https://docs.djangoproject.com/en/3.0/ref/models/querysets/) represents -a database query. One can add querysets together to build ever-more complicated queries. Only when -you are trying to use the results of the queryset will it actually call the database. +A [django queryset](https://docs.djangoproject.com/en/3.0/ref/models/querysets/) represents a database query. One can add querysets together to build ever-more complicated queries. Only when you are trying to use the results of the queryset will it actually call the database. -The normal way to build a queryset is to define what class of entity you want to search by getting its -`.objects` resource, and then call various methods on that. We've seen this one before: +The normal way to build a queryset is to define what class of entity you want to search by getting its `.objects` resource, and then call various methods on that. We've seen variants of this before: all_weapons = Weapon.objects.all() -This is now a queryset representing all instances of `Weapon`. If `Weapon` had a subclass `Cannon` and we -only wanted the cannons, we would do +This is now a queryset representing all instances of `Weapon`. If `Weapon` had a subclass `Cannon` and we only wanted the cannons, we would do all_cannons = Cannon.objects.all() -Note that `Weapon` and `Cannon` are _different_ typeclasses. This means that you -won't find any `Weapon`-typeclassed results in `all_cannons`. Vice-versa, you -won't find any `Cannon`-typeclassed results in `all_weapons`. This may not be -what you expect. +Note that `Weapon` and `Cannon` are _different_ typeclasses. This means that you won't find any `Weapon`-typeclassed results in `all_cannons`. Vice-versa, you won't find any `Cannon`-typeclassed results in `all_weapons`. This may not be what you expect. -If you want to get all entities with typeclass `Weapon` _as well_ as all the -subclasses of `Weapon`, such as `Cannon`, you need to use the `_family` type of -query: +If you want to get all entities with typeclass `Weapon` _as well_ as all the subclasses of `Weapon`, such as `Cannon`, you need to use the `_family` type of query: ```{sidebar} _family -The all_family, filter_family etc is an Evennia-specific -thing. It's not part of regular Django. - +The `all_family` and `filter_family` (as well as `get_family` for getting exactly one result) are Evennia-specific. They are not part of regular Django. ``` really_all_weapons = Weapon.objects.all_family() @@ -84,25 +73,14 @@ get results out of it to be able to loop): for rose in local_non_red_roses: print(rose) -From now on, the queryset is _evaluated_ and we can't keep adding more queries to it - we'd need to -create a new queryset if we wanted to find some other result. Other ways to evaluate the queryset is to -print it, convert it to a list with `list()` and otherwise try to access its results. - -Note how we use `db_key` and `db_location`. This is the actual names of these -database fields. By convention Evennia uses `db_` in front of every database -field. When you use the normal Evennia search helpers and objects you can skip -the `db_` but here we are calling the database directly and need to use the -'real' names. +From now on, the queryset is _evaluated_ and we can't keep adding more queries to it - we'd need to create a new queryset if we wanted to find some other result. Other ways to evaluate the queryset is to print it, convert it to a list with `list()` and otherwise try to access its results. ```{sidebar} database fields -Each database table have only a few fields. For `Objects`, the most common ones -are `db_key`, `db_location` and `db_destination`. When accessing them they are -normally accessed just as `obj.key`, `obj.location` and `obj.destination`. You -only need to remember the `db_` when using them in database queries. The object -description, `obj.db.desc` is not such a hard-coded field, but one of many -arbitrary Attributes attached to the Object. - +Each database table have only a few fields. For `DefaultObject`, the most common ones are `db_key`, `db_location` and `db_destination`. When accessing them they are normally accessed just as `obj.key`, `obj.location` and `obj.destination`. You only need to remember the `db_` when using them in database queries. The object description, `obj.db.desc` is not such a hard-coded field, but one of many +Attributes attached to the Object. ``` +Note how we use `db_key` and `db_location`. This is the actual names of these database fields. By convention Evennia uses `db_` in front of every database field. When you use the normal Evennia search helpers and objects you can skip the `db_` but here we are calling the database directly and need to use the 'real' names. + Here are the most commonly used methods to use with the `objects` managers: @@ -111,12 +89,11 @@ were found. - `get` - query for a single match - raises exception if none were found, or more than one was found. - `all` - get all instances of the particular type. -- `filter_family` - like `filter`, but search all sub classes as well. -- `get_family` - like `get`, but search all sub classes as well. +- `filter_family` - like `filter`, but search all subclasses as well. +- `get_family` - like `get`, but search all subclasses as well. - `all_family` - like `all`, but return entities of all subclasses as well. -> All of Evennia search functions use querysets under the hood. The `evennia.search_*` functions actually -> return querysets, which means you could in principle keep adding queries to their results as well. +> All of Evennia search functions use querysets under the hood. The `evennia.search_*` functions actually return querysets (we have just been treating them as lists so far). This means you could in principle add a `.filter` query to the result of `evennia.search_object` to further refine the search. ## Queryset field lookups @@ -124,44 +101,60 @@ found. Above we found roses with exactly the `db_key` `"rose"`. This is an _exact_ match that is _case sensitive_, so it would not find `"Rose"`. - # this is case-sensitive and the same as = - roses = Flower.objects.filter(db_key__exact="rose" - - # the i means it's case-insensitive - roses = Flower.objects.filter(db_key__iexact="rose") +```python +# this is case-sensitive and the same as = +roses = Flower.objects.filter(db_key__exact="rose" +# the i means it's case-insensitive +roses = Flower.objects.filter(db_key__iexact="rose") +``` The Django field query language uses `__` similarly to how Python uses `.` to access resources. This is because `.` is not allowed in a function keyword. - roses = Flower.objects.filter(db_key__icontains="rose") +```python +roses = Flower.objects.filter(db_key__icontains="rose") +``` -This will find all flowers whose name contains the string `"rose"`, like `"roses"`, `"wild rose"` etc. The -`i` in the beginning makes the search case-insensitive. Other useful variations to use -are `__istartswith` and `__iendswith`. You can also use `__gt`, `__ge` for "greater-than"/"greater-or-equal-than" -comparisons (same for `__lt` and `__le`). There is also `__in`: +This will find all flowers whose name contains the string `"rose"`, like `"roses"`, `"wild rose"` etc. The `i` in the beginning makes the search case-insensitive. Other useful variations to use +are `__istartswith` and `__iendswith`. You can also use `__gt`, `__ge` for "greater-than"/"greater-or-equal-than" comparisons (same for `__lt` and `__le`). There is also `__in`: - swords = Weapons.objects.filter(db_key__in=("rapier", "two-hander", "shortsword")) +```python +swords = Weapons.objects.filter(db_key__in=("rapier", "two-hander", "shortsword")) +``` One also uses `__` to access foreign objects like Tags. Let's for example assume this is how we have identified mages: - char.tags.add("mage", category="profession") +```python +char.tags.add("mage", category="profession") +``` -Now, in this case we have an Evennia helper to do this search: +Now, in this case we already have an Evennia helper to do this search: - mages = evennia.search_tags("mage", category="profession") +```python +mages = evennia.search_tags("mage", category="profession") +``` -But this will find all Objects with this tag+category. Maybe you are only looking for Vampire mages: +Here is what it would look as a query if you were only looking for Vampire mages: - sparkly_mages = Vampire.objects.filter(db_tags__db_key="mage", db_tags__db_category="profession") +```{sidebar} Breaking lines of code +In Python you can wrap code in `(...)` to break it over multiple lines. Doing this doesn't affect functionality, but can make it easier to read. +``` + +```python +sparkly_mages = ( + Vampire.objects.filter( + db_tags__db_key="mage", + db_tags__db_category="profession") + ) +``` This looks at the `db_tags` field on the `Vampire` and filters on the values of each tag's `db_key` and `db_category` together. -For more field lookups, see the -[django docs](https://docs.djangoproject.com/en/3.0/ref/models/querysets/#field-lookups) on the subject. +For more field lookups, see the [django docs](https://docs.djangoproject.com/en/3.0/ref/models/querysets/#field-lookups) on the subject. -## Get that werewolf ... +## Let's get that werewolf ... Let's see if we can make a query for the werewolves in the moonlight we mentioned at the beginning of this lesson. @@ -174,14 +167,10 @@ Firstly, we make ourselves and our current location match the criteria, so we ca This is an example of a more complex query. We'll consider it an example of what is possible. -```{sidebar} Line breaks +```{code-block} python +:linenos: +:emphasize-lines: 4,6,7,8 -Note the way of writing this code. It would have been very hard to read if we -just wrote it in one long line. But since we wrapped it in `(...)` we can spread -it out over multiple lines without worrying about line breaks! -``` - -```python from typeclasses.characters import Character will_transform = ( @@ -194,31 +183,27 @@ will_transform = ( ) ``` -- We want to find `Character`s, so we access `.objects` on the `Character` typeclass. +```{sidebar} Attributes vs database fields +Don't confuse database fields with [Attributes](../../../Components/Attributes.md) you set via `obj.db.attr = 'foo'` or `obj.attributes.add()`. Attributes are custom database entities *linked* to an object. They are not separate fields *on* that object like `db_key` or `db_location` are. +``` +- **Line 4** We want to find `Character`s, so we access `.objects` on the `Character` typeclass. - We start to filter ... -- - - ... by accessing the `db_location` field (usually this is a Room) - - ... and on that location, we get the value of `db_tags` (this is a _many-to-many_ database field + - **Line 6**: ... by accessing the `db_location` field (usually this is a Room) + - ... and on that location, we get the value of `db_tags` (this is a _many-to-many_ database field that we can treat like an object for this purpose; it references all Tags on the location) - - ... and from those `Tags`, we looking for `Tags` whose `db_key` is "monlit" (non-case sensitive). -- ... We also want only Characters with `Attributes` whose `db_key` is exactly `"lycantrophy"` -- ... at the same time as the `Attribute`'s `db_value` is greater-than 2. + - ... and from those `Tags`, we looking for `Tags` whose `db_key` is "monlit" (non-case sensitive). + - **Line 7**: ... We also want only Characters with `Attributes` whose `db_key` is exactly `"lycantrophy"` + - **Line 8** :... at the same time as the `Attribute`'s `db_value` is greater-than 2. Running this query makes our newly lycantrophic Character appear in `will_transform` so we know to transform it. Success! -> Don't confuse database fields with [Attributes](../../../Components/Attributes.md) you set via `obj.db.attr = 'foo'` or -`obj.attributes.add()`. Attributes are custom database entities *linked* to an object. They are not -separate fields *on* that object like `db_key` or `db_location` are. - -## Complex queries +## Queries with OR or NOT All examples so far used `AND` relations. The arguments to `.filter` are added together with `AND` ("we want tag room to be "monlit" _and_ lycantrhopy be > 2"). -For queries using `OR` and `NOT` we need Django's -[Q object](https://docs.djangoproject.com/en/1.11/topics/db/queries/#complex-lookups-with-q-objects). It is -imported from Django directly: +For queries using `OR` and `NOT` we need Django's [Q object](https://docs.djangoproject.com/en/1.11/topics/db/queries/#complex-lookups-with-q-objects). It is imported from Django directly: from django.db.models import Q @@ -230,10 +215,10 @@ You can then use this `Q` instance as argument in a `filter`: q1 = Q(db_key="foo") Character.objects.filter(q1) + # this is the same as + Character.objects.filter(db_key="foo") - -The useful thing about `Q` is that these objects can be chained together with special symbols (bit operators): -`|` for `OR` and `&` for `AND`. A tilde `~` in front negates the expression inside the `Q` and thus +The useful thing about `Q` is that these objects can be chained together with special symbols (bit operators): `|` for `OR` and `&` for `AND`. A tilde `~` in front negates the expression inside the `Q` and thus works like `NOT`. q1 = Q(db_key="Dalton") @@ -243,12 +228,9 @@ works like `NOT`. Would get all Characters that are either named "Dalton" _or_ which is _not_ in prison. The result is a mix of Daltons and non-prisoners. -Let us expand our original werewolf query. Not only do we want to find all -Characters in a moonlit room with a certain level of `lycanthrophy`. Now we also -want the full moon to immediately transform people who were recently bitten, -even if their `lycantrophy` level is not yet high enough (more dramatic this -way!). When you get bitten, you'll get a Tag `recently_bitten` put on you to -indicate this. +Let us expand our original werewolf query. Not only do we want to find all Characters in a moonlit room with a certain level of `lycanthrophy` - we decide that if they have been _newly bitten_, they should also turn, _regardless_ of their lycantrophy level (more dramatic that way!). + +Let's say that getting bitten means that you'll get assigned a Tag `recently_bitten`. This is how we'd change our query: @@ -302,32 +284,39 @@ sure that there is only one instance of each Character in the result. ## Annotations What if we wanted to filter on some condition that isn't represented easily by a -field on the object? Maybe we want to find rooms only containing five or more -objects? +field on the object? An example would wanting to find rooms only containing _five or more objects_. We *could* do it like this (don't actually do it this way!): ```python - from typeclasses.rooms import Room +from typeclasses.rooms import Room - all_rooms = Rooms.objects.all() + all_rooms = Rooms.objects.all() - rooms_with_five_objects = [] - for room in all_rooms: - if len(room.contents) >= 5: - rooms_with_five_objects.append(room) + rooms_with_five_objects = [] + for room in all_rooms: + if len(room.contents) >= 5: + rooms_with_five_objects.append(room) ``` -Above we get all rooms and then use `list.append()` to keep adding the right +```{sidebar} list.append, extend and .pop + +Use `mylist.append(obj)` to add new items to a list. Use `mylist.extend(another_list))` or `list1 + list2` to merge two lists together. Use `mylist.pop()` to remove an item from the end or `.pop(0)` to remove from the beginning of the list. Remember all indices start from `0` in Python. +``` + +Above we get _all_ rooms and then use `list.append()` to keep adding the right rooms to an ever-growing list. This is _not_ a good idea, once your database -grows this will be unnecessarily computing-intensive. The database is much more -suitable for this. +grows this will be unnecessarily compute-intensive. It's much better to query the +database directly _Annotations_ allow you to set a 'variable' inside the query that you can then access from other parts of the query. Let's do the same example as before directly in the database: -```python +```{code-block} python +:linenos: +:emphasize-lines: 6,8 + from typeclasses.rooms import Room from django.db.models import Count @@ -339,27 +328,26 @@ rooms = ( ) ``` +```{sidebar} locations_set +Note the use of `locations_set` in that `Count`. The `*s_set` is a back-reference automatically created by Django. In this case it allows you to find all objects that *has the current object as location*. +``` + `Count` is a Django class for counting the number of things in the database. -Here we first create an annotation `num_objects` of type `Count`. It creates an in-database function -that will count the number of results inside the database. - -> Note the use of `location_set` in that `Count`. The `*_set` is a back-reference automatically created by -Django. In this case it allows you to find all objects that *has the current object as location*. - -Next we filter on this annotation, using the name `num_objects` as something we +- **Line 6-7**: Here we first create an annotation `num_objects` of type `Count`. It creates an in-database function that will count the number of results inside the database. The fact annotation means that now `num_objects` is avaiable to be used in other parts of the query. +- **Line 8** We filter on this annotation, using the name `num_objects` as something we can filter for. We use `num_objects__gte=5` which means that `num_objects` -should be greater than or equal to 5. This is a little harder to get one's head -around but much more efficient than lopping over all objects in Python. +should be greater than or equal to 5. + +Annotations can be a little harder to get one's head around but much more efficient than lopping over all objects in Python. ## F-objects What if we wanted to compare two dynamic parameters against one another in a query? For example, what if instead of having 5 or more objects, we only wanted objects that had a bigger inventory than they had tags (silly example, but ...)? -This can be with Django's [F objects](https://docs.djangoproject.com/en/1.11/ref/models/expressions/#f-expressions). -So-called F expressions allow you to do a query that looks at a value of each -object in the database. + +This can be with Django's [F objects](https://docs.djangoproject.com/en/4.1/ref/models/expressions/#f-expressions). So-called F expressions allow you to do a query that looks at a value of each object in the database. ```python from django.db.models import Count, F @@ -380,16 +368,15 @@ condition to be calculated on the fly, completely within the database. ## Grouping and returning only certain properties -Suppose you used tags to mark someone belonging to an organization. Now you want to make a list and -need to get the membership count of every organization all at once. +Suppose you used tags to mark someone belonging to an organization. Now you want to make a list and need to get the membership count of every organization all at once. -The `.annotate`, `.values_list`, and `.order_by` queryset methods are useful for this. Normally when -you run a `.filter`, what you get back is a bunch of full typeclass instances, like roses or swords. -Using `.values_list` you can instead choose to only get back certain properties on objects. -The `.order_by` method finally allows for sorting the results according to some criterion: +The `.annotate`, `.values_list`, and `.order_by` queryset methods are useful for this. Normally when you run a `.filter`, what you get back is a bunch of full typeclass instances, like roses or swords. Using `.values_list` you can instead choose to only get back certain properties on objects. The `.order_by` method finally allows for sorting the results according to some criterion: -```python +```{code-block} python +:linenos: +:emphasize-lines: 6,7,8,9 + from django.db.models import Count from typeclasses.rooms import Room @@ -402,15 +389,12 @@ result = ( ``` Here we fetch all Characters who ... -- ... has a tag of category "organization" on them -- ... along the way we count how many different Characters (each `id` is unique) we find for each organization - and store it in a 'variable' `tagcount` using `.annotate` and `Count` -- ... we use this count to sort the result in descending order of `tagcount` (descending because there is a minus sign, - default is increasing order but we want the most popular organization to be first). -- ... and finally we make sure to only return exactly the properties we want, namely the name of the organization tag -and how many matches we found for that organization. +- **Line 6**: ... has a tag of category "organization" on them +- **Line 7**:... along the way we count how many different Characters (each `id` is unique) we find for each organization and store it in a 'variable' `tagcount` using `.annotate` and `Count` +- **Line 8**: ... we use this count to sort the result in descending order of `tagcount` (descending because there is a minus sign, default is increasing order but we want the most popular organization to be first). +- **Line 9**: ... and finally we make sure to only return exactly the properties we want, namely the name of the organization tag and how many matches we found for that organization. For this we use the `values_list` method on the queryset. This will evaluate the queryset immediately. -The result queryset will be a list of tuples ordered in descending order by the number of matches, +The result will be a list of tuples ordered in descending order by the number of matches, in a format like the following: ``` [ @@ -424,9 +408,8 @@ in a format like the following: ## Conclusions -We have covered a lot of ground in this lesson and covered several more complex -topics. Knowing how to query using Django is a powerful skill to have. +We have covered a lot of ground in this lesson and covered several more complex topics. Knowing how to query using Django is a powerful skill to have. This concludes the first part of the Evennia starting tutorial - "What we have". Now we have a good foundation to understand how to plan what our tutorial game -will be about. +will be about. \ No newline at end of file diff --git a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Learning-Typeclasses.md b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Learning-Typeclasses.md index c20b60778a..e923ef685a 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Learning-Typeclasses.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Learning-Typeclasses.md @@ -3,8 +3,7 @@ Now that we have learned a little about how to find things in the Evennia library, let's use it. In the [Python classes and objects](./Beginner-Tutorial-Python-classes-and-objects.md) lesson we created the dragons Fluffy, Cuddly -and Smaug and made them fly and breathe fire. So far our dragons are short-lived - whenever we `restart` -the server or `quit()` out of python mode they are gone. +and Smaug and made them fly and breathe fire. So far our dragons are short-lived - whenever we `restart` the server or `quit()` out of python mode they are gone. This is what you should have in `mygame/typeclasses/monsters.py` so far: diff --git a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-More-on-Commands.md b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-More-on-Commands.md index 289b656692..168a04af06 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-More-on-Commands.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-More-on-Commands.md @@ -5,7 +5,7 @@ also learn how to add, modify and extend Evennia's default commands. ## More advanced parsing -In the last lesson we made a `hit` Command and hit a dragon with it. You should have the code +In the [last lesson](./Beginner-Tutorial-Adding-Commands.md) we made a `hit` Command and struck a dragon with it. You should have the code from that still around. Let's expand our simple `hit` command to accept a little more complex input: @@ -19,11 +19,12 @@ That is, we want to support all of these forms hit target with weapon If you don't specify a weapon you'll use your fists. It's also nice to be able to skip "with" if -you are in a hurry. Time to modify `mygame/commands/mycommands.py` again. Let us break out the parsing -a little, in a new method `parse`: +you are in a hurry. Time to modify `mygame/commands/mycommands.py` again. Let us break out the parsing a little, in a new method `parse`: -```python +```{code-block} python +:linenos: +:emphasize-lines: 14,15,16,18,29,35,41 #... class CmdHit(Command): @@ -67,13 +68,9 @@ class CmdHit(Command): self.caller.msg(f"You hit {target.key} with {weaponstr}!") target.msg(f"You got hit by {self.caller.key} with {weaponstr}!") # ... - ``` -The `parse` method is called before `func` and has access to all the same on-command variables as in `func`. Using -`parse` not only makes things a little easier to read, it also means you can easily let other Commands _inherit_ -your parsing - if you wanted some other Command to also understand input on the form ` with ` you'd inherit -from this class and just implement the `func` needed for that command without implementing `parse` anew. +The `parse` method is a special one Evennia knows to call _before_ `func`. At this time it has access to all the same on-command variables as `func` does. Using `parse` not only makes things a little easier to read, it also means you can easily let other Commands _inherit_ your parsing - if you wanted some other Command to also understand input on the form ` with ` you'd inherit from this class and just implement the `func` needed for that command without implementing `parse` anew. ```{sidebar} Tuples and Lists @@ -111,14 +108,16 @@ from this class and just implement the `func` needed for that command without im before running `strip()` on it. This is because we know that if it's falsy, it's an empty list `[]` and lists don't have the `.strip()` method on them (so if we tried to use it, we'd get an error). -Now onto the `func` method. The main difference is we now have `self.target` and `self.weapon` available for -convenient use. +Now onto the `func` method. The main difference is we now have `self.target` and `self.weapon` available for convenient use. +```{sidebar} +Here we create the messages to send to each side of the fight explicitly. Later we'll find out how to use Evennia's [inline functions](../../../Components/FuncParser.md) to send a single string that looks different depending on who sees it. +``` + - **Lines 29 and 35** - We make use of the previously parsed search terms for the target and weapon to find the respective resource. - **Lines 34-39** - Since the weapon is optional, we need to supply a default (use our fists!) if it's not set. We use this to create a `weaponstr` that is different depending on if we have a weapon or not. -- **Lines 41-42** - We merge the `weaponstr` with our attack text. - +- **Lines 41-42** - We merge the `weaponstr` with our attack texts and send it to attacker and target respectively. Let's try it out! > reload @@ -126,9 +125,9 @@ Let's try it out! Could not find 'sword'. You hit smaug with bare fists! -Oops, our `self.caller.search(self.weapon)` is telling us that it found no sword. Since we are not `return`ing -in this situation (like we do if failing to find `target`) we still continue fighting with our bare hands. -This won't do. Let's make ourselves a sword. +Oops, our `self.caller.search(self.weapon)` is telling us that it found no sword. This is reasonable (we don't have a sword). Since we are not `return`ing when failing to find a weapon in the way we do if we find no `target`, we still continue fighting with our bare hands. + +This won't do. Let's make ourselves a sword: > create sword @@ -139,13 +138,16 @@ change (no code changed, only stuff in the database). > hit smaug with sword You hit smaug with sword! +Poor Smaug. ## Adding a Command to an object -The commands of a cmdset attached to an object with `obj.cmdset.add()` will by default be made available to that object -but _also to those in the same location as that object_. If you did the [Building introduction](./Beginner-Tutorial-Building-Quickstart.md) -you've seen an example of this with the "Red Button" object. The [Tutorial world](./Beginner-Tutorial-Tutorial-World.md) -also has many examples of objects with commands on them. +```{sidebar} Command Sets on Characters +In case you wonder, the 'Character CmdSet' on `Characters` is configured to be available to _only_ that Character. If not, you'd get command multi-matches for things like `look` whenever you were in the same room with another character using the same command set. See [Command Sets](../../../Components/Command-Sets.md) docs for more info. +``` +As we learned in the lesson about [Adding commands](./Beginner-Tutorial-Adding-Commands.md), Commands are are grouped in Command Sets. Such Command Sets are attached to an object with `obj.cmdset.add()` and will then be available for that object to use. + +What we didn't mention before is that by default those commands are _also available to those in the same location as that object_. If you did the [Building quickstart lesson](./Beginner-Tutorial-Building-Quickstart.md) you've seen an example of this with the "Red Button" object. The [Tutorial world](./Beginner-Tutorial-Tutorial-World.md) also has many examples of objects with commands on them. To show how this could work, let's put our 'hit' Command on our simple `sword` object from the previous section. @@ -163,12 +165,10 @@ Let's try to swing it! ```{sidebar} Multi-matches -Some game engines will just pick the first hit when finding more than one. Evennia will always give you a choice. The reason for this is that Evennia cannot know if `hit` and `hit` are different or the same - maybe it behaves differently depending on the object it sits on? Besides, imagine if you had a red and a blue button both with the command `push` on it. Now you just write `push`. Wouldn't you prefer to be asked `which` button you really wanted to push? +Some game engines will just pick the first hit when finding more than one. Evennia will always give you a choice. The reason for this is that Evennia cannot know if `hit` and `hit` are different or the same - maybe it behaves differently depending on the object it sits on? Besides, imagine if you had a red and a blue button both with the command `push` on it. Now you just write `push`. Wouldn't you prefer to be asked _which_ button you really wanted to push? ``` -Woah, that didn't go as planned. Evennia actually found _two_ `hit` commands to didn't know which one to use -(_we_ know they are the same, but Evennia can't be sure of that). As we can see, `hit-1` is the one found on -the sword. The other one is from adding `MyCmdSet` to ourself earlier. It's easy enough to tell Evennia which -one you meant: + +Woah, that didn't go as planned. Evennia actually found _two_ `hit` commands and didn't know which one to use (_we_ know they are the same, but Evennia can't be sure of that). As we can see, `hit-1` is the one found on the sword. The other one is from adding `MyCmdSet` to ourself earlier. It's easy enough to tell Evennia which one you meant: > hit-1 Who do you want to hit? @@ -181,7 +181,7 @@ In this case we don't need both command-sets, so let's just keep the one on the > hit Who do you want to hit? -Now try this: +Now try making a new location and then drop the sword in it. > tunnel n = kitchen > n @@ -193,33 +193,30 @@ Now try this: > hit Who do you want to hit? -The `hit` command is now only available if you hold or are in the same room as the sword. +The `hit` command is only available if you hold _or_ are in the same room as the sword. ### You need to hold the sword! -Let's get a little ahead of ourselves and make it so you have to _hold_ the sword for the `hit` command to -be available. This involves a _Lock_. We've cover locks in more detail later, just know that they are useful -for limiting the kind of things you can do with an object, including limiting just when you can call commands on -it. ```{sidebar} Locks Evennia Locks are defined as a mini-language defined in `lockstrings`. The lockstring is on a form `:`, where `situation` determines when this lock applies and the `lockfuncs` (there can be more than one) are run to determine if the lock-check passes or not depending on circumstance. ``` +Let's get a little ahead of ourselves and make it so you have to _hold_ the sword for the `hit` command to be available. This involves a [Lock](../../../Components/Locks.md). We've cover locks in more detail later, just know that they are useful for limiting the kind of things you can do with an object, including limiting just when you can call commands on it. + > py self.search("sword").locks.add("call:holds()") -We added a new lock to the sword. The _lockstring_ `"call:holds()"` means that you can only _call_ commands on -this object if you are _holding_ the object (that is, it's in your inventory). +We added a new lock to the sword. The _lockstring_ `"call:holds()"` means that you can only _call_ commands on this object if you are _holding_ the object (that is, it's in your inventory). + +For locks to work, you cannot be _superuser_, since the superuser passes all locks. You need to `quell` yourself first: -For locks to work, you cannot be _superuser_, since the superuser passes all locks. You need to `quell` yourself -first: ```{sidebar} quell/unquell Quelling allows you as a developer to take on the role of players with less priveleges. This is useful for testing and debugging, in particular since a superuser has a little `too` much power sometimes. Use `unquell` to get back to your normal self. ``` > quell - + If the sword lies on the ground, try > hit @@ -227,10 +224,8 @@ If the sword lies on the ground, try > get sword > hit > Who do you want to hit? - - -Finally, we get rid of ours sword so we have a clean slate with no more `hit` commands floating around. -We can do that in two ways: + +Finally, we get rid of ours sword so we have a clean slate with no more `hit` commands floating around. We can do that in two ways: delete sword @@ -242,18 +237,7 @@ or ## Adding the Command to a default Cmdset -As we have seen we can use `obj.cmdset.add()` to add a new cmdset to objects, whether that object -is ourself (`self`) or other objects like the `sword`. - -This is how all commands in Evennia work, including default commands like `look`, `dig`, `inventory` and so on. -All these commands are in just loaded on the default objects that Evennia provides out of the box. - -- Characters (that is 'you' in the gameworld) has the `CharacterCmdSet`. -- Accounts (the thing that represents your out-of-character existence on the server) has the `AccountCmdSet` -- Sessions (representing one single client connection) has the `SessionCmdSet` -- Before you log in (at the connection screen) you'll have access to the `UnloggedinCmdSet`. - -The thing must commonly modified is the `CharacterCmdSet`. +As we have seen we can use `obj.cmdset.add()` to add a new cmdset to objects, whether that object is ourself (`self`) or other objects like the `sword`. Doing this this way is a little cumbersome though. It would be better to add this to all characters. The default cmdset are defined in `mygame/commands/default_cmdsets.py`. Open that file now: @@ -313,15 +297,15 @@ class SessionCmdSet(default_cmds.SessionCmdSet): The `super()` function refers to the parent of the current class and is commonly used to call same-named methods on the parent. ``` -`evennia.default_cmds` is a container that holds all of Evennia's default commands and cmdsets. In this module -we can see that this was imported and then a new child class was made for each cmdset. Each class looks familiar -(except the `key`, that's mainly used to easily identify the cmdset in listings). In each `at_cmdset_creation` all -we do is call `super().at_cmdset_creation` which means that we call `at_cmdset_creation() on the _parent_ CmdSet. +`evennia.default_cmds` is a container that holds all of Evennia's default commands and cmdsets. In this module we can see that this was imported and then a new child class was made for each cmdset. Each class looks familiar (except the `key`, that's mainly used to easily identify the cmdset in listings). In each `at_cmdset_creation` all we do is call `super().at_cmdset_creation` which means that we call `at_cmdset_creation() on the _parent_ CmdSet. This is what adds all the default commands to each CmdSet. -To add even more Commands to a default cmdset, we can just add them below the `super()` line. Usefully, if we were to -add a Command with the same `.key` as a default command, it would completely replace that original. So if you were -to add a command with a key `look`, the original `look` command would be replaced by your own version. +When the `DefaultCharacter` (or a child of it) is created, you'll find that the equivalence of `self.cmdset.add("default_cmdsets.CharacterCmdSet, persistent=True")` gets called. This means that all new Characters get this cmdset. After adding more commands to it, you just need to reload to have all characters see it. + +- Characters (that is 'you' in the gameworld) has the `CharacterCmdSet`. +- Accounts (the thing that represents your out-of-character existence on the server) has the `AccountCmdSet` +- Sessions (representing one single client connection) has the `SessionCmdSet` +- Before you log in (at the connection screen) your Session have access to the `UnloggedinCmdSet`. For now, let's add our own `hit` and `echo` commands to the `CharacterCmdSet`: @@ -350,8 +334,7 @@ class CharacterCmdSet(default_cmds.CharacterCmdSet): > hit Who do you want to hit? -Your new commands are now available for all player characters in the game. There is another way to add a bunch -of commands at once, and that is to add a _CmdSet_ to the other cmdset. All commands in that cmdset will then be added: +Your new commands are now available for all player characters in the game. There is another way to add a bunch of commands at once, and that is to add your own _CmdSet_ to the other cmdset. ```python from commands import mycommands @@ -379,7 +362,7 @@ To remove your custom commands again, you of course just delete the change you d We already know that we use `cmdset.remove()` to remove a cmdset. It turns out you can do the same in `at_cmdset_creation`. For example, let's remove the default `get` Command -from Evennia. We happen to know this can be found as `default_cmds.CmdGet`. +from Evennia. If you investigate the `default_cmds.CharacterCmdSet` parent, you'll find that its class is `default_cmds.CmdGet` (the 'real' location is `evennia.commands.default.general.CmdGet`). ```python @@ -410,11 +393,12 @@ class CharacterCmdSet(default_cmds.CharacterCmdSet): At this point you already have all the pieces for how to do this! We just need to add a new command with the same `key` in the `CharacterCmdSet` to replace the default one. -Let's combine this with what we know about classes and -how to _override_ a parent class. Open `mygame/commands/mycommands.py` and lets override -that `CmdGet` command. +Let's combine this with what we know about classes and how to _override_ a parent class. Open `mygame/commands/mycommands.py` and make a new `get` command: + +```{code-block} python +:linenos: +:emphasize-lines: 2,7,8,9 -```python # up top, by the other imports from evennia import default_cmds @@ -424,10 +408,9 @@ class MyCmdGet(default_cmds.CmdGet): def func(self): super().func() self.caller.msg(str(self.caller.location.contents)) - ``` -- **Line2**: We import `default_cmds` so we can get the parent class. +- **Line 2**: We import `default_cmds` so we can get the parent class. We made a new class and we make it _inherit_ `default_cmds.CmdGet`. We don't need to set `.key` or `.parse`, that's already handled by the parent. In `func` we call `super().func()` to let the parent do its normal thing, @@ -462,6 +445,9 @@ class CharacterCmdSet(default_cmds.CharacterCmdSet): self.add(mycommands.MyCmdGet) # ... ``` + +We don't need to use `self.remove()` first; just adding a command with the same `key` (`get`) will replace the default `get` we had from before. + ```{sidebar} Another way Instead of adding `MyCmdGet` explicitly in default_cmdset.py, you could also add it to `mycommands.MyCmdSet` and let it be added automatically here for you. @@ -472,12 +458,10 @@ Instead of adding `MyCmdGet` explicitly in default_cmdset.py, you could also add Get What? [smaug, fluffy, YourName, ...] -We just made a new `get`-command that tells us everything we could pick up (well, we can't pick up ourselves, so -there's some room for improvement there). +We just made a new `get`-command that tells us everything we could pick up (well, we can't pick up ourselves, so there's some room for improvement there ...). ## Summary -In this lesson we got into some more advanced string formatting - many of those tricks will help you a lot in -the future! We also made a functional sword. Finally we got into how to add to, extend and replace a default -command on ourselves. +In this lesson we got into some more advanced string formatting - many of those tricks will help you a lot in the future! We also made a functional sword. Finally we got into how to add to, extend and replace a default command on ourselves. Knowing to add commands is a big part of making a game! +We have been beating on poor Smaug for too long. Next we'll create more things to play around with. \ No newline at end of file diff --git a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Searching-Things.md b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Searching-Things.md index ece57fb47a..5aa84e0a95 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Searching-Things.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Searching-Things.md @@ -7,23 +7,26 @@ if we cannot find and use it afterwards. The base tools are the `evennia.search_*` functions, such as `evennia.search_object`. - rose = evennia.search_object(key="rose") - acct = evennia.search_account(key="MyAccountName", email="foo@bar.com") +```python +import evennia + +roses = evennia.search_object(key="rose") +accts = evennia.search_account(key="MyAccountName", email="foo@bar.com") +``` +``` ```{sidebar} Querysets What is returned from the main search functions is actually a `queryset`. They can be treated like lists except that they can't modified in-place. We'll discuss querysets in the `next lesson` `_. ``` -Strings are always case-insensitive, so searching for `"rose"`, `"Rose"` or `"rOsE"` give the same results. -It's important to remember that what is returned from these search methods is a _listing_ of 0, one or more -elements - all the matches to your search. To get the first match: +Strings are always case-insensitive, so searching for `"rose"`, `"Rose"` or `"rOsE"` give the same results. It's important to remember that what is returned from these search methods is a _listing_ of zero, one or more elements - all the matches to your search. To get the first match: - rose = rose[0] + rose = roses[0] -Often you really want all matches to the search parameters you specify. In other situations, having zero or -more than one match is a sign of a problem and you need to handle this case yourself. +Often you really want all matches to the search parameters you specify. In other situations, having zero or more than one match is a sign of a problem and you need to handle this case yourself. +```python the_one_ring = evennia.search_object(key="The one Ring") if not the_one_ring: # handle not finding the ring at all @@ -32,21 +35,20 @@ more than one match is a sign of a problem and you need to handle this case your else: # ok - exactly one ring found the_one_ring = the_one_ring[0] +``` There are equivalent search functions for all the main resources. You can find a listing of them [in the Search functions section](../../../Evennia-API.md) of the API frontpage. ## Searching using Object.search -On the `DefaultObject` is a `.search` method which we have already tried out when we made Commands. For -this to be used you must already have an object available: +On the `DefaultObject` is a `.search` method which we have already tried out when we made Commands. For this to be used you must already have an object available: rose = obj.search("rose") -The `.search` method wraps `evennia.search_object` and handles its output in various ways. +This searches for objects based on `key` or aliases. The `.search` method wraps `evennia.search_object` and handles its output in various ways. -- By default it will always search for objects among those in `obj.location.contents` and `obj.contents` (that is, -things in obj's inventory or in the same room). +- By default it will always search for objects among those in `obj.location.contents` and `obj.contents` (that is, things in obj's inventory or in the same room). - It will always return exactly one match. If it found zero or more than one match, the return is `None`. - On a no-match or multimatch, `.search` will automatically send an error message to `obj`. @@ -55,32 +57,39 @@ So this method handles error messaging for you. A very common way to use it is i ```python from evennia import Command -class MyCommand(Command): +class CmdQuickFind(Command): + """ + Find an item in your current location. - key = "findfoo" + Usage: + quickfind + + """ + + key = "quickfind" def func(self): - - foo = self.caller.search("foo") - if not foo: + query = self.args + result = self.caller.search(query) + if not result return + self.caller.msg(f"Found match for {query}: {foo}") ``` Remember, `self.caller` is the one calling the command. This is usually a Character, which -inherits from `DefaultObject`! This (rather stupid) Command searches for an object named "foo" in -the same location. If it can't find it, `foo` will be `None`. The error has already been reported -to `self.caller` so we just abort with `return`. +inherits from `DefaultObject`! + +This simple little Command takes its arguments and searches for a match. If it can't find it, `result` will be `None`. The error has already been reported to `self.caller` so we just abort with `return`. You can use `.search` to find anything, not just stuff in the same room: - volcano = self.caller.search("Volcano", global=True) + volcano = self.caller.search("Vesuvio", global=True) If you only want to search for a specific list of things, you can do so too: stone = self.caller.search("MyStone", candidates=[obj1, obj2, obj3, obj4]) -This will only return a match if MyStone is one of the four provided candidate objects. This is quite powerful, -here's how you'd find something only in your inventory: +This will only return a match if "MyStone" is in the room (or in your inventory) _and_ is one of the four provided candidate objects. This is quite powerful, here's how you'd find something only in your inventory: potion = self.caller.search("Healing potion", candidates=self.caller.contents) @@ -88,8 +97,7 @@ You can also turn off the automatic error handling: swords = self.caller.search("Sword", quiet=True) -With `quiet=True` the user will not be notified on zero or multi-match errors. Instead you are expected to handle this -yourself and what you get back is now a list of zero, one or more matches! +With `quiet=True` the user will not be notified on zero or multi-match errors. Instead you are expected to handle this yourself. Furthermore, what is returned is now a list of zero, one or more matches! ## What can be searched for @@ -98,9 +106,9 @@ These are the main database entities one can search for: - [Objects](../../../Components/Objects.md) - [Accounts](../../../Components/Accounts.md) - [Scripts](../../../Components/Scripts.md), -- [Channels](../../../Components/Channels.md), -- [Messages](../../../Components/Msg.md) -- [Help Entries](../../../Components/Help-System.md). +- [Channels](../../../Components/Channels.md) +- [Messages](../../../Components/Msg.md) (used by `page` command by default) +- [Help Entries](../../../Components/Help-System.md) (help entries created manually) Most of the time you'll likely spend your time searching for Objects and the occasional Accounts. @@ -112,8 +120,7 @@ The `key` is the name of the entity. Searching for this is always case-insensiti ### Search by aliases -Objects and Accounts can have any number of aliases. When searching for `key` these will searched too, -you can't easily search only for aliases. +Objects and Accounts can have any number of aliases. When searching for `key` these will searched too, you can't easily search only for aliases. rose.aliases.add("flower") @@ -122,26 +129,26 @@ you can assign new aliases to things with the `alias` command. ### Search by location -Only Objects (things inheriting from `evennia.DefaultObject`) has a location. This is usually a room. -The `Object.search` method will automatically limit it search by location, but it also works for the -general search function. If we assume `room` is a particular Room instance, +Only Objects (things inheriting from `evennia.DefaultObject`) has a location. The location is usually a room. The `Object.search` method will automatically limit it search by location, but it also works for the general search function. If we assume `room` is a particular Room instance, chest = evennia.search_object("Treasure chest", location=room) ### Search by Tags -Think of a [Tag](../../../Components/Tags.md) as the label the airport puts on your luggage when flying. -Everyone going on the same plane gets a tag grouping them together so the airport can know what should -go to which plane. Entities in Evennia can be grouped in the same way. Any number of tags can be attached +Think of a [Tag](../../../Components/Tags.md) as the label the airport puts on your luggage when flying. Everyone going on the same plane gets a tag grouping them together so the airport can know what should go to which plane. Entities in Evennia can be grouped in the same way. Any number of tags can be attached to each object. rose.tags.add("flowers") + rose.tags.add("thorny") daffodil.tags.add("flowers") tulip.tags.add("flowers") + cactus.tags.add("flowers") + cactus.tags.add("thorny") You can now find all flowers using the `search_tag` function: all_flowers = evennia.search_tag("flowers") + roses_and_cactii = evennia.search_tag("thorny") Tags can also have categories. By default this category is `None` which is also considered a category. @@ -196,13 +203,15 @@ You can also search using the typeclass itself: all_roses = Rose.objects.all() -This last way of searching is a simple form of a Django _query_. This is a way to express SQL queries using -Python. +This last way of searching is a simple form of a Django _query_. This is a way to express SQL queries using Python. See [the next lesson](./Beginner-Tutorial-Django-queries.md), where we'll explore this way to searching in more detail. ### Search by dbref -The database id or `#dbref` is unique and never-reused within each database table. In search methods you can -replace the search for `key` with the dbref to search for. This must be written as a string `#dbref`: +```{sidebar} Will I run out of dbrefs? + +Since dbrefs are not reused, do you need to worry about your database ids 'running out' in the future? [No, and here's why](../../../Components/Typeclasses.md#will-i-run-out-of-dbrefs). +``` +The database id or `#dbref` is unique and never-reused within each database table. In search methods you can replace the search for `key` with the dbref to search for. This must be written as a string `#dbref`: the_answer = self.caller.search("#42") eightball = evennia.search_object("#8") @@ -211,16 +220,28 @@ Since `#dbref` is always unique, this search is always global. ```{warning} Relying on #dbrefs - You may be used to using #dbrefs a lot from other codebases. It is however considered - `bad practice` in Evennia to rely on hard-coded #dbrefs. It makes your code hard to maintain - and tied to the exact layout of the database. In 99% of cases you should pass the actual objects - around and search by key/tags/attribute instead. +In legacy code bases you may be used to relying a lot on #dbrefs to find and track things. Looking something up by #dbref can be practical - if used occationally. It is however considered **bad practice** to *rely* on hard-coded #dbrefs in Evennia. Especially to expect end users to know them. It makes your code fragile and hard to maintain, while tying your code to the exact layout of the database. In 99% of use cases you should organize your code such that you pass the actual objects around and search by key/tags/attribute instead. ``` + ## Finding objects relative each other -Let's consider a `chest` with a `coin` inside it. The chests stand in a room `dungeon`. In the dungeon is also -a `door`. This is an exit leading outside. +It's important to understand how objects relate to one another when searching. +Let's consider a `chest` with a `coin` inside it. The chests stand in a room `dungeon`. In the dungeon is also a `door`. This is an exit leading outside. + +``` +┌───────────────────────┐ +│dungeon │ +│ ┌─────────┐ │ +│ │chest │ ┌────┐ │ +│ │ ┌────┐ │ │door│ │ +│ │ │coin│ │ └────┘ │ +│ │ └────┘ │ │ +│ │ │ │ +│ └─────────┘ │ +│ │ +└───────────────────────┘ +``` - `coin.location` is `chest`. - `chest.location` is `dungeon`. @@ -249,11 +270,22 @@ There is a property `.destination` which is only used by exits: - `door.destination` is `outside` (or wherever the door leads) - `room.destination` is `None` (same for all the other non-exit objects) +You can also include this information in searches: + +```python +from evennia import search_object + +# we assume only one match of each +dungeons = search_object("dungeon", typeclass="typeclasses.rooms.Room") +chests = search_object("chest", location=dungeons[0]) +# find if there are any skulls in the chest +skulls = search_object("Skull", candidates=chests[0].contents) +``` + +More advanced, nested queries like this can however often be made more efficient by using the hints in the next lesson. + ## Summary -Knowing how to find things is important and the tools from this section will serve you well. For most of your needs -these tools will be all you need ... - -... but not always. In the next lesson we will dive further into more complex searching when we look at -Django queries and querysets in earnest. +Knowing how to find things is important and the tools from this section will serve you well. These tools will cover most of your needs ... +... but not always. In the next lesson we will dive further into more complex searching when we look at Django queries and querysets in earnest. \ No newline at end of file diff --git a/docs/source/_static/nature.css b/docs/source/_static/nature.css index 868232b76b..8d20cda367 100644 --- a/docs/source/_static/nature.css +++ b/docs/source/_static/nature.css @@ -228,8 +228,17 @@ p.admonition-title { div.highlight { background-color: #f1f1ef; + border-left: 4px; + border-color: #7ea6b3; + border-left-style: double; } +.highlight .hll { + background-color: #ffffcc54 !important; + border-radius: 5px; +} + + div.note { background-color: #eee; border: 1px solid #ccc; @@ -325,9 +334,10 @@ code { background-color: #ecf0f3; color: #6e0e0e; /* padding: 1px 2px; */ - font-size: 0.8em; + font-size: 0.9em; font-family: "Courier Prime", Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal, monospace; font-weight: bold; + background-color: #f7f7f7; } .viewcode-back { @@ -419,6 +429,13 @@ code.descname { padding: 5px; } +div.linenodiv>pre { + font-size: 0.9em; + line-height: 153%; + padding-right: 0; + padding-top: 8px; +} + /* The develop-branch warning header */ .develop {