From 830f793aa428c36a7f9e3a77df53873b68a2da0b Mon Sep 17 00:00:00 2001 From: Griatch Date: Fri, 3 Jul 2020 01:00:13 +0200 Subject: [PATCH] Continued with adding commands --- .../Howto/Starting/Adding-Command-Tutorial.md | 171 ----- .../Howto/Starting/Part1/Adding-Commands.md | 643 +++++++++++++++++ .../Part1/Python-classes-and-objects.md | 648 ++++++------------ docs/source/_static/nature.css | 6 + docs/source/toc.md | 2 +- 5 files changed, 843 insertions(+), 627 deletions(-) delete mode 100644 docs/source/Howto/Starting/Adding-Command-Tutorial.md create mode 100644 docs/source/Howto/Starting/Part1/Adding-Commands.md diff --git a/docs/source/Howto/Starting/Adding-Command-Tutorial.md b/docs/source/Howto/Starting/Adding-Command-Tutorial.md deleted file mode 100644 index f4724b4167..0000000000 --- a/docs/source/Howto/Starting/Adding-Command-Tutorial.md +++ /dev/null @@ -1,171 +0,0 @@ -# Adding Command Tutorial - -This is a quick first-time tutorial expanding on the [Commands](../../Component/Commands) documentation. - -Let's assume you have just downloaded Evennia, installed it and created your game folder (let's call -it just `mygame` here). Now you want to try to add a new command. This is the fastest way to do it. - -## Step 1: Creating a custom command - -1. Open `mygame/commands/command.py` in a text editor. This is just one place commands could be -placed but you get it setup from the onset as an easy place to start. It also already contains some -example code. -1. Create a new class in `command.py` inheriting from `default_cmds.MuxCommand`. Let's call it - `CmdEcho` in this example. -1. Set the class variable `key` to a good command name, like `echo`. -1. Give your class a useful _docstring_. A docstring is the string at the very top of a class or -function/method. The docstring at the top of the command class is read by Evennia to become the help -entry for the Command (see - [Command Auto-help](../../Component/Help-System#command-auto-help-system)). -1. Define a class method `func(self)` that echoes your input back to you. - -Below is an example how this all could look for the echo command: - -```python - # file mygame/commands/command.py - #[...] - from evennia import default_cmds - class CmdEcho(default_cmds.MuxCommand): - """ - Simple command example - - Usage: - echo [text] - - This command simply echoes text back to the caller. - """ - - key = "echo" - - def func(self): - "This actually does things" - if not self.args: - self.caller.msg("You didn't enter anything!") - else: - self.caller.msg("You gave the string: '%s'" % self.args) -``` - -## Step 2: Adding the Command to a default Cmdset - -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" - -See `evennia/settings_default.py` for the other settings. \ No newline at end of file diff --git a/docs/source/Howto/Starting/Part1/Adding-Commands.md b/docs/source/Howto/Starting/Part1/Adding-Commands.md new file mode 100644 index 0000000000..5919093c12 --- /dev/null +++ b/docs/source/Howto/Starting/Part1/Adding-Commands.md @@ -0,0 +1,643 @@ +# Adding Command Tutorial + +[prev lesson](Python-classes-and-objects) | [next lesson]() + +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 +what is in it. + +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- +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 +as a bag holding many different commands. One CmdSet could for example hold all commands for +combat, another for building etc. + +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 + +## Creating a custom command + +Open `mygame/commands/command.py`: + +```python +""" +(module docstring) +""" + +from evennia import Command as BaseCommand +# from evennia import default_cmds + +class Command(BaseCommand): + """ + (class docstring) + """ + pass + +# (lots of commented-out stuff) +# ... +``` + +Ignoring the docstrings (which you can read if you want), this is the only really active code in the module. + +We can see that we import `Command` from `evennia` and use the `from ... import ... as ...` form to rename it +to `BaseCommand`. This is so we can let our child class also be named `Command` for reference. The class +itself doesn't do anything, it just has `pass`. So in the same way as `Object` in the previous lesson, this +class is identical to its parent. + +> The commented out `default_cmds` gives us access to Evennia's default commands for easy overriding. We'll try +> that a little later. + +We could modify this module directly, but to train imports we'll work in a separate module. Open a new file +`mygame/commands/mycommands.py` and add the following code: + +```python + +from commands.command import Command + +class CmdEcho(Command): + key = "echo" + +``` + +This is the simplest form of command you can imagine. It just gives itself a name, "echo". This is +what you will use to call this command later. + +Next we need to put this in a CmdSet. It will be a one-command CmdSet for now! Change your file as such: + + +```python + +from commands.command import Command +from evennia import CmdSet + +class CmdEcho(Command): + key = "echo" + + +class MyCmdSet(CmdSet): + + def at_cmdset_creation(self): + self.add(CmdEcho) + +``` + +Our `EchoCmdSet` class must have an `at_cmdset_creation` method, named exactly +like this - this is what Evennia will be looking for when setting up the cmdset later, so +if you didn't set it up, it will use the parent's version, which is empty. Inside we add the +command class to the cmdset by `self.add()`. If you wanted to add more commands to this CmdSet you +could just add more lines of `self.add` after this. + +Finally, let's add this command to ourselves so we can try it out. In-game you can experiment with `py` again: + + > py self.cmdset.add("commands.mycommands.MyCmdSet") + +Now try + + > echo + Command echo has no defined `func()` - showing on-command variables: + ... + ... + +You should be getting a long list of outputs. The reason for this is that your `echo` function is not really +"doing" anything yet and the default function is then to show all useful resources available to you when you +use your Command. Let's look at some of those listed: + + Command echo has no defined `func()` - showing on-command variables: + obj (): YourName + lockhandler (): cmd:all() + caller (): YourName + cmdname (): echo + raw_cmdname (): echo + cmdstring (): echo + args (): + cmdset (): @mail, about, access, accounts, addcom, alias, allcom, ban, batchcode, batchcommands, boot, cboot, ccreate, + cdesc, cdestroy, cemit, channels, charcreate, chardelete, checklockstring, clientwidth, clock, cmdbare, cmdsets, color, copy, cpattr, create, cwho, delcom, + desc, destroy, dig, dolphin, drop, echo, emit, examine, find, force, get, give, grapevine2chan, help, home, ic, inventory, irc2chan, ircstatus, link, lock, + look, menutest, mudinfo, mvattr, name, nick, objects, ooc, open, option, page, password, perm, pose, public, py, quell, quit, reload, reset, rss2chan, say, + script, scripts, server, service, sessions, set, setdesc, sethelp, sethome, shutdown, spawn, style, tag, tel, test2010, test2028, testrename, testtable, + tickers, time, tunnel, typeclass, unban, unlink, up, up, userpassword, wall, whisper, who, wipe + session (): Griatch(#1)@1:2:7:.:0:.:0:.:1 + account (): Griatch(account 1) + raw_string (): echo + + -------------------------------------------------- + echo - Command variables from evennia: + -------------------------------------------------- + name of cmd (self.key): echo + cmd aliases (self.aliases): [] + cmd locks (self.locks): cmd:all(); + help category (self.help_category): General + object calling (self.caller): Griatch + object storing cmdset (self.obj): Griatch + command string given (self.cmdstring): echo + current cmdset (self.cmdset): ChannelCmdSet + +These are all properties you can access with `.` on the Command instance, such as `.key`, `.args` and so on. +Evennia makes these available to you and they will be different every time a command is run. The most +important ones we will make use of now are: + + - `caller` - this is 'you', the person calling the command. + - `args` - this is all arguments to the command. Now it's empty, but if you tried `echo foo bar` you'd find + that this would be `" foo bar"`. + - `obj` - this is object on which this Command (and CmdSet) "sits". So you, in this case. + +The reason our command doesn't do anything yet is because it's missing a `func` method. This is what Evennia +looks for to figure out what a Command actually does. Modify your `CmdEcho` class: + +```python +# ... + +class CmdEcho(Command): + """ + A simple echo command + + Usage: + echo + + """ + key = "echo" + + def func(self): + self.caller.msg(f"Echo: '{self.args}'") + +# ... +``` + +First we added a docstring. This is always a good thing to do in general, but for a Command class, it will also +automatically become the in-game help entry! Next we add the `func` method. It has one active line where it +makes use of some of those variables we found the Command offers to us. If you did the +[basic Python tutorial](Python-basic-introduction), you will recognize `.msg` - this will send a message +to the object it is attached to us - in this case `self.caller`, that is, us. We grab `self.args` and includes +that in the message. + +Since we haven't changed `MyCmdSet`, that will work as before. Reload and re-add this command to ourselves to +try out the new version: + + > reload + > py self.cmdset.add("commands.mycommands.MyCmdSet") + > echo + Echo: '' + +Try to pass an argument: + + > echo Woo Tang! + Echo: ' Woo Tang!' + +Note that there is an extra space before `Woo!`. That is because self.args contains the _everything_ after +the command name, including spaces. Evennia will happily understand if you skip that space too: + + > echoWoo Tang! + Echo: 'Woo Tang!' + +There are ways to force Evennia to _require_ an initial space, but right now we want to just ignore it since +it looks a bit weird for our echo example. Tweak the code: + +```python +# ... + +class CmdEcho(Command): + """ + A simple echo command + + Usage: + echo + + """ + key = "echo" + + def func(self): + self.caller.msg(f"Echo: '{self.args.strip()}'") + +# ... +``` + +The only difference is that we called `.strip()` on `self.args`. This is a helper method available on all +strings - it strips out all whitespace before and after the string. Now the Command-argument will no longer +have any space in front of it. + + > reload + > py self.cmdset.add("commands.mycommands.MyCmdSet") + > echo Woo Tang! + Echo: 'Woo Tang!' + +Don't forget to look at the help for the echo command: + + > help echo + +You will get the docstring you put in your Command-class. + +### Making our cmdset persistent + +It's getting a little annoying to have to re-add our cmdset every time we reload, right? It's simple +enough to make `echo` a _permanent_ change though: + + > py self.cmdset.add("commands.mycommands.MyCmdSet", permanent=True) + +Now you can `reload` as much as you want and your code changes will be available directly without +needing to re-add the MyCmdSet again. To remove the cmdset again, do + + > py self.cmdset.remove("commands.mycommands.MyCmdSet") + +But for now, keep it around, we'll expand it with some more examples. + +### Figuring out who to hit + +Let's try something a little more exciting than just echo. Let's make a `hit` command, for punching +someone in the face! This is how we want it to work: + + > hit + You hit with full force! + +Not only that, we want the to see + + You got hit by with full force! + +Here, `` would be the one using the `hit` command and `` is the one doing the punching. + +Still in `mygame/commands/mycommands.py`, add a new class, between `CmdEcho` and `MyCmdSet`. + +```python +# ... + +class CmdHit(Command): + """ + Hit a target. + + Usage: + hit + + """ + key = "hit" + + def func(self): + args = self.args.strip() + if not args: + self.caller.msg("Who do you want to hit?") + return + target = self.caller.search(args) + if not target: + return + self.caller.msg(f"You hit {target.key} with full force!") + target.msg(f"You got hit by {self.caller.key} with full force!") +# ... + +``` + +A lot of things to dissect here: +- **Line 4**: The normal `class` header. We inherit from `Command` which we imported at the top of this file. +- **Lines 5**-11: The docstring and help-entry for the command. You could expand on this as much as you wanted. +- **Line 12**: We want to write `hit` to use this command. +- **Line 15**: We strip the whitespace from the argument like before. Since we don't want to have to do + `self.args.strip()` over and over, we store the stripped version + in a _local variable_ `args`. Note that we don't modify `self.args` by doing this, `self.args` will still + have the whitespace and is not the same as `args` in this example. +```sidebar:: if-statements + + The full form of the if statement is + + if condition: + ... + elif othercondition: + ... + else: + ... + + There can be any number of `elifs` to mark when different branches of the code should run. If + the `else` condition is given, it will run if none of the other conditions was truthy. In Python + the `if..elif..else` structure also serves the same function as `case` in some other languages. + +``` +- **Line 16** has our first _conditional_, an `if` statement. This is written on the form `if :` and only + if that condition is 'truthy' will the indented code block under the `if` statement run. To learn what is truthy in + Python it's usually easier to learn what is "falsy": + - `False` - this is a reserved boolean word in Python. The opposite is `True`. + - `None` - another reserved word. This represents nothing, a null-result or value. + - `0` or `0.0` + - The empty string `""` or `''` or `""""""` or `''''''` + - Empty _iterables_ we haven't seen yet, like empty lists `[]`, empty tuples `()` and empty dicts `{}`. + - Everything else is "truthy". + + Line 16's condition is `not args`. The `not` _inverses_ the result, so if `args` is the empty string (falsy), the + whole conditional becomes truthy. Let's continue in the code: +- **Lines 17-18**: This code will only run if the `if` statement is truthy, in this case if `args` is the empty string. +- **Line 18**: `return` is a reserved Python word that exits `func` immediately. +- **Line 19**: We use `self.caller.search` to look for the target in the current location. +- **Lines 20-21**: A feature of `.search` is that it will already inform `self.caller` if it couldn't find the target. + In that case, `target` will be `None` and we should just directly `return`. +- **Lines 22-23**: At this point we have a suitable target and can send our punching strings to each. + +Finally we must also add this to a CmdSet. Let's add it to `MyCmdSet` which we made permanent earlier. + +```python +# ... + +class MyCmdSet(CmdSet): + + def at_cmdset_creation(self): + self.add(CmdEcho) + self.add(CmdHit) + +``` + +```sidebar:: Errors in your code + + With longer code snippets to try, it gets more and more likely you'll + make an error and get a `traceback` when you reload. This will either appear + directly in-game or in your log (view it with `evennia -l` in a terminal). + Don't panic; tracebacks are your friends - they are to be read bottom-up and usually describe + exactly where your problem is. Refer to `The Python intro `_ for + more hints. If you get stuck, reach out to the Evennia community for help. + +``` + +Next we reload to let Evennia know of these code changes and try it out: + + > reload + hit + Who do you want to hit? + hit me + You hit YourName with full force! + You got hit by YourName with full force! + +Lacking a target, we hit ourselves. If you have one of the dragons still around from the previous lesson +you could try to hit it (if you dare): + + hit smaug + You hit Smaug with full force! + +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: + + 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 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]() 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 83c421b343..7f4a299435 100644 --- a/docs/source/Howto/Starting/Part1/Python-classes-and-objects.md +++ b/docs/source/Howto/Starting/Part1/Python-classes-and-objects.md @@ -1,6 +1,6 @@ -# Continuing on with Python and Evennia +# Python Classes and Evennia Typeclasses -[prev lesson](Gamedir-Overview) | [next lesson]() +[prev lesson](Gamedir-Overview) | [next lesson](Adding-Commands) 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. @@ -291,518 +291,256 @@ objects in turn: - The current policy positions of a political party - A rule with methods for resolving challenges or roll dice - A multi-dimenstional data-point for a complex economic simulation +- And so much more! ### Classes can have children 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: + +```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): + print(f"{self.key} flies through the air high above!") -[In the first part](Python-basic-introduction) of this Python-for-Evennia basic tutorial we learned -how to run some simple Python code from inside the game. We also made our first new *module* -containing a *function* that we called. Now we're going to start exploring the very important -subject of *objects*. - -**Contents:** -- [On the subject of objects](Python-basic-tutorial-part-two#on-the-subject-of-objects) -- [Exploring the Evennia library](Python-basic-tutorial-part-two#exploring-the-evennia-library) -- [Tweaking our Character class](Python-basic-tutorial-part-two#tweaking-our-character-class) -- [The Evennia shell](Python-basic-tutorial-part-two#the-evennia-shell) -- [Where to go from here](Python-basic-tutorial-part-two#where-to-go-from-here) - -### On the subject of objects - -In the first part of the tutorial we did things like - - > py me.msg("Hello World!") - -To learn about functions and imports we also passed that `me` on to a function `hello_world` in -another module. - -Let's learn some more about this `me` thing we are passing around all over the place. In the -following we assume that we named our superuser Character "Christine". - - > py me - Christine - > py me.key - Christine - -These returns look the same at first glance, but not if we examine them more closely: - - > py type(me) - - > py type(me.key) - - -> Note: In some MU clients, such as Mudlet and MUSHclient simply returning `type(me)`, you may not -see the proper return from the above commands. This is likely due to the HTML-like tags `<...>`, -being swallowed by the client. - -The `type` function is, like `print`, another in-built function in Python. It -tells us that we (`me`) are of the *class* `typeclasses.characters.Character`. -Meanwhile `me.key` is a *property* on us, a string. It holds the name of this -object. - -> When you do `py me`, the `me` is defined in such a way that it will use its `.key` property to -represent itself. That is why the result is the same as when doing `py me.key`. Also, remember that -as noted in the first part of the tutorial, the `me` is *not* a reserved Python word; it was just -defined by the Evennia developers as a convenient short-hand when creating the `py` command. So -don't expect `me` to be available elsewhere. - -A *class* is like a "factory" or blueprint. From a class you then create individual *instances*. So -if class is`Dog`, an instance of `Dog` might be `fido`. Our in-game persona is of a class -`Character`. The superuser `christine` is an *instance* of the `Character` class (an instance is -also often referred to as an *object*). This is an important concept in *object oriented -programming*. You are wise to [familiarize yourself with it](https://en.wikipedia.org/wiki/Class- -based_programming) a little. - -> In other terms: -> * class: A description of a thing, all the methods (code) and data (information) -> * object: A thing, defined as an *instance* of a class. -> -> So in "Fido is a Dog", "Fido" is an object--a unique thing--and "Dog" is a class. Coders would -also say, "Fido is an instance of Dog". There can be other dogs too, such as Butch and Fifi. They, -too, would be instances of Dog. -> -> As another example: "Christine is a Character", or "Christine is an instance of -typeclasses.characters.Character". To start, all characters will be instances of -typeclass.characters.Character. -> -> You'll be writing your own class soon! The important thing to know here is how classes and objects -relate. - -The string `'typeclasses.characters.Character'` we got from the `type()` function is not arbitrary. -You'll recognize this from when we _imported_ `world.test` in part one. This is a _path_ exactly -describing where to find the python code describing this class. Python treats source code files on -your hard drive (known as *modules*) as well as folders (known as *packages*) as objects that you -access with the `.` operator. It starts looking at a place that Evennia has set up for you - namely -the root of your own game directory. - -Open and look at your game folder (named `mygame` if you exactly followed the Getting Started -instructions) in a file editor or in a new terminal/console. Locate the file -`mygame/typeclasses/characters.py` + def firebreath(self): + """ + Let our dragon breathe fire. + """ + print(f"{self.key} breathes fire!") ``` -mygame/ - typeclasses - characters.py + +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 +the parent in parenthesis. `class Classname(Parent)` is the way to do this. + +```sidebar:: Multi-inheritance + + It's possible to add more comma-separated parents to a class. You should usually avoid + this until you `really` know what you are doing. A single parent will be enough for almost + every case you'll need. + ``` -This represents the first part of the python path - `typeclasses.characters` (the `.py` file ending -is never included in the python path). The last bit, `.Character` is the actual class name inside -the `characters.py` module. Open that file in a text editor and you will see something like this: +Let's try out our new class. First `reload` the server and the do + + > py + > from typeclasses.mobile import Dragon + > smaug = Dragon("Smaug") + > smaug.move_around() + Smaug flies through the air high above! + > smaug.firebreath() + Smaug breathes fire! + +Because we didn't implement `__init__` in `Dragon`, we got the one from `Monster` instead. But since we +implemented our own `move_around` in `Dragon`, it _overrides_ the one in `Monster`. And `firebreath` is only +available for `Dragon`s of course. Having that on `Monster` would not have made much sense, since not every monster +can breathe fire. + +One can also force a class to use resources from the parent even if you are overriding some of it. This is done +with the `super()` method. Modify your `Dragon` class as follows: + + +```python +# ... + +class Dragon(Monster): + + def move_around(self): + super().move_around() + print("The world trembles.") + + # ... +``` +> Keep `Monster` and the `firebreath` method, `# ...` indicates the rest of the code is untouched. +> + +The `super().move_around()` line means that we are calling `move_around()` on the parent of the class. So in this +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 + > smaug = Dragon("Smaug") + > smaug.move_around() + Smaug is moving! + The world trembles. + +We can see that `Monster.move_around()` is calls first and prints "Smaug is moving!", followed by the extra bit +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. + + +## Our first persistent object + +Now we should know enough to understand what is happening in `mygame/typeclasses/objects.py`. +Open it again: ```python """ -(Doc string for module) +module docstring """ +from evennia import DefaultObject -from evennia import DefaultCharacter - -class Character(DefaultCharacter): +class Object(DefaultObject): """ - (Doc string for class) + class docstring """ pass - ``` -There is `Character`, the last part of the path. Note how empty this file is. At first glance one -would think a Character had no functionality at all. But from what we have used already we know it -has at least the `key` property and the method `msg`! Where is the code? The answer is that this -'emptiness' is an illusion caused by something called *inheritance*. Read on. +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! -Firstly, in the same way as the little `hello.py` we did in the first part of the tutorial, this is -an example of full, multi-line Python code. Those triple-quoted strings are used for strings that -have line breaks in them. When they appear on their own like this, at the top of a python module, -class or similar they are called *doc strings*. Doc strings are read by Python and is used for -producing online help about the function/method/class/module. By contrast, a line starting with `#` -is a *comment*. It is ignored completely by Python and is only useful to help guide a human to -understand the code. +> 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. -The line +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. -```python - class Character(DefaultCharacter): -``` - -means that the class `Character` is a *child* of the class `DefaultCharacter`. This is called -*inheritance* and is another fundamental concept. The answer to the question "where is the code?" is -that the code is *inherited* from its parent, `DefaultCharacter`. And that in turn may inherit code -from *its* parent(s) and so on. Since our child, `Character` is empty, its functionality is *exactly -identical* to that of its parent. The moment we add new things to Character, these will take -precedence. And if we add something that already existed in the parent, our child-version will -*override* the version in the parent. This is very practical: It means that we can let the parent do -the heavy lifting and only tweak the things we want to change. It also means that we could easily -have many different Character classes, all inheriting from `DefaultCharacter` but changing different -things. And those can in turn also have children ... - -Let's go on an expedition up the inheritance tree. - -### Exploring the Evennia library - -Let's figure out how to tweak `Character`. Right now we don't know much about `DefaultCharacter` -though. Without knowing that we won't know what to override. At the top of the file you find - -```python -from evennia import DefaultCharacter -``` - -This is an `import` statement again, but on a different form to what we've seen before. `from ... -import ...` is very commonly used and allows you to precisely dip into a module to extract just the -component you need to use. In this case we head into the `evennia` package to get -`DefaultCharacter`. - -Where is `evennia`? To find it you need to go to the `evennia` folder (repository) you originally -cloned from us. If you open it, this is how it looks: - -``` -evennia/ - __init__.py - bin/ - CHANGELOG.txt etc. - ... - evennia/ - ... -``` -There are lots of things in there. There are some docs but most of those have to do with the -distribution of Evennia and does not concern us right now. The `evennia` subfolder is what we are -looking for. *This* is what you are accessing when you do `from evennia import ...`. It's set up by -Evennia as a good place to find modules when the server starts. The exact layout of the Evennia -library [is covered by our directory overview](Directory-Overview#evennia-library-layout). You can -also explore it [online on github](https://github.com/evennia/evennia/tree/master/evennia). - -The structure of the library directly reflects how you import from it. - -- To, for example, import [the text justify -function](https://github.com/evennia/evennia/blob/master/evennia/utils/utils.py#L201) from -`evennia/utils/utils.py` you would do `from evennia.utils.utils import justify`. In your code you -could then just call `justify(...)` to access its functionality. -- You could also do `from evennia.utils import utils`. In code you would then have to write -`utils.justify(...)`. This is practical if want a lot of stuff from that `utils.py` module and don't -want to import each component separately. -- You could also do `import evennia`. You would then have to enter the full -`evennia.utils.utils.justify(...)` every time you use it. Using `from` to only import the things you -need is usually easier and more readable. -- See [this overview](http://effbot.org/zone/import-confusion.htm) about the different ways to -import in Python. - -Now, remember that our `characters.py` module did `from evennia import DefaultCharacter`. But if we -look at the contents of the `evennia` folder, there is no `DefaultCharacter` anywhere! This is -because Evennia gives a large number of optional "shortcuts", known as [the "flat" API](Evennia- -API). The intention is to make it easier to remember where to find stuff. The flat API is defined in -that weirdly named `__init__.py` file. This file just basically imports useful things from all over -Evennia so you can more easily find them in one place. - -We could [just look at the documenation](github:evennia#typeclasses) to find out where we can look -at our `DefaultCharacter` parent. But for practice, let's figure it out. Here is where -`DefaultCharacter` [is imported -from](https://github.com/evennia/evennia/blob/master/evennia/__init__.py#L188) inside `__init__.py`: - -```python -from .objects.objects import DefaultCharacter -``` - -The period at the start means that it imports beginning from the same location this module sits(i.e. -the `evennia` folder). The full python-path accessible from the outside is thus -`evennia.objects.objects.DefaultCharacter`. So to import this into our game it'd be perfectly valid -to do - -```python -from evennia.objects.objects import DefaultCharacter -``` - -Using - -```python -from evennia import DefaultCharacter -``` - -is the same thing, just a little easier to remember. - -> To access the shortcuts of the flat API you *must* use `from evennia import -> ...`. Using something like `import evennia.DefaultCharacter` will not work. -> See [more about the Flat API here](../../../Evennia-API). - - -### Tweaking our Character class - -In the previous section we traced the parent of our `Character` class to be -`DefaultCharacter` in -[evennia/objects/objects.py](https://github.com/evennia/evennia/blob/master/evennia/objects/objects.py). -Open that file and locate the `DefaultCharacter` class. It's quite a bit down -in this module so you might want to search using your editor's (or browser's) -search function. Once you find it, you'll find that the class starts like this: +Go back to `mygame/typeclasses/mobile.py`. Change it as follows: ```python -class DefaultCharacter(DefaultObject): +from typeclasses.objects import Object + +class Mobile(Object): """ - This implements an Object puppeted by a Session - that is, a character - avatar controlled by an account. + 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 basetype_setup(self): - """ - Setup character-specific security. - You should normally not need to overload this, but if you do, - make sure to reproduce at least the two last commands in this - method (unless you want to fundamentally change how a - Character object works). - """ - super().basetype_setup() - self.locks.add(";".join(["get:false()", # noone can pick up the character - "call:false()"])) # no commands can be called on character from -outside - # add the default cmdset - self.cmdset.add_default(settings.CMDSET_CHARACTER, permanent=True) + def move_around(self): + super().move_around() + print("The world trembles.") - def at_after_move(self, source_location, **kwargs): + def firebreath(self): + """ + Let our dragon breathe fire. """ - We make sure to look around after a move. - """ - if self.location.access(self, "view"): - self.msg(self.at_look(self.location)) - - def at_pre_puppet(self, account, session=None, **kwargs): - """ - Return the character from storage in None location in `at_post_unpuppet`. - """ - - # ... + print(f"{self.key} breathes fire!") ``` -... And so on (you can see the full [class online -here](https://github.com/evennia/evennia/blob/master/evennia/objects/objects.py#L1915)). Here we -have functional code! These methods may not be directly visible in `Character` back in our game dir, -but they are still available since `Character` is a child of `DefaultCharacter` above. Here is a -brief summary of the methods we find in `DefaultCharacter` (follow in the code to see if you can see -roughly where things happen):: +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! -- `basetype_setup` is called by Evennia only once, when a Character is first created. In the -`DefaultCharacter` class it sets some particular [Locks](../../../Component/Locks) so that people can't pick up and -puppet Characters just like that. It also adds the [Character Cmdset](../../../Component/Command-Sets) so that -Characters always can accept command-input (this should usually not be modified - the normal hook to -override is `at_object_creation`, which is called after `basetype_setup` (it's in the parent)). -- `at_after_move` makes it so that every time the Character moves, the `look` command is -automatically fired (this would not make sense for just any regular Object). -- `at_pre_puppet` is called when an Account begins to puppet this Character. When not puppeted, the -Character is hidden away to a `None` location. This brings it back to the location it was in before. -Without this, "headless" Characters would remain in the game world just standing around. -- `at_post_puppet` is called when puppeting is complete. It echoes a message to the room that his -Character has now connected. -- `at_post_unpuppet` is called once stopping puppeting of the Character. This hides away the -Character to a `None` location again. -- There are also some utility properties which makes it easier to get some time stamps from the -Character. +### Creating by calling the class (less common way) -Reading the class we notice another thing: +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. -```python -class DefaultCharacter(DefaultObject): - # ... ``` + > 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. -This means that `DefaultCharacter` is in *itself* a child of something called `DefaultObject`! Let's -see what this parent class provides. It's in the same module as `DefaultCharacter`, you just need to -[scroll up near the -top](https://github.com/evennia/evennia/blob/master/evennia/objects/objects.py#L182): +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. -```python -class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): - # ... -``` + > quit() + Python Console is closing. + > look + +You should now see that Smaug _is in the room with you_. Woah! -This is a really big class where the bulk of code defining an in-game object resides. It consists of -a large number of methods, all of which thus also becomes available on the `DefaultCharacter` class -below *and* by extension in your `Character` class over in your game dir. In this class you can for -example find the `msg` method we have been using before. + > 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). -> You should probably not expect to understand all details yet, but as an exercise, find and read -the doc string of `msg`. +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()`: -> As seen, `DefaultObject` actually has multiple parents. In one of those the basic `key` property -is defined, but we won't travel further up the inheritance tree in this tutorial. If you are -interested to see them, you can find `TypeclassBase` in -[evennia/typeclasses/models.py](https://github.com/evennia/evennia/blob/master/evennia/typeclasses/models.py#L93) -and `ObjectDB` in -[evennia/objects/models.py](https://github.com/evennia/evennia/blob/master/evennia/objects/models.py#L121). -We will also not go into the details of [Multiple -Inheritance](https://docs.python.org/2/tutorial/classes.html#multiple-inheritance) or -[Metaclasses](http://www.onlamp.com/pub/a/python/2003/04/17/metaclasses.html) here. The general rule -is that if you realize that you need these features, you already know enough to use them. + > py smaug = me.search("Smaug") ; smaug.firebreath() + Smaug breathes fire! -Remember the `at_pre_puppet` method we looked at in `DefaultCharacter`? If you look at the -`at_pre_puppet` hook as defined in `DefaultObject` you'll find it to be completely empty (just a -`pass`). So if you puppet a regular object it won't be hiding/retrieving the object when you -unpuppet it. The `DefaultCharacter` class *overrides* its parent's functionality with a version of -its own. And since it's `DefaultCharacter` that our `Character` class inherits back in our game dir, -it's *that* version of `at_pre_puppet` we'll get. Anything not explicitly overridden will be passed -down as-is. +### Creating using create_object -While it's useful to read the code, we should never actually modify anything inside the `evennia` -folder. Only time you would want that is if you are planning to release a bug fix or new feature for -Evennia itself. Instead you *override* the default functionality inside your game dir. +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`: -So to conclude our little foray into classes, objects and inheritance, locate the simple little -`at_before_say` method in the `DefaultObject` class: + > 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. -```python - def at_before_say(self, message, **kwargs): - """ - (doc string here) - """ - return message -``` +If you want to find Fluffy from anywhere, you can use Evennia's `search_object` helper: -If you read the doc string you'll find that this can be used to modify the output of `say` before it -goes out. You can think of it like this: Evennia knows the name of this method, and when someone -speaks, Evennia will make sure to redirect the outgoing message through this method. It makes it -ripe for us to replace with a version of our own. + > fluffy = evennia.search_object("Fluffy")[0] ; fluffy.move_around() + Fluffy is moving! -> In the Evennia documentation you may sometimes see the term *hook* used for a method explicitly -meant to be overridden like this. +> 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]`. -As you can see, the first argument to `at_before_say` is `self`. In Python, the first argument of a -method is *always a back-reference to the object instance on which the method is defined*. By -convention this argument is always called `self` but it could in principle be named anything. The -`self` is very useful. If you wanted to, say, send a message to the same object from inside -`at_before_say`, you would do `self.msg(...)`. +### Creating using create-command -What can trip up newcomers is that you *don't* include `self` when you *call* the method. Try: +Finally, you can also create a new Dragon using the familiar builder-commands we explored a few lessons ago: - > @py me.at_before_say("Hello World!") - Hello World! + > create/drop Cuddly:typeclasses.mymobile.Mobile -Note that we don't send `self` but only the `message` argument. Python will automatically add `self` -for us. In this case, `self` will become equal to the Character instance `me`. +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 -By default the `at_before_say` method doesn't do anything. It just takes the `message` input and -`return`s it just the way it was (the `return` is another reserved Python word). + evennia.create_object("typeclasses.mymobile.Mobile", key="Cuddly", location=here) -> We won't go into `**kwargs` here, but it (and its sibling `*args`) is also important to -understand, extra reading is [here for -`**kwargs`](https://stackoverflow.com/questions/1769403/understanding-kwargs-in-python). +That's pretty much all there is to the mighty `create` command. -Now, open your game folder and edit `mygame/typeclasses/characters.py`. Locate your `Character` -class and modify it as such: +... And speaking of Commands, we should try to add one of our own next. -```python -class Character(DefaultCharacter): - """ - (docstring here) - """ - def at_before_say(self, message, **kwargs): - "Called before say, allows for tweaking message" - return f"{message} ..." -``` -So we add our own version of `at_before_say`, duplicating the `def` line from the parent but putting -new code in it. All we do in this tutorial is to add an ellipsis (`...`) to the message as it passes -through the method. - -Note that `f` in front of the string, it means we turned the string into a 'formatted string'. We -can now easily inject stuff directly into the string by wrapping them in curly brackets `{ }`. In -this example, we put the incoming `message` into the string, followed by an ellipsis. This is only -one way to format a string. Python has very powerful [string -formatting](https://docs.python.org/2/library/string.html#format-specification-mini-language) and -you are wise to learn it well, considering your game will be mainly text-based. - -> You could also copy & paste the relevant method from `DefaultObject` here to get the full doc -string. For more complex methods, or if you only want to change some small part of the default -behavior, copy & pasting will eliminate the need to constantly look up the original method and keep -you sane. - -In-game, now try - - > @reload - > say Hello - You say, "Hello ..." - -An ellipsis `...` is added to what you said! This is a silly example but you have just made your -first code change to core functionality - without touching any of Evennia's original code! We just -plugged in our own version of the `at_before_say` method and it replaced the default one. Evennia -happily redirected the message through our version and we got a different output. - -> For sane overriding of parent methods you should also be aware of Python's -[super](https://docs.python.org/3/library/functions.html#super), which allows you to call the -methods defined on a parent in your child class. - -### The Evennia shell - -Now on to some generally useful tools as you continue learning Python and Evennia. We have so far -explored using `py` and have inserted Python code directly in-game. We have also modified Evennia's -behavior by overriding default functionality with our own. There is a third way to conveniently -explore Evennia and Python - the Evennia shell. - -Outside of your game, `cd` to your mygame folder and make sure any needed virtualenv is running. -Next: - - > pip install ipython # only needed once - -The [`IPython`](https://en.wikipedia.org/wiki/IPython) program is just a nicer interface to the -Python interpreter - you only need to install it once, after which Evennia will use it -automatically. - - > evennia shell - -If you did this call from your game dir you will now be in a Python prompt managed by the IPython -program. - - IPython ... - ... - In [1]: -IPython has some very nice ways to explore what Evennia has to offer. - - > import evennia - > evennia. - -That is, write `evennia.` and press the Tab key. You will be presented with a list of all available -resources in the Evennia Flat API. We looked at the `__init__.py` file in the `evennia` folder -earlier, so some of what you see should be familiar. From the IPython prompt, do: - - > from evennia import DefaultCharacter - > DefaultCharacter.at_before_say? - -Don't forget that you can use `` to auto-complete code as you write. Appending a single `?` to -the end will show you the doc-string for `at_before_say` we looked at earlier. Use `??` to get the -whole source code. - -Let's look at our over-ridden version instead. Since we started the `evennia shell` from our game -dir we can easily get to our code too: - - > from typeclasses.characters import Character - > Character.at_before_say?? - -This will show us the changed code we just did. Having a window with IPython running is very -convenient for quickly exploring code without having to go digging through the file structure! - -### Where to go from here - -This should give you a running start using Python with Evennia. If you are completely new to -programming or Python you might want to look at a more formal Python tutorial. You can find links -and resources [on our link page](../../../Links). - -We have touched upon many of the concepts here but to use Evennia and to be able to follow along in -the code, you will need basic understanding of Python -[modules](http://docs.python.org/2/tutorial/modules.html), -[variables](http://www.tutorialspoint.com/python/python_variable_types.htm), [conditional -statements](http://docs.python.org/tutorial/controlflow.html#if-statements), -[loops](http://docs.python.org/tutorial/controlflow.html#for-statements), -[functions](http://docs.python.org/tutorial/controlflow.html#defining-functions), [lists, -dictionaries, list comprehensions](http://docs.python.org/tutorial/datastructures.html) and [string -formatting](http://docs.python.org/tutorial/introduction.html#strings). You should also have a basic -understanding of [object-oriented -programming](http://www.tutorialspoint.com/python/python_classes_objects.htm) and what Python -[Classes](http://docs.python.org/tutorial/classes.html) are. - -Once you have familiarized yourself, or if you prefer to pick Python up as you go, continue to one -of the beginning-level [Evennia tutorials](Tutorials) to gradually build up your understanding. - -Good luck! - -[prev lesson](Gamedir-Overview) | [next lesson]() +[prev lesson](Gamedir-Overview) | [next lesson](Adding-Commands) diff --git a/docs/source/_static/nature.css b/docs/source/_static/nature.css index 75eaeb0111..0f23db11c0 100644 --- a/docs/source/_static/nature.css +++ b/docs/source/_static/nature.css @@ -255,6 +255,11 @@ li > p:first-child { margin-bottom: 0px; } +li > p { + margin-top: 0px; + margin-bottom: 0px; +} + .admonition.important { background-color: #fbf7c3; border: 1px solid #c8c59b; @@ -361,6 +366,7 @@ dl.method > dd > p { font-style: italic; } + code.descname { font-size: 0.9em; } diff --git a/docs/source/toc.md b/docs/source/toc.md index c52d468289..0951b7e09a 100644 --- a/docs/source/toc.md +++ b/docs/source/toc.md @@ -93,7 +93,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 Command Tutorial](Howto/Starting/Adding-Command-Tutorial) - [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) @@ -101,6 +100,7 @@ - [Howto/Starting/Game Planning](Howto/Starting/Game-Planning) - [Howto/Starting/Implementing a game rule system](Howto/Starting/Implementing-a-game-rule-system) - [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/Gamedir Overview](Howto/Starting/Part1/Gamedir-Overview) - [Howto/Starting/Part1/Python basic introduction](Howto/Starting/Part1/Python-basic-introduction)