diff --git a/docs/source/Howto/Starting/Part1/Adding-Commands.md b/docs/source/Howto/Starting/Part1/Adding-Commands.md index c63a0b76b0..ec14454636 100644 --- a/docs/source/Howto/Starting/Part1/Adding-Commands.md +++ b/docs/source/Howto/Starting/Part1/Adding-Commands.md @@ -9,8 +9,15 @@ A Command is something that handles the input from a user and causes a result to An example is `look`, which examines your current location and tells how it looks like and what is in it. +```sidebar:: Commands are not typeclassed + + If you just came from the previous lesson, you might want to know that Commands and + CommandSets are not `typeclassed`. That is, instances of them are not saved to the + database. They are "just" normal Python classes. +``` + In Evennia, a Command is a Python _class_. If you are unsure about what a class is, review the -previous lesson. A Command inherits from `evennia.Command` or from one of the alternative command- +previous lessons! A Command inherits from `evennia.Command` or from one of the alternative command- classes, such as `MuxCommand` which is what most default commands use. All Commands are in turn grouped in another class called a _Command Set_. Think of a Command Set @@ -18,8 +25,8 @@ as a bag holding many different commands. One CmdSet could for example hold all combat, another for building etc. By default, Evennia groups all character-commands into one big cmdset. -Command-Sets are then associated with objects. Doing so makes the commands in that cmdset available -to the object. So, to summarize: +Command-Sets are then associated with objects, for example with your Character. Doing so makes the +commands in that cmdset available to the object. So, to summarize: - Commands are classes - A group of Commands is stored in a CmdSet @@ -380,7 +387,7 @@ You won't see the second string. Only Smaug sees that (and is not amused). ## Summary -In this lesson we learned how to create our own Command, add it to a CmdSet and then ourselves. +In this lesson we learned how to create our own Command, add it to a CmdSet and then to ourselves. We also upset a dragon. In the next lesson we'll learn how to hit Smaug with different weapons. We'll also diff --git a/docs/source/Howto/Starting/Tutorial-Searching-For-Objects.md b/docs/source/Howto/Starting/Part1/Evennia-API-Overview.md similarity index 82% rename from docs/source/Howto/Starting/Tutorial-Searching-For-Objects.md rename to docs/source/Howto/Starting/Part1/Evennia-API-Overview.md index db33675f19..dbd5777955 100644 --- a/docs/source/Howto/Starting/Tutorial-Searching-For-Objects.md +++ b/docs/source/Howto/Starting/Part1/Evennia-API-Overview.md @@ -1,5 +1,69 @@ -# Tutorial Searching For Objects +# Overview of the Evennia API +In the last few lessons we have explored the gamedir, learned about typeclasses and commands. In the process +we have used several resources from the Evennia library. Some examples: + +- `evennia.DefaultObject`, `evennia.DefaultCharacter` and (inherited) methods on these classes like `.msg` + and `at_object_create` but also `.cmdset.add` for adding new cmdsets. +- `evennia.search_object` for finding lists of objects anywhere. +- `evennia.create_object` for creating objects in code instead of using the in-game `create` command. +- `evennia.Command` with methods like `func` and `parse` to implement new commands +- `evennia.CmdSet` for storing commands +- `evennia.default_cmds` holding references to all default Command classes like `look`, `dig` and so on. + +Evennia has a lot of resources to help you make your game. We have just given a selection of them for you to try +out so far (and we'll show off many more in the lessons to come). Now we'll teach you how find them +for yourself. + +## Exploring the API + +The Evennia _API_ +([Application Programming Interface](https://en.wikipedia.org/wiki/Application_programming_interface)) is what +you use to access things inside the `evennia` package. You can examine this in many ways: + +- The easiest is to browse the [API auto-docs](api:evennia) coming with this very documentation. This is built + automatically from the latest sources. The auto-docs give you each class, function and method along with the + docstring and everything you need to use that resource. If you want to go deeper you can also click the `[src]` + link next to e.g. a class to see its full python code. The documentation is also searchable. +- You can browse [the evennia repository on github](https://github.com/evennia/evennia). This is exactly + what you can download from us. The github repo is also searchable. +- You can also clone the evennia repo to your own computer and read the sources locally. This is necessary + if you want to help with Evennia's development itself. See the + [extended install instructions](../../../Setup/Extended-Installation) if you want to do this. The short of is to install `git` and run + + git clone https://github.com/evennia/evennia.git + + In the terminal/console you can search for anything using `git` (make sure you are inside the repo): + + git grep "class DefaultObject" + + will quickly tell you where the DefaultObject class is defined. + +### Side note for those reading the code directly (optional) + +If you read the code on `github` or cloned the repo yourself, you will find this being the outermost folder +structure: + + evennia/ + bin/ + CHANGELOG.md + ... + ... + docs/ + evennia/ + +That internal folder `evennia/evennia/` is the actual library, the thing covered by the API auto-docs and +what you get when you do `import evennia`. The outermost level is part of the Evennia package distribution and +installation. It's not something we'll bother with for this tutorial. + +> The `evennia/docs/` folder contains, well, this documentation. See [contributing to the docs](../../../Contributing-Docs) if you +want to learn more about how this works. + +## Overview of the library + + + +# Tutorial Searching For Objects You will often want to operate on a specific object in the database. For example when a player attacks a named target you'll need to find that target so it can be attacked. Or when a rain storm @@ -9,8 +73,8 @@ explains Evennia's tools for searching. ## Things to search for The first thing to consider is the base type of the thing you are searching for. Evennia organizes -its database into a few main tables: [Objects](../../Component/Objects), [Accounts](../../Component/Accounts), [Scripts](../../Component/Scripts), -[Channels](../../Component/Communications#channels), [Messages](Communication#Msg) and [Help Entries](../../Component/Help-System). +its database into a few main tables: [Objects](../../../Component/Objects), [Accounts](../../../Component/Accounts), [Scripts](../../../Component/Scripts), +[Channels](../../../Component/Communications#channels), [Messages](Communication#Msg) and [Help Entries](../../../Component/Help-System). Most of the time you'll likely spend your time searching for Objects and the occasional Accounts. So to find an entity, what can be searched for? @@ -22,20 +86,20 @@ the database field for `.key` is instead named `username` (this is a Django requ don't specify search-type, you'll usually search based on key. *Aliases* are extra names given to Objects using something like `@alias` or `obj.aliases.add('name')`. The main search functions (see below) will automatically search for aliases whenever you search by-key. - - [Tags](../../Component/Tags) are the main way to group and identify objects in Evennia. Tags can most often be + - [Tags](../../../Component/Tags) are the main way to group and identify objects in Evennia. Tags can most often be used (sometimes together with keys) to uniquely identify an object. For example, even though you have two locations with the same name, you can separate them by their tagging (this is how Evennia implements 'zones' seen in other systems). Tags can also have categories, to further organize your data for quick lookups. - - An object's [Attributes](../../Component/Attributes) can also used to find an object. This can be very useful but + - An object's [Attributes](../../../Component/Attributes) can also used to find an object. This can be very useful but since Attributes can store almost any data they are far less optimized to search for than Tags or keys. -- The object's [Typeclass](../../Component/Typeclasses) indicate the sub-type of entity. A Character, Flower or +- The object's [Typeclass](../../../Component/Typeclasses) indicate the sub-type of entity. A Character, Flower or Sword are all types of Objects. A Bot is a kind of Account. The database field is called `typeclass_path` and holds the full Python-path to the class. You can usually specify the `typeclass` as an argument to Evennia's search functions as well as use the class directly to limit queries. -- The `location` is only relevant for [Objects](../../Component/Objects) but is a very common way to weed down the +- The `location` is only relevant for [Objects](../../../Component/Objects) but is a very common way to weed down the number of candidates before starting to search. The reason is that most in-game commands tend to operate on things nearby (in the same room) so the choices can be limited from the start. - The database id or the '#dbref' is unique (and never re-used) within each database table. So while @@ -50,7 +114,7 @@ around and searching by Tags and/or keys will usually get you what you need. ## Getting objects inside another -All in-game [Objects](../../Component/Objects) have a `.contents` property that returns all objects 'inside' them +All in-game [Objects](../../../Component/Objects) have a `.contents` property that returns all objects 'inside' them (that is, all objects which has its `.location` property set to that object. This is a simple way to get everything in a room and is also faster since this lookup is cached and won't hit the database. @@ -64,7 +128,7 @@ location except `obj`. ## Searching using `Object.search` -Say you have a [command](../../Component/Commands), and you want it to do something to a target. You might be +Say you have a [command](../../../Component/Commands), and you want it to do something to a target. You might be wondering how you retrieve that target in code, and that's where Evennia's search utilities come in. In the most common case, you'll often use the `search` method of the `Object` or `Account` typeclasses. In a command, the `.caller` property will refer back to the object using the command @@ -133,7 +197,7 @@ class CmdListHangouts(default_cmds.MuxCommand): ", ".join(str(ob) for ob in hangouts))) ``` -This uses the `search_tag` function to find all objects previously tagged with [Tags](../../Component/Tags) +This uses the `search_tag` function to find all objects previously tagged with [Tags](../../../Component/Tags) "hangout" and with category "location tags". Other important search methods in `utils.search` are @@ -303,7 +367,7 @@ nice enough to alias the `db_key` field so you can normally just do `char.key` t name, the database field is actually called `db_key` and the real name must be used for the purpose of building a query. -> Don't confuse database fields with [Attributes](../../Component/Attributes) you set via `obj.db.attr = 'foo'` or +> Don't confuse database fields with [Attributes](../../../Component/Attributes) 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. You can get attached Attributes manually through the `db_attributes` many-to-many field in the same way as `db_tags` above. diff --git a/docs/source/Howto/Starting/Part1/Learning-Typeclasses.md b/docs/source/Howto/Starting/Part1/Learning-Typeclasses.md index 1c1443e461..a2629ce9fb 100644 --- a/docs/source/Howto/Starting/Part1/Learning-Typeclasses.md +++ b/docs/source/Howto/Starting/Part1/Learning-Typeclasses.md @@ -6,14 +6,14 @@ In the last lesson we created the dragons Fluffy, Cuddly and Smaug and made the learned a bit about _classes_ in the process. But 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/mobile.py` so far: +This is what you should have in `mygame/typeclasses/monsters.py` so far: ```python -class Mobile: +class Monster: """ - This is a base class for Mobiles. + This is a base class for Monsters. """ def __init__(self, key): @@ -23,9 +23,9 @@ class Mobile: print(f"{self.key} is moving!") -class Dragon(Mobile): +class Dragon(Monster): """ - This is a dragon-specific mobile. + This is a dragon-specific monster. """ def move_around(self): @@ -42,8 +42,8 @@ class Dragon(Mobile): ## Our first persistent object -Now we should know enough to understand what is happening in `mygame/typeclasses/objects.py`. -Open it again: +At this point we should know enough to understand what is happening in `mygame/typeclasses/objects.py`. Let's +open it: ```python """ @@ -68,26 +68,26 @@ change the way it works! > easiest is to peek at its [API documentation](api:evennia.objects.objects#DefaultObject). The docstring for > the `Object` class can also help. -One thing that Evennia offers and which you don't get with vanilla Python classes is _persistence_. As you've -found, Fluffy, Cuddly and Smaug are gone once we reload the server. Let's see if we can fix this. +One thing that Evennia classes offers and which you don't get with vanilla Python classes is _persistence_. As +you've found, Fluffy, Cuddly and Smaug are gone once we reload the server. Let's see if we can fix this. -Go back to `mygame/typeclasses/mobile.py`. Change it as follows: +Go back to `mygame/typeclasses/monsters.py`. Change it as follows: ```python from typeclasses.objects import Object -class Mobile(Object): +class Monster(Object): """ - This is a base class for Mobiles. + This is a base class for Monsters. """ def move_around(self): print(f"{self.key} is moving!") -class Dragon(Mobile): +class Dragon(Monster): """ - This is a dragon-specific mobile. + This is a dragon-specific Monster. """ def move_around(self): @@ -102,11 +102,11 @@ class Dragon(Mobile): ``` -Don't forget to save. We removed `Monster.__init__` and made `Mobile` inherit from Evennia's `Object` (which in turn +Don't forget to save. We removed `Monster.__init__` and made `Monster` inherit from Evennia's `Object` (which in turn inherits from Evennia's `DefaultObject`, as we saw). By extension, this means that `Dragon` also inherits from `DefaultObject`, just from further away! -### Creating by calling the class (less common way) +### Making a new object by calling the class First reload the server as usual. We will need to create the dragon a little differently this time: @@ -119,7 +119,7 @@ First reload the server as usual. We will need to create the dragon a little dif ``` > py - > from typeclasses.mymobile import Dragon + > from typeclasses.monsters import Dragon > smaug = Dragon(db_key="Smaug", db_location=here) > smaug.save() > smaug.move_around() @@ -138,7 +138,7 @@ You should now see that Smaug _is in the room with you_. Woah! > reload > look -_He's still there_... What we just did is to create a new entry in the database for Smaug. We gave the object +_He's still there_... What we just did was to create a new entry in the database for Smaug. We gave the object its name (key) and set its location to our current location (remember that `here` is just something available in the `py` command, you can't use it elsewhere). @@ -155,7 +155,7 @@ bound Python instances before. But you need to use `db_key` instead of `key` and remember to call `.save()` afterwards. Evennia has a helper function that is more common to use, called `create_object`: - > py fluffy = evennia.create_object('typeclases.mymobile.Mobile', key="Fluffy", location=here) + > py fluffy = evennia.create_object('typeclases.monster.Monster', key="Fluffy", location=here) > look Boom, Fluffy should now be in the room with you, a little less scary than Smaug. You specify the @@ -174,130 +174,450 @@ multiple Fluffies we could get the second one with `[1]`. Finally, you can also create a new Dragon using the familiar builder-commands we explored a few lessons ago: - > create/drop Cuddly:typeclasses.mymobile.Mobile + > create/drop Cuddly:typeclasses.monsters.Monster Cuddly is now in the room. After learning about how objects are created you'll realize that all this command really does is to parse your input, figure out that `/drop` means to "give the object the same location as the caller", and then do a call akin to - evennia.create_object("typeclasses.mymobile.Mobile", key="Cuddly", location=here) + evennia.create_object("typeclasses.monsters.Monster", key="Cuddly", location=here) -That's pretty much all there is to the mighty `create` command. +That's pretty much all there is to the mighty `create` command! The rest is just parsing for the command +to understand just what the user wants to create. -... And speaking of Commands, we should try to add one of our own next. +## Typeclasses +The `Object` (and `DefafultObject` class we inherited from above is what we refer to as a _Typeclass_. This +is an Evennia thing. The instance of a typeclass saves itself to the database when it is created, and after +that you can just search for it to get it back. We use the term _typeclass_ or _typeclassed_ to differentiate +these types of classes and objects from the normal Python classes, whose instances go away on a reload. +The number of typeclasses in Evennia are so few they can be learned by heart: - - -# Adding Object Typeclass Tutorial - -Evennia comes with a few very basic classes of in-game entities: - - DefaultObject - | - DefaultCharacter - DefaultRoom - DefaultExit - DefaultChannel - -When you create a new Evennia game (with for example `evennia --init mygame`) Evennia will -automatically create empty child classes `Object`, `Character`, `Room` and `Exit` respectively. They -are found `mygame/typeclasses/objects.py`, `mygame/typeclasses/rooms.py` etc. - -> Technically these are all [Typeclassed](../../../Component/Typeclasses), which can be ignored for now. In -> `mygame/typeclasses` are also base typeclasses for out-of-character things, notably -> [Channels](../../../Component/Communications), [Accounts](../../../Component/Accounts) and [Scripts](../../../Component/Scripts). We don't cover those in -> this tutorial. - -For your own game you will most likely want to expand on these very simple beginnings. It's normal -to want your Characters to have various attributes, for example. Maybe Rooms should hold extra -information or even *all* Objects in your game should have properties not included in basic Evennia. - -## Change Default Rooms, Exits, Character Typeclass - -This is the simplest case. - -The default build commands of a new Evennia game is set up to use the `Room`, `Exit` and `Character` -classes found in the same-named modules under `mygame/typeclasses/`. By default these are empty and -just implements the default parents from the Evennia library (`DefaultRoom`etc). Just add the -changes you want to these classes and run `@reload` to add your new functionality. - -## Create a new type of object - -Say you want to create a new "Heavy" object-type that characters should not have the ability to pick -up. - -1. Edit `mygame/typeclasses/objects.py` (you could also create a new module there, named something - like `heavy.py`, that's up to how you want to organize things). -1. Create a new class inheriting at any distance from `DefaultObject`. It could look something like - this: -```python - # end of file mygame/typeclasses/objects.py - from evennia import DefaultObject +- `evennia.DefaultObject`: This is the parent of all in-game entities - everything with a location. Evennia makes + a few very useful child classes of this class: + - `evennia.DefaultCharacter`: The default entity represening a player avatar in-game. + - `evennia.DefaultRoom`: A location in the game world. + - `evennia.DefaultExit`: A link between locations. +- `evennia.DefaultAccount`: The OOC representation of a player, holds password and account info. +- `evennia.DefaultChannel`: In-game channels. These could be used for all sorts of in-game communication. +- `evennia.DefaultScript`: Out-of-game objects, with no presence in the game world. Anything you want to create that + needs to be persistent can be stored with these entities, such as combat state, economic systems or what have you. - class Heavy(DefaultObject): - "Heavy object" - def at_object_creation(self): - "Called whenever a new object is created" - # lock the object down by default - self.locks.add("get:false()") - # the default "get" command looks for this Attribute in order - # to return a customized error message (we just happen to know - # this, you'd have to look at the code of the 'get' command to - # find out). - self.db.get_err_msg = "This is too heavy to pick up." +If you take a look in `mygame/typeclasses/` you'll find modules for each of these. Each contains an empty child +class ready that already inherits from the right parent, ready for you to modify or build from: + +- `mygame/typeclasses/objects.py` has `class Object(DefaultObject)`, a class directly inheriting the basic in-game entity, this + works as a base for any object. +- `mygame/typeclasses/characters.py` has `class Character(DefaultCharacter)` +- `mygame/typeclasses/rooms.py` has `class Room(DefaultRoom)` +- `mygame/typeclasses/exits.py` has `class Exit(DefaultExit)` +- `mygame/typeclasses/accounts.py` has `class Account(DefaultAccount)` +- `mygame/typeclasses/channels.py` has `class Channel(DefaultChannel)` +- `mygame/typeclasses/scripts.py` has `class Script(DefaultScript)` + +> Notice that the classes in `mygame/typeclasses/` are _not inheriting from each other_. For example, +> `Character` is inheriting from `evennia.DefaultCharacter` and not from `typeclasses.objects.Object`. +> So if you change `Object` you will not cause any change in the `Character` class. If you want that you +> can easily just change the child classes to inherit in that way instead; Evennia doesn't care. + +As seen with our `Dragon` example, you don't _have_ to modify these modules directly. You can just make your +own modules and import the base class. + +### Examining and defaults + +When you do + + > create/drop giantess:typeclasses.monsters.Monster + You create a new Monster: giantess. + +or + + > py evennia.create_object("typeclasses.monsters.Monster", key="Giantess", location=here) + +You are specifying exactly which typeclass you want to use to build the Giantess. Let's examine the result: + + > examine giantess + ------------------------------------------------------------------------------- + Name/key: Giantess (#14) + Typeclass: Monster (typeclasses.monsters.Monster) + Location: Limbo (#2) + Home: Limbo (#2) + Permissions: + Locks: call:true(); control:id(1) or perm(Admin); delete:id(1) or perm(Admin); + drop:holds(); edit:perm(Admin); examine:perm(Builder); get:all(); + puppet:pperm(Developer); tell:perm(Admin); view:all() + Persistent attributes: + desc = You see nothing special. + ------------------------------------------------------------------------------- + +We used the `examine` command briefly in the [lesson about building in-game](Building-Quickstart). Now these lines +may be more useful to us: +- **Name/key** - The name of this thing. The value `(#14)` is probably different for you. This is the + unique 'primary key' or _dbref_ for this entity in the database. +- **Typeclass**: This show the typeclass we specified, and the path to it. +- **Location**: We are in Limbo. If you moved elsewhere you'll see that instead. Also the `#dbref` is shown. +- **Permissions**: _Permissions_ are like the inverse to _Locks_ - they are like keys to unlock access to other things. + The giantess have no such keys (maybe fortunately). +- **Locks**: Locks are the inverse of _Permissions_ - specify what criterion _other_ objects must fulfill in order to + access the `giantess` object. This uses a very flexible mini-language. For examine, the line `examine:perm(Builders)` + is read as "Only those with permission _Builder_ or higher can _examine_ this object". Since we are the superuser + we pass (even bypass) such locks with ease. +- **Persistent attributes**: This allows for storing arbitrary, persistent data on the typeclassed entity. We'll get +to those in the next section. + +Note how the **Typeclass** line describes exactly where to find the code of this object? This is very useful for +understanding how any object in Evennia works. + +What happens if we _don't_ specify the typeclass though? + + > create/drop box + You create a new Object: box. + +or + + > py create.create_object(None, key="box", location=here) + +Now check it out: + + > examine box + +You will find that the **Typeclass** line now reads + + Typeclass: Object (typeclasses.objects.Object) + +So when you didn't specify a typeclass, Evennia used a default, more specifically the (so far) empty `Object` class in +`mygame/typeclasses/objects.py`. This is usually what you want, especially since you can tweak that class as much +as you like. + +But the reason Evennia knows to fall back to this class is not hard-coded - it's a setting. The default is +in [evennia/settings_default.py](https://github.com/evennia/evennia/blob/master/evennia/settings_default.py#L465), +with the name `BASE_OBJECT_TYPECLASS`, which is set to `typeclasses.objects.Object`. + +```sidebar:: Changing things + + While it's tempting to change folders around to your liking, this can + make it harder to follow tutorials and may confuse if + you are asking others for help. So don't overdo it unless you really + know what you are doing. ``` -1. Once you are done, log into the game with a build-capable account and do `@create/drop - rock:objects.Heavy` to drop a new heavy "rock" object in your location. Next try to pick it up -(`@quell` yourself first if you are a superuser). If you get errors, look at your log files where -you will find the traceback. The most common error is that you have some sort of syntax error in -your class. -Note that the [Locks](../../../Component/Locks) and [Attribute](../../../Component/Attributes) which are set in the typeclass could just -as well have been set using commands in-game, so this is a *very* simple example. +So if you wanted the creation commands and methods to default to some other class you could +add your own `BASE_OBJECT_TYPECLASS` line to `mygame/server/conf/settings.py`. The same is true for all the other +typeclasseses, like characters, rooms and accounts. This way you can change the +layout of your game dir considerably if you wanted. You just need to tell Evennia where everything is. + +## Modifying ourselves -## Storing data on initialization - -The `at_object_creation` is only called once, when the object is first created. This makes it ideal -for database-bound things like [Attributes](../../../Component/Attributes). But sometimes you want to create temporary -properties (things that are not to be stored in the database but still always exist every time the -object is created). Such properties can be initialized in the `at_init` method on the object. -`at_init` is called every time the object is loaded into memory. - -> Note: It's usually pointless and wasteful to assign database data in `at_init`, since this will -> hit the database with the same value over and over. Put those in `at_object_creation` instead. - -You are wise to use `ndb` (non-database Attributes) to store these non-persistent properties, since -ndb-properties are protected against being cached out in various ways and also allows you to list -them using various in-game tools: +Let's try to modify ourselves a little. Open up `mygame/typeclasses/characters.py`. ```python -def at_init(self): - self.ndb.counter = 0 - self.ndb.mylist = [] +""" +(module docstring) +""" +from evennia import DefaultCharacter + +class Character(DefaultCharacter): + """ + (class docstring) + """ + pass ``` -> Note: As mentioned in the [Typeclasses](../../../Component/Typeclasses) documentation, `at_init` replaces the use of -> the standard `__init__` method of typeclasses due to how the latter may be called in situations -> other than you'd expect. So use `at_init` where you would normally use `__init__`. +This looks quite familiar now - an empty class inheriting from the Evennia base typeclass. As you would expect, +this is also the default typeclass used for creating Characters if you don't specify it. You can verify it: + > examine me + ------------------------------------------------------------------------------ + Name/key: YourName (#1) + Session id(s): #1 + Account: YourName + Account Perms: (quelled) + Typeclass: Character (typeclasses.characters.Character) + Location: Limbo (#2) + Home: Limbo (#2) + Permissions: developer, player + Locks: boot:false(); call:false(); control:perm(Developer); delete:false(); + drop:holds(); edit:false(); examine:perm(Developer); get:false(); + msg:all(); puppet:false(); tell:perm(Admin); view:all() + Stored Cmdset(s): + commands.default_cmdsets.CharacterCmdSet [DefaultCharacter] (Union, prio 0) + Merged Cmdset(s): + ... + Commands available to YourName (result of Merged CmdSets): + ... + Persistent attributes: + desc = This is User #1. + prelogout_location = Limbo + Non-Persistent attributes: + last_cmd = None + ------------------------------------------------------------------------------ + +You got a lot longer output this time. You have a lot more going on than a simple Object. Here are some new fields of note: +- **Session id(s)**: This identifies the _Session_ (that is, the individual connection to a player's game client). +- **Account** shows, well the `Account` object associated with this Character and Session. +- **Stored/Merged Cmdsets** and **Commands available** is related to which _Commands_ are stored on you. We will + get to them in the [next lesson](Adding-Commands). For now it's enough to know these consitute all the + commands available to you at a given moment. +- **Non-Persistent attributes** are Attributes that are only stored temporarily and will go away on next reload. -## Updating existing objects +Look at the **Typeclass** field and you'll find that it points to `typeclasses.character.Character` as expected. +So if we modify this class we'll also modify ourselves. -If you already have some `Heavy` objects created and you add a new `Attribute` in -`at_object_creation`, you will find that those existing objects will not have this Attribute. This -is not so strange, since `at_object_creation` is only called once, it will not be called again just -because you update it. You need to update existing objects manually. +### A method on ourselves -If the number of objects is limited, you can use `@typeclass/force/reload objectname` to force a -re-load of the `at_object_creation` method (only) on the object. This case is common enough that -there is an alias `@update objectname` you can use to get the same effect. If there are multiple -objects you can use `@py` to loop over the objects you need: +Let's try something simple first. Back in `mygame/typeclasses/characters.py`: + +```python + +class Character(DefaultCharacter): + """ + (class docstring) + """ + + str = 10 + dex = 12 + int = 15 + + def get_stats(self): + """ + Get the main stats of this character + """ + return self.str, self.dex, self.int ``` -@py from typeclasses.objects import Heavy; [obj.at_object_creation() for obj in Heavy.objects.all()] + + > reload + > py self.get_stats() + (10, 12, 15) + +```sidebar:: Tuples and lists + + - A `list` is written `[a, b, c, d, ...]`. It can be modified after creation. + - A `tuple` is written `(a, b, c, ...)`. It cannot be modified once created. +``` +We made a new method, gave it a docstring and had it `return` the RP-esque values we set. It comes back as a +_tuple_ `(10, 12, 15)`. To get a specific value you could specify the _index_ of the value you want, +starting from zero: + + > py stats = self.get_stats() ; print(f"Strength is {stats[0]}.") + Strength is 10. + +### Attributes + +So what happens when we increase our strength? This would be one way: + + > py self.str = self.str + 1 + > py self.str + 11 + +Here we set the strength equal to its previous value + 1. A shorter way to write this is to use Python's `+=` +operator: + + > py self.str += 1 + > py self.str + 12 + > py self.get_stats() + (12, 12, 15) + +This looks correct! Try to change the values for dex and int too; it works fine. However: + + > reload + > py self.get_stats() + (10, 12, 15) + +After a reload all our changes were forgotten. When we change properties like this, it only changes in memory, +not in the database (nor do we modify the python module's code). So when we reloaded, the 'fresh' `Character` +class was loaded, and it still has the original stats we wrote to it. + +In principle we could change the python code. But we don't want to do that manually every time. And more importantly +since we have the stats hardcoded in the class, _every_ character instance in the game will have exactly the +same `str`, `dex` and `int` now! This is clearly not what we want. + +Evennia offers a special, persistent type of property for this, called an `Attribute`. Rework your +`mygame/typeclasses/characters.py` like this: + +```python + +class Character(DefaultCharacter): + """ + (class docstring) + """ + + def get_stats(self): + """ + Get the main stats of this character + """ + return self.db.str, self.db.dex, self.db.int +``` + +```sidebar:: Spaces in Attribute name? + + What if you want spaces in your Attribute name? Or you want to assign the + name of the Attribute on-the fly? Then you can use `.attributes.add(name, value)` instead, + for example `self.attributes.add("str", 10)`. + +``` + +We removed the hard-coded stats and added added `.db` for every stat. The `.db` handler makes the stat +into an an Evennia `Attribute`. + + > reload + > py self.get_stats() + (None, None, None) + +Since we removed the hard-coded values, Evennia don't know what they should be (yet). So all we get back +is `None`, which is a Python reserved word to represent nothing, a no-value. This is different from a normal python +property: + + > py self.str + AttributeError: 'Character' object has no attribute 'str' + > py self.db.str + (nothing will be displayed, because it's None) + +Trying to get an unknown normal Python property will give an error. Getting an unknown Evennia `Attribute` will +never give an error, but only result in `None` being returned. This is often very practical. + + > py self.db.str, self.db.dex, self.db.int = 10, 12, 15 + > py self.get_stats() + (10, 12, 15) + > reload + > py self.get_stats() + (10, 12, 15) + +Now we set the Attributes to the right values. We can see that things work the same as before, also after a +server reload. Let's modify the strength: + + > py self.db.str += 2 + > py self.get_stats() + (12, 12, 15) + > reload + > py self.get_stats() + (12, 12, 15) + + Our change now survives a reload since Evennia automatically saves the Attribute to the database for us. + +### Setting things on new Characters + +Things a looking better, but one thing remains strange - the stats start out with a value `None` and we +have to manually set them to something reasonable. In a later lesson we will investigate character-creation +in more detail. For now, let's give every new character some random stats to start with. + +We want those stats to be set only once, when the object is first created. For the Character, this method +is called `at_object_creation`. + +```sidebar:: __init__ vs at_object_creation + + For the `Monster` class we used `__init__` to set up the class. We can't use this + for a typeclass because it will be called more than once, at the very least after + every reload and maybe more depending on caching. Even if you are familiar with Python, + avoid touching `__init__` for typeclasses, the results will not be what you expect. + +``` + +```python +# up by the other imports +import random + +class Character(DefaultCharacter): + """ + (class docstring) + """ + + def at_object_creation(self): + self.db.str = random.randint(3, 18) + self.db.dex = random.randint(3, 18) + self.db.int = random.randint(3, 18) + + def get_stats(self): + """ + Get the main stats of this character + """ + return self.db.str, self.db.dex, self.db.int +``` + +We imported a new module, `random`. This is part of Python's standard library. We used `random.randint` to +set a random value from 3 to 18 to each stat. Simple, but for some classical RPGs this is all you need! + + > reload + > py self.get_stats() + (12, 12, 15) + +Hm, this is the same values we set before. They are not random. The reason for this is of course that, as said, +`at_object_creation` only runs _once_, the very first time a character is created. Our character object was already +created long before, so it will not be called again. + +It's simple enough to run it manually though: + + > self.at_object_creation() + > py self.get_stats() + (5, 4, 8) + +Lady luck didn't smile on us for this example; maybe you'll fare better. Evennia has a helper command +`update` that re-runs the creation hook and also cleans up any other Attributes not re-created by `at_object_creation`: + + > update self + > py self.get_stats() + (8, 16, 14) + +### Updating all Characters in a loop + +Needless to say, for your game you are wise to have a feel for what you want to go into the `at_object_creation` hook +before you create a lot of objects (characters in this case). But should it come to that you don't want to have to +go around and re-run the method on everyone manually. For the Python beginner, doing this will also give a chance to +try out Python _loops_. We try them out in multi-line Python mode: + + > py + > for a in [1, 2, "foo"]: + > print(a) + 1 + 2 + foo + +A python _for-loop_ allows us to loop over something. Above, we made a _list_ of two numbers and a string. In +every iteration of the loop, the variable `a` becomes one element in turn, and we print that. + +For our list, we want to loop over all Characters, and want to call `.at_object_creation` on each. This is how +this is done (still in python multi-line mode): + + > from typeclasses.characters import Character + > for char in Character.objects.all() + > char.at_object_creation() + +```sidebar:: Database queries + + `Character.objects.all()` is an example of a database query expressed in Python. This will be converted + into a database query under the hood. This syntax is part of + `Django's query language `_. You don't need to + know Django to use Evennia, but if you ever need more specific database queries, this is always available + when you need it. ``` +We import the `Character` class and then we use `.objects.all()` to get all `Character` instances. Simplified, +`.objects` is a resource from which one can _query_ for all `Characters`. Using `.all()` gets us a listing +of all of them that we then immediately loop over. Boom, we just updated all Characters, including ourselves: + + > quit() + Closing the Python console. + > self.get_stats() + (3, 18, 10) + +## Extra Credits + +This principle is the same for other typeclasses. So using the tools explored in this lesson, try to expand +the default room with an `is_dark` flag. It can be either `True` or `False`. +Have all new rooms start with `is_dark = False` and make it so that once you change it, it survives a reload. +Oh, and if you created any other rooms before, make sure they get the new flag too! + +## Conclusions + +In this lesson we created database-persistent dragons by having their classes inherit from one `Object`, one +of Evennia's _typeclasses_. We explored where Evennia looks for typeclasses if we don't specify the path +explicitly. We then modified ourselves - via the `Character` class - to give us some simple RPG stats. This +led to the need to use Evennia's _Attributes_, settable via `.db` and to use a for-loop to update ourselves. + +Typeclasses are a fundamental part of Evennia and we will see a lot of more uses of them in the course of +this tutorial. But that's enough of them for now. It's time to take some action. Let's learn about _Commands_. + [prev lesson](Python-classes-and-objects) | [next lesson](Adding-Commands) diff --git a/docs/source/Howto/Starting/Part1/More-on-Commands.md b/docs/source/Howto/Starting/Part1/More-on-Commands.md index d372441c82..f6d9a90d5b 100644 --- a/docs/source/Howto/Starting/Part1/More-on-Commands.md +++ b/docs/source/Howto/Starting/Part1/More-on-Commands.md @@ -1,6 +1,6 @@ # More about Commands -[prev lesson](Adding-Commands) | [next lesson](Learning-Typeclasses) +[prev lesson](Adding-Commands) | [next lesson](Evennia-API-Overview) In this lesson we learn some basics about parsing the input of Commands. We will also learn how to add, modify and extend Evennia's default commands. @@ -497,6 +497,8 @@ In this lesson we got into some more advanced string formatting - many of those the future! We also made a functional sword. Finally we got into how to add to, extend and replace a default command on ourselves. -Let's explore 'ourselves' and other 'things' in the game next. +In the last few lessons we have made use of resources from Evennia. Now that we have had some experience of how +classes and inheritance work, we can start exploring this in earnest. -[prev lesson](Adding-Commands) | [next lesson](Learning-Typeclasses) + +[prev lesson](Adding-Commands) | [next lesson](Evennia-API-Overview) diff --git a/docs/source/Howto/Starting/Part1/Python-classes-and-objects.md b/docs/source/Howto/Starting/Part1/Python-classes-and-objects.md index 1414c037fb..a8806eae2a 100644 --- a/docs/source/Howto/Starting/Part1/Python-classes-and-objects.md +++ b/docs/source/Howto/Starting/Part1/Python-classes-and-objects.md @@ -165,14 +165,14 @@ things to understand before you can use Evennia efficiently. ### Classes and instances A 'class' can be seen as a 'template' for a 'type' of object. The class describes the basic functionality -of everyone of that class. For example, we could have a class `Mobile` which has resources for moving itself +of everyone of that class. For example, we could have a class `Monster` which has resources for moving itself from room to room. -Open a new file `mygame/typeclasses/mymobile.py`. Add the following simple class: +Open a new file `mygame/typeclasses/monsters.py`. Add the following simple class: ```python -class Mobile: +class Monster: key = "Monster" @@ -181,7 +181,7 @@ class Mobile: ``` -Above we have defined a `Mobile` class with one variable `key` (that is, the name) and one +Above we have defined a `Monster` class with one variable `key` (that is, the name) and one _method_ on it. A method is like a function except it sits "on" the class. It also always has at least one argument (almost always written as `self` although you could in principle use another name), which is a reference back to itself. So when we print `self.key` we are referring @@ -194,20 +194,20 @@ back to the `key` on the class. ``` A class is just a template. Before it can be used, we must create an _instance_ of the class. If -`Mobile` is a class, then an instance is Fluffy, the individual red dragon. You instantiate +`Monster` is a class, then an instance is Fluffy, the individual red dragon. You instantiate by _calling_ the class, much like you would a function: - fluffy = Mobile() + fluffy = Monster() Let's try it in-game (we use multi-line mode, it's easier) > py - > from typeclasses.mymobile import Mobile - > fluffy = Mobile() + > from typeclasses.monsters import Monster + > fluffy = Monster() > fluffy.move_around() Monster is moving! -We created an _instance_ of `Mobile`, which we stored in the variable `fluffy`. We then +We created an _instance_ of `Monster`, which we stored in the variable `fluffy`. We then called the `move_around` method on fluffy to get the printout. > Note how we _didn't_ call the method as `fluffy.move_around(self)`. While the `self` has to be @@ -216,7 +216,7 @@ called the `move_around` method on fluffy to get the printout. Let's create the sibling of Fluffy, Cuddly: - > cuddly = Mobile() + > cuddly = Monster() > cuddly.move_around() Monster is moving! @@ -228,7 +228,7 @@ Let's make the class a little more flexible: ```python -class Mobile: +class Monster: def __init__(self, key): self.key = key @@ -239,7 +239,7 @@ class Mobile: ``` The `__init__` is a special method that Python recognizes. If given, this handles extra arguments -when you instantiate a new Mobile. We have it add an argument `key` that we store on `self`. +when you instantiate a new Monster. We have it add an argument `key` that we store on `self`. Now, for Evennia to see this code change, we need to reload the server. You can either do it this way: @@ -262,8 +262,8 @@ Or you can use a separate terminal and restart from outside the game: Either way you'll need to go into `py` again: > py - > from typeclasses.mymobile import Mobile - fluffy = Mobile("Fluffy") + > from typeclasses.monsters import Monster + fluffy = Monster("Fluffy") fluffy.move_around() Fluffy is moving! @@ -276,7 +276,7 @@ So far all we've seen a class do is to behave our first `hello_world` function b could just have made a function: ```python - def mobile_move_around(key): + def monster_move_around(key): print(f"{key} is moving!") ``` @@ -306,13 +306,13 @@ objects in turn: Classes can _inherit_ from each other. A "child" class will inherit everything from its "parent" class. But if the child adds something with the same name as its parent, it will _override_ whatever it got from its parent. -Let's expand `mygame/typeclasses/mymobile.py` with another class: +Let's expand `mygame/typeclasses/monsters.py` with another class: ```python -class Mobile: +class Monster: """ - This is a base class for Mobiles. + This is a base class for Monster. """ def __init__(self, key): @@ -322,9 +322,9 @@ class Mobile: print(f"{self.key} is moving!") -class Dragon(Mobile): +class Dragon(Monster): """ - This is a dragon-specific mobile. + This is a dragon-specific monster. """ def move_around(self): @@ -341,7 +341,7 @@ class Dragon(Mobile): We added some docstrings for clarity. It's always a good idea to add doc strings; you can do so also for methods, as exemplified for the new `firebreath` method. -We created the new class `Dragon` but we also specified that `Mobile` is the _parent_ of `Dragon` but adding +We created the new class `Dragon` but we also specified that `Monster` is the _parent_ of `Dragon` but adding the parent in parenthesis. `class Classname(Parent)` is the way to do this. ```sidebar:: Multi-inheritance @@ -355,7 +355,7 @@ the parent in parenthesis. `class Classname(Parent)` is the way to do this. Let's try out our new class. First `reload` the server and the do > py - > from typeclasses.mobile import Dragon + > from typeclasses.monsters import Dragon > smaug = Dragon("Smaug") > smaug.move_around() Smaug flies through the air high above! @@ -391,7 +391,7 @@ case, we will call `Monster.move_around` first, before doing our own thing. Now `reload` the server and then: > py - > from typeclasses.mobile import Dragon + > from typeclasses.monsters import Dragon > smaug = Dragon("Smaug") > smaug.move_around() Smaug is moving! diff --git a/docs/source/toc.md b/docs/source/toc.md index af442cf68e..7daeeab52b 100644 --- a/docs/source/toc.md +++ b/docs/source/toc.md @@ -101,6 +101,7 @@ - [Howto/Starting/Parsing command arguments, theory and best practices](Howto/Starting/Parsing-command-arguments,-theory-and-best-practices) - [Howto/Starting/Part1/Adding Commands](Howto/Starting/Part1/Adding-Commands) - [Howto/Starting/Part1/Building Quickstart](Howto/Starting/Part1/Building-Quickstart) +- [Howto/Starting/Part1/Evennia API Overview](Howto/Starting/Part1/Evennia-API-Overview) - [Howto/Starting/Part1/Gamedir Overview](Howto/Starting/Part1/Gamedir-Overview) - [Howto/Starting/Part1/Learning Typeclasses](Howto/Starting/Part1/Learning-Typeclasses) - [Howto/Starting/Part1/More on Commands](Howto/Starting/Part1/More-on-Commands) @@ -113,7 +114,6 @@ - [Howto/Starting/Starting Part4](Howto/Starting/Starting-Part4) - [Howto/Starting/Starting Part5](Howto/Starting/Starting-Part5) - [Howto/Starting/Turn based Combat System](Howto/Starting/Turn-based-Combat-System) -- [Howto/Starting/Tutorial Searching For Objects](Howto/Starting/Tutorial-Searching-For-Objects) - [Howto/Starting/Tutorial for basic MUSH like game](Howto/Starting/Tutorial-for-basic-MUSH-like-game) - [Howto/Starting/Web Tutorial](Howto/Starting/Web-Tutorial) - [Howto/Tutorial Aggressive NPCs](Howto/Tutorial-Aggressive-NPCs)