diff --git a/docs/source/Howto/Starting/Coding-Introduction.md b/docs/source/Coding/Coding-Introduction.md similarity index 95% rename from docs/source/Howto/Starting/Coding-Introduction.md rename to docs/source/Coding/Coding-Introduction.md index 7ac39a06df..f2b49cea8b 100644 --- a/docs/source/Howto/Starting/Coding-Introduction.md +++ b/docs/source/Coding/Coding-Introduction.md @@ -10,7 +10,7 @@ Here are some pointers to get you going. Evennia is developed using Python. Even if you are more of a designer than a coder, it is wise to learn how to read and understand basic Python code. If you are new to Python, or need a refresher, -take a look at our two-part [Python introduction](Part1/Python-basic-introduction). +take a look at our two-part [Python introduction](../Howto/Starting/Part1/Python-basic-introduction). ### Explore Evennia interactively @@ -31,7 +31,7 @@ This will open an Evennia-aware python shell (using ipython). From within this s evennia. That is, enter `evennia.` and press the `` key. This will show you all the resources made -available at the top level of Evennia's "flat API". See the [flat API](../../Evennia-API) page for more +available at the top level of Evennia's "flat API". See the [flat API](../Evennia-API) page for more info on how to explore it efficiently. You can complement your exploration by peeking at the sections of the much more detailed [Developer @@ -52,7 +52,7 @@ using such a checker can be a good start to weed out the simple problems. ### Plan before you code -Before you start coding away at your dream game, take a look at our [Game Planning](Game-Planning) +Before you start coding away at your dream game, take a look at our [Game Planning](../Howto/Starting/Game-Planning) page. It might hopefully help you avoid some common pitfalls and time sinks. ### Code in your game folder, not in the evennia/ repository @@ -64,7 +64,7 @@ it out into your game folder and edit it there. If you find that Evennia doesn't support some functionality you need, make a [Feature Request](feature-request) about it. Same goes for [bugs][bug]. If you add features or fix bugs -yourself, please consider [Contributing](../../Contributing) your changes upstream! +yourself, please consider [Contributing](../Contributing) your changes upstream! ### Learn to read tracebacks diff --git a/docs/source/Howto/Starting/Adding-Object-Typeclass-Tutorial.md b/docs/source/Howto/Starting/Adding-Object-Typeclass-Tutorial.md deleted file mode 100644 index cb33c2d12e..0000000000 --- a/docs/source/Howto/Starting/Adding-Object-Typeclass-Tutorial.md +++ /dev/null @@ -1,109 +0,0 @@ -# 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 - - 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." -``` -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. - -## 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: - -```python -def at_init(self): - self.ndb.counter = 0 - self.ndb.mylist = [] -``` - -> 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__`. - - -## Updating existing objects - -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. - -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: - -``` -@py from typeclasses.objects import Heavy; [obj.at_object_creation() for obj in Heavy.objects.all()] - -``` \ No newline at end of file diff --git a/docs/source/Howto/Starting/First-Steps-Coding.md b/docs/source/Howto/Starting/First-Steps-Coding.md index d915d782cb..c40f88cd92 100644 --- a/docs/source/Howto/Starting/First-Steps-Coding.md +++ b/docs/source/Howto/Starting/First-Steps-Coding.md @@ -10,7 +10,7 @@ Started](Getting-Started) instructions. You should have initialized a new game f `evennia --init foldername` command. We will in the following assume this folder is called "mygame". -It might be a good idea to eye through the brief [Coding Introduction](Coding-Introduction) too +It might be a good idea to eye through the brief [Coding Introduction](../../Coding/Coding-Introduction) too (especially the recommendations in the section about the evennia "flat" API and about using `evennia shell` will help you here and in the future). diff --git a/docs/source/Howto/Starting/Part1/Adding-Commands.md b/docs/source/Howto/Starting/Part1/Adding-Commands.md index 5919093c12..c63a0b76b0 100644 --- a/docs/source/Howto/Starting/Part1/Adding-Commands.md +++ b/docs/source/Howto/Starting/Part1/Adding-Commands.md @@ -1,6 +1,9 @@ -# Adding Command Tutorial +# Our own commands -[prev lesson](Python-classes-and-objects) | [next lesson]() +[prev lesson](Python-classes-and-objects) | [next lesson](More-on-Commands) + +In this lesson we'll learn how to create our own Evennia _Commands_. If you are new to Python you'll +also learn some more basics about how to manipulate strings and get information out of Evennia. A Command is something that handles the input from a user and causes a result to happen. An example is `look`, which examines your current location and tells how it looks like and @@ -10,16 +13,17 @@ In Evennia, a Command is a Python _class_. If you are unsure about what a class previous lesson. 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 +All Commands are in turn grouped in another class called a _Command Set_. Think of a Command Set as a bag holding many different commands. One CmdSet could for example hold all commands for -combat, another for building etc. +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: - Commands are classes - A group of Commands is stored in a CmdSet -- Putting a CmdSet on an object makes all commands in it available to the object +- CmdSets are stored on objects - this defines which commands are available to that object. ## Creating a custom command @@ -373,271 +377,14 @@ you could try to hit it (if you dare): You won't see the second string. Only Smaug sees that (and is not amused). -## More advanced parsing -Let's expand our simple `hit` command to accept a little more complex input: +## Summary - hit [[with] ] - -That is, we want to support all of these forms +In this lesson we learned how to create our own Command, add it to a CmdSet and then ourselves. +We also upset a dragon. - hit target - hit target weapon - 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`: +In the next lesson we'll learn how to hit Smaug with different weapons. We'll also +get into how we replace and extend Evennia's default Commands. -```python -#... - -class CmdHit(Command): - """ - Hit a target. - - Usage: - hit - - """ - key = "hit" - - def parse(self): - self.args = self.args.strip() - target, *weapon = self.args.split(" with ", 1) - if not weapon: - target, *weapon = target.split(" ", 1) - self.target = target.strip() - if weapon: - self.weapon = weapon.strip() - else: - self.weapon = "" - - def func(self): - if not self.args: - self.caller.msg("Who do you want to hit?") - return - # get the target for the hit - target = self.caller.search(self.target) - if not target: - return - # get and handle the weapon - weapon = None - if self.weapon: - weapon = self.caller.search(self.weapon) - if weapon: - weaponstr = f"{weapon.key}" - else: - weaponstr = "bare fists" - - 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. - -```sidebar:: Tuples and Lists - - - A `list` is written as `[a, b, c, d, ...]`. You can add and grow/shrink a list after it was first created. - - A `tuple` is written as `(a, b, c, d, ...)`. A tuple cannot be modified once it is created. - -``` -- **Line 14** - We do the stripping of `self.args` once and for all here. We also store the stripped version back - into `self.args`, overwriting it. So there is no way to get back the non-stripped version from here on, which is fine - for this command. -- **Line 15** - This makes use of the `.split` method of strings. `.split` will, well, split the string by some criterion. - `.split(" with ", 1)` means "split the string once, around the substring `" with "` if it exists". The result - of this split is a _list_. Just how that list looks depends on the string we are trying to split: - 1. If we entered just `hit smaug`, we'd be splitting just `"smaug"` which would give the result `["smaug"]`. - 2. `hit smaug sword` gives `["smaug sword"]` - 3. `hit smaug with sword` gives `["smaug", "sword"]` - - So we get a list of 1 or 2 elements. We assign it to two variables like this, `target, *weapon = `. That - asterisk in `*weapon` is a nifty trick - it will automatically become a list of _0 or more_ values. It sorts of - "soaks" up everything left over. - 1. `target` becomes `"smaug"` and `weapon` becomes `[]` - 2. `target` becomes `"smaug sword"` and `weapon` becomes `[]` - 3. `target` becomes `"smaug"` and `weapon` becomes `sword` -- **Lines 16-17** - In this `if` condition we check if `weapon` is falsy (that is, the empty list). This can happen - under two conditions (from the example above): - 1. `target` is simply `smaug` - 2. `target` is `smaug sword` - - To separate these cases we split `target` once again, this time by empty space `" "`. Again we store the - result back with `target, *weapon =`. The result will be one of the following: - 1. `target` remains `smaug` and `weapon` remains `[]` - 2. `target` becomes `smaug` and `weapon` becomes `sword` -- **Lines 18-22** - We now store `target` and `weapon` into `self.target` and `self.weapon`. We must do this in order - for these local variables to made available in `func` later. Note how we need to check so `weapon` is not falsy - 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. -- **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. - -Let's try it out! - - > reload - > hit smaug with sword - 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. - - > create sword - -Since we didn't specify `/drop`, the sword will end up in our inventory and can seen with the `i` or -`inventory` command. The `.search` helper will still find it there. There is no need to reload to see this -change (no code changed, only stuff in the database). - - > hit smaug with sword - You hit smaug with sword! - - -## Adding the Command to a default Cmdset - - -For now, let's drop MyCmdSet: - - > py self.cmdset.remove("commands.mycommands.MyCmdSet") - - - - - -The command is not available to use until it is part of a [Command Set](../../../Component/Command-Sets). In this -example we will go the easiest route and add it to the default Character commandset that already -exists. - -1. Edit `mygame/commands/default_cmdsets.py` -1. Import your new command with `from commands.command import CmdEcho`. -1. Add a line `self.add(CmdEcho())` to `CharacterCmdSet`, in the `at_cmdset_creation` method (the - template tells you where). - -This is approximately how it should look at this point: - -```python - # file mygame/commands/default_cmdsets.py - #[...] - from commands.command import CmdEcho - #[...] - class CharacterCmdSet(default_cmds.CharacterCmdSet): - - key = "DefaultCharacter" - - def at_cmdset_creation(self): - - # this first adds all default commands - super(DefaultSet, self).at_cmdset_creation() - - # all commands added after this point will extend or - # overwrite the default commands. - self.add(CmdEcho()) -``` - -Next, run the `@reload` command. You should now be able to use your new `echo` command from inside -the game. Use `help echo` to see the documentation for the command. - -If you have trouble, make sure to check the log for error messages (probably due to syntax errors in -your command definition). - -> Note: Typing `echotest` will also work. It will be handled as the command `echo` directly followed -by -its argument `test` (which will end up in `self.args). To change this behavior, you can add the -`arg_regex` property alongside `key`, `help_category` etc. [See the arg_regex -documentation](Commands#on-arg_regex) for more info. - -If you want to overload existing default commands (such as `look` or `get`), just add your new -command with the same key as the old one - it will then replace it. Just remember that you must use -`@reload` to see any changes. - -See [Commands](../../../Component/Commands) for many more details and possibilities when defining Commands and using -Cmdsets in various ways. - - -## Adding the command to specific object types - -Adding your Command to the `CharacterCmdSet` is just one easy exapmple. The cmdset system is very -generic. You can create your own cmdsets (let's say in a module `mycmdsets.py`) and add them to -objects as you please (how to control their merging is described in detail in the [Command Set -documentation](Command-Sets)). - -```python - # file mygame/commands/mycmdsets.py - #[...] - from commands.command import CmdEcho - from evennia import CmdSet - #[...] - class MyCmdSet(CmdSet): - - key = "MyCmdSet" - - def at_cmdset_creation(self): - self.add(CmdEcho()) -``` -Now you just need to add this to an object. To test things (as superuser) you can do - - @py self.cmdset.add("mycmdsets.MyCmdSet") - -This will add this cmdset (along with its echo command) to yourself so you can test it. Note that -you cannot add a single Command to an object on its own, it must be part of a CommandSet in order to -do so. - -The Command you added is not there permanently at this point. If you do a `@reload` the merger will -be gone. You *could* add the `permanent=True` keyword to the `cmdset.add` call. This will however -only make the new merged cmdset permanent on that *single* object. Often you want *all* objects of -this particular class to have this cmdset. - -To make sure all new created objects get your new merged set, put the `cmdset.add` call in your -custom [Typeclasses](../../../Component/Typeclasses)' `at_object_creation` method: - -```python - # e.g. in mygame/typeclasses/objects.py - - from evennia import DefaultObject - class MyObject(DefaultObject): - - def at_object_creation(self): - "called when the object is first created" - self.cmdset.add("mycmdset.MyCmdSet", permanent=True) -``` - -All new objects of this typeclass will now start with this cmdset and it will survive a `@reload`. - -*Note:* An important caveat with this is that `at_object_creation` is only called *once*, when the -object is first created. This means that if you already have existing objects in your databases -using that typeclass, they will not have been initiated the same way. There are many ways to update -them; since it's a one-time update you can usually just simply loop through them. As superuser, try -the following: - - @py from typeclasses.objects import MyObject; [o.cmdset.add("mycmdset.MyCmdSet") for o in -MyObject.objects.all()] - -This goes through all objects in your database having the right typeclass, adding the new cmdset to -each. The good news is that you only have to do this if you want to post-add *cmdsets*. If you just -want to add a new *command*, you can simply add that command to the cmdset's `at_cmdset_creation` -and `@reload` to make the Command immediately available. - -## Change where Evennia looks for command sets - -Evennia uses settings variables to know where to look for its default command sets. These are -normally not changed unless you want to re-organize your game folder in some way. For example, the -default character cmdset defaults to being defined as - - CMDSET_CHARACTER="commands.default_cmdset.CharacterCmdSet" - - -[prev lesson](Python-classes-and-objects) | [next lesson]() +[prev lesson](Python-classes-and-objects) | [next lesson](More-on-Commands) diff --git a/docs/source/Howto/Starting/Part1/Learning-Typeclasses.md b/docs/source/Howto/Starting/Part1/Learning-Typeclasses.md new file mode 100644 index 0000000000..1c1443e461 --- /dev/null +++ b/docs/source/Howto/Starting/Part1/Learning-Typeclasses.md @@ -0,0 +1,303 @@ +# Persistent objects and typeclasses + +[prev lesson](Python-classes-and-objects) | [next lesson](Adding-Commands) + +In the last lesson we created the dragons Fluffy, Cuddly and Smaug and made the fly and breathe fire. We +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: + + +```python + +class Mobile: + """ + This is a base class for Mobiles. + """ + + def __init__(self, key): + self.key = key + + def move_around(self): + print(f"{self.key} is moving!") + + +class Dragon(Mobile): + """ + This is a dragon-specific mobile. + """ + + def move_around(self): + super().move_around() + print("The world trembles.") + + def firebreath(self): + """ + Let our dragon breathe fire. + """ + print(f"{self.key} breathes fire!") + +``` + +## Our first persistent object + +Now we should know enough to understand what is happening in `mygame/typeclasses/objects.py`. +Open it again: + +```python +""" +module docstring +""" +from evennia import DefaultObject + +class Object(DefaultObject): + """ + class docstring + """ + pass +``` + +So we have a class `Object` that _inherits_ from `DefaultObject`, which we have imported from Evennia. +The class itself doesn't do anything (it just `pass`es) but that doesn't mean it's useless. As we've seen, +it inherits all the functionality of its parent. It's in fact an _exact replica_ of `DefaultObject` right now. +If we knew what kind of methods and resources were available on `DefaultObject` we could add our own and +change the way it works! + +> Hint: We will get back to this, but to learn what resources an Evennia parent like `DefaultObject` offers, +> 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. + +Go back to `mygame/typeclasses/mobile.py`. Change it as follows: + +```python + +from typeclasses.objects import Object + +class Mobile(Object): + """ + This is a base class for Mobiles. + """ + def move_around(self): + print(f"{self.key} is moving!") + + +class Dragon(Mobile): + """ + This is a dragon-specific mobile. + """ + + def move_around(self): + super().move_around() + print("The world trembles.") + + def firebreath(self): + """ + Let our dragon breathe fire. + """ + print(f"{self.key} breathes fire!") + +``` + +Don't forget to save. We removed `Monster.__init__` and made `Mobile` 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) + +First reload the server as usual. We will need to create the dragon a little differently this time: + +```sidebar:: Keyword arguments + + Keyword arguments (like `db_key="Smaug"`) is a way to + name the input arguments to a function or method. They make + things easier to read but also allows for conveniently setting + defaults for values not given explicitly. + +``` + > py + > from typeclasses.mymobile import Dragon + > smaug = Dragon(db_key="Smaug", db_location=here) + > smaug.save() + > smaug.move_around() + Smaug is moving! + The world trembles. + +Smaug works the same as before, but we created him differently: first we used +`Dragon(db_key="Smaug", db_location=here)` to create the object, and then we used `smaug.save()` afterwards. + + > quit() + Python Console is closing. + > look + +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 +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). + +To make use of Smaug in code we must first find him in the database. For an object in the current +location we can easily do this in `py` by using `me.search()`: + + > py smaug = me.search("Smaug") ; smaug.firebreath() + Smaug breathes fire! + +### Creating using create_object + +Creating Smaug like we did above is nice because it's similar to how we created non-database +bound Python instances before. But you need to use `db_key` instead of `key` and you also have to +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) + > look + +Boom, Fluffy should now be in the room with you, a little less scary than Smaug. You specify the +python-path to the code you want and then set the key and location. Evennia sets things up and saves for you. + +If you want to find Fluffy from anywhere, you can use Evennia's `search_object` helper: + + > fluffy = evennia.search_object("Fluffy")[0] ; fluffy.move_around() + Fluffy is moving! + +> The `[0]` is because `search_object` always returns a _list_ of zero, one or more found objects. The `[0]` +means that we want the first element of this list (counting in Python always starts from 0). If there were +multiple Fluffies we could get the second one with `[1]`. + +### Creating using create-command + +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 + +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) + +That's pretty much all there is to the mighty `create` command. + +... And speaking of Commands, we should try to add one of our own next. + + + + + +# 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 + + 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." +``` +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. + +## 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: + +```python +def at_init(self): + self.ndb.counter = 0 + self.ndb.mylist = [] +``` + +> 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__`. + + +## Updating existing objects + +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. + +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: + +``` +@py from typeclasses.objects import Heavy; [obj.at_object_creation() for obj in Heavy.objects.all()] + +``` + +[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 new file mode 100644 index 0000000000..d372441c82 --- /dev/null +++ b/docs/source/Howto/Starting/Part1/More-on-Commands.md @@ -0,0 +1,502 @@ +# More about Commands + +[prev lesson](Adding-Commands) | [next lesson](Learning-Typeclasses) + +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. + +## More advanced parsing + +In the last lesson we made a `hit` Command and hit 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: + + hit [[with] ] + +That is, we want to support all of these forms + + hit target + hit target weapon + 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`: + + +```python +#... + +class CmdHit(Command): + """ + Hit a target. + + Usage: + hit + + """ + key = "hit" + + def parse(self): + self.args = self.args.strip() + target, *weapon = self.args.split(" with ", 1) + if not weapon: + target, *weapon = target.split(" ", 1) + self.target = target.strip() + if weapon: + self.weapon = weapon.strip() + else: + self.weapon = "" + + def func(self): + if not self.args: + self.caller.msg("Who do you want to hit?") + return + # get the target for the hit + target = self.caller.search(self.target) + if not target: + return + # get and handle the weapon + weapon = None + if self.weapon: + weapon = self.caller.search(self.weapon) + if weapon: + weaponstr = f"{weapon.key}" + else: + weaponstr = "bare fists" + + 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. + +```sidebar:: Tuples and Lists + + - A `list` is written as `[a, b, c, d, ...]`. You can add and grow/shrink a list after it was first created. + - A `tuple` is written as `(a, b, c, d, ...)`. A tuple cannot be modified once it is created. + +``` +- **Line 14** - We do the stripping of `self.args` once and for all here. We also store the stripped version back + into `self.args`, overwriting it. So there is no way to get back the non-stripped version from here on, which is fine + for this command. +- **Line 15** - This makes use of the `.split` method of strings. `.split` will, well, split the string by some criterion. + `.split(" with ", 1)` means "split the string once, around the substring `" with "` if it exists". The result + of this split is a _list_. Just how that list looks depends on the string we are trying to split: + 1. If we entered just `hit smaug`, we'd be splitting just `"smaug"` which would give the result `["smaug"]`. + 2. `hit smaug sword` gives `["smaug sword"]` + 3. `hit smaug with sword` gives `["smaug", "sword"]` + + So we get a list of 1 or 2 elements. We assign it to two variables like this, `target, *weapon = `. That + asterisk in `*weapon` is a nifty trick - it will automatically become a list of _0 or more_ values. It sorts of + "soaks" up everything left over. + 1. `target` becomes `"smaug"` and `weapon` becomes `[]` + 2. `target` becomes `"smaug sword"` and `weapon` becomes `[]` + 3. `target` becomes `"smaug"` and `weapon` becomes `sword` +- **Lines 16-17** - In this `if` condition we check if `weapon` is falsy (that is, the empty list). This can happen + under two conditions (from the example above): + 1. `target` is simply `smaug` + 2. `target` is `smaug sword` + + To separate these cases we split `target` once again, this time by empty space `" "`. Again we store the + result back with `target, *weapon =`. The result will be one of the following: + 1. `target` remains `smaug` and `weapon` remains `[]` + 2. `target` becomes `smaug` and `weapon` becomes `sword` +- **Lines 18-22** - We now store `target` and `weapon` into `self.target` and `self.weapon`. We must do this in order + for these local variables to made available in `func` later. Note how we need to check so `weapon` is not falsy + 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. +- **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. + +Let's try it out! + + > reload + > hit smaug with sword + 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. + + > create sword + +Since we didn't specify `/drop`, the sword will end up in our inventory and can seen with the `i` or +`inventory` command. The `.search` helper will still find it there. There is no need to reload to see this +change (no code changed, only stuff in the database). + + > hit smaug with sword + You hit smaug with sword! + + +## 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](Building-Quickstart) +you've seen an example of this with the "Red Button" object. The [Tutorial world](Tutorial-World-Introduction) +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. + + > self.search("sword").cmdset.add("commands.mycommands.MyCmdSet", permanent=True) + +We find the sword (it's still in our inventory so `self.search` should be able to find it), then +add `MyCmdSet` to it. This actually adds both `hit` and `echo` to the sword, which is fine. + +Let's try to swing it! + + > hit + More than one match for 'hit' (please narrow target): + hit-1 (sword #11) + hit-2 + +```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? +``` +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: + + > hit-1 + Who do you want to hit? + > hit-2 + Who do you want to hit? + +In this case we don't need both command-sets, so let's just keep the one on the sword: + + > self.cmdset.remove("commands.mycommands.MyCmdSet") + > hit + Who do you want to hit? + +Now try this: + + > tunnel n = kitchen + > n + > drop sword + > s + > hit + Command 'hit' is not available. Maybe you meant ... + > n + > 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. + +### 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. +``` + + > 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). + +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 + Command 'hit' is not available. .. + > 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: + + delete sword + +or + + py self.search("sword").delete() + + +## 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`. + +The default cmdset are defined in `mygame/commands/default_cmdsets.py`. Open that file now: + +```python +""" +(module docstring) +""" + +from evennia import default_cmds + +class CharacterCmdSet(default_cmds.CharacterCmdSet): + + key = "DefaultCharacter" + + def at_cmdset_creation(self): + + super().at_cmdset_creation() + # + # any commands you add below will overload the default ones + # + +class AccountCmdSet(default_cmds.AccountCmdSet): + + key = "DefaultAccount" + + def at_cmdset_creation(self): + + super().at_cmdset_creation() + # + # any commands you add below will overload the default ones + # + +class UnloggedinCmdSet(default_cmds.UnloggedinCmdSet): + + key = "DefaultUnloggedin" + + def at_cmdset_creation(self): + + super().at_cmdset_creation() + # + # any commands you add below will overload the default ones + # + +class SessionCmdSet(default_cmds.SessionCmdSet): + + key = "DefaultSession" + + def at_cmdset_creation(self): + + super().at_cmdset_creation() + # + # any commands you add below will overload the default ones + # +``` + +```sidebar:: super() + + 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. +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. + +For now, let's add our own `hit` and `echo` commands to the `CharacterCmdSet`: + + +```python +# ... + +from commands import mycommands + +class CharacterCmdSet(default_cmds.CharacterCmdSet): + + key = "DefaultCharacter" + + def at_cmdset_creation(self): + + super().at_cmdset_creation() + # + # any commands you add below will overload the default ones + # + self.add(mycommands.CmdEcho) + self.add(mycommands.CmdHit) + +``` + + > reload + > 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: + +```python +from commands import mycommands + +class CharacterCmdSet(default_cmds.CharacterCmdSet): + + key = "DefaultCharacter" + + def at_cmdset_creation(self): + + super().at_cmdset_creation() + # + # any commands you add below will overload the default ones + # + self.add(mycommands.MyCmdSet) +``` + +Which way you use depends on how much control you want, but if you already have a CmdSet, +this is practical. A Command can be a part of any number of different CmdSets. + +### Removing Commands + +To remove your custom commands again, you of course just delete the change you did to +`mygame/commands/default_cmdsets.py`. But what if you want to remove a default command? + +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`. + + +```python +# ... +from commands import mycommands + +class CharacterCmdSet(default_cmds.CharacterCmdSet): + + key = "DefaultCharacter" + + def at_cmdset_creation(self): + + super().at_cmdset_creation() + # + # any commands you add below will overload the default ones + # + self.add(mycommands.MyCmdSet) + self.remove(default_cmds.CmdGet) +# ... +``` + + > reload + > get + Command 'get' is not available ... + +## Replace a default command + +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. + +```python +# up top, by the other imports +from evennia import default_cmds + +# somewhere below +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. +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, +- **Line 7**: By adding our own `func` we replace the one in the parent. +- **Line 8**: For this simple change we still want the command to work the + same as before, so we use `super()` to call `func` on the parent. +- **Line 9**: `.location` is the place an object is at. `.contents` contains, well, the + contents of an object. If you tried `py self.contents` you'd get a list that equals + your inventory. For a room, the contents is everything in it. + So `self.caller.location.contents` gets the contents of our current location. This is + a _list_. In order send this to us with `.msg` we turn the list into a string. Python + has a special function `str()` to do this. + +We now just have to add this so it replaces the default `get` command. Open +`mygame/commands/default_cmdsets.py` again: + +```python +# ... +from commands import mycommands + +class CharacterCmdSet(default_cmds.CharacterCmdSet): + + key = "DefaultCharacter" + + def at_cmdset_creation(self): + + super().at_cmdset_creation() + # + # any commands you add below will overload the default ones + # + self.add(mycommands.MyCmdSet) + self.add(mycommands.MyCmdGet) +# ... +``` +```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 for you. +``` + + > reload + > get + 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). + +## 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. + +Let's explore 'ourselves' and other 'things' in the game next. + +[prev lesson](Adding-Commands) | [next lesson](Learning-Typeclasses) 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 7f4a299435..1414c037fb 100644 --- a/docs/source/Howto/Starting/Part1/Python-classes-and-objects.md +++ b/docs/source/Howto/Starting/Part1/Python-classes-and-objects.md @@ -1,11 +1,11 @@ -# Python Classes and Evennia Typeclasses +# Python Classes and objects -[prev lesson](Gamedir-Overview) | [next lesson](Adding-Commands) +[prev lesson](Gamedir-Overview) | [next lesson](Learning-Typeclasses) We have now learned how to run some simple Python code from inside (and outside) your game server. We have also taken a look at what our game dir looks and what is where. Now we'll start to use it. -## Importing +## Importing things No one writes something as big as an online game in one single huge file. Instead one breaks up the code into separate files (modules). Each module is dedicated to different purposes. Not only does @@ -153,7 +153,9 @@ Next we have a `class` named `Object`, which _inherits_ from `DefaultObject`. Th actually do anything on its own, its only code (except the docstring) is `pass` which means, well, to pass and don't do anything. -To understand what we are looking at, we need to explain what a 'class', an 'object' and an 'instance' is. +We will get back to this module in the [next lesson](Learning-Typeclasses). First we need to do a +little detour to understand what a 'class', an 'object' or 'instance' is. These are fundamental +things to understand before you can use Evennia efficiently. ```sidebar:: OOP Classes, objects, instances and inheritance are fundamental to Python. This and some @@ -185,6 +187,12 @@ at least one argument (almost always written as `self` although you could in pri another name), which is a reference back to itself. So when we print `self.key` we are referring back to the `key` on the class. +```sidebar:: Terms + + - A `class` is a code template describing a 'type' of something + - An `object` is an `instance` of a `class`. Like using a mold to cast tin soldiers, one class can be `instantiated` into any number of object-instances. + +``` 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 by _calling_ the class, much like you would a function: @@ -395,152 +403,14 @@ about the trembling world we added in the `Dragon` class. Inheritance is very powerful because it allows you to organize and re-use code while only adding the special things you want to change. Evennia uses this concept a lot. +## Summary -## Our first persistent object +We have created our first dragons from classes. We have learned a little about how you _instantiate_ a class +into an _object_. We have seen some examples of _inheritance_ and we tested to _override_ a method in the parent +with one in the child class. We also used `super()` to good effect. -Now we should know enough to understand what is happening in `mygame/typeclasses/objects.py`. -Open it again: - -```python -""" -module docstring -""" -from evennia import DefaultObject - -class Object(DefaultObject): - """ - class docstring - """ - pass -``` - -So we have a class `Object` that _inherits_ from `DefaultObject`, which we have imported from Evennia. -The class itself doesn't do anything (it just `pass`es) but that doesn't mean it's useless. As we've seen, -it inherits all the functionality of its parent. It's in fact an _exact replica_ of `DefaultObject` right now. -If we knew what kind of methods and resources were available on `DefaultObject` we could add our own and -change the way it works! - -> Hint: We will get back to this, but to learn what resources an Evennia parent like `DefaultObject` offers, -> 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. - -Go back to `mygame/typeclasses/mobile.py`. Change it as follows: - -```python - -from typeclasses.objects import Object - -class Mobile(Object): - """ - This is a base class for Mobiles. - """ - def move_around(self): - print(f"{self.key} is moving!") +But so far our dragons are gone as soon as we `restart` the server or `quit()` the Python interpreter. In the +next lesson we'll get up close and personal with Smaug. -class Dragon(Mobile): - """ - This is a dragon-specific mobile. - """ - - def move_around(self): - super().move_around() - print("The world trembles.") - - def firebreath(self): - """ - Let our dragon breathe fire. - """ - print(f"{self.key} breathes fire!") - -``` - -Don't forget to save. We removed `Monster.__init__` and made `Mobile` 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) - -First reload the server as usual. We will need to create the dragon a little differently this time: - -```sidebar:: Keyword arguments - - Keyword arguments (like `db_key="Smaug"`) is a way to - name the input arguments to a function or method. They make - things easier to read but also allows for conveniently setting - defaults for values not given explicitly. - -``` - > py - > from typeclasses.mymobile import Dragon - > smaug = Dragon(db_key="Smaug", db_location=here) - > smaug.save() - > smaug.move_around() - Smaug is moving! - The world trembles. - -Smaug works the same as before, but we created him differently: first we used -`Dragon(db_key="Smaug", db_location=here)` to create the object, and then we used `smaug.save()` afterwards. - - > quit() - Python Console is closing. - > look - -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 -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). - -To make use of Smaug in code we must first find him in the database. For an object in the current -location we can easily do this in `py` by using `me.search()`: - - > py smaug = me.search("Smaug") ; smaug.firebreath() - Smaug breathes fire! - -### Creating using create_object - -Creating Smaug like we did above is nice because it's similar to how we created non-database -bound Python instances before. But you need to use `db_key` instead of `key` and you also have to -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) - > look - -Boom, Fluffy should now be in the room with you, a little less scary than Smaug. You specify the -python-path to the code you want and then set the key and location. Evennia sets things up and saves for you. - -If you want to find Fluffy from anywhere, you can use Evennia's `search_object` helper: - - > fluffy = evennia.search_object("Fluffy")[0] ; fluffy.move_around() - Fluffy is moving! - -> The `[0]` is because `search_object` always returns a _list_ of zero, one or more found objects. The `[0]` -means that we want the first element of this list (counting in Python always starts from 0). If there were -multiple Fluffies we could get the second one with `[1]`. - -### Creating using create-command - -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 - -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) - -That's pretty much all there is to the mighty `create` command. - -... And speaking of Commands, we should try to add one of our own next. - - -[prev lesson](Gamedir-Overview) | [next lesson](Adding-Commands) +[prev lesson](Gamedir-Overview) | [next lesson](Learning-Typeclasses) diff --git a/docs/source/Howto/Starting/Starting-Part1.md b/docs/source/Howto/Starting/Starting-Part1.md index 6d2419953d..8a6e574a22 100644 --- a/docs/source/Howto/Starting/Starting-Part1.md +++ b/docs/source/Howto/Starting/Starting-Part1.md @@ -26,10 +26,11 @@ own first little game in Evennia. Let's get started! 1. [The Tutorial World](Part1/Tutorial-World-Introduction) 1. [Python basics](Part1/Python-basic-introduction) 1. [Game dir overview](Part1/Gamedir-Overview) -1. [Python classes](Python-basic-tutorial-part-two) -1. [Running Python in- and outside the game](../../Coding/Execute-Python-Code) -1. [Understanding errors](Understanding-Errors) -1. [Searching for things](Tutorial-Searching-For-Objects) +1. [Python classes and objects](Part1/Python-classes-and-objects) +1. [Persistent objects](Part1/Learning-Typeclasses) +1. [Making our first own commands](Part1/Adding-Commands) +1. [Parsing and replacing default Commands](Part1/More-on-Commands) +1. [Searching and creating things](Tutorial-Searching-For-Objects) 1. [A walkthrough of the API](Walkthrough-of-API) In this first part we'll focus on what we get out of the box in Evennia - we'll get used to the tools, diff --git a/docs/source/toc.md b/docs/source/toc.md index 0951b7e09a..af442cf68e 100644 --- a/docs/source/toc.md +++ b/docs/source/toc.md @@ -1,5 +1,6 @@ # Toc +- [Coding/Coding Introduction](Coding/Coding-Introduction) - [Coding/Coding Overview](Coding/Coding-Overview) - [Coding/Continuous Integration](Coding/Continuous-Integration) - [Coding/Debugging](Coding/Debugging) @@ -93,8 +94,6 @@ - [Howto/NPC shop Tutorial](Howto/NPC-shop-Tutorial) - [Howto/Starting/API Overview](Howto/Starting/API-Overview) - [Howto/Starting/Add a simple new web page](Howto/Starting/Add-a-simple-new-web-page) -- [Howto/Starting/Adding Object Typeclass Tutorial](Howto/Starting/Adding-Object-Typeclass-Tutorial) -- [Howto/Starting/Coding Introduction](Howto/Starting/Coding-Introduction) - [Howto/Starting/Coordinates](Howto/Starting/Coordinates) - [Howto/Starting/First Steps Coding](Howto/Starting/First-Steps-Coding) - [Howto/Starting/Game Planning](Howto/Starting/Game-Planning) @@ -103,6 +102,8 @@ - [Howto/Starting/Part1/Adding Commands](Howto/Starting/Part1/Adding-Commands) - [Howto/Starting/Part1/Building Quickstart](Howto/Starting/Part1/Building-Quickstart) - [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) - [Howto/Starting/Part1/Python basic introduction](Howto/Starting/Part1/Python-basic-introduction) - [Howto/Starting/Part1/Python classes and objects](Howto/Starting/Part1/Python-classes-and-objects) - [Howto/Starting/Part1/Tutorial World Introduction](Howto/Starting/Part1/Tutorial-World-Introduction) diff --git a/evennia/contrib/tutorial_examples/mirror.py b/evennia/contrib/tutorial_examples/mirror.py index fb3ee0eb7b..94c0798195 100644 --- a/evennia/contrib/tutorial_examples/mirror.py +++ b/evennia/contrib/tutorial_examples/mirror.py @@ -6,7 +6,7 @@ A simple mirror object to experiment with. """ from evennia import DefaultObject -from evennia.utils import make_iter +from evennia.utils import make_iter, is_iter from evennia import logger @@ -51,6 +51,7 @@ class TutorialMirror(DefaultObject): """ if not text: text = "" + text = text[0] if is_iter(text) else text if from_obj: for obj in make_iter(from_obj): obj.msg(f"{self.key} echoes back to you:\n\"{text}\".") diff --git a/evennia/settings_default.py b/evennia/settings_default.py index 49f9f5fe42..7a3b1d6fa9 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -299,7 +299,7 @@ COMMAND_PARSER = "evennia.commands.cmdparser.cmdparser" # parser expects this. It should also involve a number starting from 1. # When changing this you must also update SEARCH_MULTIMATCH_TEMPLATE # to properly describe the syntax. -SEARCH_MULTIMATCH_REGEX = r"(?P[0-9]+)-(?P.*)" +SEARCH_MULTIMATCH_REGEX = r"(?P.*)-(?P[0-9]+)" # To display multimatch errors in various listings we must display # the syntax in a way that matches what SEARCH_MULTIMATCH_REGEX understand. # The template will be populated with data and expects the following markup: @@ -307,7 +307,7 @@ SEARCH_MULTIMATCH_REGEX = r"(?P[0-9]+)-(?P.*)" # name (key) of the multimatched entity; {aliases} - eventual # aliases for the entity; {info} - extra info like #dbrefs for staff. Don't # forget a line break if you want one match per line. -SEARCH_MULTIMATCH_TEMPLATE = " {number}-{name}{aliases}{info}\n" +SEARCH_MULTIMATCH_TEMPLATE = " {name}-{number}{aliases}{info}\n" # The handler that outputs errors when using any API-level search # (not manager methods). This function should correctly report errors # both for command- and object-searches. This allows full control